- Project Setup
- Intro to CoValues
- Refs & Auto-Subscribe
- Groups & Permissions
- Auth, Accounts & Migrations
- Edit Metadata & Time Travel
- Backend Workers
jazz-tools
CoValues
Identity & Permissions
Abstract interfaces
Media
Other
jazz-react
Context & Hooks
Auth Providers
Invite Links
Other
jazz-browser
Context Creation
Auth Providers
Other
jazz-browser-media-images
Image creation
jazz-nodejs
Context Creation
Learn some
Our issues app will be quite simple, but it will have team collaboration. Let's call it... “Circular.”
We'll build everything step-by-step, in typical, immediately usable stages. We'll explore many important things Jazz does — so follow along or just pick things out.
Project Setup
-
Create a project called "circular" from a generic Vite starter template:
>npx degit gardencmp/vite-ts-react-tailwind circular>cd circular>npm install>npm run devYou should now have an empty app running, typically at localhost:5173.
(If you make changes to the code, the app will automatically refresh.)
-
Install
(in a new terminal window):jazz-tools
andjazz-react
>cd circular>npm install jazz-tools jazz-react -
Modify
src/main.tsx
to set up a Jazz context:1import React from "react";2import ReactDOM from "react-dom/client";3import App from "./App.tsx";4import "./index.css";5import { createJazzReactContext, DemoAuth } from "jazz-react";67const Jazz = createJazzReactContext({8auth: DemoAuth({ appName: "Circular" }),9peer: "wss://mesh.jazz.tools/?key=you@example.com", // <- put your email here to get a proper API key later10});11export const { useAccount, useCoState } = Jazz;1213ReactDOM.createRoot(document.getElementById("root")!).render(14<Jazz.Provider>15<React.StrictMode>16{" "}17<App />18</React.StrictMode>{" "}19</Jazz.Provider>,20);This sets Jazz up, extracts app-specific hooks for later, and wraps our app in the provider.
TODO: explain Auth
Intro to CoValues
Let's learn about the central idea behind Jazz: Collaborative Values.
What if we could treat distributed state like local state? That's what CoValues do.
We can
- create CoValues, anywhere
- load CoValues by
ID
, from anywhere else - edit CoValues, from anywhere, by mutating them like local state
- subscribe to edits in CoValues, whether they're local or remote
Declaring our own CoValues
To make our own CoValues, we first need to declare a schema for them. Think of a schema as a combination of TypeScript types and runtime type information.
Let's start by defining a schema for our most central entity in Circular: an Issue.
Create a new file src/schema.ts
and add the following:
1import { CoMap, co } from "jazz-tools";
2
3export class Issue extends CoMap {
4 title = co.string;
5 description = co.string;
6 estimate = co.number;
7 status? = co.literal("backlog", "in progress", "done");
8}
TODO: explain what's happening
Reading from CoValues
CoValues are designed to be read like simple local JSON state. Let's see how we can read from an Issue by building a component to render one.
Create a new file src/components/Issue.tsx
and add the following:
1import { Issue } from "../schema";
2
3export function IssueComponent({ issue }: { issue: Issue }) {
4 return (
5 <div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
6 <h2>{issue.title}</h2>
7 <p className="col-span-3">{issue.description}</p>
8 <p>Estimate: {issue.estimate}</p>
9 <p>Status: {issue.status}</p>
10 </div>
11 );
12}
Simple enough!
Creating CoValues
To actually see an Issue, we have to create one. This is where things start to get interesting...
Let's modify src/App.tsx
to prepare for creating an Issue and then rendering it:
1import { useState } from "react";
2import { Issue } from "./schema";
3import { IssueComponent } from "./components/Issue.tsx";
4
5function App() {
6 const [issue, setIssue] = useState<Issue>();
7
8 if (issue) {
9 return <IssueComponent issue={issue} />;
10 } else {
11 return <button>Create Issue</button>;
12 }
13}
14
15export default App;
Now, finally, let's implement creating an issue:
1import { useState } from "react";
2import { Issue } from "./schema";
3import { IssueComponent } from "./components/Issue.tsx";
4import { useAccount } from "./main";
5
6function App() {
7 const { me } = useAccount();
8 const [issue, setIssue] = useState<Issue>();
9
10 const createIssue = () => {
11 const newIssue = Issue.create(
12 {
13 title: "Buy terrarium",
14 description: "Make sure it's big enough for 10 snails.",
15 estimate: 5,
16 status: "backlog",
17 },
18 { owner: me },
19 );
20 setIssue(newIssue);
21 };
22
23 if (issue) {
24 return <IssueComponent issue={issue} />;
25 } else {
26 return <button onClick={createIssue}>Create Issue</button>;
27 }
28}
29
30export default App;
🏁 Now you should be able to create a new issue by clicking the button and then see it rendered!
Preview
Buy terrarium
Make sure it's big enough for 10 snails.
Estimate: 5
Status: backlog
We'll already notice one interesting thing here:
- We have to create every CoValue with an
owner
!- this will determine access rights on the CoValue, which we'll learn about in "Groups & Permissions"
- here we set
owner
to the current userme
, which we get from the Jazz context /useAccount
Behind the scenes, Jazz not only creates the Issue in memory but also automatically syncs an encrypted version to the cloud and persists it locally. The Issue also has a globally unique ID.
We'll make use of both of these facts in a bit, but for now let's start with local editing and subscribing.
Editing CoValues and subscribing to edits
Since we're the owner of the CoValue, we should be able to edit it, right?
And since this is a React app, it would be nice to subscribe to edits of the CoValue and reactively re-render the UI, like we can with local state.
This is exactly what the useCoState
hook is for!
- Note that
useCoState
doesn't take a CoValue directly, but rather a CoValue's schema, plus itsID
.- So we'll slightly adapt our
useState
to only keep track of an issue ID... - ...and then use
useCoState
to get the actual issue
- So we'll slightly adapt our
Let's modify src/App.tsx
:
1import { useState } from "react";
2import { Issue } from "./schema";
3import { IssueComponent } from "./components/Issue.tsx";
4import { useAccount, useCoState } from "./main";
5import { ID } from "jazz-tools"
6
7function App() {
8 const { me } = useAccount();
9 const [issueID, setIssueID] = useState<ID<Issue>>();
10
11 const issue = useCoState(Issue, issueID);
12
13 const createIssue = () => {
14 const newIssue = Issue.create(
15 {
16 title: "Buy terrarium",
17 description: "Make sure it's big enough for 10 snails.",
18 estimate: 5,
19 status: "backlog",
20 },
21 { owner: me },
22 );
23 setIssueID(newIssue.id);
24 };
25
26 if (issue) {
27 return <IssueComponent issue={issue} />;
28 } else {
29 return <button onClick={createIssue}>Create Issue</button>;
30 }
31}
32
33export default App;
And now for the exciting part! Let's make src/components/Issue.tsx
an editing component.
1import { Issue } from "../schema";
2
3export function IssueComponent({ issue }: { issue: Issue }) {
4 return (
5 <div className="grid grid-cols-6 text-sm border-r border-b [&>*]:p-2 [&>*]:border-l [&>*]:border-t">
6 <input type="text"
7 value={issue.title}
8 onChange={(event) => { issue.title = event.target.value }}/>
9 <textarea className="col-span-3"
10 value={issue.description}
11 onChange={(event) => { issue.description = event.target.value }}/>
12 <label className="flex">
13 Estimate:
14 <input type="number" className="text-right min-w-0"
15 value={issue.estimate}
16 onChange={(event) => { issue.estimate = Number(event.target.value) }}/>
17 </label>
18 <select
19 value={issue.status}
20 onChange={(event) => {
21 issue.status = event.target.value as "backlog" | "in progress" | "done"
22 }}
23 >
24 <option value="backlog">Backlog</option>
25 <option value="in progress">In Progress</option>
26 <option value="done">Done</option>
27 </select>
28 </div>
29 );
30}
Preview
🏁 Now you should be able to edit the issue after creating it!
You'll immediately notice that we're doing something non-idiomatic for React: we mutate the issue directly, by assigning to its properties.
This works because CoValues
- intercept these edits
- update their local view accordingly (React doesn't really care after rendering)
- notify subscribers of the change (who will receive a fresh, updated view of the CoValue)
We have one subscriber on our Issue, with useCoState
in src/App.tsx
, which will cause the App
component and its children to re-render whenever the Issue changes.
Automatic local & cloud persistence
So far our Issue CoValues just looked like ephemeral local state. We'll now start exploring the first main feature that makes CoValues special: automatic persistence.
Actually, all the Issue CoValues we've created so far have already been automatically persisted to the cloud and locally - but we loose track of their ID after a reload.
So let's store the ID in window URL state and make sure our useState is in sync with that.
1import { useState } from "react";
2import { Issue } from "./schema";
3import { IssueComponent } from "./components/Issue.tsx";
4import { useAccount, useCoState } from "./main";
5import { ID } from "jazz-tools"
6
7function App() {
8 const { me } = useAccount();
9 const [issueID, setIssueID] = useState<ID<Issue> | undefined>(
10 (window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,
11 );
12
13 const issue = useCoState(Issue, issueID);
14
15 const createIssue = () => {
16 const newIssue = Issue.create(
17 {
18 title: "Buy terrarium",
19 description: "Make sure it's big enough for 10 snails.",
20 estimate: 5,
21 status: "backlog",
22 },
23 { owner: me },
24 );
25 setIssueID(newIssue.id);
26 window.history.pushState({}, "", `?issue=${newIssue.id}`);
27 };
28
29 if (issue) {
30 return <IssueComponent issue={issue} />;
31 } else {
32 return <button onClick={createIssue}>Create Issue</button>;
33 }
34}
35
36export default App;
🏁 Now you should be able to create an issue, edit it, reload the page, and still see the same issue.
Remote sync
To see that sync is also already working, try the following:
- copy the URL to a new tab in the same browser window and see the same issue
- edit the issue and see the changes reflected in the other tab!
This works because we load the issue as the same account that created it and owns it (remember setting { owner: me }
?).
We'll learn more about access control in "Groups & Permissions", but for now let's build a super simple way of sharing an Issue by just making it publicly readable & writable.
All we have to do is create a new group to own each new issue and add "everyone" as a "writer":
1import { useState } from "react";
2import { Issue } from "./schema";
3import { IssueComponent } from "./components/Issue.tsx";
4import { useAccount, useCoState } from "./main";
5import { ID, Group } from "jazz-tools"
6
7function App() {
8 const { me } = useAccount();
9 const [issueID, setIssueID] = useState<ID<Issue> | undefined>(
10 (window.location.search?.replace("?issue=", "") || undefined) as ID<Issue> | undefined,
11 );
12
13 const issue = useCoState(Issue, issueID);
14
15 const createIssue = () => {
16 const group = Group.create({ owner: me });
17 group.addMember("everyone", "writer");
18
19 const newIssue = Issue.create(
20 {
21 title: "Buy terrarium",
22 description: "Make sure it's big enough for 10 snails.",
23 estimate: 5,
24 status: "backlog",
25 },
26 { owner: group },
27 );
28 setIssueID(newIssue.id);
29 window.history.pushState({}, "", `?issue=${newIssue.id}`);
30 };
31
32 if (issue) {
33 return <IssueComponent issue={issue} />;
34 } else {
35 return <button onClick={createIssue}>Create Issue</button>;
36 }
37}
38
39export default App;
🏁 Now you should be able to open the Issue (with its unique URL) on another device or browser, or send it to a friend and you should be able to edit it together in realtime!
This concludes our intro to the essence of CoValues. Hopefully you're starting to have a feeling for how CoValues behave and how they're magically available everywhere.
Refs & Auto-Subscribe
Now let's have a look at how to compose CoValues into more complex structures and build a whole app around them.
Let's extend our two data model to include "Projects" which have a list of tasks and some properties of their own.
Using plain objects, you would probably type a Project like this:
1type Project = {
2 name: string;
3 issues: Issue[];
4};
In order to create this more complex structure in a fully collaborative way, we're going to need references that allow us to nest or link CoValues.
Add the following to src/schema.ts
:
1import { CoMap, CoList, co } from "jazz-tools";
2
3export class Issue extends CoMap {
4 title = co.string;
5 description = co.string;
6 estimate = co.number;
7 status? = co.literal("backlog", "in progress", "done");
8}
9
10export class ListOfIssues extends CoList.Of(co.ref(Issue)) {}
11
12export class Project extends CoMap {
13 name = co.string;
14 issues = co.ref(ListOfIssues);
15}
Now let's change things up a bit in terms of components as well.
First, we'll change App.tsx
to create and render Project
s instead of Issue
s. (We'll move the useCoState
into the ProjectComponent
we'll create in a second).
1import { useState } from "react";
2import { Project, ListOfIssues } from "./schema";
3import { ProjectComponent } from "./components/Project.tsx";
4import { useAccount } from "./main";
5import { ID, Group } from "jazz-tools"
6
7function App() {
8 const { me } = useAccount();
9 const [projectID, setProjectID] = useState<ID<Project> | undefined>(
10 (window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined,
11 );
12
✕ const issue = useCoState(Issue, issueID);
13
14 const createProject = () => {
15 const group = Group.create({ owner: me });
16 group.addMember("everyone", "writer");
17
18 const newProject = Project.create(
19 {
20 name: "New Project",
21 issues: ListOfIssues.create([], { owner: group })
22 },
23 { owner: group },
24 );
25 setProjectID(newProject.id);
26 window.history.pushState({}, "", `?project=${newProject.id}`);
27 };
28
29 if (projectID) {
30 return <ProjectComponent projectID={projectID} />;
31 } else {
32 return <button onClick={createProject}>Create Project</button>;
33 }
34}
35
36export default App;
Now we'll actually create the ProjectComponent
that renders a Project
and its Issue
s.
Create a new file src/components/Project.tsx
and add the following:
1import { ID } from "jazz-tools";
2import { Project, Issue } from "../schema";
3import { IssueComponent } from "./Issue.tsx";
4import { useCoState } from "../main";
5
6export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
7 const project = useCoState(Project, projectID);
8
9 const createAndAddIssue = () => {
10 project?.issues?.push(Issue.create({
11 title: "",
12 description: "",
13 estimate: 0,
14 status: "backlog",
15 }, { owner: project._owner }));
16 };
17
18 return project ? (
19 <div>
20 <h1>{project.name}</h1>
21 <div className="border-r border-b">
22 {project.issues?.map((issue) => (
23 issue && <IssueComponent key={issue.id} issue={issue} />
24 ))}
25 <button onClick={createAndAddIssue}>Create Issue</button>
26 </div>
27 </div>
28 ) : (
29 <div>Loading project...</div>
30 );
31}
🏁 Now you should be able to create a project, add issues to it, share it, and edit it collaboratively!
Two things to note here:
- We create a new Issue like before, and then push it into the
issues
list of the Project. By setting theowner
to the Project's owner, we ensure that the Issue has the same access rights as the project itself. - We only need to use
useCoState
on the Project, and the nestedListOfIssues
and eachIssue
will be automatically loaded and subscribed to when we access them. - However, because either the
Project
,ListOfIssues
, or eachIssue
might not be loaded yet, we have to check for them being defined.
The load-and-subscribe-on-access is a convenient way to have your rendering drive data loading (including in nested components!) and lets you quickly chuck UIs together without worrying too much about the shape of all data you'll need.
But you can also take more precise control over loading by defining a minimum-depth to load in useCoState
:
1import { ID } from "jazz-tools";
2import { Project, Issue } from "../schema";
3import { IssueComponent } from "./Issue.tsx";
4import { useCoState } from "../main";
5
6export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
7 const project = useCoState(Project, projectID, { issues: [{}] });
8
9 const createAndAddIssue = () => {
10 project?.issues.push(Issue.create({
11 title: "",
12 description: "",
13 estimate: 0,
14 status: "backlog",
15 }, { owner: project._owner }));
16 };
17
18 return project ? (
19 <div>
20 <h1>{project.name}</h1>
21 <div className="border-r border-b">
22 {project.issues.map((issue) => (
23 <IssueComponent key={issue.id} issue={issue} />
24 ))}
25 <button onClick={createAndAddIssue}>Create Issue</button>
26 </div>
27 </div>
28 ) : (
29 <div>Loading project...</div>
30 );
31}
The loading-depth spec { issues: [{}] }
means "in Project
, load issues
and load each item in issues
shallowly". (Since an Issue
doesn't have any further references, "shallowly" actually means all its properties will be available).
- Now, we can get rid of a lot of coniditional accesses because we know that once
project
is loaded,project.issues
and eachIssue
in it will be loaded as well. - This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
TODO: explainer about not loaded vs not set/defined and _refs
basics
🚧 OH NO - This is as far as we've written the Guide. 🚧
Groups & Permissions
Auth, Accounts & Migrations
Edit Metadata & Time Travel
Backend Workers
FAQ
🚧 OH NO - We don't have any FAQ yet. 🚧 -> Complain on GitHub
API Reference
🚧 OH NO - These docs are still highly work-in-progress. 🚧 -> Complain on GitHub
jazz-tools
CoValues
type ID
type ID<T> = CojsonInternalTypes.RawCoID & IDMarker<T>
class CoMap
CoMap
and assigning field schemas with co
.Optional
co.ref(...)
fields must be marked with { optional: true }
.import { co, CoMap } from "jazz-tools";
class Person extends CoMap {
name = co.string;
age = co.number;
pet = co.ref(Animal);
car = co.ref(Car, { optional: true });
}
CoMap.Record<Value>(value):
RecordLikeCoMap
value: IfCo<Value, Value>
CoMap.Record(...)
and passing the value schema using co
. Keys are always string
.import { co, CoMap } from "jazz-tools";
class ColorToFruitMap extends CoMap.Record(
co.ref(Fruit)
) {}
// assume we have map: ColorToFruitMap
// and strawberry: Fruit
map["red"] = strawberry;
CoMap
(using co
) as if they were normal properties on a plain object, using dot notation, Object.keys()
, etc.person.name;
person["age"];
person.age = 42;
person.pet?.name;
Object.keys(person);
// => ["name", "age", "pet"]
.id:
ID<CoMap>
CoMap
._refs:
{[Key in string]: IfCo<this[Key], RefIfCoValue<this[Key]>>}
prop
is a co.ref(...)
, you can use coMaps._refs.prop
to accessthe
Ref
instead of the potentially loaded/null value.This allows you to always get the ID or load the value manually.
person._refs.pet.id; // => ID<Animal>
person._refs.pet.value;
// => Animal | null
const pet = await person._refs.pet.load();
CoMap.create<M>(init, options):
M
M extends CoMap
this: CoValueClass<M>,
init: Simplify<CoMapInit<M>>,
options: { owner: Group | Account }
The owner (a Group or Account) determines access rights to the CoMap.
The CoMap will immediately be persisted and synced to connected peers.
const person = Person.create({
name: "Alice",
age: 42,
pet: cat,
}, { owner: friendGroup });
CoMap.load<M, Depth>(id, as, depth):
Promise<DeeplyLoaded<M, Depth> | undefined>
M extends CoMap
this: CoValueClass<M>,
id: ID<M>,
as: Account,
depth: Depth & DepthsIn<M>
CoMap
with a given ID, as a given account.depth
specifies which (if any) fields that reference other CoValues to load as well before resolving.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or {}
for shallowly loading only this CoMap, or { fieldA: depthA, fieldB: depthB }
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.const person = await Person.load(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ pet: {} }
);
CoMap.subscribe<M, Depth>(id, as, depth, listener):
() => void
M extends CoMap
this: CoValueClass<M>,
id: ID<M>,
as: Account,
depth: Depth & DepthsIn<M>,
listener: (value: DeeplyLoaded<M, Depth>) => void
CoMap
with a given ID, as a given account.Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
depth
specifies which (if any) fields that reference other CoValues to load as well before calling listener
for the first time.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or {}
for shallowly loading only this CoMap, or { fieldA: depthA, fieldB: depthB }
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.Returns an unsubscribe function that you should call when you no longer need updates.
Also see the
useCoState
hook to reactively subscribe to a CoValue in a React component.const unsub = Person.subscribe(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ pet: {} },
(person) => console.log(person)
);
.ensureLoaded<M, Depth>(depth):
Promise<DeeplyLoaded<M, Depth> | undefined>
M extends CoMap
this: M,
depth: Depth & DepthsIn<M>
CoMap
, ensure that the specified fields are loaded to the specified depth.Works like
CoMap.load()
, but you don't need to pass the ID or the account to load as again..subscribe<M, Depth>(depth, listener):
() => void
M extends CoMap
this: M,
depth: Depth & DepthsIn<M>,
listener: (value: DeeplyLoaded<M, Depth>) => void
CoMap
, subscribe to updates to the CoMap
and ensure that the specified fields are loaded to the specified depth.Works like
CoMap.subscribe()
, but you don't need to pass the ID or the account to load as again.Returns an unsubscribe function that you should call when you no longer need updates.
._edits:
{[Key in string]: IfCo<this[Key], CoMapEdit<this[Key]>>}
._owner:
Group | Account
.toJSON(_key, seenAbove):
any[]
_key: string,
seenAbove: ID<CoValue>[]
.[inspect]():
any[]
CoMap.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawCoMap<{ undefined }, JsonObject | null>
._instanceID:
string
._type:
"CoMap"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
.applyDiff<N>(newValues):
CoMap
N extends Partial<CoMapInit<CoMap>>
newValues: N
class CoList<Item>
*
CoList.Of<Item>(item):
CoList
item: Item
CoList
by subclassing CoList.Of(...)
and passing the item schema using co
.class ColorList extends CoList.Of(
co.string
) {}
class AnimalList extends CoList.Of(
co.ref(Animal)
) {}
CoList
as if they were normal items on a plain array, using []
notation, etc.Since
CoList
is a subclass of Array
, you can use all the normal array methods like push
, pop
, splice
, etc.colorList[0];
colorList[3] = "yellow";
colorList.push("Kawazaki Green");
colorList.splice(1, 1);
.id:
ID<CoList<Item>>
CoList
._refs:
{ undefined } & { length: number, [iterator]: NO TYPE }
CoList
's items are a co.ref(...)
, you can use coList._refs[i]
to accessthe
Ref
instead of the potentially loaded/null value.This allows you to always get the ID or load the value manually.
animals._refs[0].id; // => ID<Animal>
animals._refs[0].value;
// => Animal | null
const animal = await animals._refs[0].load();
CoList.create<L>(items, options):
L
L extends CoList<any>
this: CoValueClass<L>,
items: UnCo<L[number]>[],
options: { owner: Group | Account }
The owner (a Group or Account) determines access rights to the CoMap.
The CoList will immediately be persisted and synced to connected peers.
const colours = ColorList.create(
["red", "green", "blue"],
{ owner: me }
);
const animals = AnimalList.create(
[cat, dog, fish],
{ owner: me }
);
CoList.load<L, Depth>(id, as, depth):
Promise<DeeplyLoaded<L, Depth> | undefined>
L extends CoList<any>
this: CoValueClass<L>,
id: ID<L>,
as: Account,
depth: Depth & DepthsIn<L>
CoList
with a given ID, as a given account.depth
specifies if item CoValue references should be loaded as well before resolving.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or for shallowly loading only this CoList, or [itemDepth]
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.const animalsWithVets =
await ListOfAnimals.load(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
[{ vet: {} }]
);
CoList.subscribe<L, Depth>(id, as, depth, listener):
() => void
L extends CoList<any>
this: CoValueClass<L>,
id: ID<L>,
as: Account,
depth: Depth & DepthsIn<L>,
listener: (value: DeeplyLoaded<L, Depth>) => void
CoList
with a given ID, as a given account.Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
depth
specifies if item CoValue references should be loaded as well before calling listener
for the first time.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or for shallowly loading only this CoList, or [itemDepth]
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.Returns an unsubscribe function that you should call when you no longer need updates.
Also see the
useCoState
hook to reactively subscribe to a CoValue in a React component.const unsub = ListOfAnimals.subscribe(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ vet: {} },
(animalsWithVets) => console.log(animalsWithVets)
);
.ensureLoaded<L, Depth>(depth):
Promise<DeeplyLoaded<L, Depth> | undefined>
L extends CoList<any>
this: L,
depth: Depth & DepthsIn<L>
CoList
, ensure that items are loaded to the specified depth.Works like
CoList.load()
, but you don't need to pass the ID or the account to load as again..subscribe<L, Depth>(depth, listener):
() => void
L extends CoList<any>
this: L,
depth: Depth & DepthsIn<L>,
listener: (value: DeeplyLoaded<L, Depth>) => void
CoList
, subscribe to updates to the CoList
and ensure that items are loaded to the specified depth.Works like
CoList.subscribe()
, but you don't need to pass the ID or the account to load as again.Returns an unsubscribe function that you should call when you no longer need updates.
._owner:
Group | Account
.toJSON(_key, seenAbove):
any[]
_key: string,
seenAbove: ID<CoValue>[]
.[inspect]():
any[]
CoList.fromRaw<V>(raw):
V & CoList<any>
V extends CoList<any>
this: CoValueClass<V> & CoList,
raw: RawCoList<JsonValue, JsonObject | null>
._raw:
RawCoList<JsonValue, JsonObject | null>
._instanceID:
string
._type:
"CoList"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
CoList[species]:
ArrayConstructor
CoList.isArray(arg):
arg is any[]
arg: any
CoList.from<T>(arrayLike):
T[]
arrayLike: ArrayLike<T>
CoList.from<T, U>(arrayLike, mapfn, thisArg):
U[]
arrayLike: ArrayLike<T>,
mapfn: (v: T, k: number) => U,
thisArg: any
CoList.from<T>(iterable):
T[]
iterable: ArrayLike<T> | Iterable<T>
CoList.from<T, U>(iterable, mapfn, thisArg):
U[]
iterable: ArrayLike<T> | Iterable<T>,
mapfn: (v: T, k: number) => U,
thisArg: any
.constructor:
NO TYPE
._edits:
{ undefined }
._loadedAs:
Account
.push(items):
number
items: Item[]
.unshift(items):
number
items: Item[]
.pop():
Item | undefined
.shift():
Item | undefined
.splice(start, deleteCount, items):
Item[]
start: number,
deleteCount: number,
items: Item[]
.length:
number
.toString():
string
.toLocaleString():
string
.concat(items):
Item[]
items: ConcatArray<Item>[]
This method returns a new array without modifying any existing arrays.
.concat(items):
Item[]
items: ConcatArray<Item> | Item[]
This method returns a new array without modifying any existing arrays.
.join(separator):
string
separator: string
.reverse():
Item[]
This method mutates the array and returns a reference to the same array.
.slice(start, end):
Item[]
start: number,
end: number
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.
.sort(compareFn):
this
compareFn: (a: Item, b: Item) => number
This method mutates the array and returns a reference to the same array.
.indexOf(searchElement, fromIndex):
number
searchElement: Item,
fromIndex: number
.lastIndexOf(searchElement, fromIndex):
number
searchElement: Item,
fromIndex: number
.every<S>(predicate, thisArg):
this is S[]
predicate: (value: Item, index: number, array: Item[]) => value is S,
thisArg: any
.every(predicate, thisArg):
boolean
predicate: (value: Item, index: number, array: Item[]) => unknown,
thisArg: any
.some(predicate, thisArg):
boolean
predicate: (value: Item, index: number, array: Item[]) => unknown,
thisArg: any
.forEach(callbackfn, thisArg):
void
callbackfn: (value: Item, index: number, array: Item[]) => void,
thisArg: any
.map<U>(callbackfn, thisArg):
U[]
callbackfn: (value: Item, index: number, array: Item[]) => U,
thisArg: any
.filter<S>(predicate, thisArg):
S[]
predicate: (value: Item, index: number, array: Item[]) => value is S,
thisArg: any
.filter(predicate, thisArg):
Item[]
predicate: (value: Item, index: number, array: Item[]) => unknown,
thisArg: any
.reduce(callbackfn):
Item
callbackfn: (previousValue: Item, currentValue: Item, currentIndex: number, array: Item[]) => Item
.reduce(callbackfn, initialValue):
Item
callbackfn: (previousValue: Item, currentValue: Item, currentIndex: number, array: Item[]) => Item,
initialValue: Item
.reduce<U>(callbackfn, initialValue):
U
callbackfn: (previousValue: U, currentValue: Item, currentIndex: number, array: Item[]) => U,
initialValue: U
.reduceRight(callbackfn):
Item
callbackfn: (previousValue: Item, currentValue: Item, currentIndex: number, array: Item[]) => Item
.reduceRight(callbackfn, initialValue):
Item
callbackfn: (previousValue: Item, currentValue: Item, currentIndex: number, array: Item[]) => Item,
initialValue: Item
.reduceRight<U>(callbackfn, initialValue):
U
callbackfn: (previousValue: U, currentValue: Item, currentIndex: number, array: Item[]) => U,
initialValue: U
.find<S>(predicate, thisArg):
S | undefined
predicate: (value: Item, index: number, obj: Item[]) => value is S,
thisArg: any
otherwise.
.find(predicate, thisArg):
Item | undefined
predicate: (value: Item, index: number, obj: Item[]) => unknown,
thisArg: any
.findIndex(predicate, thisArg):
number
predicate: (value: Item, index: number, obj: Item[]) => unknown,
thisArg: any
otherwise.
.fill(value, start, end):
this
value: Item,
start: number,
end: number
start
to end
index to a static value
and returns the modified array.copyWithin(target, start, end):
this
target: number,
start: number,
end: number
to the same array starting at position target
.entries():
IterableIterator<[number, Item]>
.keys():
IterableIterator<number>
.values():
IterableIterator<Item>
.includes(searchElement, fromIndex):
boolean
searchElement: Item,
fromIndex: number
.flatMap<U, This>(callback, thisArg):
U[]
callback: (this: This, value: Item, index: number, array: Item[]) => TODO type typeOperator | U,
thisArg: This
a new array.
This is identical to a map followed by flat with depth 1.
.flat<A, D>(depth):
FlatArray<A, D>[]
D extends number
this: A,
depth: D
specified depth.
.at(index):
Item | undefined
index: number
.findLast<S>(predicate, thisArg):
S | undefined
predicate: (value: Item, index: number, array: Item[]) => value is S,
thisArg: any
otherwise.
.findLast(predicate, thisArg):
Item | undefined
predicate: (value: Item, index: number, array: Item[]) => unknown,
thisArg: any
.findLastIndex(predicate, thisArg):
number
predicate: (value: Item, index: number, array: Item[]) => unknown,
thisArg: any
otherwise.
.toReversed():
Item[]
.toSorted(compareFn):
Item[]
compareFn: (a: Item, b: Item) => number
.toSpliced(start, deleteCount, items):
Item[]
start: number,
deleteCount: number,
items: Item[]
.toSpliced(start, deleteCount):
Item[]
start: number,
deleteCount: number
.with(index, value):
Item[]
index: number,
value: Item
given value. If the index is negative, then it replaces from the end
of the array.
.[iterator]():
IterableIterator<Item>
[unscopables]:
{ length: boolean, toString: NO TYPE, toLocaleString: NO TYPE, pop: NO TYPE, push: NO TYPE, concat: NO TYPE, join: NO TYPE, reverse: NO TYPE, shift: NO TYPE, slice: NO TYPE, sort: NO TYPE, splice: NO TYPE, unshift: NO TYPE, indexOf: NO TYPE, lastIndexOf: NO TYPE, every: NO TYPE, some: NO TYPE, forEach: NO TYPE, map: NO TYPE, filter: NO TYPE, reduce: NO TYPE, reduceRight: NO TYPE, find: NO TYPE, findIndex: NO TYPE, fill: NO TYPE, copyWithin: NO TYPE, entries: NO TYPE, keys: NO TYPE, values: NO TYPE, includes: NO TYPE, flatMap: NO TYPE, flat: NO TYPE, at: NO TYPE, findLast: NO TYPE, findLastIndex: NO TYPE, toReversed: NO TYPE, toSorted: NO TYPE, toSpliced: NO TYPE, with: NO TYPE, [iterator]: NO TYPE, [unscopables]: boolean }
when they will be absent when used in a 'with' statement.
class CoStream<Item>
.id:
ID<CoStream<Item>>
CoStream.load<S, Depth>(id, as, depth):
Promise<DeeplyLoaded<S, Depth> | undefined>
S extends CoStream<any>
this: CoValueClass<S>,
id: ID<S>,
as: Account,
depth: Depth & DepthsIn<S>
CoStream.subscribe<S, Depth>(id, as, depth, listener):
() => void
S extends CoStream<any>
this: CoValueClass<S>,
id: ID<S>,
as: Account,
depth: Depth & DepthsIn<S>,
listener: (value: DeeplyLoaded<S, Depth>) => void
.ensureLoaded<S, Depth>(depth):
Promise<DeeplyLoaded<S, Depth> | undefined>
S extends CoStream<any>
this: S,
depth: Depth & DepthsIn<S>
.subscribe<S, Depth>(depth, listener):
() => void
S extends CoStream<any>
this: S,
depth: Depth & DepthsIn<S>,
listener: (value: DeeplyLoaded<S, Depth>) => void
._owner:
Group | Account
.toJSON():
{ in: { undefined }, id: ID<CoStream<Item>>, _type: "CoStream" }
.[inspect]():
{ in: { undefined }, id: ID<CoStream<Item>>, _type: "CoStream" }
CoStream.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawCoStream<JsonValue, JsonObject | null>
._instanceID:
string
._type:
"CoStream"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
CoStream.Of<Item>(item):
CoStream
item: IfCo<Item, Item>
CoStream._schema:
any
CoStream.create<S>(init, options):
S
S extends CoStream<any>
this: CoValueClass<S>,
init: TODO type conditional,
options: { owner: Group | Account }
CoStream.schema<V>(def):
void
V extends CoStream<any>
this: TODO reflection type 512 & CoStream,
def: { [ItemsSym]: V["_schema"][ItemsSym] }
.constructor:
NO TYPE
._schema:
{ [ItemsSym]: SchemaFor<Item> }
.byMe:
CoStreamEntry<Item> | undefined
.perSession:
{ undefined }
.inCurrentSession:
CoStreamEntry<Item> | undefined
.push(items):
void
items: Item[]
.pushItem(item):
void
item: Item
._loadedAs:
Account
class BinaryCoStream
.id:
ID<BinaryCoStream>
BinaryCoStream.load<B, Depth>(id, as, depth):
Promise<DeeplyLoaded<B, Depth> | undefined>
B extends BinaryCoStream
this: CoValueClass<B>,
id: ID<B>,
as: Account,
depth: Depth & DepthsIn<B>
BinaryCoStream.subscribe<B, Depth>(id, as, depth, listener):
() => void
B extends BinaryCoStream
this: CoValueClass<B>,
id: ID<B>,
as: Account,
depth: Depth & DepthsIn<B>,
listener: (value: DeeplyLoaded<B, Depth>) => void
.ensureLoaded<B, Depth>(depth):
Promise<DeeplyLoaded<B, Depth> | undefined>
B extends BinaryCoStream
this: B,
depth: Depth & DepthsIn<B>
.subscribe<B, Depth>(depth, listener):
() => void
B extends BinaryCoStream
this: B,
depth: Depth & DepthsIn<B>,
listener: (value: DeeplyLoaded<B, Depth>) => void
._owner:
Group | Account
.toJSON():
{ id: ID<BinaryCoStream>, _type: "BinaryCoStream", mimeType: string, fileName: string, totalSizeBytes: number, chunks: Uint8Array[], finished: boolean }
.[inspect]():
{ id: ID<BinaryCoStream>, _type: "BinaryCoStream", mimeType: string, fileName: string, totalSizeBytes: number, chunks: Uint8Array[], finished: boolean }
BinaryCoStream.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawBinaryCoStream<{ type: "binary" }>
._instanceID:
string
._type:
"BinaryCoStream"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
BinaryCoStream.create<S>(options):
S
S extends BinaryCoStream
this: CoValueClass<S>,
options: { owner: Group | Account }
BinaryCoStream.loadAsBlob(id, as, options):
Promise<Blob | undefined>
id: ID<BinaryCoStream>,
as: Account,
options: { allowUnfinished: boolean }
BinaryCoStream.createFromBlob(blob, options):
Promise<BinaryCoStream>
blob: File | Blob,
options: { owner: Group | Account, onProgress: (progress: number) => void }
.constructor:
NO TYPE
.getChunks(options):
BinaryStreamInfo & { chunks: Uint8Array[], finished: boolean } | undefined
options: { allowUnfinished: boolean }
.isBinaryStreamEnded():
boolean
.start(options):
void
options: BinaryStreamInfo
.push(data):
void
data: Uint8Array
.end():
void
.toBlob(options):
Blob | undefined
options: { allowUnfinished: boolean }
._loadedAs:
Account
Identity & Permissions
class Group
.id:
ID<Group>
Group.load<G, Depth>(id, as, depth):
Promise<DeeplyLoaded<G, Depth> | undefined>
G extends Group
this: CoValueClass<G>,
id: ID<G>,
as: Account,
depth: Depth & DepthsIn<G>
Group.subscribe<G, Depth>(id, as, depth, listener):
() => void
G extends Group
this: CoValueClass<G>,
id: ID<G>,
as: Account,
depth: Depth & DepthsIn<G>,
listener: (value: DeeplyLoaded<G, Depth>) => void
.ensureLoaded<G, Depth>(depth):
Promise<DeeplyLoaded<G, Depth> | undefined>
G extends Group
this: G,
depth: Depth & DepthsIn<G>
.subscribe<G, Depth>(depth, listener):
() => void
G extends Group
this: G,
depth: Depth & DepthsIn<G>,
listener: (value: DeeplyLoaded<G, Depth>) => void
._owner:
Group | Account
.toJSON():
any[] | string | object
.[inspect]():
any[] | string | object
Group.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawGroup<JsonObject | null>
._instanceID:
string
._type:
"Group"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
Group._schema:
any
Group.create<G>(options):
G
G extends Group
this: CoValueClass<G>,
options: { owner: Account }
.constructor:
NO TYPE
._schema:
{ profile: Schema, root: Schema, [MembersSym]: RefEncoded<Account> }
.profile:
Profile | null
.root:
CoMap | null
._refs:
{ profile: (TODO type conditional) | undefined, root: (TODO type conditional) | undefined }
.myRole():
Role | undefined
.addMember(member, role):
Group
member: Account | "everyone",
role: Role
.members:
{ id: ID<this[MembersSym]> | "everyone", role: Role | undefined, ref: Ref<NonNullable<this[MembersSym]>> | undefined, account: NO TYPE }[]
[MembersSym]:
Account | null
._loadedAs:
Account
class Profile
Profile.Record<Value>(value):
RecordLikeCoMap
value: IfCo<Value, Value>
CoMap.Record(...)
and passing the value schema using co
. Keys are always string
.import { co, CoMap } from "jazz-tools";
class ColorToFruitMap extends CoMap.Record(
co.ref(Fruit)
) {}
// assume we have map: ColorToFruitMap
// and strawberry: Fruit
map["red"] = strawberry;
.id:
ID<Profile>
CoMap
._refs:
{[Key in string]: IfCo<this[Key], RefIfCoValue<this[Key]>>}
prop
is a co.ref(...)
, you can use coMaps._refs.prop
to accessthe
Ref
instead of the potentially loaded/null value.This allows you to always get the ID or load the value manually.
person._refs.pet.id; // => ID<Animal>
person._refs.pet.value;
// => Animal | null
const pet = await person._refs.pet.load();
Profile.create<M>(init, options):
M
M extends CoMap
this: CoValueClass<M>,
init: Simplify<CoMapInit<M>>,
options: { owner: Group | Account }
The owner (a Group or Account) determines access rights to the CoMap.
The CoMap will immediately be persisted and synced to connected peers.
const person = Person.create({
name: "Alice",
age: 42,
pet: cat,
}, { owner: friendGroup });
Profile.load<M, Depth>(id, as, depth):
Promise<DeeplyLoaded<M, Depth> | undefined>
M extends CoMap
this: CoValueClass<M>,
id: ID<M>,
as: Account,
depth: Depth & DepthsIn<M>
CoMap
with a given ID, as a given account.depth
specifies which (if any) fields that reference other CoValues to load as well before resolving.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or {}
for shallowly loading only this CoMap, or { fieldA: depthA, fieldB: depthB }
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.const person = await Person.load(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ pet: {} }
);
Profile.subscribe<M, Depth>(id, as, depth, listener):
() => void
M extends CoMap
this: CoValueClass<M>,
id: ID<M>,
as: Account,
depth: Depth & DepthsIn<M>,
listener: (value: DeeplyLoaded<M, Depth>) => void
CoMap
with a given ID, as a given account.Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
depth
specifies which (if any) fields that reference other CoValues to load as well before calling listener
for the first time.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or {}
for shallowly loading only this CoMap, or { fieldA: depthA, fieldB: depthB }
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.Returns an unsubscribe function that you should call when you no longer need updates.
Also see the
useCoState
hook to reactively subscribe to a CoValue in a React component.const unsub = Person.subscribe(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ pet: {} },
(person) => console.log(person)
);
.ensureLoaded<M, Depth>(depth):
Promise<DeeplyLoaded<M, Depth> | undefined>
M extends CoMap
this: M,
depth: Depth & DepthsIn<M>
CoMap
, ensure that the specified fields are loaded to the specified depth.Works like
CoMap.load()
, but you don't need to pass the ID or the account to load as again..subscribe<M, Depth>(depth, listener):
() => void
M extends CoMap
this: M,
depth: Depth & DepthsIn<M>,
listener: (value: DeeplyLoaded<M, Depth>) => void
CoMap
, subscribe to updates to the CoMap
and ensure that the specified fields are loaded to the specified depth.Works like
CoMap.subscribe()
, but you don't need to pass the ID or the account to load as again.Returns an unsubscribe function that you should call when you no longer need updates.
._edits:
{[Key in string]: IfCo<this[Key], CoMapEdit<this[Key]>>}
._owner:
Group | Account
.toJSON(_key, seenAbove):
any[]
_key: string,
seenAbove: ID<CoValue>[]
.[inspect]():
any[]
Profile.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawCoMap<{ undefined }, JsonObject | null>
._instanceID:
string
._type:
"CoMap"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
.name:
co<string>
.applyDiff<N>(newValues):
Profile
N extends Partial<CoMapInit<Profile>>
newValues: N
class Account
.id:
ID<Account>
Account.load<A, Depth>(id, as, depth):
Promise<DeeplyLoaded<A, Depth> | undefined>
A extends Account
this: CoValueClass<A>,
id: ID<A>,
as: Account,
depth: Depth & DepthsIn<A>
Account.subscribe<A, Depth>(id, as, depth, listener):
() => void
A extends Account
this: CoValueClass<A>,
id: ID<A>,
as: Account,
depth: Depth & DepthsIn<A>,
listener: (value: DeeplyLoaded<A, Depth>) => void
.ensureLoaded<A, Depth>(depth):
Promise<DeeplyLoaded<A, Depth> | undefined>
A extends Account
this: A,
depth: Depth & DepthsIn<A>
.subscribe<A, Depth>(depth, listener):
() => void
A extends Account
this: A,
depth: Depth & DepthsIn<A>,
listener: (value: DeeplyLoaded<A, Depth>) => void
._owner:
Account
.toJSON():
any[] | object
.[inspect]():
any[] | object
Account.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawControlledAccount<AccountMeta> | RawAccount<AccountMeta>
._instanceID:
string
._type:
"Account"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
Account._schema:
any
Account.create<A>(options):
Promise<A>
A extends Account
this: CoValueClass<A> & Account,
options: { creationProps: { name: string }, crypto: CryptoProvider<any>, initialAgentSecret: TODO type templateLiteral, peersToLoadFrom: Peer[] }
Account.become<A>(options):
Promise<A>
A extends Account
this: CoValueClass<A> & Account,
options: { accountID: ID<Account>, accountSecret: TODO type templateLiteral, peersToLoadFrom: Peer[], crypto: CryptoProvider<any>, sessionID: TODO type templateLiteral | TODO type templateLiteral }
Account.createAs<A>(as, options):
Promise<A>
A extends Account
this: CoValueClass<A> & Account,
as: Account,
options: { creationProps: { name: string } }
Account.fromNode<A>(node):
A
A extends Account
this: CoValueClass<A>,
node: LocalNode
.constructor:
NO TYPE
._schema:
{ profile: Schema, root: Schema }
._loadedAs:
Account
.profile:
Profile | null
.root:
CoMap | null
._refs:
{ profile: RefIfCoValue<this["profile"]> | undefined, root: RefIfCoValue<this["root"]> | undefined }
.isMe:
boolean
.sessionID:
TODO type templateLiteral | TODO type templateLiteral | undefined
.myRole():
"admin" | undefined
.acceptInvite<V>(valueID, inviteSecret, coValueClass):
Promise<(TODO type conditional) | undefined>
V extends CoValue
valueID: ID<V>,
inviteSecret: TODO type templateLiteral,
coValueClass: CoValueClass<V>
.migrate(creationProps):
Promise<void> | void
this: Account,
creationProps: { name: string }
isControlledAccount(account):
account is Account & { isMe: true, sessionID: TODO type templateLiteral | TODO type templateLiteral, _raw: RawControlledAccount<AccountMeta> }
account: Account
Schema definition
Encoders reflection
co reflection
type co
type co<T> = T & CoMarker | T
Abstract interfaces
interface CoValue
.id:
ID<CoValue>
._owner:
Group | Account
.toJSON(key, seenAbove):
any[] | string | object
key: string,
seenAbove: ID<CoValue>[]
.[inspect]():
any
._raw:
RawCoValue
._type:
string
Media
class ImageDefinition
ImageDefinition.Record<Value>(value):
RecordLikeCoMap
value: IfCo<Value, Value>
CoMap.Record(...)
and passing the value schema using co
. Keys are always string
.import { co, CoMap } from "jazz-tools";
class ColorToFruitMap extends CoMap.Record(
co.ref(Fruit)
) {}
// assume we have map: ColorToFruitMap
// and strawberry: Fruit
map["red"] = strawberry;
.id:
ID<ImageDefinition>
CoMap
._refs:
{[Key in string]: IfCo<this[Key], RefIfCoValue<this[Key]>>}
prop
is a co.ref(...)
, you can use coMaps._refs.prop
to accessthe
Ref
instead of the potentially loaded/null value.This allows you to always get the ID or load the value manually.
person._refs.pet.id; // => ID<Animal>
person._refs.pet.value;
// => Animal | null
const pet = await person._refs.pet.load();
ImageDefinition.create<M>(init, options):
M
M extends CoMap
this: CoValueClass<M>,
init: Simplify<CoMapInit<M>>,
options: { owner: Group | Account }
The owner (a Group or Account) determines access rights to the CoMap.
The CoMap will immediately be persisted and synced to connected peers.
const person = Person.create({
name: "Alice",
age: 42,
pet: cat,
}, { owner: friendGroup });
ImageDefinition.load<M, Depth>(id, as, depth):
Promise<DeeplyLoaded<M, Depth> | undefined>
M extends CoMap
this: CoValueClass<M>,
id: ID<M>,
as: Account,
depth: Depth & DepthsIn<M>
CoMap
with a given ID, as a given account.depth
specifies which (if any) fields that reference other CoValues to load as well before resolving.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or {}
for shallowly loading only this CoMap, or { fieldA: depthA, fieldB: depthB }
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.const person = await Person.load(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ pet: {} }
);
ImageDefinition.subscribe<M, Depth>(id, as, depth, listener):
() => void
M extends CoMap
this: CoValueClass<M>,
id: ID<M>,
as: Account,
depth: Depth & DepthsIn<M>,
listener: (value: DeeplyLoaded<M, Depth>) => void
CoMap
with a given ID, as a given account.Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
depth
specifies which (if any) fields that reference other CoValues to load as well before calling listener
for the first time.The
DeeplyLoaded
return type guarantees that corresponding referenced CoValues are loaded to the specified depth.You can pass
[]
or {}
for shallowly loading only this CoMap, or { fieldA: depthA, fieldB: depthB }
for recursively loading referenced CoValues.Check out the
load
methods on CoMap
/CoList
/CoStream
/Group
/Account
to see which depth structures are valid to nest.Returns an unsubscribe function that you should call when you no longer need updates.
Also see the
useCoState
hook to reactively subscribe to a CoValue in a React component.const unsub = Person.subscribe(
"co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
me,
{ pet: {} },
(person) => console.log(person)
);
.ensureLoaded<M, Depth>(depth):
Promise<DeeplyLoaded<M, Depth> | undefined>
M extends CoMap
this: M,
depth: Depth & DepthsIn<M>
CoMap
, ensure that the specified fields are loaded to the specified depth.Works like
CoMap.load()
, but you don't need to pass the ID or the account to load as again..subscribe<M, Depth>(depth, listener):
() => void
M extends CoMap
this: M,
depth: Depth & DepthsIn<M>,
listener: (value: DeeplyLoaded<M, Depth>) => void
CoMap
, subscribe to updates to the CoMap
and ensure that the specified fields are loaded to the specified depth.Works like
CoMap.subscribe()
, but you don't need to pass the ID or the account to load as again.Returns an unsubscribe function that you should call when you no longer need updates.
._edits:
{[Key in string]: IfCo<this[Key], CoMapEdit<this[Key]>>}
._owner:
Group | Account
.toJSON(_key, seenAbove):
any[]
_key: string,
seenAbove: ID<CoValue>[]
.[inspect]():
any[]
ImageDefinition.fromRaw<V>(raw):
V
V extends CoValue
this: CoValueClass<V>,
raw: RawCoValue
._raw:
RawCoMap<{ undefined }, JsonObject | null>
._instanceID:
string
._type:
"CoMap"
.castAs<Cl>(cl):
InstanceType<Cl>
Cl extends CoValueClass<CoValue> & CoValueFromRaw<CoValue>
cl: Cl
.originalSize:
co<[number, number]>
.highestResAvailable(options):
{ res: TODO type templateLiteral, stream: BinaryCoStream } | undefined
options: { maxWidth: number }
[ItemsSym]:
co<BinaryCoStream | null>
.applyDiff<N>(newValues):
ImageDefinition
N extends Partial<CoMapInit<ImageDefinition>>
newValues: N
.placeholderDataURL:
co<string>
Other
MAX_RECOMMENDED_TX_SIZE intrinsic
type InviteSecret
type InviteSecretundefined = TODO type templateLiteral
type SessionID
type SessionIDundefined = TODO type templateLiteral
type AgentID
type AgentIDundefined = TODO type templateLiteral
type SyncMessage
type SyncMessageundefined = DoneMessage | NewContentMessage | KnownStateMessage | LoadMessage
type CoMapInit
type CoMapInit<Map extends object> = {[Key in CoKeys<Map>]: ForceRequiredRef<Map[Key]>} & {[Key in CoKeys<Map>]: ForceRequiredRef<Map[Key]>}
type DepthsIn
type DepthsIn<V, DepthLimit extends number, CurrentDepth extends number[]> = never[] | (TODO type conditional)
type DeeplyLoaded
type DeeplyLoaded<V, Depth, DepthLimit extends number, CurrentDepth extends number[]> = TODO type conditional
loadCoValue<V, Depth>(cls, id, as, depth):
Promise<DeeplyLoaded<V, Depth> | undefined>
cls: CoValueClass<V>,
id: ID<V>,
as: Account,
depth: Depth & DepthsIn<V>
subscribeToCoValue<V, Depth>(cls, id, as, depth, listener, onUnavailable):
() => void
cls: CoValueClass<V>,
id: ID<V>,
as: Account,
depth: Depth & DepthsIn<V>,
listener: (value: DeeplyLoaded<V, Depth>) => void,
onUnavailable: () => void
jazz-react
Context & Hooks
createJazzReactContext<Acc>({ auth, peer, storage? }):
JazzReactContext<Acc>
{ auth: ReactAuthHook<Acc>, peer: TODO type templateLiteral | TODO type templateLiteral, storage: "singleTabOPFS" | "indexedDB" }
interface JazzReactContext<Acc>
Acc extends Account
.useAccount():
{ me: Acc, logOut: () => void }
.useAccount<D>(depth):
{ me: DeeplyLoaded<Acc, D> | undefined, logOut: () => void }
D extends { profile: DepthsIn<Profile, 5, [0]> | undefined, root: DepthsIn<CoMap, 5, [0]> | undefined } | never[]
depth: D
.useCoState<V, D>(Schema, id, depth):
DeeplyLoaded<V, D> | undefined
V extends CoValue
Schema: TODO reflection type 512 & CoValueClass<CoValue>,
id: ID<V> | undefined,
depth: TODO type unknown
.useAcceptInvite<V>({ invitedObjectSchema, onAccept, forValueHint? }):
void
V extends CoValue
{ invitedObjectSchema: CoValueClass<V>, onAccept: (projectID: ID<V>) => void, forValueHint: string }
.Provider:
FC<{ children: ReactNode, loading: ReactNode }>
Media
useProgressiveImg({ image, maxWidth? }):
{ src: undefined | string, res: "placeholder" | TODO type templateLiteral | undefined, originalSize: co<[number, number]> | undefined }
{ image: ImageDefinition | null | undefined, maxWidth: number }
ProgressiveImg({ children, image, maxWidth? }):
ReactNode
{ children: (result: { src: undefined | string, res: "placeholder" | TODO type templateLiteral | undefined, originalSize: TODO type typeOperator | undefined }) => ReactNode, image: ImageDefinition | null | undefined, maxWidth: number }
Auth Providers
type ReactAuthHook
type ReactAuthHook<Acc extends Account> = (setJazzAuthState: (state: AuthState) => void) => { auth: AuthProvider<Acc>, AuthUI: React.ReactNode, logOut: () => void }
DemoAuth<Acc>({ appName, accountSchema?, appHostname?, Component?, seedAccounts? }):
ReactAuthHook<Acc>
{ appName: string, accountSchema: CoValueClass<Acc> & Account, appHostname: string, Component: Component, seedAccounts: { undefined } }
DemoAuth
PasskeyAuth<Acc>({ accountSchema, appName, appHostname?, Component? }):
ReactAuthHook<Acc>
{ accountSchema: CoValueClass<Acc> & Account, appName: string, appHostname: string, Component: Component }
PasskeyAuth
PassphraseAuth<Acc>({ accountSchema, appName, wordlist, appHostname?, Component? }):
ReactAuthHook<Acc>
{ accountSchema: CoValueClass<Acc> & Account, appName: string, wordlist: string[], appHostname: string, Component: Component }
PassphraseAuth
Invite Links
createInviteLink<C>(value, role, { baseURL?, valueHint? }):
string
value: C,
role: "admin" | "writer" | "reader",
{ baseURL: string, valueHint: string }
parseInviteLink<C>(inviteURL):
{ valueID: ID<C>, inviteSecret: InviteSecret, valueHint: string } | undefined
inviteURL: string
Other
type AuthState
type AuthStateundefined = "signedIn" | "ready" | "loading"
jazz-browser
Context Creation
createJazzBrowserContext<Acc>({ auth, peer, reconnectionTimeout?, storage?, crypto? }):
Promise<BrowserContext<Acc>>
{ auth: AuthProvider<Acc>, peer: TODO type templateLiteral | TODO type templateLiteral, reconnectionTimeout: number, storage: "singleTabOPFS" | "indexedDB", crypto: CryptoProvider<any> }
type BrowserContext
type BrowserContext<Acc extends Account> = { me: Acc, done: () => void }
Auth Providers
type SessionProvider
type SessionProviderundefined = (accountID: AgentID | ID<Account>) => Promise<SessionID>
type SessionHandle
type SessionHandleundefined = { session: Promise<SessionID>, done: () => void }
BrowserDemoAuth
BrowserPasskeyAuth
BrowserPassphraseAuth
Invite Links
createInviteLink<C>(value, role, { baseURL?, valueHint? }):
string
value: C,
role: "admin" | "writer" | "reader",
{ baseURL: string, valueHint: string }
parseInviteLink<C>(inviteURL):
{ valueID: ID<C>, inviteSecret: InviteSecret, valueHint: string } | undefined
inviteURL: string
consumeInviteLinkFromWindowLocation<V>({ as, invitedObjectSchema, forValueHint? }):
Promise<{ valueID: ID<V>, inviteSecret: InviteSecret, valueHint: string } | undefined>
{ as: Account, invitedObjectSchema: CoValueClass<V>, forValueHint: string }
Other
getSessionHandleFor(accountID):
SessionHandle
accountID: ID<Account> | TODO type templateLiteral
jazz-browser-media-images
Image creation
createImage(imageBlobOrFile, options):
Promise<ImageDefinition>
imageBlobOrFile: File | Blob,
options: { owner: Group | Account, maxSize: 2048 | 256 | 1024 }
jazz-nodejs
Context Creation
startWorker<Acc>({ accountID?, accountSecret?, sessionID?, syncServer?, accountSchema? }):
Promise<{ worker: Acc }>
{ accountID: string, accountSecret: string, sessionID: string, syncServer: string, accountSchema: CoValueClass<Acc> & Account }