W↓
All docs
🔑
Sign Up/Sign In
jazz.tools/docs/react-native
Public Link
Apr 8, 2025, 12:43:50 PM - complete - 457.9 kB
Starting URLs:
https://jazz.tools/docs/react-native
## Page: https://jazz.tools/docs/react-native Welcome to the Jazz documentation! The Jazz docs are currently heavily work in progress, sorry about that! ## Quickstart Run the following command to create a new Jazz project from one of our example apps: npx create-jazz-app@latest Or set up Jazz yourself, using the following instructions for your framework of choice: * React * Next.js * React Native * React Native Expo * Vue * Svelte ## Example apps You can also find example apps with code most similar to what you want to build. These apps make use of different features such as auth, file upload, and more. ## Sync and storage Sync and persist your data by setting up a sync and storage infrastructure using Jazz Cloud, or do it yourself. ## Collaborative values Learn how to structure your data using collaborative values. ## API Reference Many of the packages provided are documented in the API Reference. ## LLM Docs Get better results with AI by importing the Jazz docs into your context window. ## Get support If you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native/guide ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native/faq ## How established is Jazz? Jazz is backed by fantastic angel and institutional investors with experience and know-how in devtools and has been in development since 2020. ## Will Jazz be around long-term? We're committed to Jazz being around for a long time! We understand that when you choose Jazz for your projects, you're investing time and making a significant architectural choice, and we take that responsibility seriously. That's why we've designed Jazz with longevity in mind from the start: * The open source nature of our sync server means you'll always be able to run your own infrastructure * Your data remains accessible even if our cloud services change * We're designing the protocol as an open specification This approach creates a foundation that can continue regardless of any single company's involvement. The local-first architecture means your apps will always work, even offline, and your data remains yours. --- ## Page: https://jazz.tools/docs/react-native/project-setup This guide covers setting up Jazz for React Native applications from scratch. If you're using Expo, please refer to the React Native - Expo guide instead. If you just want to get started quickly, you can use our React Native Chat Demo as a starting point. Jazz supports the New Architecture for React Native. Tested with: "react-native": "0.78.22", "react": "18.3.1" ## Installation ### Create a new project (Skip this step if you already have one) npx @react-native-community/cli init myjazzapp cd myjazzapp If you intend to build for iOS, you can accept the invitation to install CocoaPods. If you decline, or you get an error, you can install it with `pod-install`. ### Install dependencies # React Native dependencies npm install @react-native-community/netinfo @bam.tech/react-native-image-resizer # React Native polyfills npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @craftzdog/react-native-buffer @op-engineering/op-sqlite react-native-mmkv # Jazz dependencies npm i -S jazz-tools jazz-react-native jazz-react-native-media-images **Note**: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`. ### Configure Metro #### Regular repositories If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content: // metro.config.js const { const getDefaultConfig: anygetDefaultConfig, const mergeConfig: anymergeConfig } = var require: NodeRequire (id: string) => anyrequire('@react-native/metro-config'); const const config: { resolver: { sourceExts: string[]; requireCycleIgnorePatterns: RegExp[]; }; }config = { resolver: { sourceExts: string[]; requireCycleIgnorePatterns: RegExp[]; }resolver: { sourceExts: string[]sourceExts: ["mjs", "js", "json", "ts", "tsx"], requireCycleIgnorePatterns: RegExp[]requireCycleIgnorePatterns: [/(^|\/|\\)node_modules($|\/|\\)/] } }; var module: NodeModulemodule.NodeJS.Module.exports: anyexports = const mergeConfig: anymergeConfig(const getDefaultConfig: anygetDefaultConfig(var __dirname: string__dirname), const config: { resolver: { sourceExts: string[]; requireCycleIgnorePatterns: RegExp[]; }; }config); #### Monorepos For monorepos, use the following `metro.config.js`: // metro.config.js const const path: anypath = var require: NodeRequire (id: string) => anyrequire("path"); const { const makeMetroConfig: anymakeMetroConfig } = var require: NodeRequire (id: string) => anyrequire("@rnx-kit/metro-config"); const const MetroSymlinksResolver: anyMetroSymlinksResolver = var require: NodeRequire (id: string) => anyrequire("@rnx-kit/metro-resolver-symlinks"); // Define workspace root const const projectRoot: stringprojectRoot = var __dirname: string__dirname; const const workspaceRoot: anyworkspaceRoot = const path: anypath.resolve(const projectRoot: stringprojectRoot, "../.."); // Add packages paths const const extraNodeModules: { modules: any; }extraNodeModules = { modules: anymodules: const path: anypath.resolve(const workspaceRoot: anyworkspaceRoot, "node_modules"), }; const const watchFolders: any[]watchFolders = [ const path: anypath.resolve(const workspaceRoot: anyworkspaceRoot, "node_modules"), const path: anypath.resolve(const workspaceRoot: anyworkspaceRoot, "packages"), ]; const const nodeModulesPaths: any[]nodeModulesPaths = [ const path: anypath.resolve(const projectRoot: stringprojectRoot, "node_modules"), const path: anypath.resolve(const workspaceRoot: anyworkspaceRoot, "node_modules"), ]; var module: NodeModulemodule.NodeJS.Module.exports: anyexports = const makeMetroConfig: anymakeMetroConfig({ resolver: { resolveRequest: any; extraNodeModules: { modules: any; }; nodeModulesPaths: any[]; }resolver: { resolveRequest: anyresolveRequest: const MetroSymlinksResolver: anyMetroSymlinksResolver(), extraNodeModules: { modules: any; }extraNodeModules, nodeModulesPaths: any[]nodeModulesPaths, }, sourceExts: string[]sourceExts: ["mjs", "js", "json", "ts", "tsx"], watchFolders: any[]watchFolders, }); ### Additional monorepo configuration (for pnpm) * Add `node-linker=hoisted` to the root `.npmrc` (create this file if it doesn't exist). * Add the following to the root `package.json`: // package.json "pnpm": { "peerDependencyRules": { "ignoreMissing": [ "@babel/*", "typescript" ] } } ### Add polyfills Create a file `polyfills.js` at the project root with the following content: // polyfills.js import { import polyfillGlobalpolyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; import { class BufferBuffer } from "@craftzdog/react-native-buffer"; import polyfillGlobalpolyfillGlobal("Buffer", () => class BufferBuffer); // polyfill Buffer import { import ReadableStreamReadableStream } from "readable-stream"; import polyfillGlobalpolyfillGlobal("ReadableStream", () => import ReadableStreamReadableStream); // polyfill ReadableStream import "@azure/core-asynciterator-polyfill"; // polyfill Async Iterator import "@bacons/text-decoder/install"; // polyfill Text Decoder import 'react-native-get-random-values'; // polyfill getRandomValues Update `index.js`: // index.js import { AppRegistry } from 'react-native'; import import AppApp from './App'; import { name as import appNameappName } from './app.json'; import './src/polyfills'; AppRegistry.function AppRegistry.registerComponent(appKey: string, getComponentFunc: ComponentProvider, section?: boolean): stringregisterComponent(import appNameappName, () => import AppApp); Lastly, ensure that the `"main"` field in your `package.json` points to `index.js`: // package.json { "main": "index.js", ... } ## Authentication Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication, check our Authentication Overview guide and see the React Native Chat Demo for a complete example. ## Next Steps Now that you've set up your React Native project for Jazz, you'll need to: 1. Set up the Jazz Provider - Configure how your app connects to Jazz 2. Add authentication (optional) - Enable users to access data across devices 3. Define your schema - See the schema docs for more information 4. Run your app: npx react-native run-ios npx react-native run-android ## Verification Ready to see if everything's working? Let's fire up your app: npx react-native run-ios # or npx react-native run-android If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation. If you run into any issues that aren't covered in the Common Issues section, drop by our Discord for help. ## Common Issues * **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported in your `polyfills.js` file. * **iOS build failures**: Make sure you've run `pod install` after adding the dependencies. * **Android build failures**: Ensure your Android SDK and NDK versions are compatible with the native modules. ### Install CocoaPods If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using `pod-install`: npx pod-install --- ## Page: https://jazz.tools/docs/react-native/sync-and-storage ## Using Jazz Cloud Simply use `wss://cloud.jazz.tools/?key=...` as the sync server URL. Jazz Cloud will * sync CoValues in real-time between users and devices * safely persist CoValues on redundant storage nodes with additional backups * make use of geographically distributed cache nodes for low latency ### Free public alpha * Jazz Cloud is free during the public alpha, with no strict usage limits * We plan to keep a free tier, so you'll always be able to get started with zero setup * See Jazz Cloud pricing for more details * ⚠️ Please use a valid email address as your API key. Your full sync server URL should look something like `wss://cloud.jazz.tools/?key=you@example.com` Once we support per-app API keys, we'll email you an API key you can use instead. ## Running your own sync server You can run your own sync server using: npx jazz-run sync And then use `ws://localhost:4200` as the sync server URL. You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS. In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL. ### Command line options: * `--port` / `-p` - the port to run the sync server on. Defaults to 4200. * `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default. * `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`. ### Source code The implementation of this simple sync server is available open-source on GitHub. --- ## Page: https://jazz.tools/docs/react-native/project-setup/server-side The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do. This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users. ## Generating credentials Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret. To generate new credentials for a Server Worker, you can run: npx jazz-run account create --name "My Server Worker" The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done. ## Storing & providing credentials Server Worker credentials are typically stored and provided as environmental variables. **Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.** ## Starting a server worker You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly to setting up a client-side Jazz context, it: * takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in it's private account root) * takes a URL for a sync & storage server `startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere. import { startWorker } from 'jazz-nodejs'; const { worker } = await startWorker({ AccountSchema: MyWorkerAccount, syncServer: 'wss://cloud.jazz.tools/?key=you@example.com', }); `worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to: * load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})` * create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })` ## Using CoValues instead of requests Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about. What's less clear is how you can trigger this work to happen. * One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients * The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients. * A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`). * This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible. --- ## Page: https://jazz.tools/docs/react-native/project-setup/providers `<JazzProvider />` is the core component that connects your React Native application to Jazz. It handles: // App.tsx import { JazzProvider } from "jazz-react-native"; import { MyAppAccount } from "./schema"; export function MyJazzProvider({ children }: { children: React.ReactNode }) { return ( <JazzProvider sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} AccountSchema={MyAppAccount} > {children} </JazzProvider> ); } // Register the Account schema so `useAccount` returns our custom `MyAppAccount` declare module "jazz-react-native" { interface Register { Account: MyAppAccount; } } `<JazzProvider />` works with various authentication methods, with PassphraseAuth being the easiest way to get started for development and testing. For authentication details, refer to our Authentication Overview guide. The authentication hooks must always be used inside the `<JazzProvider />` component. // Example with PassphraseAuth import { JazzProvider, usePassphraseAuth } from "jazz-react-native"; import { englishWordlist } from "./wordlist"; function JazzAuthentication({ children }: { children: ReactNode }) { const auth = usePassphraseAuth({ wordlist: englishWordlist, }); // If the user is already signed in, render the App if (auth.state === "signedIn") { return children } // Otherwise, show a sign-in screen return <SignInScreen auth={auth} />; } function AuthenticatedProvider({ children }: { children: ReactNode }) { return ( <JazzProvider sync={{ peer: "wss://cloud.jazz.tools/?key=your-api-key" }} > <JazzAuthentication> {children} </JazzAuthentication> </JazzProvider> ); } Jazz for React Native includes built-in local persistence using SQLite. This implementation uses: Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts. --- ## Page: https://jazz.tools/docs/react-native/ai-tools AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at. However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong. To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor. Every tool is different, but generally, you'll need to either paste the contents of the llms-full.txt file directly in your prompt, or attach the file to the tool. Upload the txt file in your prompt. https://jazz.tools/llms-full.txt We follow the llms.txt proposed standard for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing. AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good). We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on Discord. --- ## Page: https://jazz.tools/docs/react-native/inspector For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app. In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`. Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window. Install the package. npm install jazz-inspector Render the component within your `JazzProvider`. This will show the Inspector launch button on the right of your page. --- ## Page: https://jazz.tools/docs/react-native/upgrade/0-13-0 Version 0.13.0 introduces a significant architectural change in how Jazz supports React Native applications. We've separated the React Native implementation into two distinct packages to better serve different React Native development approaches: 1. **jazz-react-native**: Focused package for framework-less React Native applications 2. **jazz-expo**: Dedicated package for Expo applications 3. **jazz-react-native-core**: Shared core functionality used by both implementations This guide focuses on upgrading **React Native without Expo** applications. If you're using Expo, please see the Expo upgrade guide. ## Migration Steps for React Native 1. **Update Dependencies** # Ensure you have the required dependencies npm install @op-engineering/op-sqlite react-native-mmkv @react-native-community/netinfo # Remove the old packages npm install jazz-react-native-expo # Install the new packages npm install jazz-react-native jazz-react-native-media-images # Run pod install for iOS npx pod-install 2. **No Import Changes Required** Your existing imports from `jazz-react-native` should continue to work, but the implementation now uses a different storage solution (op-sqlite and MMKV). ## Storage Adapter Changes The `jazz-react-native` package now uses: * `OpSQLiteAdapter` for database storage (using `@op-engineering/op-sqlite`) * `MMKVStoreAdapter` for key-value storage (using `react-native-mmkv`) These are now the default storage adapters in the `JazzProvider` for framework-less React Native applications. ## Example Provider Setup // App.tsx import { import JazzProviderJazzProvider } from "jazz-react-native"; import { import MyAppAccountMyAppAccount } from "./schema"; export function function MyJazzProvider({ children }: { children: React.ReactNode; }): React.JSX.ElementMyJazzProvider({ children: React.ReactNodechildren }: { children: React.ReactNodechildren: export namespace ReactReact.type React.ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefinedRepresents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}@example```tsx // Typing children type Props = { children: ReactNode } const Component = ({ children }: Props) => <div>{children}</div> <Component>hello</Component> ```@example```tsx // Typing a custom element type Props = { customElement: ReactNode } const Component = ({ customElement }: Props) => <div>{customElement}</div> <Component customElement={<div>hello</div>} /> ```ReactNode }) { return ( <import JazzProviderJazzProvider sync: { peer: string; }sync={{ peer: stringpeer: "wss://cloud.jazz.tools/?key=you@example.com" }} type AccountSchema: anyAccountSchema={import MyAppAccountMyAppAccount} > {children: React.ReactNodechildren} </import JazzProviderJazzProvider> ); } // Register the Account schema declare module "jazz-react-native" { interface Register { Register.Account: MyAppAccountAccount: import MyAppAccountMyAppAccount; } } ## New Architecture Support The `jazz-react-native` implementation fully supports the React Native New Architecture. This includes compatibility with: * JavaScript Interface (JSI) for more efficient JavaScript-to-native communication * Fabric rendering system for improved UI performance * TurboModules for better native module management * Codegen for type-safe interfaces No additional configuration is needed to use Jazz with the New Architecture. ## Potential Podfile Issues If you encounter pod installation issues in a pnpm workspace environment (such as `undefined method '[]' for nil` in the Podfile at the line `config = use_native_modules!`), replace the problematic line with a manual path reference: react_native_path = "../node_modules/react-native" config = { :reactNativePath => react_native_path } This approach bypasses issues with dependency resolution in workspace setups where packages may be hoisted to the root `node_modules`. ## For More Information For detailed setup instructions, refer to the React Native Setup Guide --- ## Page: https://jazz.tools/docs/react-native/upgrade/0-12-0 Jazz 0.12.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable. ## What's new? * New resolve API for a more type-safe deep loading * A single, consistent load option for all loading methods * Improved permission checks on deep loading * Easier type safety with the `Resolved` type helper ## Breaking changes ### New Resolve API We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs. **Major changes:** 1. Functions and hooks for loading now take the resolve query as an explicit nested `resolve` prop 2. Shallowly loading a collection is now done with `true` instead of `[]` or `{}` // Before // @ts-expect-error const { const me: MyAppAccount | ({ root: AccountRoot | ({ friends: ListOfAccounts | ((Account | (Account & { root: CoMap; profile: Profile; }))[] & ListOfAccounts); } & AccountRoot); profile: Profile; } & MyAppAccount) | null | undefinedme } = useAccount<MyAppAccount, RefsToResolve<MyAppAccount>>(options?: { resolve?: RefsToResolve<MyAppAccount, 10, []> | undefined; } | undefined): { ...; } (+1 overload)useAccount({ root: { friends: never[]; }root: { friends: never[]friends: [] } }); // After const { const me: ({ root: { friends: ListOfAccounts; } & AccountRoot; } & MyAppAccount) | null | undefinedme } = useAccount<MyAppAccount, { root: { friends: true; }; }>(options?: { resolve?: RefsToResolve<MyAppAccount, 10, []> | undefined; } | undefined): { me: ({ ...; } & MyAppAccount) | ... 1 more ... | undefined; logOut: () => void; } (+1 overload)useAccount({ resolve?: RefsToResolve<MyAppAccount, 10, []> | undefinedresolve: { root?: RefsToResolve<AccountRoot, 10, [0]> | undefinedroot: { friends?: RefsToResolve<ListOfAccounts, 10, [0, 0]> | undefinedfriends: true } } }); 3. For collections, resolving items deeply is now done with a special `$each` key. For a `CoList`: class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { } class class ListOfTasksListOfTasks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Task | null>>(item: co<Task | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<Task | null>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Task>(arg: typeof Task | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Task), options?: never) => co<...> (+1 overload)ref(class TaskTask)) {} const const id: ID<Task>id = "co_123" as type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class TaskTask>; // Before // @ts-expect-error const const tasks: ListOfTasks | null | undefinedtasks = useCoState<ListOfTasks, true>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefined; } | undefined): ListOfTasks | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<Task>id, [{}]); // After const const tasks: (Task[] & ListOfTasks) | null | undefinedtasks = useCoState<ListOfTasks, { $each: boolean; }>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefined; } | undefined): (Task[] & ListOfTasks) | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<Task>id, { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefinedresolve: { $each: RefsToResolve<Task, 10, [0]>$each: true } }); For a `CoMap.Record`: class class UsersByUsernameUsersByUsername extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap.CoMap.Record<co<MyAppAccount | null>>(value: co<MyAppAccount | null>): { new (options: { fromRaw: RawCoMap; } | undefined): { ...; }; ... 6 more ...; fromRaw<V extends CoValue>(this: CoValueClass<V>, raw: import("/vercel/path0/packages/cojson/dist/coValue").RawCoValue): V; }Declare a Record-like CoMap schema, by extending `CoMap.Record(...)` and passing the value schema using `co`. Keys are always `string`.@example```ts 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; ```@categoryDeclarationRecord(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof MyAppAccount>(arg: typeof MyAppAccount | ((_raw: RawAccount<AccountMeta> | RawControlledAccount<AccountMeta>) => typeof MyAppAccount), options?: never) => co<...> (+1 overload)ref(class MyAppAccountMyAppAccount)) {} // Before // @ts-expect-error const const usersByUsername: UsersByUsername | null | undefinedusersByUsername = useCoState<UsersByUsername, true>(Schema: CoValueClass<UsersByUsername>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<...> | undefined; } | undefined): UsersByUsername | ... 1 more ... | undefineduseCoState(class UsersByUsernameUsersByUsername, const id: ID<UsersByUsername>id, [{}]); // After const const usersByUsername: ({ [key: string]: MyAppAccount; } & UsersByUsername) | null | undefinedusersByUsername = useCoState<UsersByUsername, { $each: boolean; }>(Schema: CoValueClass<UsersByUsername>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): ({ ...; } & UsersByUsername) | ... 1 more ... | undefineduseCoState(class UsersByUsernameUsersByUsername, const id: ID<UsersByUsername>id, { resolve?: RefsToResolve<UsersByUsername, 10, []> | undefinedresolve: { $each: RefsToResolve<MyAppAccount, 10, [0]>$each: true } }); Nested loading — note how it's now less terse, but more readable: class class OrgOrg extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Org.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; } class class AssigneeAssignee extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Assignee.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Assignee.org: co<Org | null>org = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Org>(arg: typeof Org | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Org), options?: never) => co<...> (+1 overload)ref(class OrgOrg); } class class ListOfAssigneesListOfAssignees extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Assignee | null>>(item: co<Assignee | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<...>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Assignee>(arg: typeof Assignee | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Assignee), options?: never) => co<...> (+1 overload)ref(class AssigneeAssignee)) {} class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Task.content: co<string>content = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.assignees: co<ListOfAssignees | null>assignees = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof ListOfAssignees>(arg: typeof ListOfAssignees | ((_raw: RawCoList<JsonValue, JsonObject | null>) => typeof ListOfAssignees), options?: never) => co<...> (+1 overload)ref(class ListOfAssigneesListOfAssignees); } class class ListOfTasksListOfTasks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Task | null>>(item: co<Task | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<Task | null>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Task>(arg: typeof Task | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Task), options?: never) => co<...> (+1 overload)ref(class TaskTask)) {} // Before // @ts-expect-error const const tasksWithAssigneesAndTheirOrgs: ListOfTasks | null | undefinedtasksWithAssigneesAndTheirOrgs = useCoState<ListOfTasks, true>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefined; } | undefined): ListOfTasks | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<ListOfTasks>id, [{ assignees: { org: {}; }[]assignees: [{ org: {}org: {}}]} ]); // After const const tasksWithAssigneesAndTheirOrgs: ((Task & { assignees: (Assignee & { org: Org; })[] & ListOfAssignees; })[] & ListOfTasks) | null | undefinedtasksWithAssigneesAndTheirOrgs = useCoState<ListOfTasks, { $each: { assignees: { $each: { org: boolean; }; }; }; }>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): ((Task & { ...; })[] & ListOfTasks) | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<ListOfTasks>id, { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefinedresolve: { $each: RefsToResolve<Task, 10, [0]>$each: { assignees?: RefsToResolve<ListOfAssignees, 10, [0, 0]> | undefinedassignees: { $each: RefsToResolve<Assignee, 10, [0, 0, 0]>$each: { org?: RefsToResolve<Org, 10, [0, 0, 0, 0]> | undefinedorg: true } } } } }); It's also a lot more auto-complete friendly: const const tasksWithAssigneesAndTheirOrgs: ListOfTasks | ((Task | (Task & { assignees: ListOfAssignees | ((Assignee | (Assignee & { org: Org; }))[] & ListOfAssignees); }))[] & ListOfTasks) | null | undefinedtasksWithAssigneesAndTheirOrgs = useCoState<ListOfTasks, RefsToResolve<ListOfTasks>>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): ListOfTasks | ... 2 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<ListOfTasks>id, { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefinedresolve: { $each: RefsToResolve<Task, 10, [0]>$each: { assignees?: RefsToResolve<ListOfAssignees, 10, [0, 0]> | undefinedassignees: { $$each } } } }); ### A single, consistent load option The new API works across all loading methods, and separating out the resolve query means other options with default values are easier to manage, for example: loading a value as a specific account instead of using the implicit current account: // Before // @ts-expect-error class PlaylistPlaylist.CoMap.load<Playlist, true>(this: CoValueClass<...>, id: ID<Playlist>, options?: { resolve?: RefsToResolve<Playlist, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<Playlist>id, const otherAccount: AccountotherAccount, { tracks: never[]tracks: [], }); // After class PlaylistPlaylist.CoMap.load<Playlist, { tracks: boolean; }>(this: CoValueClass<...>, id: ID<Playlist>, options?: { resolve?: RefsToResolve<Playlist, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<Playlist>id, { loadAs?: Account | AnonymousJazzAgent | undefinedloadAs: const otherAccount: AccountotherAccount, resolve?: RefsToResolve<Playlist, 10, []> | undefinedresolve: { tracks?: RefsToResolve<ListOfTracks, 10, [0]> | undefinedtracks: true } }); ### Improved permission checks on deep loading Now `useCoState` will return `null` when the current user lacks permissions to load requested data. Previously, `useCoState` would return `undefined` if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing. Now `undefined` means that the value is definitely loading, and `null` means that the value is temporarily missing. We also have implemented a more granular permission checking, where if an _optional_ CoValue cannot be accessed, `useCoState` will return the data stripped of that CoValue. **Note:** The state handling around loading and error states will become more detailed and easy-to-handle in future releases, so this is just a small step towards consistency. class class ListOfTracksListOfTracks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Track | null | undefined>>(item: co<Track | null | undefined>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<...>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }optional.ref: <typeof Track>(arg: typeof Track | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Track)) => co<Track | ... 1 more ... | undefined>ref(class TrackTrack)) {} function function TrackListComponent({ id }: { id: ID<ListOfTracks>; }): React.JSX.Element | (React.JSX.Element | null | undefined)[]TrackListComponent({ id: ID<ListOfTracks>id }: { id: ID<ListOfTracks>id: type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class ListOfTracksListOfTracks> }) { // Before (ambiguous states) // @ts-expect-error const const tracks: ListOfTracks | null | undefinedtracks = useCoState<ListOfTracks, true>(Schema: CoValueClass<ListOfTracks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTracks, 10, []> | undefined; } | undefined): ListOfTracks | ... 1 more ... | undefineduseCoState(class ListOfTracksListOfTracks, id: ID<ListOfTracks>id, [{}]); if (const tracks: ListOfTracks | null | undefinedtracks === var undefinedundefined) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Loading or access denied</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; if (const tracks: ListOfTracks | nulltracks === null) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Not found</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; // After const const tracks: ListOfTracks | null | undefinedtracks = useCoState<ListOfTracks, { $each: boolean; }>(Schema: CoValueClass<ListOfTracks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<...> | undefined; } | undefined): ListOfTracks | ... 1 more ... | undefineduseCoState(class ListOfTracksListOfTracks, id: ID<ListOfTracks>id, { resolve?: RefsToResolve<ListOfTracks, 10, []> | undefinedresolve: { $each: RefsToResolve<Track | undefined, 10, [0]>$each: true } }); if (const tracks: ListOfTrackstracks === var undefinedundefined) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Loading...</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; if (const tracks: ListOfTrackstracks === null) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Not found or access denied</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; // This will only show tracks that we have access to and that are loaded. return const tracks: ListOfTrackstracks.Array<co<Track | null | undefined>>.map<React.JSX.Element | null | undefined>(callbackfn: (value: co<Track | null | undefined>, index: number, array: co<Track | null | undefined>[]) => React.JSX.Element | null | undefined, thisArg?: any): (React.JSX.Element | ... 1 more ... | undefined)[]Calls a defined callback function on each element of an array, and returns an array that contains the results.@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.map(track: co<Track | null | undefined>track => track: co<Track | null | undefined>track && <function TrackComponent({ track }: { track: Track; }): stringTrackComponent track: Tracktrack={track: Track | (Track & CoMarker)track} />); } The same change is applied to the load function, so now it returns `null` instead of `undefined` when the value is missing. // Before // @ts-expect-error const const map: MyCoMap | nullmap = await class MyCoMapMyCoMap.CoMap.load<MyCoMap, true>(this: CoValueClass<...>, id: ID<MyCoMap>, options?: { resolve?: RefsToResolve<MyCoMap, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<MyCoMap>id); if (const map: MyCoMap | nullmap === var undefinedundefined) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Map not found"); } // After const const map: MyCoMap | nullmap = await class MyCoMapMyCoMap.CoMap.load<MyCoMap, true>(this: CoValueClass<...>, id: ID<MyCoMap>, options?: { resolve?: RefsToResolve<MyCoMap, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<MyCoMap>id); if (const map: MyCoMap | nullmap === null) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Map not found or access denied"); } ## New Features ### The `Resolved` type helper The new `Resolved` type can be used to define what kind of deeply loaded data you expect in your parameters, using the same resolve query syntax as the new loading APIs: type type PlaylistResolved = { tracks: Track[] & ListOfTracks; } & PlaylistPlaylistResolved = type Resolved<T, R extends RefsToResolve<T> | undefined> = R extends boolean | undefined ? T : [T] extends [(infer Item)[]] ? UnCo<Exclude<Item, null>> extends CoValue ? R extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue ...Resolved<class PlaylistPlaylist, { tracks: { $each: true; }tracks: { $each: true$each: true } }>; function function TrackListComponent({ playlist }: { playlist: PlaylistResolved; }): React.JSX.Element[]TrackListComponent({ playlist: { tracks: Track[] & ListOfTracks; } & Playlistplaylist }: { playlist: { tracks: Track[] & ListOfTracks; } & Playlistplaylist: type PlaylistResolved = { tracks: Track[] & ListOfTracks; } & PlaylistPlaylistResolved }) { // Safe access to resolved tracks return playlist: { tracks: Track[] & ListOfTracks; } & Playlistplaylist.Playlist.tracks: Track[] & ListOfTracks & co<ListOfTracks | null>tracks.Array<T>.map<React.JSX.Element>(callbackfn: (value: Track, index: number, array: Track[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[] (+1 overload)Calls a defined callback function on each element of an array, and returns an array that contains the results.@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.map(track: Tracktrack => <function TrackComponent({ track }: { track: Track; }): stringTrackComponent track: Tracktrack={track: Tracktrack} />); } --- ## Page: https://jazz.tools/docs/react-native/upgrade/0-11-0 Jazz 0.11.0 brings several improvements to member handling, roles, and permissions management. This guide will help you upgrade your application to the latest version. ## What's new? Here is what's changed in this release: * New permissions check APIs: New methods like `canRead`, `canWrite`, `canAdmin`, and `getRoleOf` to simplify permission checks. * Group.revokeExtend: New method to revoke group extension permissions. * Group.getParentGroups: New method to get all the parent groups of an account. * Account Profile & Migrations: Fixed issues with custom account profile migrations for a more consistent experience * Dropped support for Accounts owning Profiles: Profiles can now only be owned by Groups. * Group.members now includes inherited members: Updated behavior for the `members` getter method to include inherited members and have a more intuitive type definition. ## New Features ### New permissions check APIs New methods have been added to both `Account` and `Group` classes to improve permission handling: #### Permission checks The new `canRead`, `canWrite` and `canAdmin` methods on `Account` allow you to easily check if the account has specific permissions on a CoValue: const me = Account.getMe(); if (me.canAdmin(value)) { console.log("I can share value with others"); } else if (me.canWrite(value)) { console.log("I can edit value"); } else if (me.canRead(value)) { console.log("I can view value"); } else { console.log("I cannot access value"); } #### Getting the role of an account The `getRoleOf` method has been added to query the role of specific entities: const group = Group.create(); group.getRoleOf(me); // admin group.getRoleOf(Eve); // undefined group.addMember(Eve, "writer"); group.getRoleOf(Eve); // writer #### Group.revokeExtend We added a new method to revoke the extend of a Group: function addTrackToPlaylist(playlist: Playlist, track: MusicTrack) { const trackGroup = track._owner.castAs(Group); trackGroup.extend(playlist._owner, "reader"); // Grant read access to the track to the playlist accounts playlist.tracks.push(track); } function removeTrackFromPlaylist(playlist: Playlist, track: MusicTrack) { const trackGroup = track._owner.castAs(Group); trackGroup.revokeExtend(playlist._owner); // Revoke access to the track to the playlist accounts const index = playlist.tracks.findIndex(t => t.id === track.id); if (index !== -1) { playlist.tracks.splice(index, 1); } } ### Group.getParentGroups The `getParentGroups` method has been added to `Group` to get all the parent groups of a group. const childGroup = Group.create(); const parentGroup = Group.create(); childGroup.extend(parentGroup); console.log(childGroup.getParentGroups()); // [parentGroup] ## Breaking Changes ### Account Profile & Migrations The previous way of making the `Profile` migration work was to assume that the profile was always already there: export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps: { name: string, lastName: string }) { if (creationProps) { const { profile } = await this.ensureLoaded({ profile: {} }); profile.name = creationProps.name; profile.bookmarks = ListOfBookmarks.create([], profileGroup); } } } This was kind-of tricky to picture, and having different migration strategies for different CoValues was confusing. We changed the logic so the default profile is created only if you didn't provide one in your migration. This way you can use the same pattern for both `root` and `profile` migrations: export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps?: { name: string }) { if (this.profile === undefined) { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); this.profile = MyAppProfile.create({ name: creationProps?.name, bookmarks: ListOfBookmarks.create([], profileGroup), }, profileGroup); } } } Warning If you provide a custom `Profile` in your `Account` schema and migration for a Worker account, make sure to also add `everyone` as member with `reader` role to the owning group. Failing to do so will prevent any account from sending messages to the Worker's Inbox. ### Dropped support for Accounts owning Profiles Starting from `0.11.0` `Profile`s can only be owned by `Group`s. Note Existing profiles owned by `Account`s will still work, but you will get incorrect types when accessing a `Profile`'s `_owner`. ### Member Inheritance Changes The behavior of groups' `members` getter method has been updated to return both direct members and inherited ones from ancestor groups. This might affect your application if you were relying on only direct members being returned. /** * The following pseudocode only illustrates the inheritance logic, * the actual implementation is different. */ const parentGroup = Group.create(); parentGroup.addMember(John, "admin"); const childGroup = Group.create(); childGroup.addMember(Eve, "admin"); childGroup.extend(parentGroup); console.log(childGroup.members); // Before 0.11.0 // [Eve] // After 0.11.0 // [Eve, John] Additionally: * now `Group.members` doesn't include the `everyone` member anymore * the account type in `Group.members` is now the globally registered Account schema and we have removed the `co.members` way to define an AccountSchema for members If you need to explicitly check if "everyone" is a member of a group, you can use the `getRoleOf` method instead: if (group.getRoleOf("everyone")) { console.log("Everyone has access to the group"); } #### Migration Steps 1. Review your member querying logic to account for inherited members. 2. Update your permission checking code to utilize the new `hasPermissions` and `getRoleOf` methods. 3. Consider implementing `"everyone"` role checks where appropriate. ### Removed auto-update of `profile.name` in `usePasskeyAuth` The `usePasskeyAuth` hook now doesn't update the `profile.name` if the provided username is empty. ## Troubleshooting > I'm getting the following error: `Error: Profile must be owned by a Group` If you previously forced a migration of your `Account` schema to include a custom `Profile`, and assigned its ownership to an `Account`, you need to recreate your profile code and assign it to a `Group` instead. export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); override async migrate() { // ... const me = await this.ensureLoaded({ profile: {}, }); if ((me.profile._owner as Group | Account)._type === "Account") { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); // recreate your profile here... me.profile = Profile.create( { name: me.profile.name, }, profileGroup, ); } } } --- ## Page: https://jazz.tools/docs/react-native/schemas/covalues **CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app. As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.** **Think of CoValues as "super-fast Git for lots of tiny data."** * CoValues keep their full edit histories, from which they derive their "current state". * The fact that this happens in an eventually-consistent way makes them CRDTs. * Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's edit metadata. CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams. ## Start your app with a schema Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app. This helps correctness and development speed, but is particularly important... * when you evolve your app and need migrations * when different clients and server workers collaborate on CoValues and need to make compatible changes Thinking about the shape of your data is also a great first step to model your app. Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other. Jazz makes it quick to declare schemas, since they are simple TypeScript classes: export class TodoProject extends CoMap { title = co.string; tasks = co.ref(ListOfTasks); } Here you can see how we extend a CoValue type and use `co` for declaring (collaboratively) editable fields. This means that schema info is available for type inference _and_ at runtime. Classes might look old-fashioned, but Jazz makes use of them being both types and values in TypeScript, letting you refer to either with a single definition and import. import { TodoProject, ListOfTasks } from "./schema"; const project: TodoProject = TodoProject.create( { title: "New Project", tasks: ListOfTasks.create([], Group.create()), }, Group.create() ); ## Types of CoValues ### `CoMap` (declaration) CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects. (Collaborative editing follows a last-write-wins strategy per-key.) You can either declare struct-like CoMaps: class Person extends CoMap { name = co.string; age = co.number; pet = co.optional.ref(Pet); } Or record-like CoMaps (key-value pairs, where keys are always `string`): class ColorToHex extends CoMap.Record(co.string) {} class ColorToFruit extends CoMap.Record(co.ref(Fruit)) {} See the corresponding sections for creating, subscribing/loading, reading from and writing to CoMaps. ### `CoList` (declaration) CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.) You define them by specifying the type of the items they contain: class ListOfColors extends CoList.Of(co.string) {} class ListOfTasks extends CoList.Of(co.ref(Task)) {} See the corresponding sections for creating, subscribing/loading, reading from and writing to CoLists. ### `CoFeed` (declaration) CoFeeds are a special CoValue type that represent a feed of values for a set of users / sessions. (Each session of a user gets its own append-only feed.) They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc. You define them by specifying the type of feed item: class FeedOfTasks extends CoFeed.Of(co.ref(Task)) {} See the corresponding sections for creating, subscribing/loading, reading from and writing to CoFeeds. ### `FileStream` (declaration) FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.) They allow you to upload and reference files, images, etc. You typically don't need to declare or extend them yourself, you simply refer to the built-in `FileStream` from another CoValue: import { FileStream } from "jazz-tools"; class UserProfile extends CoMap { name = co.string; avatar = co.ref(FileStream); } See the corresponding sections for creating, subscribing/loading, reading from and writing to FileStreams. ### `SchemaUnion` (declaration) SchemaUnion is a helper type that allows you to load and refer to multiple subclasses of a CoMap schema, distinguished by a discriminating field. You declare them with a base class type and discriminating lambda, in which you have access to the `RawCoMap`, on which you can call `get` with the field name to get the discriminating value. import { SchemaUnion, CoMap } from "jazz-tools"; class BaseWidget extends CoMap { type = co.string; } class ButtonWidget extends BaseWidget { type = co.literal("button"); label = co.string; } class SliderWidget extends BaseWidget { type = co.literal("slider"); min = co.number; max = co.number; } const WidgetUnion = SchemaUnion.Of<BaseWidget>((raw) => { switch (raw.get("type")) { case "button": return ButtonWidget; case "slider": return SliderWidget; default: throw new Error("Unknown widget type"); } }); See the corresponding sections for creating, subscribing/loading and narrowing SchemaUnions. ## CoValue field/item types Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain. ### Primitive fields You can declare primitive field types using the `co` declarer: import { co } from "jazz-tools"; export class Person extends CoMap { title = co.string; } export class ListOfColors extends CoList.Of(co.string) {} Here's a quick overview of the primitive types you can use: co.string; co.number; co.boolean; co.null; co.Date; co.literal("waiting", "ready"); Finally, for more complex JSON data, that you _don't want to be collaborative internally_ (but only ever update as a whole), you can use `co.json<T>()`: co.json<{ name: string }>(); For more detail, see the API Reference for the `co` field declarer. ### Refs to other CoValues To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other. Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert. The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in Subscribing and Deep Loading. In Schemas, you declare Refs using the `co.ref<T>()` declarer: class Company extends CoMap { members = co.ref(ListOfPeople); } class ListOfPeople extends CoList.Of(co.ref(Person)) {} #### Optional Refs ⚠️ If you want to make a referenced CoValue field optional, you _have to_ use `co.optional.ref<T>()`: ⚠️ class Person extends CoMap { pet = co.optional.ref(Pet); } ### Computed fields & methods Since CoValue schemas are based on classes, you can easily add computed fields and methods: class Person extends CoMap { firstName = co.string; lastName = co.string; dateOfBirth = co.Date; get name() { return `${this.firstName} ${this.lastName}`; } ageAsOf(date: Date) { return differenceInYears(date, this.dateOfBirth); } } --- ## Page: https://jazz.tools/docs/react-native/schemas/accounts-and-migrations ## CoValues as a graph of data rooted in accounts Compared to traditional relational databases with tables and foreign keys, Jazz is more like a graph database, or GraphQL APIs — where CoValues can arbitrarily refer to each other and you can resolve references without having to do a join. (See Subscribing & deep loading). To find all data related to a user, the account acts as a root node from where you can resolve all the data they have access to. These root references are modeled explicitly in your schema, distinguishing between data that is typically public (like a user's profile) and data that is private (like their messages). ### `Account.root` - private data a user cares about Every Jazz app that wants to refer to per-user data needs to define a custom root `CoMap` schema and declare it in a custom `Account` schema as the `root` field: import { Account, CoMap } from "jazz-tools"; export class MyAppAccount extends Account { root = co.ref(MyAppRoot); } export class MyAppRoot extends CoMap { myChats = co.ref(ListOfChats); myContacts = co.ref(ListOfAccounts); } // Register the Account schema so `useAccount` returns our custom `MyAppAccount` declare module "jazz-react" { interface Register { Account: MyAppAccount; } } ### `Account.profile` - public data associated with a user The built-in `Account` schema class comes with a default `profile` field, which is a CoMap (in a Group with `"everyone": "reader"` - so publicly readable permissions) that is set up for you based on the username the `AuthMethod` provides on account creation. Their pre-defined schemas roughly look like this: // ...somehwere in jazz-tools itself... export class Account extends Group { profile = co.ref(Profile); } export class Profile extends CoMap { name = co.string; } If you want to keep the default `Profile` schema, but customise your account's private `root`, all you have to do is define a new `root` field in your account schema: (You don't have to explicitly re-define the `profile` field, but it makes it more readable that the Account contains both `profile` and `root`) import { Account, Profile } from "jazz-tools"; export class MyAppAccount extends Account { profile = co.ref(Profile); root = co.ref(MyAppRoot); } If you want to extend the `profile` to contain additional fields (such as an avatar `ImageDefinition`), you can declare your own profile schema class that extends `Profile`: import { Account, Profile, ImageDefinition } from "jazz-tools"; export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); root = co.ref(MyAppRoot); } export class MyAppRoot extends CoMap { myChats = co.ref(ListOfChats); myContacts = co.ref(ListOfAccounts); } export class MyAppProfile extends Profile { name = co.string; // compatible with default Profile schema avatar = co.optional.ref(ImageDefinition); } // Register the Account schema so `useAccount` returns our custom `MyAppAccount` declare module "jazz-react" { interface Register { Account: MyAppAccount; } } ## Resolving CoValues starting at `profile` or `root` ## Populating and evolving `root` and `profile` schemas with migrations As you develop your app, you'll likely want to * initialise data in a user's `root` and `profile` * add more data to your `root` and `profile` schemas You can achieve both by overriding the `migrate()` method on your `Account` schema class. ### When migrations run Migrations are run after account creation and every time a user logs in. Jazz waits for the migration to finish before passing the account to your app's context. ### Initialising user data after account creation export class MyAppAccount extends Account { root = co.ref(MyAppRoot); profile = co.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps?: { name: string }) { // we specifically need to check for undefined, // because the root might simply be not loaded (`null`) yet if (this.root === undefined) { this.root = MyAppRoot.create({ // Using a group to set the owner is always a good idea. // This way if in the future we want to share // this coValue we can do so easily. myChats: ListOfChats.create([], Group.create()), myContacts: ListOfAccounts.create([], Group.create()) }); } if (this.profile === undefined) { const profileGroup = Group.create(); // Unlike the root, we want the profile to be publicly readable. profileGroup.addMember("everyone", "reader"); this.profile = MyAppProfile.create({ name: creationProps?.name, bookmarks: ListOfBookmarks.create([], profileGroup), }, profileGroup); } } } ### Adding/changing fields to `root` and `profile` To add new fields to your `root` or `profile` schemas, amend their corresponding schema classes with new fields, and then implement a migration that will populate the new fields for existing users (by using initial data, or by using existing data from old fields). To do deeply nested migrations, you might need to use the asynchronous `ensureLoaded()` method before determining whether the field already exists, or is simply not loaded yet. Now let's say we want to add a `myBookmarks` field to the `root` schema: export class MyAppAccount extends Account { root = co.ref(MyAppRoot); async migrate(this: MyAppAccount) { if (this.root === undefined) { this.root = MyAppRoot.create({ myChats: ListOfChats.create([], Group.create()), myContacts: ListOfAccounts.create([], Group.create()) }); } // We need to load the root field to check for the myContacts field const { root } = await this.ensureLoaded({ root: {}, }); // we specifically need to check for undefined, // because myBookmarks might simply be not loaded (`null`) yet if (root.myBookmarks === undefined) { root.myBookmarks = ListOfBookmarks.create([], Group.create()); } } } --- ## Page: https://jazz.tools/docs/react-native/using-covalues/comaps CoMaps are key-value objects that work like JavaScript objects. You can access properties with dot notation and define typed fields that provide TypeScript safety. They're ideal for structured data that needs type validation. ## Creating CoMaps CoMaps are typically defined by extending the `CoMap` class and specifying primitive fields using the `co` declarer (see Defining schemas: CoValues for more details on primitive fields): class Project extends CoMap { name = co.string; startDate = co.Date; status = co.literal("planning", "active", "completed"); coordinator = co.optional.ref(Member); } You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs: class Inventory extends CoMap.Record(co.number) {} To instantiate a CoMap: const project = Project.create({ name: "Spring Planting", startDate: new Date("2025-03-15"), status: "planning", }); const inventory = Inventory.create({ tomatoes: 48, basil: 12, }); ### Ownership When creating CoMaps, you can specify ownership to control access: // Create with default owner (current user) const privateProject = Project.create({ name: "My Herb Garden", startDate: new Date("2025-04-01"), status: "planning", }); // Create with shared ownership const gardenGroup = Group.create(); gardenGroup.addMember(memberAccount, "writer"); const communityProject = Project.create( { name: "Community Vegetable Plot", startDate: new Date("2025-03-20"), status: "planning", }, { owner: gardenGroup }, ); ## Reading from CoMaps CoMaps can be accessed using familiar JavaScript object notation: console.log(project.name); // "Spring Planting" console.log(project.status); // "planning" ### Handling Optional Fields Optional fields require checks before access: if (project.coordinator) { console.log(project.coordinator.name); // Safe access } ### Working with Record CoMaps For record-type CoMaps, you can access values using bracket notation: const inventory = Inventory.create({ tomatoes: 48, peppers: 24, basil: 12 }); console.log(inventory["tomatoes"]); // 48 ## Updating CoMaps Updating CoMap properties uses standard JavaScript assignment: project.name = "Spring Vegetable Garden"; // Update name project.startDate = new Date("2025-03-20"); // Update date ### Type Safety CoMaps are fully typed in TypeScript, giving you autocomplete and error checking: project.name = "Spring Vegetable Planting"; // ✓ Valid string project.startDate = "2025-03-15"; // ✗ Type error: expected Date ### Deleting Properties You can delete properties from CoMaps: delete inventory["basil"]; // Remove a key-value pair // For optional fields in struct-like CoMaps project.coordinator = null; // Remove the reference ## Best Practices ### Structuring Data * Use struct-like CoMaps for entities with fixed, known properties * Use record-like CoMaps for dynamic key-value collections * Group related properties into nested CoMaps for better organization ### Common Patterns #### Using Computed Properties CoMaps support computed properties and methods: class ComputedProject extends CoMap { name = co.string; startDate = co.Date; endDate = co.optional.Date; get isActive() { const now = new Date(); return now >= this.startDate && (!this.endDate || now <= this.endDate); } formatDuration(format: "short" | "full") { const start = this.startDate.toLocaleDateString(); if (!this.endDate) { return format === "full" ? `Started on ${start}, ongoing` : `From ${start}`; } const end = this.endDate.toLocaleDateString(); return format === "full" ? `From ${start} to ${end}` : `${(this.endDate.getTime() - this.startDate.getTime()) / 86400000} days`; } } // ... console.log(computedProject.isActive); // false console.log(computedProject.formatDuration("short")); // "3 days" --- ## Page: https://jazz.tools/docs/react-native/using-covalues/colists CoLists are ordered collections that work like JavaScript arrays. They provide indexed access, iteration methods, and length properties, making them perfect for managing sequences of items. ## Creating CoLists CoLists are defined by specifying the type of items they contain: class ListOfResources extends CoList.Of(co.string) {} class ListOfTasks extends CoList.Of(co.ref(Task)) {} To create a `CoList`: // Create an empty list const resources = ListOfResources.create([]); // Create a list with initial items const tasks = ListOfTasks.create([ Task.create({ title: "Prepare soil beds", status: "in-progress" }), Task.create({ title: "Order compost", status: "todo" }) ]); Like other CoValues, you can specify ownership when creating CoLists. ## Reading from CoLists CoLists support standard array access patterns: // Access by index const firstTask = tasks[0]; console.log(firstTask.title); // "Prepare soil beds" // Get list length console.log(tasks.length); // 2 // Iteration tasks.forEach(task => { console.log(task.title); // "Prepare soil beds" // "Order compost" }); // Array methods const todoTasks = tasks.filter(task => task.status === "todo"); console.log(todoTasks.length); // 1 ## Updating CoLists Update `CoList`s just like you would JavaScript arrays: // Add items resources.push("Tomatoes"); // Add to end resources.unshift("Lettuce"); // Add to beginning tasks.push(Task.create({ // Add complex items title: "Install irrigation", status: "todo" })); // Replace items resources[0] = "Cucumber"; // Replace by index // Modify nested items tasks[0].status = "complete"; // Update properties of references ### Deleting Items Remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`: // Remove 2 items starting at index 1 resources.splice(1, 2); console.log(resources); // ["Cucumber", "Peppers"] // Remove a single item at index 0 resources.splice(0, 1); console.log(resources); // ["Peppers"] // Remove items const lastItem = resources.pop(); // Remove and return last item resources.shift(); // Remove first item ### Array Methods `CoList`s support the standard JavaScript array methods you already know: // Add multiple items at once resources.push("Tomatoes", "Basil", "Peppers"); // Find items const basil = resources.find(r => r === "Basil"); // Filter (returns regular array, not a CoList) const tItems = resources.filter(r => r.startsWith("T")); console.log(tItems); // ["Tomatoes"] // Sort (modifies the CoList in-place) resources.sort(); console.log(resources); // ["Basil", "Peppers", "Tomatoes"] ### Type Safety CoLists maintain type safety for their items: // TypeScript catches type errors resources.push("Carrots"); // ✓ Valid string resources.push(42); // ✗ Type error: expected string // For lists of references tasks.forEach(task => { console.log(task.title); // TypeScript knows task has title }); ## Best Practices ### Common Patterns #### List Rendering CoLists work well with UI rendering libraries: // React example function TaskList({ tasks }) { return ( <ul> {tasks.map(task => ( <li key={task.id}> {task.title} - {task.status} </li> ))} </ul> ); } #### Managing Relations CoLists can be used to create one-to-many relationships: class Project extends CoMap { name = co.string; tasks = co.ref(ListOfTasks); } // ... const task = Task.create({ title: "Plant seedlings", status: "todo", project: project, // Add a reference to the project }); // Add a task to a garden project project.tasks.push(task); // Access the project from the task console.log(task.project); // { name: "Garden Project", tasks: [task] } --- ## Page: https://jazz.tools/docs/react-native/using-covalues/cofeeds CoFeeds are append-only data structures that track entries from different user sessions and accounts. Unlike other CoValues where everyone edits the same data, CoFeeds maintain separate streams for each session. Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems. The following examples demonstrate a practical use of CoFeeds: * Multi-cursors - track user presence on a canvas with multiple cursors and out of bounds indicators * Reactions - store per-user emoji reaction using a CoFeed ## Creating CoFeeds CoFeeds are defined by specifying the type of items they'll contain, similar to how you define CoLists: // Define a schema for feed items class Activity extends CoMap { timestamp = co.Date; action = co.literal("watering", "planting", "harvesting", "maintenance"); notes = co.optional.string; } // Define a feed of garden activities class ActivityFeed extends CoFeed.Of(co.ref(Activity)) {} // Create a feed instance const activityFeed = ActivityFeed.create([]); Like other CoValues, you can specify ownership when creating CoFeeds. ## Reading from CoFeeds Since CoFeeds are made of entries from users over multiple sessions, you can access entries in different ways - from a specific user's session or from their account as a whole. ### Per-Session Access To retrieve entries from a session: // Get the feed for a specific session const sessionFeed = activityFeed.perSession[sessionId]; // Latest entry from a session console.log(sessionFeed.value.action); // "watering" For convenience, you can also access the latest entry from the current session with `inCurrentSession`: // Get the feed for the current session const currentSessionFeed = activityFeed.inCurrentSession; // Latest entry from the current session console.log(currentSessionFeed.value.action); // "harvesting" ### Per-Account Access To retrieve entries from a specific account you can use bracket notation with the account ID: // Get the feed for a specific account const accountFeed = activityFeed[accountId]; // Latest entry from the account console.log(accountFeed.value.action); // "watering" For convenience, you can also access the latest entry from the current account with `byMe`: // Get the feed for the current account const myLatestEntry = activityFeed.byMe; // Latest entry from the current account console.log(myLatestEntry.value.action); // "harvesting" ### Feed Entries #### All Entries To retrieve all entries from a CoFeed: // Get the feeds for a specific account and session const accountFeed = activityFeed[accountId]; const sessionFeed = activityFeed.perSession[sessionId]; // Iterate over all entries from the account for (const entry of accountFeed.all) { console.log(entry.value); } // Iterate over all entries from the session for (const entry of sessionFeed.all) { console.log(entry.value); } #### Latest Entry To retrieve the latest entry from a CoFeed, ie. the last update: // Get the latest entry from the current account const latestEntry = activityFeed.byMe; console.log(`My last action was ${latestEntry.value.action}`); // "My last action was harvesting" // Get the latest entry from each account const latestEntriesByAccount = Object.values(activityFeed).map(entry => ({ accountName: entry.by?.profile?.name, value: entry.value, })); ## Writing to CoFeeds CoFeeds are append-only; you can add new items, but not modify existing ones. This creates a chronological record of events or activities. ### Adding Items // Log a new activity activityFeed.push(Activity.create({ timestamp: new Date(), action: "watering", notes: "Extra water for new seedlings" })); Each item is automatically associated with the current user's session. You don't need to specify which session the item belongs to - Jazz handles this automatically. ### Understanding Session Context Each entry is automatically added to the current session's feed. When a user has multiple open sessions (like both a mobile app and web browser), each session creates its own separate entries: // On mobile device: fromMobileFeed.push(Activity.create({ timestamp: new Date(), action: "harvesting", location: "Vegetable patch" })); // On web browser (same user): fromBrowserFeed.push(Activity.create({ timestamp: new Date(), action: "planting", location: "Flower bed" })); // These are separate entries in the same feed, from the same account ## Metadata CoFeeds support metadata, which is useful for tracking information about the feed itself. ### By The `by` property is the account that made the entry. const accountFeed = activityFeed[accountId]; // Get the account that made the last entry console.log(accountFeed?.by); ### MadeAt The `madeAt` property is a timestamp of when the entry was added to the feed. const accountFeed = activityFeed[accountId]; // Get the timestamp of the last update console.log(accountFeed?.madeAt); // Get the timestamp of each entry for (const entry of accountFeed.all) { console.log(entry.madeAt); } ## Best Practices ### When to Use CoFeeds * **Use CoFeeds when**: * You need to track per-user/per-session data * Time-based information matters (activity logs, presence) * **Consider alternatives when**: * Data needs to be collaboratively edited (use CoMaps or CoLists) * You need structured relationships (use CoMaps/CoLists with references) --- ## Page: https://jazz.tools/docs/react-native/using-covalues/filestreams FileStreams handle binary data in Jazz applications - think documents, audio files, and other non-text content. They're essentially collaborative versions of `Blob`s that sync automatically across devices. Use FileStreams when you need to: * Distribute documents across devices * Store audio or video files * Sync any binary data between users **Note:** For images specifically, Jazz provides the higher-level `ImageDefinition` abstraction which manages multiple image resolutions - see the ImageDefinition documentation for details. FileStreams provide automatic chunking when using the `createFromBlob` method, track upload progress, and handle MIME types and metadata. In your schema, reference FileStreams like any other CoValue: import { CoMap, FileStream, co } from "jazz-tools"; class Document extends CoMap { title = co.string; file = co.ref(FileStream); // Store a document file } ## Creating FileStreams There are two main ways to create FileStreams: creating empty ones for manual data population or creating directly from existing files or blobs. ### Creating from Blobs and Files For files from input elements or drag-and-drop interfaces, use `createFromBlob`: // From a file input const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', async () => { const file = fileInput.files[0]; if (file) { // Create FileStream from user-selected file const fileStream = await FileStream.createFromBlob(file); // Or with progress tracking for better UX const fileWithProgress = await FileStream.createFromBlob(file, { onProgress: (progress) => { // progress is a value between 0 and 1 const percent = Math.round(progress * 100); console.log(`Upload progress: ${percent}%`); progressBar.style.width = `${percent}%`; } }); } }); ### Creating Empty FileStreams Create an empty FileStream when you want to manually add binary data in chunks: import { FileStream } from "jazz-tools"; // Create a new empty FileStream const fileStream = FileStream.create(); ## Reading from FileStreams `FileStream`s provide several ways to access their binary content, from raw chunks to convenient Blob objects. ### Getting Raw Data Chunks To access the raw binary data and metadata: // Get all chunks and metadata const fileData = fileStream.getChunks(); if (fileData) { console.log(`MIME type: ${fileData.mimeType}`); console.log(`Total size: ${fileData.totalSizeBytes} bytes`); console.log(`File name: ${fileData.fileName}`); console.log(`Is complete: ${fileData.finished}`); // Access raw binary chunks for (const chunk of fileData.chunks) { // Each chunk is a Uint8Array console.log(`Chunk size: ${chunk.length} bytes`); } } By default, `getChunks()` only returns data for completely synced `FileStream`s. To start using chunks from a `FileStream` that's currently still being synced use the `allowUnfinished` option: // Get data even if the stream isn't complete const partialData = fileStream.getChunks({ allowUnfinished: true }); ### Converting to Blobs For easier integration with web APIs, convert to a `Blob`: // Convert to a Blob const blob = fileStream.toBlob(); if (blob) { // Use with URL.createObjectURL const url = URL.createObjectURL(blob); // Create a download link const link = document.createElement('a'); link.href = url; link.download = fileData?.fileName || 'document.pdf'; link.click(); // Clean up when done URL.revokeObjectURL(url); } ### Loading FileStreams as Blobs You can directly load a `FileStream` as a `Blob` when you only have its ID: // Load directly as a Blob when you have an ID const blob = await FileStream.loadAsBlob(fileStreamId); // By default, waits for complete uploads // For in-progress uploads: const partialBlob = await FileStream.loadAsBlob(fileStreamId, { allowUnfinished: true }); ### Checking Completion Status Check if a `FileStream` is fully synced: if (fileStream.isBinaryStreamEnded()) { console.log('File is completely synced'); } else { console.log('File upload is still in progress'); } ## Writing to FileStreams When creating a `FileStream` manually (not using `createFromBlob`), you need to manage the upload process yourself. This gives you more control over chunking and progress tracking. ### The Upload Lifecycle `FileStream` uploads follow a three-stage process: 1. **Start** - Initialize with metadata 2. **Push** - Send one or more chunks of data 3. **End** - Mark the stream as complete ### Starting a `FileStream` Begin by providing metadata about the file: // Create an empty FileStream const fileStream = FileStream.create(); // Initialize with metadata fileStream.start({ mimeType: 'application/pdf', // MIME type (required) totalSizeBytes: 1024 * 1024 * 2, // Size in bytes (if known) fileName: 'document.pdf' // Original filename (optional) }); ### Pushing Data Add binary data in chunks - this helps with large files and progress tracking: // Create a sample Uint8Array (in real apps, this would be file data) const data = new Uint8Array([...]); // For large files, break into chunks (e.g., 100KB each) const chunkSize = 1024 * 100; for (let i = 0; i < data.length; i += chunkSize) { // Create a slice of the data const chunk = data.slice(i, i + chunkSize); // Push chunk to the FileStream fileStream.push(chunk); // Track progress const progress = Math.min(100, Math.round((i + chunk.length) * 100 / data.length)); console.log(`Upload progress: ${progress}%`); } ### Completing the Upload Once all chunks are pushed, mark the `FileStream` as complete: // Finalize the upload fileStream.end(); console.log('Upload complete!'); ## Subscribing to `FileStream`s Like other CoValues, you can subscribe to `FileStream`s to get notified of changes as they happen. This is especially useful for tracking upload progress when someone else is uploading a file. ### Loading by ID Load a `FileStream` when you have its ID: // Load a FileStream by ID const fileStream = await FileStream.load(fileStreamId, []); if (fileStream) { console.log('FileStream loaded successfully'); // Check if it's complete if (fileStream.isBinaryStreamEnded()) { // Process the completed file const blob = fileStream.toBlob(); } } ### Subscribing to Changes Subscribe to a `FileStream` to be notified when chunks are added or when the upload is complete: // Subscribe to a FileStream by ID const unsubscribe = FileStream.subscribe(fileStreamId, [], (fileStream) => { // Called whenever the FileStream changes console.log('FileStream updated'); // Get current status const chunks = fileStream.getChunks({ allowUnfinished: true }); if (chunks) { const uploadedBytes = chunks.chunks.reduce((sum, chunk) => sum + chunk.length, 0); const totalBytes = chunks.totalSizeBytes || 1; const progress = Math.min(100, Math.round(uploadedBytes * 100 / totalBytes)); console.log(`Upload progress: ${progress}%`); if (fileStream.isBinaryStreamEnded()) { console.log('Upload complete!'); // Now safe to use the file const blob = fileStream.toBlob(); // Clean up the subscription if we're done unsubscribe(); } } }); ### Waiting for Upload Completion If you need to wait for a `FileStream` to be fully synchronized across devices: // Wait for the FileStream to be fully synced await fileStream.waitForSync({ timeout: 5000 // Optional timeout in ms }); console.log('FileStream is now synced to all connected devices'); This is useful when you need to ensure that a file is available to other users before proceeding with an operation. --- ## Page: https://jazz.tools/docs/react-native/using-covalues/imagedef `ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz. It extends beyond basic file storage by supporting multiple resolutions of the same image, optimized for mobile devices. **Note**: This guide applies to both Expo and framework-less React Native implementations. The functionality described here is identical regardless of which implementation you're using, though you'll need to import from the appropriate package (`jazz-expo` or `jazz-react-native`). Jazz offers several tools to work with images in React Native: * `createImage()` - function to create an `ImageDefinition` from a base64 image data URI * `ProgressiveImg` - React component to display an image with progressive loading * `useProgressiveImg` - React hook to load an image in your own component For examples of use, see our example apps: * React Native Chat (Framework-less implementation) * React Native Expo Chat (Expo implementation) * React Native Expo Clerk Chat (Expo implementation with Clerk) ## Creating Images The easiest way to create and use images in your Jazz application is with the `createImage()` function: import { createImage } from "jazz-react-native-media-images"; import { launchImageLibrary } from 'react-native-image-picker'; async function handleImagePicker() { try { // Launch the image picker const result = await launchImageLibrary({ mediaType: 'photo', includeBase64: true, quality: 1, }); if (!result.canceled) { const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`; // Creates ImageDefinition with multiple resolutions automatically const image = await createImage(base64Uri, { owner: me.profile._owner, maxSize: 2048, // Optional: limit maximum resolution }); // Store the image me.profile.image = image; } } catch (error) { console.error("Error creating image:", error); } } The `createImage()` function: * Creates an `ImageDefinition` with the right properties * Generates a small placeholder for immediate display * Creates multiple resolution variants of your image * Returns the created `ImageDefinition` ### Configuration Options You can configure `createImage()` with additional options: // Configuration options const options = { owner: me, // Owner for access control maxSize: 1024 // Maximum resolution to generate }; // Setting maxSize controls which resolutions are generated: // 256: Only creates the smallest resolution (256px on longest side) // 1024: Creates 256px and 1024px resolutions // 2048: Creates 256px, 1024px, and 2048px resolutions // undefined: Creates all resolutions including the original size const image = await createImage(base64Uri, options); ## Displaying Images with `ProgressiveImg` For a complete progressive loading experience, use the `ProgressiveImg` component: import { ProgressiveImg } from "jazz-react-native"; import { Image, StyleSheet } from "react-native"; function GalleryView({ image }) { return ( <ProgressiveImg image={image} // The image definition to load targetWidth={800} // Looks for the best available resolution for a 800px image > {({ src }) => ( <Image source={{ uri: src }} style={styles.galleryImage} resizeMode="cover" /> )} </ProgressiveImg> ); } const styles = StyleSheet.create({ galleryImage: { width: '100%', height: 200, borderRadius: 8, } }); The `ProgressiveImg` component handles: * Showing a placeholder while loading * Automatically selecting the appropriate resolution * Progressive enhancement as higher resolutions become available * Cleaning up resources when unmounted ## Using `useProgressiveImg` Hook For more control over image loading, you can implement your own progressive image component: import { useProgressiveImg } from "jazz-react-native"; import { Image, View, Text, ActivityIndicator } from "react-native"; function CustomImageComponent({ image }) { const { src, // Data URI containing the image data as a base64 string, // or a placeholder image URI res, // The current resolution originalSize // The original size of the image } = useProgressiveImg({ image: image, // The image definition to load targetWidth: 800 // Limit to resolutions up to 800px wide }); // When image is not available yet if (!src) { return ( <View style={{ height: 200, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f0f0f0' }}> <ActivityIndicator size="small" color="#0000ff" /> <Text style={{ marginTop: 10 }}>Loading image...</Text> </View> ); } // When using placeholder if (res === "placeholder") { return ( <View style={{ position: 'relative' }}> <Image source={{ uri: src }} style={{ width: '100%', height: 200, opacity: 0.7 }} resizeMode="cover" /> <ActivityIndicator size="large" color="#ffffff" style={{ position: 'absolute', top: '50%', left: '50%', marginLeft: -20, marginTop: -20 }} /> </View> ); } // Full image display with custom overlay return ( <View style={{ position: 'relative', width: '100%', height: 200 }}> <Image source={{ uri: src }} style={{ width: '100%', height: '100%' }} resizeMode="cover" /> <View style={{ position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(0,0,0,0.5)', padding: 8 }}> <Text style={{ color: 'white' }}>Resolution: {res}</Text> </View> </View> ); } ## Understanding ImageDefinition Behind the scenes, `ImageDefinition` is a specialized CoValue that stores: * The original image dimensions (`originalSize`) * An optional placeholder (`placeholderDataURL`) for immediate display * Multiple resolution variants of the same image as `FileStream`s Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`). // Structure of an ImageDefinition const image = ImageDefinition.create({ originalSize: [1920, 1080], placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...", }); // Accessing the highest available resolution const highestRes = image.highestResAvailable(); if (highestRes) { console.log(`Found resolution: ${highestRes.res}`); console.log(`Stream: ${highestRes.stream}`); } For more details on using `ImageDefinition` directly, see the VanillaJS docs. ### Fallback Behavior `highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution. const image = ImageDefinition.create({ originalSize: [1920, 1080], }); image["1920x1080"] = FileStream.create(); // Empty image upload image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob); const highestRes = image.highestResAvailable(); console.log(highestRes.res); // 800x450 --- ## Page: https://jazz.tools/docs/react-native/using-covalues/schemaunions ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native/using-covalues/subscription-and-loading Jazz's Collaborative Values (such as CoMaps or CoLists) work like reactive state. By subscribing to them, you can react to both local and remote updates. This is the main way to consume data in your application. Subscriptions also take care of loading CoValues that are not yet loaded locally and can do so _deeply_ — by resolving nested CoValues. To make use of this, we'll show you how to specify the depth of data you need with resolve queries. With each update you can also handle loading states and inaccessible CoValues. ## Manual subscriptions You can subscribe to a CoValue from anywhere in your code (if you have its ID) by using `CoValue.subscribe()`. **Note:** Unless you're using vanilla JavaScript, this is only used outside of React components - for example in server-side code or in tests. See the section below for convenient subscription _hooks_ that you typically use in React. class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Task.title: co<string>title = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.description: co<string>description = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.status: co<"todo" | "in-progress" | "completed">status = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.literal<["todo", "in-progress", "completed"]>(_lit_0: "todo", _lit_1: "in-progress", _lit_2: "completed"): co<"todo" | "in-progress" | "completed">literal("todo", "in-progress", "completed"); Task.assignedTo: co<string | undefined>assignedTo = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }optional.string: co<string | undefined>string; } // ... // Subscribe to a Task by ID const const unsubscribe: () => voidunsubscribe = class TaskTask.CoMap.subscribe<Task, true>(this: CoValueClass<Task>, id: ID<Task>, listener: (value: Task, unsubscribe: () => void) => void): () => void (+1 overload)Load and subscribe to a `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`/`CoFeed`/`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.@example```ts const unsub = Person.subscribe( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} }, (person) => console.log(person) ); ```@categorySubscription & Loadingsubscribe(const taskId: ID<Task>taskId, (updatedTask: TaskupdatedTask) => { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task updated:", updatedTask: TaskupdatedTask.Task.title: co<string>title); var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("New status:", updatedTask: TaskupdatedTask.Task.status: co<"todo" | "in-progress" | "completed">status); }); // Clean up when you're done const unsubscribe: () => voidunsubscribe(); If you already have a CoValue instance, you can subscribe to it by calling its `subscribe` method. const const task: Tasktask = class TaskTask.CoMap.create<Task>(this: CoValueClass<...>, init: { title: co<string> & (co<string> | undefined); description: co<string> & (co<string> | undefined); status: co<"todo" | "in-progress" | "completed"> & (co<...> | undefined); assignedTo?: string | ... 2 more ... | undefined; }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): TaskCreate a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.@example```ts const person = Person.create({ name: "Alice", age: 42, pet: cat, }, { owner: friendGroup }); ```@categoryCreationcreate({ title: co<string> & (co<string> | undefined)title: "Cut the grass", ...const otherProps: anyotherProps }); const const unsubscribe: () => voidunsubscribe = const task: Tasktask.CoMap.subscribe<Task, true>(this: Task, listener: (value: Task, unsubscribe: () => void) => void): () => void (+1 overload)Given an already loaded `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.@categorySubscription & Loadingsubscribe((updatedTask: TaskupdatedTask) => { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task updated:", updatedTask: TaskupdatedTask.Task.title: co<string>title); }); // Clean up when you're done const unsubscribe: () => voidunsubscribe(); ## Subscription hooks ### `useCoState` Jazz provides a `useCoState` hook that provides a convenient way to subscribe to CoValues and handle loading states: import { function useCoState<V extends CoValue, const R extends RefsToResolve<V> = true>(Schema: CoValueClass<V>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolveStrict<V, R>; }): Resolved<V, R> | undefined | nulluseCoState } from "jazz-react"; function function GardenPlanner({ projectId }: { projectId: ID<Project>; }): "Project not found or not accessible" | "Loading project ..." | React.JSX.ElementGardenPlanner({ projectId: ID<Project>projectId }: { projectId: ID<Project>projectId: type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class ProjectProject> }) { // Subscribe to a project and its tasks const const project: ({ tasks: Task[] & ListOfTasks; } & Project) | null | undefinedproject = useCoState<Project, { tasks: { $each: boolean; }; }>(Schema: CoValueClass<Project>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; } | undefined): ({ ...; } & Project) | ... 1 more ... | undefineduseCoState(class ProjectProject, projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true }, }, }); if (!const project: ({ tasks: Task[] & ListOfTasks; } & Project) | null | undefinedproject) { return const project: null | undefinedproject === null ? "Project not found or not accessible" : "Loading project ..."; } return ( <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> <JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const project: { tasks: Task[] & ListOfTasks; } & Projectproject.Project.name: co<string>name}</JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> <function TaskList({ tasks }: { tasks: Task[]; }): React.JSX.ElementTaskList tasks: Task[]tasks={const project: { tasks: Task[] & ListOfTasks; } & Projectproject.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks} /> </JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ); } function function TaskList({ tasks }: { tasks: Task[]; }): React.JSX.ElementTaskList({ tasks: Task[]tasks }: { tasks: Task[]tasks: class TaskTask[] }) { return ( <JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> {tasks: Task[]tasks.Array<Task>.map<React.JSX.Element>(callbackfn: (value: Task, index: number, array: Task[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[]Calls a defined callback function on each element of an array, and returns an array that contains the results.@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.map((task: Tasktask) => ( <JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li React.Attributes.key?: React.Key | null | undefinedkey={task: Tasktask.CoMap.id: ID<Task>The ID of this `CoMap`@categoryContentid}> <JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>span>{task: Tasktask.Task.title: co<string>title}</JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>span> <JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>span>{task: Tasktask.Task.status: co<"todo" | "in-progress" | "completed">status}</JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>span> </JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li> ))} </JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> ); } The `useCoState` hook handles subscribing when the component mounts and unsubscribing when it unmounts, making it easy to keep your UI in sync with the underlying data. ### `useAccount` `useAccount` is used to access the current user's account. You can use this at the top-level of your app to subscribe to the current user's account profile and root. Like `useCoState`, you can specify a resolve query to also subscribe to CoValues referenced in the account profile or root. import { function useAccount<A extends RegisteredAccount>(): { me: A; logOut: () => void; } (+1 overload)useAccount } from "jazz-react"; function function ProjectList(): React.JSX.ElementProjectList() { const { const me: ({ profile: Profile; root: { myProjects: (Project & { tasks: ListOfTasks; })[] & ListOfProjects; } & AccountRoot; } & MyAppAccount) | null | undefinedme } = useAccount<MyAppAccount, { profile: true; root: { myProjects: { $each: { tasks: true; }; }; }; }>(options?: { resolve?: RefsToResolve<MyAppAccount, 10, []> | undefined; } | undefined): { ...; } (+1 overload)useAccount({ resolve?: RefsToResolve<MyAppAccount, 10, []> | undefinedresolve: { profile?: RefsToResolve<Profile, 10, [0]> | undefinedprofile: true, root?: RefsToResolve<AccountRoot, 10, [0]> | undefinedroot: { myProjects?: RefsToResolve<ListOfProjects, 10, [0, 0]> | undefinedmyProjects: { $each: RefsToResolve<Project, 10, [0, 0, 0]>$each: { tasks?: RefsToResolve<ListOfTasks, 10, [0, 0, 0, 0]> | undefinedtasks: true } } }, }, }); if (!const me: ({ profile: Profile; root: { myProjects: (Project & { tasks: ListOfTasks; })[] & ListOfProjects; } & AccountRoot; } & MyAppAccount) | null | undefinedme) { return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Loading...</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; } return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> <JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const me: { profile: Profile; root: { myProjects: (Project & { tasks: ListOfTasks; })[] & ListOfProjects; } & AccountRoot; } & MyAppAccountme.Account.profile: Profileprofile.Profile.name: co<string>name}'s projects</JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> <JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> {const me: { profile: Profile; root: { myProjects: (Project & { tasks: ListOfTasks; })[] & ListOfProjects; } & AccountRoot; } & MyAppAccountme.MyAppAccount.root: { myProjects: (Project & { tasks: ListOfTasks; })[] & ListOfProjects; } & AccountRoot & co<AccountRoot | null>root.AccountRoot.myProjects: (Project & { tasks: ListOfTasks; })[] & ListOfProjects & co<ListOfProjects | null>myProjects.Array<T>.map<React.JSX.Element>(callbackfn: (value: Project & { tasks: ListOfTasks; }, index: number, array: (Project & { tasks: ListOfTasks; })[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[] (+1 overload)Calls a defined callback function on each element of an array, and returns an array that contains the results.@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.map(project: Project & { tasks: ListOfTasks; }project => ( <JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li React.Attributes.key?: React.Key | null | undefinedkey={project: Project & { tasks: ListOfTasks; }project.CoMap.id: ID<Project & { tasks: ListOfTasks; }>The ID of this `CoMap`@categoryContentid}> {project: Project & { tasks: ListOfTasks; }project.Project.name: co<string>name} ({project: Project & { tasks: ListOfTasks; }project.Project.tasks: co<ListOfTasks | null> & ListOfTaskstasks.Array<T>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.length} tasks) </JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li> ))} </JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> </JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> } ## Loading States and Permission Checking When subscribing to or loading a CoValue, you need to handle three possible states: * `undefined`: The initial loading state, indicating the value is being fetched * `null`: The CoValue was not found or is not accessible (e.g., due to permissions) * `Value`: The successfully loaded CoValue instance This allows you to handle loading, error, and success states in your application: class TaskTask.CoMap.subscribe<Task, true>(this: CoValueClass<Task>, id: ID<Task>, listener: (value: Task, unsubscribe: () => void) => void): () => void (+1 overload)Load and subscribe to a `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`/`CoFeed`/`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.@example```ts const unsub = Person.subscribe( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} }, (person) => console.log(person) ); ```@categorySubscription & Loadingsubscribe(const taskId: ID<Task>taskId, (task: Tasktask) => { if (task: Tasktask === var undefinedundefined) { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task is loading..."); } else if (task: Tasktask === null) { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task not found or not accessible"); } else { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task loaded:", task: Tasktask.Task.title: co<string>title); } }); ## Deep Loading When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. This is especially important when working with CoMaps that contain references to other CoValues or with CoLists that contain multiple items. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load. ### Resolve queries Resolve queries let you declare exactly which references to load and how deep to go using the `resolve` property: class class ProjectProject extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Project.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Project.tasks: co<ListOfTasks | null>tasks = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof ListOfTasks>(arg: typeof ListOfTasks | ((_raw: RawCoList<JsonValue, JsonObject | null>) => typeof ListOfTasks), options?: never) => co<...> (+1 overload)ref(class ListOfTasksListOfTasks); Project.owner: co<TeamMember | null>owner = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof TeamMember>(arg: typeof TeamMember | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof TeamMember), options?: never) => co<...> (+1 overload)ref(class TeamMemberTeamMember); } class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Task.title: co<string>title = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.subtasks: co<ListOfTasks | null>subtasks = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof ListOfTasks>(arg: typeof ListOfTasks | ((_raw: RawCoList<JsonValue, JsonObject | null>) => typeof ListOfTasks), options?: never) => co<...> (+1 overload)ref(class ListOfTasksListOfTasks); Task.assignee: co<TeamMember | null | undefined>assignee = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }optional.ref: <typeof TeamMember>(arg: typeof TeamMember | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof TeamMember)) => co<...>ref(class TeamMemberTeamMember); } class class TeamMemberTeamMember extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { TeamMember.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; } class class ListOfTasksListOfTasks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Task | null>>(item: co<Task | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<Task | null>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Task>(arg: typeof Task | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Task), options?: never) => co<...> (+1 overload)ref(class TaskTask)) {} // Load just the project, not its references const const project: Project | nullproject = await class ProjectProject.CoMap.load<Project, true>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId); if (!const project: Project | nullproject) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project not found or not accessible"); } // string - primitive fields are always loaded const project: Projectproject.Project.name: co<string>name; // undefined | null | ListOfTasks - non-requested references might not be loaded, or inaccessible const project: Projectproject.Project.tasks: co<ListOfTasks | null>tasks; // Load the project and shallowly load its list of tasks const const projectWithTasksShallow: ({ tasks: ListOfTasks; } & Project) | nullprojectWithTasksShallow = await class ProjectProject.CoMap.load<Project, { tasks: boolean; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: true } }); if (!const projectWithTasksShallow: ({ tasks: ListOfTasks; } & Project) | nullprojectWithTasksShallow) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project or required references not found or not accessible"); } // ListOfTasks - shallowly loaded const projectWithTasksShallow: { tasks: ListOfTasks; } & ProjectprojectWithTasksShallow.Project.tasks: ListOfTasks & co<ListOfTasks | null>tasks; // number - length of the list const projectWithTasksShallow: { tasks: ListOfTasks; } & ProjectprojectWithTasksShallow.Project.tasks: ListOfTasks & co<ListOfTasks | null>tasks.Array<T>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.length; // undefined | null | Task - items might not be loaded, or inaccessible const projectWithTasksShallow: { tasks: ListOfTasks; } & ProjectprojectWithTasksShallow.Project.tasks: ListOfTasks & co<ListOfTasks | null>tasks[0]; // Load the project and its tasks const const projectWithTasks: ({ tasks: Task[] & ListOfTasks; } & Project) | nullprojectWithTasks = await class ProjectProject.CoMap.load<Project, { tasks: { $each: boolean; }; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true } } }); if (!const projectWithTasks: ({ tasks: Task[] & ListOfTasks; } & Project) | nullprojectWithTasks) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project or required references not found or not accessible"); } // ListOfTasks - fully loaded const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks; // Task - fully loaded const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks[0]; // string - primitive fields are always loaded const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.title: co<string>title; // undefined | null | ListOfTasks - subtasks might not be loaded, or inaccessible const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.subtasks: co<ListOfTasks | null>subtasks; // Load the project, its tasks, and their subtasks const const projectDeep: ({ tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & Project) | nullprojectDeep = await class ProjectProject.CoMap.load<Project, { tasks: { $each: { subtasks: { $each: boolean; }; assignee: boolean; }; }; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: { subtasks?: RefsToResolve<ListOfTasks, 10, [0, 0, 0]> | undefinedsubtasks: { $each: RefsToResolve<Task, 10, [0, 0, 0, 0]>$each: true }, assignee?: RefsToResolve<TeamMember, 10, [0, 0, 0]> | undefinedassignee: true } } } }); if (!const projectDeep: ({ tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & Project) | nullprojectDeep) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project or required references not found or not accessible"); } // string - primitive fields are always loaded const projectDeep: { tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & ProjectprojectDeep.Project.tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.subtasks: co<ListOfTasks | null> & Task[] & ListOfTaskssubtasks[0].Task.title: co<string>title; // undefined | null | TeamMember - since assignee is optional: // TeamMember - set and definitely loaded // null - set but unavailable/inaccessible // undefined - not set, or loading (in case of subscription) const projectDeep: { tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & ProjectprojectDeep.Project.tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.assignee: TeamMember | (TeamMember & CoMarker) | undefinedassignee; The resolve query defines which parts of the graph you want to load, making it intuitive to express complex loading patterns. ### Loading states and permissions When loading data with references, the load operation will fail if one of the references is unavailable or if the user doesn't have read access to it. Let's explore what happens in various scenarios: #### Resolved References When a user tries to load a reference they don't have access to: // If assignee is not accessible to the user: const const task: ({ assignee: TeamMember | undefined; } & Task) | nulltask = await class TaskTask.CoMap.load<Task, { assignee: boolean; }>(this: CoValueClass<...>, id: ID<Task>, options?: { resolve?: RefsToResolve<Task, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const taskId: ID<Task>taskId, { resolve?: RefsToResolve<Task, 10, []> | undefinedresolve: { assignee?: RefsToResolve<TeamMember, 10, [0]> | undefinedassignee: true } }); const task: ({ assignee: TeamMember | undefined; } & Task) | nulltask // => null The load operation will fail and return `null` if any requested reference is inaccessible. This maintains data consistency by ensuring all requested references are available before returning the object. The behavior is the same for optional and required references. #### List References When a list contains references to items the user can't access: // If any item in the list is not accessible: const const project: ({ tasks: Task[] & ListOfTasks; } & Project) | nullproject = await class ProjectProject.CoMap.load<Project, { tasks: { $each: boolean; }; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true } } }); const project: ({ tasks: Task[] & ListOfTasks; } & Project) | nullproject // => null If any item in a list is inaccessible to the user, the entire load operation will fail and return `null`. This is because lists expect all their items to be accessible - a partially loaded list could lead to data inconsistencies. #### Reading a non-resolved inaccessible reference When trying to load an object with an inaccessible reference without directly resolving it: const const project: Project | nullproject = await class ProjectProject.CoMap.load<Project, true>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: true }); const project: Project | nullproject // => Project // The user doesn't have access to the owner const project: Project | nullproject?.Project.owner: co<TeamMember | null> | undefinedowner // => always null The load operation will succeed and return the object, but the inaccessible reference will always be `null`. ## Type Safety with Resolved Type Jazz provides the `Resolved` type to help you define and enforce the structure of deeply loaded data in your application. This makes it easier to ensure that components receive the data they expect with proper TypeScript validation. The `Resolved` type is especially useful when passing data between components, as it guarantees that all necessary nested data has been loaded: Using the `Resolved` type helps catch errors at compile time rather than runtime, ensuring that your components and functions receive data with the proper resolution depth. This is especially useful for larger applications where data is passed between many components. ## Ensuring Data is Loaded Sometimes you need to make sure data is loaded before proceeding with an operation. The `ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth: async function function completeAllTasks(projectId: ID<Project>): Promise<void>completeAllTasks(projectId: ID<Project>projectId: type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class ProjectProject>) { // Ensure the project is loaded const const project: Project | nullproject = await class ProjectProject.CoMap.load<Project, true>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: true }); if (!const project: Project | nullproject) return; // Ensure tasks are loaded const const loadedProject: { tasks: Task[] & ListOfTasks; } & ProjectloadedProject = await const project: Projectproject.CoMap.ensureLoaded<Project, { tasks: { $each: boolean; }; }>(this: Project, options: { resolve: RefsToResolve<Project, 10, []>; }): Promise<{ tasks: Task[] & ListOfTasks; } & Project>Given an already loaded `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.@categorySubscription & LoadingensureLoaded({ resolve: RefsToResolve<Project, 10, []>resolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true } } }); // Now we can safely access and modify tasks const loadedProject: { tasks: Task[] & ListOfTasks; } & ProjectloadedProject.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks.Array<T>.forEach(callbackfn: (value: Task, index: number, array: Task[]) => void, thisArg?: any): void (+1 overload)Performs the specified action for each element in an array.@paramcallbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.forEach(task: Tasktask => { task: Tasktask.Task.status: co<"todo" | "in-progress" | "completed">status = "completed"; }); } ## Best Practices 1. **Be explicit about resolution depths**: Always specify exactly what you need 2. **Use framework integrations**: They handle subscription lifecycle automatically 3. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done 4. **Handle all loading states**: Check for undefined (loading), null (not found), and success states 5. **Use the Resolved type**: Add compile-time type safety for components that require specific resolution patterns --- ## Page: https://jazz.tools/docs/react-native/using-covalues/history ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native/groups/intro Every CoValue has an owner, which can be a `Group` or an `Account`. You can use a `Group` to grant access to a CoValue to multiple users. These users can have different roles, such as "writer", "reader" or "admin". ## Creating a Group Here's how you can create a `Group`. import { Group } from "jazz-tools"; const group = Group.create(); The `Group` itself is a CoValue, and whoever owns it is the initial admin. You typically add members using public sharing or invites. But if you already know their ID, you can add them directly (see below). ## Adding group members by ID You can add group members by ID by using `Account.load` and `Group.addMember`. import { Group, Account } from "jazz-tools"; const group = Group.create(); const bob = await Account.load(bobsID, []); group.addMember(bob, "writer"); Note: if the account ID is of type `string`, because it comes from a URL parameter or something similar, you need to cast it to `ID<Account>` first: import { Group, Account, ID } from "jazz-tools"; const bob = await Account.load(bobsID as ID<Account>, []); group.addMember(bob, "writer"); ## Getting the Group of an existing CoValue You can get the group of an existing CoValue by using `coValue._owner`. const group = existingCoValue._owner; const newValue = MyCoMap.create( { color: "red"}, { owner: group } ); Because `._owner` can be an `Account` or a `Group`, in cases where you specifically need to use `Group` methods (such as for adding members or getting your own role), you can cast it to assert it to be a Group: import { Group } from "jazz-tools"; const group = existingCoValue._owner.castAs(Group); group.addMember(bob, "writer"); const role = group.getRoleOf(bob); ## Checking the permissions You can check the permissions of an account on a CoValue by using the `canRead`, `canWrite` and `canAdmin` methods. const value = await MyCoMap.load(valueID, {}); const me = Account.getMe(); if (me.canAdmin(value)) { console.log("I can share value with others"); } else if (me.canWrite(value)) { console.log("I can edit value"); } else if (me.canRead(value)) { console.log("I can view value"); } else { console.log("I cannot access value"); } To check the permissions of another account, you need to load it first: const value = await MyCoMap.load(valueID, {}); const bob = await Account.load(accountID, []); if (bob.canAdmin(value)) { console.log("Bob can share value with others"); } else if (bob.canWrite(value)) { console.log("Bob can edit value"); } else if (bob.canRead(value)) { console.log("Bob can view value"); } else { console.log("Bob cannot access value"); } --- ## Page: https://jazz.tools/docs/react-native/groups/sharing You can share CoValues publicly by setting the `owner` to a `Group`, and granting access to "everyone". const group = Group.create(); group.addMember("everyone", "writer"); // *highlight* This is done in the chat example where anyone can join the chat, and send messages. You can grant users access to a CoValue by sending them an invite link. import { createInviteLink } from "jazz-react-native"; createInviteLink(organization, "writer"); // or reader, or admin In your app, you need to handle this route, and let the user accept the invitation, as done here. useAcceptInvite({ invitedObjectSchema: PetPost, onAccept: (petPostID) => navigate("/pet/" + petPostID), }); --- ## Page: https://jazz.tools/docs/react-native/groups/inheritance Groups can inherit members from other groups using the `extend` method. When a group extends another group, members of the parent group will become automatically part of the child group. ## Basic Usage Here's how to extend a group: const playlistGroup = Group.create(); const trackGroup = Group.create(); // This way track becomes visible to the members of playlist trackGroup.extend(playlistGroup); When you extend a group: * Members of the parent group get access to the child group * Their roles are inherited (with some exceptions, see below) * Removing a member from the parent group also removes their access to child groups ## Inheriting members but overriding their role In some cases you might want to inherit all members from a parent group but override/flatten their roles to the same specific role in the child group. You can do so by passing an "override role" as a second argument to `extend`: const organizationGroup = Group.create(); organizationGroup.addMember(bob, "admin"); const billingGroup = Group.create(); // This way the members of the organization can only read the billing data billingGroup.extend(organizationGroup, "reader"); The "override role" works in both directions: const parentGroup = Group.create(); parentGroup.addMember(bob, "reader"); parentGroup.addMember(alice, "admin"); const childGroup = Group.create(); childGroup.extend(parentGroup, "writer"); // Bob and Alice are now writers in the child group ## Multiple Levels of Inheritance Groups can be extended multiple levels deep: const grandParentGroup = Group.create(); const parentGroup = Group.create(); const childGroup = Group.create(); childGroup.extend(parentGroup); parentGroup.extend(grandParentGroup); Members of the grandparent group will get access to all descendant groups based on their roles. ## Permission Changes When you remove a member from a parent group, they automatically lose access to all child groups. We handle key rotation automatically to ensure security. // Remove member from parent await parentGroup.removeMember(bob); // Bob loses access to both parent and child groups ## Role Inheritance Rules If the account is already a member of the child group, it will get the more permissive role: const parentGroup = Group.create(); parentGroup.addMember(bob, "reader"); const childGroup = Group.create(); parentGroup.addMember(bob, "writer"); childGroup.extend(parentGroup); // Bob stays a writer because his role is higher // than the inherited reader role. When extending groups, only admin, writer and reader roles are inherited: const parentGroup = Group.create(); parentGroup.addMember(bob, "writeOnly"); const childGroup = Group.create(); childGroup.extend(parentGroup); // Bob does not become a member of the child group To extend a group: 1. The current account must be an admin in the child group 2. The current account must be a member of the parent group const companyGroup = company._owner.castAs(Group) const teamGroup = Group.create(); // Works only if I'm a member of companyGroup teamGroup.extend(companyGroup); ## Revoking a group extension You can revoke a group extension by using the `revokeExtend` method: const parentGroup = Group.create(); const childGroup = Group.create(); childGroup.extend(parentGroup); // Revoke the extension await childGroup.revokeExtend(parentGroup); ## Getting all parent groups You can get all the parent groups of a group by calling the `getParentGroups` method: const childGroup = Group.create(); const parentGroup = Group.create(); childGroup.extend(parentGroup); console.log(childGroup.getParentGroups()); // [parentGroup] ## Example: Team Hierarchy Here's a practical example of using group inheritance for team permissions: // Company-wide group const companyGroup = Group.create(); companyGroup.addMember(CEO, "admin"); // Team group with elevated permissions const teamGroup = Group.create(); teamGroup.extend(companyGroup); // Inherits company-wide access teamGroup.addMember(teamLead, "admin"); teamGroup.addMember(developer, "writer"); // Project group with specific permissions const projectGroup = Group.create(); projectGroup.extend(teamGroup); // Inherits team permissions projectGroup.addMember(client, "reader"); // Client can only read project items This creates a hierarchy where: * The CEO has admin access to everything * Team members get writer access to team and project content * Team leads get admin access to team and project content * The client can only read project content --- ## Page: https://jazz.tools/docs/react-native/authentication/overview Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user. When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally. Without any additional steps the user can use Jazz normally, but they would be limited to use on only one device. To make Accounts work across devices, you can store/retrieve the account keys from an authentication method by using the corresponding hooks and providers. ## Passphrase-based authentication Passphrase authentication lets users log into any device using a Bitcoin-style passphrase. This means users are themselves responsible for storing the passphrase safely. The passphrase is generated from the local account certificate using a wordlist of your choice. You can find a set of ready-to-use wordlists in the bip39 repository. For example: import { View, TextInput, Button, Text } from 'react-native'; export function AuthModal({ open, onOpenChange }: AuthModalProps) { const [loginPassphrase, setLoginPassphrase] = useState(""); const auth = usePassphraseAuth({ wordlist: englishWordlist, }); if (auth.state === "signedIn") { return <Text>You are already signed in</Text>; } const handleSignUp = async () => { await auth.signUp(); onOpenChange(false); }; const handleLogIn = async () => { await auth.logIn(loginPassphrase); onOpenChange(false); }; return ( <View> <View> <Text>Your current passphrase</Text> <TextInput editable={false} value={auth.passphrase} multiline numberOfLines={5} /> </View> <Button title="I have stored my passphrase" onPress={handleSignUp} /> <View> <Text>Log in with your passphrase</Text> <TextInput value={loginPassphrase} onChangeText={setLoginPassphrase} placeholder="Enter your passphrase" multiline numberOfLines={5} required /> </View> <Button title="Log in" onPress={handleLogIn} /> </View> ); } You can try our passphrase authentication using our passphrase example or the todo list demo. ## Migrating data from anonymous to authenticated account You may want allow your users to use your app without authenticating (a poll response for example). When _signing up_ their anonymous account is transparently upgraded using the provided auth method, keeping the data stored in the account intact. However, a user may realise that they already have an existing account _after using the app anonymously and having already stored data in the anonymous account_. When they now _log in_, by default the anonymous account will be discarded and this could lead to unexpected data loss. To avoid this situation we provide the `onAnonymousAccountDiscarded` handler to migrate the data from the anonymous account to the existing authenticated one. This is an example from our music player: export async function onAnonymousAccountDiscarded( anonymousAccount: MusicaAccount, ) { const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({ root: { rootPlaylist: { tracks: [{}], }, }, }); const me = await MusicaAccount.getMe().ensureLoaded({ root: { rootPlaylist: { tracks: [], }, }, }); for (const track of anonymousAccountRoot.rootPlaylist.tracks) { if (track.isExampleTrack) continue; const trackGroup = track._owner.castAs(Group); trackGroup.addMember(me, "admin"); me.root.rootPlaylist.tracks.push(track); } } To see how this works in reality we suggest you to try to upload a song in the music player demo and then try to log in with an existing account. ## Disable network sync for anonymous users You can disable network sync to make your app local-only under specific circumstances. For example, you may want to give the opportunity to non-authenticated users to try your app locally-only (incurring no sync traffic), then enable the network sync only when the user is authenticated: <JazzProvider sync={{ peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // This makes the app work in local mode when the user is anonymous when: "signedUp", }} > <App /> </JazzProvider> For more complex behaviours, you can manually control sync by statefully switching when between `"always"` and `"never"`. --- ## Page: https://jazz.tools/docs/react-native/authentication/writing-your-own ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native/design-patterns/form Normally, we implement forms using the onSubmit handler, or by making a controlled form with useState, or by using special libraries like react-hook-form. In Jazz, we can do something simpler and more powerful, because CoValues give us reactive, persisted state which we can use to directly edit live objects, and represent auto-saved drafts. See the full example here. ## Updating a CoValue To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server, so we don't need to handle form submissions either. <input type="text" value={order.name} onChange={(e) => order.name = e.target.value} /> This means we can write update forms in fewer lines of code. ## Creating a CoValue However, when creating a CoValue, the CoValue does not exist yet, so we don't have the advantages previously mentioned. There's a way around this, and it provides unexpected benefits too. ### Using a Draft CoValue Let's say we have a CoValue called `BubbleTeaOrder`. We can create a "draft" CoValue, which is an empty version of a `BubbleTeaOrder`, that we can then modify when we are "creating" a new CoValue. A `DraftBubbleTeaOrder` is essentially a copy of `BubbleTeaOrder`, but with all the fields made optional. // schema.ts export class BubbleTeaOrder extends CoMap { name = co.string; } export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; } ## Writing the components in React Let's write the form component that will be used for both create and update. // OrderForm.tsx export function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }) { return ( <form onSubmit={onSave}> <label> Name <input type="text" value={order.name} onChange={(e) => (order.name = e.target.value)} required /> </label> {onSave && <button type="submit">Submit</button>} </form> ); } ### Writing the edit form To make the edit form, simply pass the `BubbleTeaOrder`. // EditOrder.tsx export function EditOrder(props: { id: ID<BubbleTeaOrder> }) { const order = useCoState(BubbleTeaOrder, props.id, []); if (!order) return; return <OrderForm order={order} />; } ### Writing the create form For the create form, we need to: 1. Create a draft order. 2. Edit the draft order. 3. Convert the draft order to a "real" order on submit. Here's how that looks like: // CreateOrder.tsx export function CreateOrder() { const { me } = useAccount(); const [draft, setDraft] = useState<DraftBubbleTeaOrder>(); useEffect(() => { setDraft(DraftBubbleTeaOrder.create({})); }, [me?.id]); const onSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!draft) return; const order = draft as BubbleTeaOrder; console.log("Order created:", order); }; if (!draft) return; return <OrderForm order={draft} onSave={onSave} />; } ## Validation In a `BubbleTeaOrder`, the `name` field is required, so it would be a good idea to validate this before turning the draft into a real order. Update the schema to include a `validate` method. // schema.ts export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; validate() { const errors: string[] = []; if (!this.name) { errors.push("Please enter a name."); } return { errors }; } } Then perform the validation on submit. // CreateOrder.tsx export function CreateOrder() { const { me } = useAccount(); const [draft, setDraft] = useState<DraftBubbleTeaOrder>(); useEffect(() => { setDraft(DraftBubbleTeaOrder.create({})); }, [me?.id]); const onSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!draft) return; const validation = draft.validate(); if (validation.errors.length > 0) { console.log(validation.errors); return; } const order = draft as BubbleTeaOrder; console.log("Order created:", order); }; if (!draft) return; return <OrderForm order={draft} onSave={onSave} />; } ## Saving the user's work-in-progress It turns out that using this pattern also provides a UX improvement. By storing the draft in the user's account, they can come back to it anytime without losing their work. 🙌 // schema.ts export class BubbleTeaOrder extends CoMap { name = co.string; } export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; } export class AccountRoot extends CoMap { draft = co.ref(DraftBubbleTeaOrder); } export class JazzAccount extends Account { root = co.ref(AccountRoot); migrate(this: JazzAccount, creationProps?: { name: string }) { if (this.root === undefined) { const draft = DraftBubbleTeaOrder.create({}); this.root = AccountRoot.create({ draft }); } } } Let's not forget to update the `AccountSchema`. import { JazzProvider } from "jazz-react"; import { JazzAccount } from "./schema"; export function MyJazzProvider({ children }: { children: React.ReactNode }) { return ( <JazzProvider sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} AccountSchema={JazzAccount} > {children} </JazzProvider> ); } // Register the Account schema so `useAccount` returns our custom `JazzAccount` declare module "jazz-react" { interface Register { Account: JazzAccount; } } Instead of creating a new draft every time we use the create form, let's use the draft from the account root. // CreateOrder.tsx export function CreateOrder() { const { me } = useAccount({ root: { draft: {} } }); if (!me?.root) return; const onSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const draft = me.root.draft; if (!draft) return; const validation = draft.validate(); if (validation.errors.length > 0) { console.log(validation.errors); return; } const order = draft as BubbleTeaOrder; console.log("Order created:", order); // create a new empty draft me.root.draft = DraftBubbleTeaOrder.create( {}, ); }; return <CreateOrderForm id={me.root.draft.id} onSave={onSave} /> } function CreateOrderForm({ id, onSave, }: { id: ID<DraftBubbleTeaOrder>; onSave: (e: React.FormEvent<HTMLFormElement>) => void; }) { const draft = useCoState(DraftBubbleTeaOrder, id); if (!draft) return; return <OrderForm order={draft} onSave={onSave} />; } When the new draft is created, we need to call `useCoState` again, so that we are passing the new draft to `<OrderForm/>`. There you have it! Notice that when you refresh the page, you will see your unsaved changes. ## Draft indicator To improve the UX even further, in just a few more steps, we can tell the user that they currently have unsaved changes. Simply add a `hasChanges` checker to your schema. // schema.ts export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; validate() { const errors: string[] = []; if (!this.name) { errors.push("Plese enter a name."); } return { errors }; } get hasChanges() { return Object.keys(this._edits).length; } } In the UI, you can choose how you want to show the draft indicator. // DraftIndicator.tsx export function DraftIndicator() { const { me } = useAccount({ root: { draft: {} }, }); if (me?.root.draft?.hasChanges) { return ( <p>You have a draft</p> ); } } A more subtle way is to show a small dot next to the Create button. ## Handling different types of data Forms can be more complex than just a single string field, so we've put together an example app that shows you how to handle single-select, multi-select, date, and boolean inputs. See the full example here. export class BubbleTeaOrder extends CoMap { baseTea = co.literal(...BubbleTeaBaseTeaTypes); addOns = co.ref(ListOfBubbleTeaAddOns); deliveryDate = co.Date; withMilk = co.boolean; instructions = co.optional.string; } --- ## Page: https://jazz.tools/docs/react-native/design-patterns/organization Organizations are a way to share a set of data between users. Different apps have different names for this concept, such as "teams" or "workspaces". We'll use the term Organization. See the full example here. ## Defining the schema for an Organization Create a CoMap shared by the users of the same organization to act as a root (or "main database") for the shared data within an organization. For this example, users within an `Organization` will be sharing `Project`s. // schema.ts export class Project extends CoMap { name = co.string; } export class ListOfProjects extends CoList.Of(co.ref(Project)) {} export class Organization extends CoMap { name = co.string; // shared data between users of each organization projects = co.ref(ListOfProjects); } export class ListOfOrganizations extends CoList.Of(co.ref(Organization)) {} Learn more about defining schemas. ## Adding a list of Organizations to the user's Account Let's add the list of `Organization`s to the user's Account `root` so they can access them. // schema.ts export class JazzAccountRoot extends CoMap { organizations = co.ref(ListOfOrganizations); } export class JazzAccount extends Account { root = co.ref(JazzAccountRoot); async migrate() { if (this.root === undefined) { // Using a Group as an owner allows you to give access to other users const organizationGroup = Group.create(); const organizations = ListOfOrganizations.create( [ // Create the first Organization so users can start right away Organization.create( { name: "My organization", projects: ListOfProjects.create([], organizationGroup), }, organizationGroup, ), ], ); this.root = JazzAccountRoot.create( { organizations }, ); } } } This schema now allows users to create `Organization`s and add `Project`s to them. See the schema for the example app here. ## Adding other users to an Organization To give users access to an `Organization`, you can either send them an invite link, or add their `Account` manually. ### Adding users through invite links Here's how you can generate an invite link. When the user accepts the invite, add the `Organization` to the user's `organizations` list. const onAccept = async (organizationId: ID<Organization>) => { const me = await MusicaAccount.getMe().ensureLoaded({ root: { organizations: [], }, }); const organization = await Organization.load(organizationId, []); if (!organization) throw new Error("Failed to load organization data"); const ids = me.root.organizations.map( (organization) => organization?.id, ); if (ids.includes(organizationId)) return; me.root.organizations.push(organization); navigate("/organizations/" + organizationId); }; useAcceptInvite({ invitedObjectSchema: Organization, onAccept, }); ### Adding users through their Account ID ...more on this coming soon --- ## Page: https://jazz.tools/docs/react-native/jazz-under-the-hood ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native-expo/project-setup Jazz supports Expo through the dedicated `jazz-expo` package, which is specifically designed for Expo applications. If you're building for React Native without Expo, please refer to the React Native guide instead. Jazz requires an Expo development build using Expo Prebuild for native code. It is **not compatible** with Expo Go. Jazz also supports the New Architecture. Tested with: "expo": "~52.0.0", "react-native": "0.76.7", "react": "18.3.1" ## Installation ### Create a new project (Skip this step if you already have one) npx create-expo-app -e with-router-tailwind my-jazz-app cd my-jazz-app npx expo prebuild ### Install dependencies # Expo dependencies npx expo install expo-linking expo-secure-store expo-file-system @react-native-community/netinfo @bam.tech/react-native-image-resizer # React Native polyfills npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @craftzdog/react-native-buffer # Jazz dependencies npm i -S jazz-tools jazz-expo jazz-react-native-media-images **Note**: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json`. Packages to try include: `text-encoding`, `base-64`, and you can drop `@bacons/text-decoder`. #### Fix incompatible dependencies If you encounter incompatible dependencies, you can try to fix them with the following command: npx expo install --fix ### Configure Metro #### Regular repositories If you are not working within a monorepo, create a new file `metro.config.js` in the root of your project with the following content: // metro.config.js const { const getDefaultConfig: anygetDefaultConfig } = var require: NodeRequire (id: string) => anyrequire("expo/metro-config"); const const config: anyconfig = const getDefaultConfig: anygetDefaultConfig(projectRoot); const config: anyconfig.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"]; const config: anyconfig.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/]; var module: NodeModulemodule.NodeJS.Module.exports: anyexports = const config: anyconfig; #### Monorepos For monorepos, use the following `metro.config.js`: // metro.config.js const { const getDefaultConfig: anygetDefaultConfig } = var require: NodeRequire (id: string) => anyrequire("expo/metro-config"); const { const FileStore: anyFileStore } = var require: NodeRequire (id: string) => anyrequire("metro-cache"); const const path: anypath = var require: NodeRequire (id: string) => anyrequire("path"); // eslint-disable-next-line no-undef const const projectRoot: stringprojectRoot = var __dirname: string__dirname; const const workspaceRoot: anyworkspaceRoot = const path: anypath.resolve(const projectRoot: stringprojectRoot, "../.."); const const config: anyconfig = const getDefaultConfig: anygetDefaultConfig(const projectRoot: stringprojectRoot); const config: anyconfig.watchFolders = [const workspaceRoot: anyworkspaceRoot]; const config: anyconfig.resolver.nodeModulesPaths = [ const path: anypath.resolve(const projectRoot: stringprojectRoot, "node_modules"), const path: anypath.resolve(const workspaceRoot: anyworkspaceRoot, "node_modules"), ]; const config: anyconfig.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"]; const config: anyconfig.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/]; const config: anyconfig.cacheStores = [ new const FileStore: anyFileStore({ root: anyroot: const path: anypath.join(const projectRoot: stringprojectRoot, "node_modules", ".cache", "metro"), }), ]; var module: NodeModulemodule.NodeJS.Module.exports: anyexports = const config: anyconfig; ### Additional monorepo configuration (for pnpm) If you're using `pnpm`, you'll need to make sure that your expo app's `package.json` has this: // package.json { "main": "index.js", ... } For more information, refer to this Expo monorepo example. ## Authentication Jazz provides authentication to help users access their data across multiple devices. For details on implementing authentication with Expo, check our Authentication Overview guide and see the Expo Chat Demo for a complete example. ## Next Steps Now that you've set up your Expo project for Jazz, you'll need to: 1. Set up the Jazz Provider - Configure how your app connects to Jazz 2. Add authentication (optional) - Enable users to access data across devices 3. Define your schema - See the schema docs for more information 4. Run your app: npx expo run:ios # or npx expo run:android ## Verification Ready to see if everything's working? Let's fire up your app: npx expo run:ios # or npx expo run:android If all goes well, your app should start up without any angry red error screens. Take a quick look at the Metro console too - no Jazz-related errors there means you're all set! If you see your app's UI come up smoothly, you've nailed the installation. If you run into any issues that aren't covered in the Common Issues section, drop by our Discord for help. ## Common Issues * **Metro bundler errors**: If you see errors about missing polyfills, ensure all polyfills are properly imported. * **iOS build failures**: Make sure you've run `pod install` after adding the dependencies. * **Android build failures**: Ensure you've run `npx expo prebuild` to generate native code. * **Expo Go incompatibility**: Remember that Jazz requires a development build and won't work with Expo Go. ### Install CocoaPods If you're compiling for iOS, you'll need to install CocoaPods for your project. If you need to install it, we recommend using `pod-install`: npx pod-install --- ## Page: https://jazz.tools/docs/react-native-expo/upgrade/0-13-0 Version 0.13.0 introduces a significant architectural change in how Jazz supports React Native applications. We've separated the React Native implementation into two distinct packages to better serve different React Native development approaches: 1. **jazz-expo**: Dedicated package for Expo applications 2. **jazz-react-native**: Focused package for framework-less React Native applications 3. **jazz-react-native-core**: Shared core functionality used by both implementations (probably not imported directly) This guide focuses on upgrading **Expo applications**. If you're using framework-less React Native, please see the React Native upgrade guide. ## Migration Steps for Expo 1. **Update Dependencies** Remove the old packages and install the new `jazz-expo` package. # Remove the old package npm uninstall jazz-react-native # Install the new Expo-specific packages npx expo install expo-sqlite expo-secure-store expo-file-system @react-native-community/netinfo # Install the new packages npm install jazz-expo jazz-react-native-media-images 2. **Update Imports** Update your imports to use the new `jazz-expo` package. // Before import { import JazzProviderJazzProvider, import useAccountuseAccount, import useCoValueuseCoValue } from "jazz-react-native"; // After import { import JazzProviderJazzProvider, import useAccountuseAccount, import useCoValueuseCoValue } from "jazz-expo"; 3. **Update Type Declarations** Update your type declarations to use the new `jazz-expo` package. // Before declare module "jazz-react-native" { interface Register { Register.Account: MyAppAccountAccount: type MyAppAccount = /*unresolved*/ anyMyAppAccount; } } // After declare module "jazz-expo" { interface Register { Register.Account: MyAppAccountAccount: type MyAppAccount = /*unresolved*/ anyMyAppAccount; } } ## Clerk Authentication Clerk authentication has been moved inside the `jazz-expo` package. This is a breaking change that requires updating both your imports and providers. If you're using Clerk auth in your Expo application, you'll need to: // Before import { import JazzProviderWithClerkJazzProviderWithClerk } from "jazz-react-native-clerk"; // After import { import JazzProviderWithClerkJazzProviderWithClerk } from "jazz-expo/auth/clerk"; Since Clerk only supports Expo applications, this consolidation makes sense and simplifies your dependency structure. You'll need to completely remove the `jazz-react-native-clerk` package from your dependencies and use the Clerk functionality that's now built into `jazz-expo`. ## Storage Adapter Changes The `jazz-expo` package now uses: * `ExpoSQLiteAdapter` for database storage (using `expo-sqlite`) * `ExpoSecureStoreAdapter` for key-value storage (using `expo-secure-store`) These are now the default storage adapters in the `JazzProvider` for Expo applications, so you don't need to specify them explicitly. ## Example Provider Setup import { import JazzProviderJazzProvider } from "jazz-react-native"; import { import JazzProviderJazzProvider } from "jazz-expo"; import { import MyAppAccountMyAppAccount } from "./schema"; export function function MyJazzProvider({ children }: { children: React.ReactNode; }): React.JSX.ElementMyJazzProvider({ children: React.ReactNodechildren }: { children: React.ReactNodechildren: export namespace ReactReact.type React.ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefinedRepresents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}@example```tsx // Typing children type Props = { children: ReactNode } const Component = ({ children }: Props) => <div>{children}</div> <Component>hello</Component> ```@example```tsx // Typing a custom element type Props = { customElement: ReactNode } const Component = ({ customElement }: Props) => <div>{customElement}</div> <Component customElement={<div>hello</div>} /> ```ReactNode }) { return ( <import JazzProviderJazzProvider sync: { peer: string; }sync={{ peer: stringpeer: "wss://cloud.jazz.tools/?key=you@example.com" }} type AccountSchema: anyAccountSchema={import MyAppAccountMyAppAccount} > {children: React.ReactNodechildren} </import JazzProviderJazzProvider> ); } // Register the Account schema declare module "jazz-react-native" { interface Register { Register.Account: MyAppAccountAccount: import MyAppAccountMyAppAccount; } } declare module "jazz-expo" { interface Register { Register.Account: MyAppAccountAccount: import MyAppAccountMyAppAccount; } } ## New Architecture Support The `jazz-expo` implementation supports the Expo New Architecture. ## For More Information For detailed setup instructions, refer to the React Native Expo Setup Guide --- ## Page: https://jazz.tools/docs/react-native-expo Welcome to the Jazz documentation! The Jazz docs are currently heavily work in progress, sorry about that! ## Quickstart Run the following command to create a new Jazz project from one of our example apps: npx create-jazz-app@latest Or set up Jazz yourself, using the following instructions for your framework of choice: * React * Next.js * React Native * React Native Expo * Vue * Svelte ## Example apps You can also find example apps with code most similar to what you want to build. These apps make use of different features such as auth, file upload, and more. ## Sync and storage Sync and persist your data by setting up a sync and storage infrastructure using Jazz Cloud, or do it yourself. ## Collaborative values Learn how to structure your data using collaborative values. ## API Reference Many of the packages provided are documented in the API Reference. ## LLM Docs Get better results with AI by importing the Jazz docs into your context window. ## Get support If you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native-expo/guide ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native-expo/faq ## How established is Jazz? Jazz is backed by fantastic angel and institutional investors with experience and know-how in devtools and has been in development since 2020. ## Will Jazz be around long-term? We're committed to Jazz being around for a long time! We understand that when you choose Jazz for your projects, you're investing time and making a significant architectural choice, and we take that responsibility seriously. That's why we've designed Jazz with longevity in mind from the start: * The open source nature of our sync server means you'll always be able to run your own infrastructure * Your data remains accessible even if our cloud services change * We're designing the protocol as an open specification This approach creates a foundation that can continue regardless of any single company's involvement. The local-first architecture means your apps will always work, even offline, and your data remains yours. --- ## Page: https://jazz.tools/docs/react-native-expo/sync-and-storage ## Using Jazz Cloud Simply use `wss://cloud.jazz.tools/?key=...` as the sync server URL. Jazz Cloud will * sync CoValues in real-time between users and devices * safely persist CoValues on redundant storage nodes with additional backups * make use of geographically distributed cache nodes for low latency ### Free public alpha * Jazz Cloud is free during the public alpha, with no strict usage limits * We plan to keep a free tier, so you'll always be able to get started with zero setup * See Jazz Cloud pricing for more details * ⚠️ Please use a valid email address as your API key. Your full sync server URL should look something like `wss://cloud.jazz.tools/?key=you@example.com` Once we support per-app API keys, we'll email you an API key you can use instead. ## Running your own sync server You can run your own sync server using: npx jazz-run sync And then use `ws://localhost:4200` as the sync server URL. You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS. In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL. ### Command line options: * `--port` / `-p` - the port to run the sync server on. Defaults to 4200. * `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default. * `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`. ### Source code The implementation of this simple sync server is available open-source on GitHub. --- ## Page: https://jazz.tools/docs/react-native-expo/project-setup/server-side The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do. This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users. ## Generating credentials Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret. To generate new credentials for a Server Worker, you can run: npx jazz-run account create --name "My Server Worker" The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done. ## Storing & providing credentials Server Worker credentials are typically stored and provided as environmental variables. **Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.** ## Starting a server worker You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly to setting up a client-side Jazz context, it: * takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in it's private account root) * takes a URL for a sync & storage server `startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere. import { startWorker } from 'jazz-nodejs'; const { worker } = await startWorker({ AccountSchema: MyWorkerAccount, syncServer: 'wss://cloud.jazz.tools/?key=you@example.com', }); `worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to: * load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})` * create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })` ## Using CoValues instead of requests Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about. What's less clear is how you can trigger this work to happen. * One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients * The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients. * A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`). * This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible. --- ## Page: https://jazz.tools/docs/react-native-expo/project-setup/providers ## Introduction `<JazzProvider />` is the core component that connects your Expo application to Jazz. It handles: * **Data Synchronization**: Manages connections to peers and the Jazz cloud * **Local Storage**: Persists data locally between app sessions * **Schema Types**: Provides APIs for the AccountSchema * **Authentication**: Connects your authentication system to Jazz ## Setting up the provider Wrap your app components with the `<JazzProvider />` component: // App.tsx import { import JazzProviderJazzProvider } from "jazz-expo"; import { import MyAppAccountMyAppAccount } from "./schema"; export function function MyJazzProvider({ children }: { children: React.ReactNode; }): React.JSX.ElementMyJazzProvider({ children: React.ReactNodechildren }: { children: React.ReactNodechildren: export namespace ReactReact.type React.ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefinedRepresents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}@example```tsx // Typing children type Props = { children: ReactNode } const Component = ({ children }: Props) => <div>{children}</div> <Component>hello</Component> ```@example```tsx // Typing a custom element type Props = { customElement: ReactNode } const Component = ({ customElement }: Props) => <div>{customElement}</div> <Component customElement={<div>hello</div>} /> ```ReactNode }) { return ( <import JazzProviderJazzProvider sync: { peer: string; }sync={{ peer: stringpeer: "wss://cloud.jazz.tools/?key=you@example.com" }} type AccountSchema: anyAccountSchema={import MyAppAccountMyAppAccount} > {children: React.ReactNodechildren} </import JazzProviderJazzProvider> ); } // Register the Account schema so `useAccount` returns our custom `MyAppAccount` declare module "jazz-expo" { interface Register { Register.Account: MyAppAccountAccount: import MyAppAccountMyAppAccount; } } ## Provider Options * `kvStore` * `ExpoSecureStoreAdapter` (default) * `AccountSchema` * `Account` (default) * `CryptoProvider` * `PureJSCrypto` (default) - Pure JavaScript crypto provider * `RNQuickCrypto` - C++ accelerated crypto provider ## Authentication in the Provider `<JazzProvider />` works with various authentication methods, with PassphraseAuth being the easiest way to get started for development and testing. For authentication details, refer to our Authentication Overview guide. The authentication hooks must always be used inside the `<JazzProvider />` component. Implementing PassphraseAuth is straightforward: 1. Import the wordlist for generating recovery phrases 2. Use the `usePassphraseAuth` hook to handle authentication 3. Create simple registration and sign-in screens // Example with PassphraseAuth import { import JazzProviderJazzProvider, import usePassphraseAuthusePassphraseAuth } from "jazz-expo"; import { import englishWordlistenglishWordlist } from "./wordlist"; function function JazzAuthentication({ children }: { children: ReactNode; }): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefinedJazzAuthentication({ children: React.ReactNodechildren }: { children: React.ReactNodechildren: type ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefinedRepresents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}@example```tsx // Typing children type Props = { children: ReactNode } const Component = ({ children }: Props) => <div>{children}</div> <Component>hello</Component> ```@example```tsx // Typing a custom element type Props = { customElement: ReactNode } const Component = ({ customElement }: Props) => <div>{customElement}</div> <Component customElement={<div>hello</div>} /> ```ReactNode }) { const const auth: anyauth = import usePassphraseAuthusePassphraseAuth({ wordlist: anywordlist: import englishWordlistenglishWordlist, }); // If the user is already signed in, render the App if (const auth: anyauth.state === "signedIn") { return children: React.ReactNodechildren } // Otherwise, show a sign-in screen return <function SignInScreen({ auth }: { auth: any; }): nullSignInScreen auth: anyauth={const auth: anyauth} />; } function function AuthenticatedProvider({ children }: { children: ReactNode; }): React.JSX.ElementAuthenticatedProvider({ children: React.ReactNodechildren }: { children: React.ReactNodechildren: type ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | null | undefinedRepresents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}@example```tsx // Typing children type Props = { children: ReactNode } const Component = ({ children }: Props) => <div>{children}</div> <Component>hello</Component> ```@example```tsx // Typing a custom element type Props = { customElement: ReactNode } const Component = ({ customElement }: Props) => <div>{customElement}</div> <Component customElement={<div>hello</div>} /> ```ReactNode }) { return ( <import JazzProviderJazzProvider sync: { peer: string; }sync={{ peer: stringpeer: "wss://cloud.jazz.tools/?key=your-api-key" }} > <function JazzAuthentication({ children }: { children: ReactNode; }): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefinedJazzAuthentication> {children: React.ReactNodechildren} </function JazzAuthentication({ children }: { children: ReactNode; }): string | number | boolean | Iterable<React.ReactNode> | React.JSX.Element | null | undefinedJazzAuthentication> </import JazzProviderJazzProvider> ); } For a complete example, see the Expo Chat Demo. ## Local Persistence Jazz for Expo includes built-in local persistence using SQLite. Following Expo's best practices, the Expo implementation uses: * **Database Storage**: `expo-sqlite` - Expo's official SQLite module * **Key-Value Storage**: `expo-secure-store` - Expo's secure storage system Local persistence is enabled by default with no additional configuration required. Your data will automatically persist across app restarts. --- ## Page: https://jazz.tools/docs/react-native-expo/ai-tools AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at. However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong. To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor. Every tool is different, but generally, you'll need to either paste the contents of the llms-full.txt file directly in your prompt, or attach the file to the tool. Upload the txt file in your prompt. https://jazz.tools/llms-full.txt We follow the llms.txt proposed standard for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing. AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good). We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on Discord. --- ## Page: https://jazz.tools/docs/react-native-expo/inspector For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app. In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`. Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window. Install the package. npm install jazz-inspector Render the component within your `JazzProvider`. This will show the Inspector launch button on the right of your page. --- ## Page: https://jazz.tools/docs/react-native-expo/upgrade/0-12-0 Jazz 0.12.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable. ## What's new? * New resolve API for a more type-safe deep loading * A single, consistent load option for all loading methods * Improved permission checks on deep loading * Easier type safety with the `Resolved` type helper ## Breaking changes ### New Resolve API We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs. **Major changes:** 1. Functions and hooks for loading now take the resolve query as an explicit nested `resolve` prop 2. Shallowly loading a collection is now done with `true` instead of `[]` or `{}` // Before // @ts-expect-error const { const me: MyAppAccount | ({ root: AccountRoot | ({ friends: ListOfAccounts | ((Account | (Account & { root: CoMap; profile: Profile; }))[] & ListOfAccounts); } & AccountRoot); profile: Profile; } & MyAppAccount) | null | undefinedme } = useAccount<MyAppAccount, RefsToResolve<MyAppAccount>>(options?: { resolve?: RefsToResolve<MyAppAccount, 10, []> | undefined; } | undefined): { ...; } (+1 overload)useAccount({ root: { friends: never[]; }root: { friends: never[]friends: [] } }); // After const { const me: ({ root: { friends: ListOfAccounts; } & AccountRoot; } & MyAppAccount) | null | undefinedme } = useAccount<MyAppAccount, { root: { friends: true; }; }>(options?: { resolve?: RefsToResolve<MyAppAccount, 10, []> | undefined; } | undefined): { me: ({ ...; } & MyAppAccount) | ... 1 more ... | undefined; logOut: () => void; } (+1 overload)useAccount({ resolve?: RefsToResolve<MyAppAccount, 10, []> | undefinedresolve: { root?: RefsToResolve<AccountRoot, 10, [0]> | undefinedroot: { friends?: RefsToResolve<ListOfAccounts, 10, [0, 0]> | undefinedfriends: true } } }); 3. For collections, resolving items deeply is now done with a special `$each` key. For a `CoList`: class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { } class class ListOfTasksListOfTasks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Task | null>>(item: co<Task | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<Task | null>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Task>(arg: typeof Task | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Task), options?: never) => co<...> (+1 overload)ref(class TaskTask)) {} const const id: ID<Task>id = "co_123" as type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class TaskTask>; // Before // @ts-expect-error const const tasks: ListOfTasks | null | undefinedtasks = useCoState<ListOfTasks, true>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefined; } | undefined): ListOfTasks | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<Task>id, [{}]); // After const const tasks: (Task[] & ListOfTasks) | null | undefinedtasks = useCoState<ListOfTasks, { $each: boolean; }>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefined; } | undefined): (Task[] & ListOfTasks) | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<Task>id, { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefinedresolve: { $each: RefsToResolve<Task, 10, [0]>$each: true } }); For a `CoMap.Record`: class class UsersByUsernameUsersByUsername extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap.CoMap.Record<co<MyAppAccount | null>>(value: co<MyAppAccount | null>): { new (options: { fromRaw: RawCoMap; } | undefined): { ...; }; ... 6 more ...; fromRaw<V extends CoValue>(this: CoValueClass<V>, raw: import("/vercel/path0/packages/cojson/dist/coValue").RawCoValue): V; }Declare a Record-like CoMap schema, by extending `CoMap.Record(...)` and passing the value schema using `co`. Keys are always `string`.@example```ts 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; ```@categoryDeclarationRecord(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof MyAppAccount>(arg: typeof MyAppAccount | ((_raw: RawAccount<AccountMeta> | RawControlledAccount<AccountMeta>) => typeof MyAppAccount), options?: never) => co<...> (+1 overload)ref(class MyAppAccountMyAppAccount)) {} // Before // @ts-expect-error const const usersByUsername: UsersByUsername | null | undefinedusersByUsername = useCoState<UsersByUsername, true>(Schema: CoValueClass<UsersByUsername>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<...> | undefined; } | undefined): UsersByUsername | ... 1 more ... | undefineduseCoState(class UsersByUsernameUsersByUsername, const id: ID<UsersByUsername>id, [{}]); // After const const usersByUsername: ({ [key: string]: MyAppAccount; } & UsersByUsername) | null | undefinedusersByUsername = useCoState<UsersByUsername, { $each: boolean; }>(Schema: CoValueClass<UsersByUsername>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): ({ ...; } & UsersByUsername) | ... 1 more ... | undefineduseCoState(class UsersByUsernameUsersByUsername, const id: ID<UsersByUsername>id, { resolve?: RefsToResolve<UsersByUsername, 10, []> | undefinedresolve: { $each: RefsToResolve<MyAppAccount, 10, [0]>$each: true } }); Nested loading — note how it's now less terse, but more readable: class class OrgOrg extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Org.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; } class class AssigneeAssignee extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Assignee.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Assignee.org: co<Org | null>org = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Org>(arg: typeof Org | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Org), options?: never) => co<...> (+1 overload)ref(class OrgOrg); } class class ListOfAssigneesListOfAssignees extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Assignee | null>>(item: co<Assignee | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<...>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Assignee>(arg: typeof Assignee | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Assignee), options?: never) => co<...> (+1 overload)ref(class AssigneeAssignee)) {} class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Task.content: co<string>content = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.assignees: co<ListOfAssignees | null>assignees = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof ListOfAssignees>(arg: typeof ListOfAssignees | ((_raw: RawCoList<JsonValue, JsonObject | null>) => typeof ListOfAssignees), options?: never) => co<...> (+1 overload)ref(class ListOfAssigneesListOfAssignees); } class class ListOfTasksListOfTasks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Task | null>>(item: co<Task | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<Task | null>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Task>(arg: typeof Task | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Task), options?: never) => co<...> (+1 overload)ref(class TaskTask)) {} // Before // @ts-expect-error const const tasksWithAssigneesAndTheirOrgs: ListOfTasks | null | undefinedtasksWithAssigneesAndTheirOrgs = useCoState<ListOfTasks, true>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefined; } | undefined): ListOfTasks | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<ListOfTasks>id, [{ assignees: { org: {}; }[]assignees: [{ org: {}org: {}}]} ]); // After const const tasksWithAssigneesAndTheirOrgs: ((Task & { assignees: (Assignee & { org: Org; })[] & ListOfAssignees; })[] & ListOfTasks) | null | undefinedtasksWithAssigneesAndTheirOrgs = useCoState<ListOfTasks, { $each: { assignees: { $each: { org: boolean; }; }; }; }>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): ((Task & { ...; })[] & ListOfTasks) | ... 1 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<ListOfTasks>id, { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefinedresolve: { $each: RefsToResolve<Task, 10, [0]>$each: { assignees?: RefsToResolve<ListOfAssignees, 10, [0, 0]> | undefinedassignees: { $each: RefsToResolve<Assignee, 10, [0, 0, 0]>$each: { org?: RefsToResolve<Org, 10, [0, 0, 0, 0]> | undefinedorg: true } } } } }); It's also a lot more auto-complete friendly: const const tasksWithAssigneesAndTheirOrgs: ListOfTasks | ((Task | (Task & { assignees: ListOfAssignees | ((Assignee | (Assignee & { org: Org; }))[] & ListOfAssignees); }))[] & ListOfTasks) | null | undefinedtasksWithAssigneesAndTheirOrgs = useCoState<ListOfTasks, RefsToResolve<ListOfTasks>>(Schema: CoValueClass<ListOfTasks>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): ListOfTasks | ... 2 more ... | undefineduseCoState(class ListOfTasksListOfTasks, const id: ID<ListOfTasks>id, { resolve?: RefsToResolve<ListOfTasks, 10, []> | undefinedresolve: { $each: RefsToResolve<Task, 10, [0]>$each: { assignees?: RefsToResolve<ListOfAssignees, 10, [0, 0]> | undefinedassignees: { $$each } } } }); ### A single, consistent load option The new API works across all loading methods, and separating out the resolve query means other options with default values are easier to manage, for example: loading a value as a specific account instead of using the implicit current account: // Before // @ts-expect-error class PlaylistPlaylist.CoMap.load<Playlist, true>(this: CoValueClass<...>, id: ID<Playlist>, options?: { resolve?: RefsToResolve<Playlist, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<Playlist>id, const otherAccount: AccountotherAccount, { tracks: never[]tracks: [], }); // After class PlaylistPlaylist.CoMap.load<Playlist, { tracks: boolean; }>(this: CoValueClass<...>, id: ID<Playlist>, options?: { resolve?: RefsToResolve<Playlist, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<Playlist>id, { loadAs?: Account | AnonymousJazzAgent | undefinedloadAs: const otherAccount: AccountotherAccount, resolve?: RefsToResolve<Playlist, 10, []> | undefinedresolve: { tracks?: RefsToResolve<ListOfTracks, 10, [0]> | undefinedtracks: true } }); ### Improved permission checks on deep loading Now `useCoState` will return `null` when the current user lacks permissions to load requested data. Previously, `useCoState` would return `undefined` if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing. Now `undefined` means that the value is definitely loading, and `null` means that the value is temporarily missing. We also have implemented a more granular permission checking, where if an _optional_ CoValue cannot be accessed, `useCoState` will return the data stripped of that CoValue. **Note:** The state handling around loading and error states will become more detailed and easy-to-handle in future releases, so this is just a small step towards consistency. class class ListOfTracksListOfTracks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Track | null | undefined>>(item: co<Track | null | undefined>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<...>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }optional.ref: <typeof Track>(arg: typeof Track | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Track)) => co<Track | ... 1 more ... | undefined>ref(class TrackTrack)) {} function function TrackListComponent({ id }: { id: ID<ListOfTracks>; }): React.JSX.Element | (React.JSX.Element | null | undefined)[]TrackListComponent({ id: ID<ListOfTracks>id }: { id: ID<ListOfTracks>id: type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class ListOfTracksListOfTracks> }) { // Before (ambiguous states) // @ts-expect-error const const tracks: ListOfTracks | null | undefinedtracks = useCoState<ListOfTracks, true>(Schema: CoValueClass<ListOfTracks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<ListOfTracks, 10, []> | undefined; } | undefined): ListOfTracks | ... 1 more ... | undefineduseCoState(class ListOfTracksListOfTracks, id: ID<ListOfTracks>id, [{}]); if (const tracks: ListOfTracks | null | undefinedtracks === var undefinedundefined) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Loading or access denied</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; if (const tracks: ListOfTracks | nulltracks === null) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Not found</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; // After const const tracks: ListOfTracks | null | undefinedtracks = useCoState<ListOfTracks, { $each: boolean; }>(Schema: CoValueClass<ListOfTracks>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<...> | undefined; } | undefined): ListOfTracks | ... 1 more ... | undefineduseCoState(class ListOfTracksListOfTracks, id: ID<ListOfTracks>id, { resolve?: RefsToResolve<ListOfTracks, 10, []> | undefinedresolve: { $each: RefsToResolve<Track | undefined, 10, [0]>$each: true } }); if (const tracks: ListOfTrackstracks === var undefinedundefined) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Loading...</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; if (const tracks: ListOfTrackstracks === null) return <JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>Not found or access denied</JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>; // This will only show tracks that we have access to and that are loaded. return const tracks: ListOfTrackstracks.Array<co<Track | null | undefined>>.map<React.JSX.Element | null | undefined>(callbackfn: (value: co<Track | null | undefined>, index: number, array: co<Track | null | undefined>[]) => React.JSX.Element | null | undefined, thisArg?: any): (React.JSX.Element | ... 1 more ... | undefined)[]Calls a defined callback function on each element of an array, and returns an array that contains the results.@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.map(track: co<Track | null | undefined>track => track: co<Track | null | undefined>track && <function TrackComponent({ track }: { track: Track; }): stringTrackComponent track: Tracktrack={track: Track | (Track & CoMarker)track} />); } The same change is applied to the load function, so now it returns `null` instead of `undefined` when the value is missing. // Before // @ts-expect-error const const map: MyCoMap | nullmap = await class MyCoMapMyCoMap.CoMap.load<MyCoMap, true>(this: CoValueClass<...>, id: ID<MyCoMap>, options?: { resolve?: RefsToResolve<MyCoMap, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<MyCoMap>id); if (const map: MyCoMap | nullmap === var undefinedundefined) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Map not found"); } // After const const map: MyCoMap | nullmap = await class MyCoMapMyCoMap.CoMap.load<MyCoMap, true>(this: CoValueClass<...>, id: ID<MyCoMap>, options?: { resolve?: RefsToResolve<MyCoMap, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const id: ID<MyCoMap>id); if (const map: MyCoMap | nullmap === null) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Map not found or access denied"); } ## New Features ### The `Resolved` type helper The new `Resolved` type can be used to define what kind of deeply loaded data you expect in your parameters, using the same resolve query syntax as the new loading APIs: type type PlaylistResolved = { tracks: Track[] & ListOfTracks; } & PlaylistPlaylistResolved = type Resolved<T, R extends RefsToResolve<T> | undefined> = R extends boolean | undefined ? T : [T] extends [(infer Item)[]] ? UnCo<Exclude<Item, null>> extends CoValue ? R extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & UnCo<...> : [...] extends [...] ? UnCo<...> extends CoValue ? ItemDepth extends { ...; } ? (CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue ...Resolved<class PlaylistPlaylist, { tracks: { $each: true; }tracks: { $each: true$each: true } }>; function function TrackListComponent({ playlist }: { playlist: PlaylistResolved; }): React.JSX.Element[]TrackListComponent({ playlist: { tracks: Track[] & ListOfTracks; } & Playlistplaylist }: { playlist: { tracks: Track[] & ListOfTracks; } & Playlistplaylist: type PlaylistResolved = { tracks: Track[] & ListOfTracks; } & PlaylistPlaylistResolved }) { // Safe access to resolved tracks return playlist: { tracks: Track[] & ListOfTracks; } & Playlistplaylist.Playlist.tracks: Track[] & ListOfTracks & co<ListOfTracks | null>tracks.Array<T>.map<React.JSX.Element>(callbackfn: (value: Track, index: number, array: Track[]) => React.JSX.Element, thisArg?: any): React.JSX.Element[] (+1 overload)Calls a defined callback function on each element of an array, and returns an array that contains the results.@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.map(track: Tracktrack => <function TrackComponent({ track }: { track: Track; }): stringTrackComponent track: Tracktrack={track: Tracktrack} />); } --- ## Page: https://jazz.tools/docs/react-native-expo/upgrade/0-11-0 Jazz 0.11.0 brings several improvements to member handling, roles, and permissions management. This guide will help you upgrade your application to the latest version. ## What's new? Here is what's changed in this release: * New permissions check APIs: New methods like `canRead`, `canWrite`, `canAdmin`, and `getRoleOf` to simplify permission checks. * Group.revokeExtend: New method to revoke group extension permissions. * Group.getParentGroups: New method to get all the parent groups of an account. * Account Profile & Migrations: Fixed issues with custom account profile migrations for a more consistent experience * Dropped support for Accounts owning Profiles: Profiles can now only be owned by Groups. * Group.members now includes inherited members: Updated behavior for the `members` getter method to include inherited members and have a more intuitive type definition. ## New Features ### New permissions check APIs New methods have been added to both `Account` and `Group` classes to improve permission handling: #### Permission checks The new `canRead`, `canWrite` and `canAdmin` methods on `Account` allow you to easily check if the account has specific permissions on a CoValue: const me = Account.getMe(); if (me.canAdmin(value)) { console.log("I can share value with others"); } else if (me.canWrite(value)) { console.log("I can edit value"); } else if (me.canRead(value)) { console.log("I can view value"); } else { console.log("I cannot access value"); } #### Getting the role of an account The `getRoleOf` method has been added to query the role of specific entities: const group = Group.create(); group.getRoleOf(me); // admin group.getRoleOf(Eve); // undefined group.addMember(Eve, "writer"); group.getRoleOf(Eve); // writer #### Group.revokeExtend We added a new method to revoke the extend of a Group: function addTrackToPlaylist(playlist: Playlist, track: MusicTrack) { const trackGroup = track._owner.castAs(Group); trackGroup.extend(playlist._owner, "reader"); // Grant read access to the track to the playlist accounts playlist.tracks.push(track); } function removeTrackFromPlaylist(playlist: Playlist, track: MusicTrack) { const trackGroup = track._owner.castAs(Group); trackGroup.revokeExtend(playlist._owner); // Revoke access to the track to the playlist accounts const index = playlist.tracks.findIndex(t => t.id === track.id); if (index !== -1) { playlist.tracks.splice(index, 1); } } ### Group.getParentGroups The `getParentGroups` method has been added to `Group` to get all the parent groups of a group. const childGroup = Group.create(); const parentGroup = Group.create(); childGroup.extend(parentGroup); console.log(childGroup.getParentGroups()); // [parentGroup] ## Breaking Changes ### Account Profile & Migrations The previous way of making the `Profile` migration work was to assume that the profile was always already there: export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps: { name: string, lastName: string }) { if (creationProps) { const { profile } = await this.ensureLoaded({ profile: {} }); profile.name = creationProps.name; profile.bookmarks = ListOfBookmarks.create([], profileGroup); } } } This was kind-of tricky to picture, and having different migration strategies for different CoValues was confusing. We changed the logic so the default profile is created only if you didn't provide one in your migration. This way you can use the same pattern for both `root` and `profile` migrations: export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps?: { name: string }) { if (this.profile === undefined) { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); this.profile = MyAppProfile.create({ name: creationProps?.name, bookmarks: ListOfBookmarks.create([], profileGroup), }, profileGroup); } } } Warning If you provide a custom `Profile` in your `Account` schema and migration for a Worker account, make sure to also add `everyone` as member with `reader` role to the owning group. Failing to do so will prevent any account from sending messages to the Worker's Inbox. ### Dropped support for Accounts owning Profiles Starting from `0.11.0` `Profile`s can only be owned by `Group`s. Note Existing profiles owned by `Account`s will still work, but you will get incorrect types when accessing a `Profile`'s `_owner`. ### Member Inheritance Changes The behavior of groups' `members` getter method has been updated to return both direct members and inherited ones from ancestor groups. This might affect your application if you were relying on only direct members being returned. /** * The following pseudocode only illustrates the inheritance logic, * the actual implementation is different. */ const parentGroup = Group.create(); parentGroup.addMember(John, "admin"); const childGroup = Group.create(); childGroup.addMember(Eve, "admin"); childGroup.extend(parentGroup); console.log(childGroup.members); // Before 0.11.0 // [Eve] // After 0.11.0 // [Eve, John] Additionally: * now `Group.members` doesn't include the `everyone` member anymore * the account type in `Group.members` is now the globally registered Account schema and we have removed the `co.members` way to define an AccountSchema for members If you need to explicitly check if "everyone" is a member of a group, you can use the `getRoleOf` method instead: if (group.getRoleOf("everyone")) { console.log("Everyone has access to the group"); } #### Migration Steps 1. Review your member querying logic to account for inherited members. 2. Update your permission checking code to utilize the new `hasPermissions` and `getRoleOf` methods. 3. Consider implementing `"everyone"` role checks where appropriate. ### Removed auto-update of `profile.name` in `usePasskeyAuth` The `usePasskeyAuth` hook now doesn't update the `profile.name` if the provided username is empty. ## Troubleshooting > I'm getting the following error: `Error: Profile must be owned by a Group` If you previously forced a migration of your `Account` schema to include a custom `Profile`, and assigned its ownership to an `Account`, you need to recreate your profile code and assign it to a `Group` instead. export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); override async migrate() { // ... const me = await this.ensureLoaded({ profile: {}, }); if ((me.profile._owner as Group | Account)._type === "Account") { const profileGroup = Group.create(); profileGroup.addMember("everyone", "reader"); // recreate your profile here... me.profile = Profile.create( { name: me.profile.name, }, profileGroup, ); } } } --- ## Page: https://jazz.tools/docs/react-native-expo/upgrade/react-native-local-persistence Version 0.9.2 introduces local persistence for React Native apps using SQLite. In version 0.12, we've separated the implementations for Expo and framework-less React Native. If you are upgrading from a version before 0.9.2, you need to enable local persistence by following the steps below. Local persistence is enabled by default in version 0.12 and higher. Depending on whether you're using Expo or framework-less React Native, you'll need to follow different steps: As SQLite package, Expo uses `expo-sqlite`. Install it with: As SQLite package, we use `@op-engineering/op-sqlite` and `react-native-mmkv` for key-value storage: In version 0.12 and higher, you need to use either the `jazz-expo` package (for Expo) or the `jazz-react-native` package (for framework-less React Native). Local persistence is enabled by default in both packages. <JazzProvider auth={auto} storage="sqlite" peer="wss://cloud.jazz.tools/?key=you@example.com" AccountSchema={MyAppAccount} > <App /> </JazzProvider> --- ## Page: https://jazz.tools/docs/react-native-expo/schemas/covalues **CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app. As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.** **Think of CoValues as "super-fast Git for lots of tiny data."** * CoValues keep their full edit histories, from which they derive their "current state". * The fact that this happens in an eventually-consistent way makes them CRDTs. * Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's edit metadata. CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams. ## Start your app with a schema Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app. This helps correctness and development speed, but is particularly important... * when you evolve your app and need migrations * when different clients and server workers collaborate on CoValues and need to make compatible changes Thinking about the shape of your data is also a great first step to model your app. Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other. Jazz makes it quick to declare schemas, since they are simple TypeScript classes: export class TodoProject extends CoMap { title = co.string; tasks = co.ref(ListOfTasks); } Here you can see how we extend a CoValue type and use `co` for declaring (collaboratively) editable fields. This means that schema info is available for type inference _and_ at runtime. Classes might look old-fashioned, but Jazz makes use of them being both types and values in TypeScript, letting you refer to either with a single definition and import. import { TodoProject, ListOfTasks } from "./schema"; const project: TodoProject = TodoProject.create( { title: "New Project", tasks: ListOfTasks.create([], Group.create()), }, Group.create() ); ## Types of CoValues ### `CoMap` (declaration) CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects. (Collaborative editing follows a last-write-wins strategy per-key.) You can either declare struct-like CoMaps: class Person extends CoMap { name = co.string; age = co.number; pet = co.optional.ref(Pet); } Or record-like CoMaps (key-value pairs, where keys are always `string`): class ColorToHex extends CoMap.Record(co.string) {} class ColorToFruit extends CoMap.Record(co.ref(Fruit)) {} See the corresponding sections for creating, subscribing/loading, reading from and writing to CoMaps. ### `CoList` (declaration) CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.) You define them by specifying the type of the items they contain: class ListOfColors extends CoList.Of(co.string) {} class ListOfTasks extends CoList.Of(co.ref(Task)) {} See the corresponding sections for creating, subscribing/loading, reading from and writing to CoLists. ### `CoFeed` (declaration) CoFeeds are a special CoValue type that represent a feed of values for a set of users / sessions. (Each session of a user gets its own append-only feed.) They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc. You define them by specifying the type of feed item: class FeedOfTasks extends CoFeed.Of(co.ref(Task)) {} See the corresponding sections for creating, subscribing/loading, reading from and writing to CoFeeds. ### `FileStream` (declaration) FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.) They allow you to upload and reference files, images, etc. You typically don't need to declare or extend them yourself, you simply refer to the built-in `FileStream` from another CoValue: import { FileStream } from "jazz-tools"; class UserProfile extends CoMap { name = co.string; avatar = co.ref(FileStream); } See the corresponding sections for creating, subscribing/loading, reading from and writing to FileStreams. ### `SchemaUnion` (declaration) SchemaUnion is a helper type that allows you to load and refer to multiple subclasses of a CoMap schema, distinguished by a discriminating field. You declare them with a base class type and discriminating lambda, in which you have access to the `RawCoMap`, on which you can call `get` with the field name to get the discriminating value. import { SchemaUnion, CoMap } from "jazz-tools"; class BaseWidget extends CoMap { type = co.string; } class ButtonWidget extends BaseWidget { type = co.literal("button"); label = co.string; } class SliderWidget extends BaseWidget { type = co.literal("slider"); min = co.number; max = co.number; } const WidgetUnion = SchemaUnion.Of<BaseWidget>((raw) => { switch (raw.get("type")) { case "button": return ButtonWidget; case "slider": return SliderWidget; default: throw new Error("Unknown widget type"); } }); See the corresponding sections for creating, subscribing/loading and narrowing SchemaUnions. ## CoValue field/item types Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain. ### Primitive fields You can declare primitive field types using the `co` declarer: import { co } from "jazz-tools"; export class Person extends CoMap { title = co.string; } export class ListOfColors extends CoList.Of(co.string) {} Here's a quick overview of the primitive types you can use: co.string; co.number; co.boolean; co.null; co.Date; co.literal("waiting", "ready"); Finally, for more complex JSON data, that you _don't want to be collaborative internally_ (but only ever update as a whole), you can use `co.json<T>()`: co.json<{ name: string }>(); For more detail, see the API Reference for the `co` field declarer. ### Refs to other CoValues To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other. Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert. The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in Subscribing and Deep Loading. In Schemas, you declare Refs using the `co.ref<T>()` declarer: class Company extends CoMap { members = co.ref(ListOfPeople); } class ListOfPeople extends CoList.Of(co.ref(Person)) {} #### Optional Refs ⚠️ If you want to make a referenced CoValue field optional, you _have to_ use `co.optional.ref<T>()`: ⚠️ class Person extends CoMap { pet = co.optional.ref(Pet); } ### Computed fields & methods Since CoValue schemas are based on classes, you can easily add computed fields and methods: class Person extends CoMap { firstName = co.string; lastName = co.string; dateOfBirth = co.Date; get name() { return `${this.firstName} ${this.lastName}`; } ageAsOf(date: Date) { return differenceInYears(date, this.dateOfBirth); } } --- ## Page: https://jazz.tools/docs/react-native-expo/schemas/accounts-and-migrations ## CoValues as a graph of data rooted in accounts Compared to traditional relational databases with tables and foreign keys, Jazz is more like a graph database, or GraphQL APIs — where CoValues can arbitrarily refer to each other and you can resolve references without having to do a join. (See Subscribing & deep loading). To find all data related to a user, the account acts as a root node from where you can resolve all the data they have access to. These root references are modeled explicitly in your schema, distinguishing between data that is typically public (like a user's profile) and data that is private (like their messages). ### `Account.root` - private data a user cares about Every Jazz app that wants to refer to per-user data needs to define a custom root `CoMap` schema and declare it in a custom `Account` schema as the `root` field: import { Account, CoMap } from "jazz-tools"; export class MyAppAccount extends Account { root = co.ref(MyAppRoot); } export class MyAppRoot extends CoMap { myChats = co.ref(ListOfChats); myContacts = co.ref(ListOfAccounts); } // Register the Account schema so `useAccount` returns our custom `MyAppAccount` declare module "jazz-react" { interface Register { Account: MyAppAccount; } } ### `Account.profile` - public data associated with a user The built-in `Account` schema class comes with a default `profile` field, which is a CoMap (in a Group with `"everyone": "reader"` - so publicly readable permissions) that is set up for you based on the username the `AuthMethod` provides on account creation. Their pre-defined schemas roughly look like this: // ...somehwere in jazz-tools itself... export class Account extends Group { profile = co.ref(Profile); } export class Profile extends CoMap { name = co.string; } If you want to keep the default `Profile` schema, but customise your account's private `root`, all you have to do is define a new `root` field in your account schema: (You don't have to explicitly re-define the `profile` field, but it makes it more readable that the Account contains both `profile` and `root`) import { Account, Profile } from "jazz-tools"; export class MyAppAccount extends Account { profile = co.ref(Profile); root = co.ref(MyAppRoot); } If you want to extend the `profile` to contain additional fields (such as an avatar `ImageDefinition`), you can declare your own profile schema class that extends `Profile`: import { Account, Profile, ImageDefinition } from "jazz-tools"; export class MyAppAccount extends Account { profile = co.ref(MyAppProfile); root = co.ref(MyAppRoot); } export class MyAppRoot extends CoMap { myChats = co.ref(ListOfChats); myContacts = co.ref(ListOfAccounts); } export class MyAppProfile extends Profile { name = co.string; // compatible with default Profile schema avatar = co.optional.ref(ImageDefinition); } // Register the Account schema so `useAccount` returns our custom `MyAppAccount` declare module "jazz-react" { interface Register { Account: MyAppAccount; } } ## Resolving CoValues starting at `profile` or `root` ## Populating and evolving `root` and `profile` schemas with migrations As you develop your app, you'll likely want to * initialise data in a user's `root` and `profile` * add more data to your `root` and `profile` schemas You can achieve both by overriding the `migrate()` method on your `Account` schema class. ### When migrations run Migrations are run after account creation and every time a user logs in. Jazz waits for the migration to finish before passing the account to your app's context. ### Initialising user data after account creation export class MyAppAccount extends Account { root = co.ref(MyAppRoot); profile = co.ref(MyAppProfile); async migrate(this: MyAppAccount, creationProps?: { name: string }) { // we specifically need to check for undefined, // because the root might simply be not loaded (`null`) yet if (this.root === undefined) { this.root = MyAppRoot.create({ // Using a group to set the owner is always a good idea. // This way if in the future we want to share // this coValue we can do so easily. myChats: ListOfChats.create([], Group.create()), myContacts: ListOfAccounts.create([], Group.create()) }); } if (this.profile === undefined) { const profileGroup = Group.create(); // Unlike the root, we want the profile to be publicly readable. profileGroup.addMember("everyone", "reader"); this.profile = MyAppProfile.create({ name: creationProps?.name, bookmarks: ListOfBookmarks.create([], profileGroup), }, profileGroup); } } } ### Adding/changing fields to `root` and `profile` To add new fields to your `root` or `profile` schemas, amend their corresponding schema classes with new fields, and then implement a migration that will populate the new fields for existing users (by using initial data, or by using existing data from old fields). To do deeply nested migrations, you might need to use the asynchronous `ensureLoaded()` method before determining whether the field already exists, or is simply not loaded yet. Now let's say we want to add a `myBookmarks` field to the `root` schema: export class MyAppAccount extends Account { root = co.ref(MyAppRoot); async migrate(this: MyAppAccount) { if (this.root === undefined) { this.root = MyAppRoot.create({ myChats: ListOfChats.create([], Group.create()), myContacts: ListOfAccounts.create([], Group.create()) }); } // We need to load the root field to check for the myContacts field const { root } = await this.ensureLoaded({ root: {}, }); // we specifically need to check for undefined, // because myBookmarks might simply be not loaded (`null`) yet if (root.myBookmarks === undefined) { root.myBookmarks = ListOfBookmarks.create([], Group.create()); } } } --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/comaps CoMaps are key-value objects that work like JavaScript objects. You can access properties with dot notation and define typed fields that provide TypeScript safety. They're ideal for structured data that needs type validation. ## Creating CoMaps CoMaps are typically defined by extending the `CoMap` class and specifying primitive fields using the `co` declarer (see Defining schemas: CoValues for more details on primitive fields): class Project extends CoMap { name = co.string; startDate = co.Date; status = co.literal("planning", "active", "completed"); coordinator = co.optional.ref(Member); } You can create either struct-like CoMaps with fixed fields (as above) or record-like CoMaps for key-value pairs: class Inventory extends CoMap.Record(co.number) {} To instantiate a CoMap: const project = Project.create({ name: "Spring Planting", startDate: new Date("2025-03-15"), status: "planning", }); const inventory = Inventory.create({ tomatoes: 48, basil: 12, }); ### Ownership When creating CoMaps, you can specify ownership to control access: // Create with default owner (current user) const privateProject = Project.create({ name: "My Herb Garden", startDate: new Date("2025-04-01"), status: "planning", }); // Create with shared ownership const gardenGroup = Group.create(); gardenGroup.addMember(memberAccount, "writer"); const communityProject = Project.create( { name: "Community Vegetable Plot", startDate: new Date("2025-03-20"), status: "planning", }, { owner: gardenGroup }, ); ## Reading from CoMaps CoMaps can be accessed using familiar JavaScript object notation: console.log(project.name); // "Spring Planting" console.log(project.status); // "planning" ### Handling Optional Fields Optional fields require checks before access: if (project.coordinator) { console.log(project.coordinator.name); // Safe access } ### Working with Record CoMaps For record-type CoMaps, you can access values using bracket notation: const inventory = Inventory.create({ tomatoes: 48, peppers: 24, basil: 12 }); console.log(inventory["tomatoes"]); // 48 ## Updating CoMaps Updating CoMap properties uses standard JavaScript assignment: project.name = "Spring Vegetable Garden"; // Update name project.startDate = new Date("2025-03-20"); // Update date ### Type Safety CoMaps are fully typed in TypeScript, giving you autocomplete and error checking: project.name = "Spring Vegetable Planting"; // ✓ Valid string project.startDate = "2025-03-15"; // ✗ Type error: expected Date ### Deleting Properties You can delete properties from CoMaps: delete inventory["basil"]; // Remove a key-value pair // For optional fields in struct-like CoMaps project.coordinator = null; // Remove the reference ## Best Practices ### Structuring Data * Use struct-like CoMaps for entities with fixed, known properties * Use record-like CoMaps for dynamic key-value collections * Group related properties into nested CoMaps for better organization ### Common Patterns #### Using Computed Properties CoMaps support computed properties and methods: class ComputedProject extends CoMap { name = co.string; startDate = co.Date; endDate = co.optional.Date; get isActive() { const now = new Date(); return now >= this.startDate && (!this.endDate || now <= this.endDate); } formatDuration(format: "short" | "full") { const start = this.startDate.toLocaleDateString(); if (!this.endDate) { return format === "full" ? `Started on ${start}, ongoing` : `From ${start}`; } const end = this.endDate.toLocaleDateString(); return format === "full" ? `From ${start} to ${end}` : `${(this.endDate.getTime() - this.startDate.getTime()) / 86400000} days`; } } // ... console.log(computedProject.isActive); // false console.log(computedProject.formatDuration("short")); // "3 days" --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/colists CoLists are ordered collections that work like JavaScript arrays. They provide indexed access, iteration methods, and length properties, making them perfect for managing sequences of items. ## Creating CoLists CoLists are defined by specifying the type of items they contain: class ListOfResources extends CoList.Of(co.string) {} class ListOfTasks extends CoList.Of(co.ref(Task)) {} To create a `CoList`: // Create an empty list const resources = ListOfResources.create([]); // Create a list with initial items const tasks = ListOfTasks.create([ Task.create({ title: "Prepare soil beds", status: "in-progress" }), Task.create({ title: "Order compost", status: "todo" }) ]); Like other CoValues, you can specify ownership when creating CoLists. ## Reading from CoLists CoLists support standard array access patterns: // Access by index const firstTask = tasks[0]; console.log(firstTask.title); // "Prepare soil beds" // Get list length console.log(tasks.length); // 2 // Iteration tasks.forEach(task => { console.log(task.title); // "Prepare soil beds" // "Order compost" }); // Array methods const todoTasks = tasks.filter(task => task.status === "todo"); console.log(todoTasks.length); // 1 ## Updating CoLists Update `CoList`s just like you would JavaScript arrays: // Add items resources.push("Tomatoes"); // Add to end resources.unshift("Lettuce"); // Add to beginning tasks.push(Task.create({ // Add complex items title: "Install irrigation", status: "todo" })); // Replace items resources[0] = "Cucumber"; // Replace by index // Modify nested items tasks[0].status = "complete"; // Update properties of references ### Deleting Items Remove specific items by index with `splice`, or remove the first or last item with `pop` or `shift`: // Remove 2 items starting at index 1 resources.splice(1, 2); console.log(resources); // ["Cucumber", "Peppers"] // Remove a single item at index 0 resources.splice(0, 1); console.log(resources); // ["Peppers"] // Remove items const lastItem = resources.pop(); // Remove and return last item resources.shift(); // Remove first item ### Array Methods `CoList`s support the standard JavaScript array methods you already know: // Add multiple items at once resources.push("Tomatoes", "Basil", "Peppers"); // Find items const basil = resources.find(r => r === "Basil"); // Filter (returns regular array, not a CoList) const tItems = resources.filter(r => r.startsWith("T")); console.log(tItems); // ["Tomatoes"] // Sort (modifies the CoList in-place) resources.sort(); console.log(resources); // ["Basil", "Peppers", "Tomatoes"] ### Type Safety CoLists maintain type safety for their items: // TypeScript catches type errors resources.push("Carrots"); // ✓ Valid string resources.push(42); // ✗ Type error: expected string // For lists of references tasks.forEach(task => { console.log(task.title); // TypeScript knows task has title }); ## Best Practices ### Common Patterns #### List Rendering CoLists work well with UI rendering libraries: // React example function TaskList({ tasks }) { return ( <ul> {tasks.map(task => ( <li key={task.id}> {task.title} - {task.status} </li> ))} </ul> ); } #### Managing Relations CoLists can be used to create one-to-many relationships: class Project extends CoMap { name = co.string; tasks = co.ref(ListOfTasks); } // ... const task = Task.create({ title: "Plant seedlings", status: "todo", project: project, // Add a reference to the project }); // Add a task to a garden project project.tasks.push(task); // Access the project from the task console.log(task.project); // { name: "Garden Project", tasks: [task] } --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/cofeeds CoFeeds are append-only data structures that track entries from different user sessions and accounts. Unlike other CoValues where everyone edits the same data, CoFeeds maintain separate streams for each session. Each account can have multiple sessions (different browser tabs, devices, or app instances), making CoFeeds ideal for building features like activity logs, presence indicators, and notification systems. The following examples demonstrate a practical use of CoFeeds: * Multi-cursors - track user presence on a canvas with multiple cursors and out of bounds indicators * Reactions - store per-user emoji reaction using a CoFeed ## Creating CoFeeds CoFeeds are defined by specifying the type of items they'll contain, similar to how you define CoLists: // Define a schema for feed items class Activity extends CoMap { timestamp = co.Date; action = co.literal("watering", "planting", "harvesting", "maintenance"); notes = co.optional.string; } // Define a feed of garden activities class ActivityFeed extends CoFeed.Of(co.ref(Activity)) {} // Create a feed instance const activityFeed = ActivityFeed.create([]); Like other CoValues, you can specify ownership when creating CoFeeds. ## Reading from CoFeeds Since CoFeeds are made of entries from users over multiple sessions, you can access entries in different ways - from a specific user's session or from their account as a whole. ### Per-Session Access To retrieve entries from a session: // Get the feed for a specific session const sessionFeed = activityFeed.perSession[sessionId]; // Latest entry from a session console.log(sessionFeed.value.action); // "watering" For convenience, you can also access the latest entry from the current session with `inCurrentSession`: // Get the feed for the current session const currentSessionFeed = activityFeed.inCurrentSession; // Latest entry from the current session console.log(currentSessionFeed.value.action); // "harvesting" ### Per-Account Access To retrieve entries from a specific account you can use bracket notation with the account ID: // Get the feed for a specific account const accountFeed = activityFeed[accountId]; // Latest entry from the account console.log(accountFeed.value.action); // "watering" For convenience, you can also access the latest entry from the current account with `byMe`: // Get the feed for the current account const myLatestEntry = activityFeed.byMe; // Latest entry from the current account console.log(myLatestEntry.value.action); // "harvesting" ### Feed Entries #### All Entries To retrieve all entries from a CoFeed: // Get the feeds for a specific account and session const accountFeed = activityFeed[accountId]; const sessionFeed = activityFeed.perSession[sessionId]; // Iterate over all entries from the account for (const entry of accountFeed.all) { console.log(entry.value); } // Iterate over all entries from the session for (const entry of sessionFeed.all) { console.log(entry.value); } #### Latest Entry To retrieve the latest entry from a CoFeed, ie. the last update: // Get the latest entry from the current account const latestEntry = activityFeed.byMe; console.log(`My last action was ${latestEntry.value.action}`); // "My last action was harvesting" // Get the latest entry from each account const latestEntriesByAccount = Object.values(activityFeed).map(entry => ({ accountName: entry.by?.profile?.name, value: entry.value, })); ## Writing to CoFeeds CoFeeds are append-only; you can add new items, but not modify existing ones. This creates a chronological record of events or activities. ### Adding Items // Log a new activity activityFeed.push(Activity.create({ timestamp: new Date(), action: "watering", notes: "Extra water for new seedlings" })); Each item is automatically associated with the current user's session. You don't need to specify which session the item belongs to - Jazz handles this automatically. ### Understanding Session Context Each entry is automatically added to the current session's feed. When a user has multiple open sessions (like both a mobile app and web browser), each session creates its own separate entries: // On mobile device: fromMobileFeed.push(Activity.create({ timestamp: new Date(), action: "harvesting", location: "Vegetable patch" })); // On web browser (same user): fromBrowserFeed.push(Activity.create({ timestamp: new Date(), action: "planting", location: "Flower bed" })); // These are separate entries in the same feed, from the same account ## Metadata CoFeeds support metadata, which is useful for tracking information about the feed itself. ### By The `by` property is the account that made the entry. const accountFeed = activityFeed[accountId]; // Get the account that made the last entry console.log(accountFeed?.by); ### MadeAt The `madeAt` property is a timestamp of when the entry was added to the feed. const accountFeed = activityFeed[accountId]; // Get the timestamp of the last update console.log(accountFeed?.madeAt); // Get the timestamp of each entry for (const entry of accountFeed.all) { console.log(entry.madeAt); } ## Best Practices ### When to Use CoFeeds * **Use CoFeeds when**: * You need to track per-user/per-session data * Time-based information matters (activity logs, presence) * **Consider alternatives when**: * Data needs to be collaboratively edited (use CoMaps or CoLists) * You need structured relationships (use CoMaps/CoLists with references) --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/filestreams FileStreams handle binary data in Jazz applications - think documents, audio files, and other non-text content. They're essentially collaborative versions of `Blob`s that sync automatically across devices. Use FileStreams when you need to: * Distribute documents across devices * Store audio or video files * Sync any binary data between users **Note:** For images specifically, Jazz provides the higher-level `ImageDefinition` abstraction which manages multiple image resolutions - see the ImageDefinition documentation for details. FileStreams provide automatic chunking when using the `createFromBlob` method, track upload progress, and handle MIME types and metadata. In your schema, reference FileStreams like any other CoValue: import { CoMap, FileStream, co } from "jazz-tools"; class Document extends CoMap { title = co.string; file = co.ref(FileStream); // Store a document file } ## Creating FileStreams There are two main ways to create FileStreams: creating empty ones for manual data population or creating directly from existing files or blobs. ### Creating from Blobs and Files For files from input elements or drag-and-drop interfaces, use `createFromBlob`: // From a file input const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', async () => { const file = fileInput.files[0]; if (file) { // Create FileStream from user-selected file const fileStream = await FileStream.createFromBlob(file); // Or with progress tracking for better UX const fileWithProgress = await FileStream.createFromBlob(file, { onProgress: (progress) => { // progress is a value between 0 and 1 const percent = Math.round(progress * 100); console.log(`Upload progress: ${percent}%`); progressBar.style.width = `${percent}%`; } }); } }); ### Creating Empty FileStreams Create an empty FileStream when you want to manually add binary data in chunks: import { FileStream } from "jazz-tools"; // Create a new empty FileStream const fileStream = FileStream.create(); ## Reading from FileStreams `FileStream`s provide several ways to access their binary content, from raw chunks to convenient Blob objects. ### Getting Raw Data Chunks To access the raw binary data and metadata: // Get all chunks and metadata const fileData = fileStream.getChunks(); if (fileData) { console.log(`MIME type: ${fileData.mimeType}`); console.log(`Total size: ${fileData.totalSizeBytes} bytes`); console.log(`File name: ${fileData.fileName}`); console.log(`Is complete: ${fileData.finished}`); // Access raw binary chunks for (const chunk of fileData.chunks) { // Each chunk is a Uint8Array console.log(`Chunk size: ${chunk.length} bytes`); } } By default, `getChunks()` only returns data for completely synced `FileStream`s. To start using chunks from a `FileStream` that's currently still being synced use the `allowUnfinished` option: // Get data even if the stream isn't complete const partialData = fileStream.getChunks({ allowUnfinished: true }); ### Converting to Blobs For easier integration with web APIs, convert to a `Blob`: // Convert to a Blob const blob = fileStream.toBlob(); if (blob) { // Use with URL.createObjectURL const url = URL.createObjectURL(blob); // Create a download link const link = document.createElement('a'); link.href = url; link.download = fileData?.fileName || 'document.pdf'; link.click(); // Clean up when done URL.revokeObjectURL(url); } ### Loading FileStreams as Blobs You can directly load a `FileStream` as a `Blob` when you only have its ID: // Load directly as a Blob when you have an ID const blob = await FileStream.loadAsBlob(fileStreamId); // By default, waits for complete uploads // For in-progress uploads: const partialBlob = await FileStream.loadAsBlob(fileStreamId, { allowUnfinished: true }); ### Checking Completion Status Check if a `FileStream` is fully synced: if (fileStream.isBinaryStreamEnded()) { console.log('File is completely synced'); } else { console.log('File upload is still in progress'); } ## Writing to FileStreams When creating a `FileStream` manually (not using `createFromBlob`), you need to manage the upload process yourself. This gives you more control over chunking and progress tracking. ### The Upload Lifecycle `FileStream` uploads follow a three-stage process: 1. **Start** - Initialize with metadata 2. **Push** - Send one or more chunks of data 3. **End** - Mark the stream as complete ### Starting a `FileStream` Begin by providing metadata about the file: // Create an empty FileStream const fileStream = FileStream.create(); // Initialize with metadata fileStream.start({ mimeType: 'application/pdf', // MIME type (required) totalSizeBytes: 1024 * 1024 * 2, // Size in bytes (if known) fileName: 'document.pdf' // Original filename (optional) }); ### Pushing Data Add binary data in chunks - this helps with large files and progress tracking: // Create a sample Uint8Array (in real apps, this would be file data) const data = new Uint8Array([...]); // For large files, break into chunks (e.g., 100KB each) const chunkSize = 1024 * 100; for (let i = 0; i < data.length; i += chunkSize) { // Create a slice of the data const chunk = data.slice(i, i + chunkSize); // Push chunk to the FileStream fileStream.push(chunk); // Track progress const progress = Math.min(100, Math.round((i + chunk.length) * 100 / data.length)); console.log(`Upload progress: ${progress}%`); } ### Completing the Upload Once all chunks are pushed, mark the `FileStream` as complete: // Finalize the upload fileStream.end(); console.log('Upload complete!'); ## Subscribing to `FileStream`s Like other CoValues, you can subscribe to `FileStream`s to get notified of changes as they happen. This is especially useful for tracking upload progress when someone else is uploading a file. ### Loading by ID Load a `FileStream` when you have its ID: // Load a FileStream by ID const fileStream = await FileStream.load(fileStreamId, []); if (fileStream) { console.log('FileStream loaded successfully'); // Check if it's complete if (fileStream.isBinaryStreamEnded()) { // Process the completed file const blob = fileStream.toBlob(); } } ### Subscribing to Changes Subscribe to a `FileStream` to be notified when chunks are added or when the upload is complete: // Subscribe to a FileStream by ID const unsubscribe = FileStream.subscribe(fileStreamId, [], (fileStream) => { // Called whenever the FileStream changes console.log('FileStream updated'); // Get current status const chunks = fileStream.getChunks({ allowUnfinished: true }); if (chunks) { const uploadedBytes = chunks.chunks.reduce((sum, chunk) => sum + chunk.length, 0); const totalBytes = chunks.totalSizeBytes || 1; const progress = Math.min(100, Math.round(uploadedBytes * 100 / totalBytes)); console.log(`Upload progress: ${progress}%`); if (fileStream.isBinaryStreamEnded()) { console.log('Upload complete!'); // Now safe to use the file const blob = fileStream.toBlob(); // Clean up the subscription if we're done unsubscribe(); } } }); ### Waiting for Upload Completion If you need to wait for a `FileStream` to be fully synchronized across devices: // Wait for the FileStream to be fully synced await fileStream.waitForSync({ timeout: 5000 // Optional timeout in ms }); console.log('FileStream is now synced to all connected devices'); This is useful when you need to ensure that a file is available to other users before proceeding with an operation. --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/imagedef `ImageDefinition` is a specialized CoValue designed specifically for managing images in Jazz. It extends beyond basic file storage by supporting multiple resolutions of the same image, optimized for mobile devices. **Note**: This guide applies to both Expo and framework-less React Native implementations. The functionality described here is identical regardless of which implementation you're using, though you'll need to import from the appropriate package (`jazz-expo` or `jazz-react-native`). Jazz offers several tools to work with images in React Native: * `createImage()` - function to create an `ImageDefinition` from a base64 image data URI * `ProgressiveImg` - React component to display an image with progressive loading * `useProgressiveImg` - React hook to load an image in your own component For examples of use, see our example apps: * React Native Chat (Framework-less implementation) * React Native Expo Chat (Expo implementation) * React Native Expo Clerk Chat (Expo implementation with Clerk) ## Creating Images The easiest way to create and use images in your Jazz application is with the `createImage()` function: import { createImage } from "jazz-react-native-media-images"; import * as ImagePicker from 'expo-image-picker'; async function handleImagePicker() { try { // Launch the image picker const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, base64: true, quality: 1, }); if (!result.canceled) { const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`; // Creates ImageDefinition with multiple resolutions automatically const image = await createImage(base64Uri, { owner: me.profile._owner, maxSize: 2048, // Optional: limit maximum resolution }); // Store the image me.profile.image = image; } } catch (error) { console.error("Error creating image:", error); } } The `createImage()` function: * Creates an `ImageDefinition` with the right properties * Generates a small placeholder for immediate display * Creates multiple resolution variants of your image * Returns the created `ImageDefinition` ### Configuration Options You can configure `createImage()` with additional options: // Configuration options const options = { owner: me, // Owner for access control maxSize: 1024 // Maximum resolution to generate }; // Setting maxSize controls which resolutions are generated: // 256: Only creates the smallest resolution (256px on longest side) // 1024: Creates 256px and 1024px resolutions // 2048: Creates 256px, 1024px, and 2048px resolutions // undefined: Creates all resolutions including the original size const image = await createImage(base64Uri, options); ## Displaying Images with `ProgressiveImg` For a complete progressive loading experience, use the `ProgressiveImg` component: import { ProgressiveImg } from "jazz-react-native"; import { Image, StyleSheet } from "react-native"; function GalleryView({ image }) { return ( <ProgressiveImg image={image} // The image definition to load targetWidth={800} // Looks for the best available resolution for a 800px image > {({ src }) => ( <Image source={{ uri: src }} style={styles.galleryImage} resizeMode="cover" /> )} </ProgressiveImg> ); } const styles = StyleSheet.create({ galleryImage: { width: '100%', height: 200, borderRadius: 8, } }); The `ProgressiveImg` component handles: * Showing a placeholder while loading * Automatically selecting the appropriate resolution * Progressive enhancement as higher resolutions become available * Cleaning up resources when unmounted ## Using `useProgressiveImg` Hook For more control over image loading, you can implement your own progressive image component: import { useProgressiveImg } from "jazz-react-native"; import { Image, View, Text, ActivityIndicator } from "react-native"; function CustomImageComponent({ image }) { const { src, // Data URI containing the image data as a base64 string, // or a placeholder image URI res, // The current resolution originalSize // The original size of the image } = useProgressiveImg({ image: image, // The image definition to load targetWidth: 800 // Limit to resolutions up to 800px wide }); // When image is not available yet if (!src) { return ( <View style={{ height: 200, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f0f0f0' }}> <ActivityIndicator size="small" color="#0000ff" /> <Text style={{ marginTop: 10 }}>Loading image...</Text> </View> ); } // When using placeholder if (res === "placeholder") { return ( <View style={{ position: 'relative' }}> <Image source={{ uri: src }} style={{ width: '100%', height: 200, opacity: 0.7 }} resizeMode="cover" /> <ActivityIndicator size="large" color="#ffffff" style={{ position: 'absolute', top: '50%', left: '50%', marginLeft: -20, marginTop: -20 }} /> </View> ); } // Full image display with custom overlay return ( <View style={{ position: 'relative', width: '100%', height: 200 }}> <Image source={{ uri: src }} style={{ width: '100%', height: '100%' }} resizeMode="cover" /> <View style={{ position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(0,0,0,0.5)', padding: 8 }}> <Text style={{ color: 'white' }}>Resolution: {res}</Text> </View> </View> ); } ## Understanding ImageDefinition Behind the scenes, `ImageDefinition` is a specialized CoValue that stores: * The original image dimensions (`originalSize`) * An optional placeholder (`placeholderDataURL`) for immediate display * Multiple resolution variants of the same image as `FileStream`s Each resolution is stored with a key in the format `"widthxheight"` (e.g., `"1920x1080"`, `"800x450"`). // Structure of an ImageDefinition const image = ImageDefinition.create({ originalSize: [1920, 1080], placeholderDataURL: "data:image/jpeg;base64,/9j/4AAQSkZJRg...", }); // Accessing the highest available resolution const highestRes = image.highestResAvailable(); if (highestRes) { console.log(`Found resolution: ${highestRes.res}`); console.log(`Stream: ${highestRes.stream}`); } For more details on using `ImageDefinition` directly, see the VanillaJS docs. ### Fallback Behavior `highestResAvailable` returns the largest resolution that fits your constraints. If a resolution has incomplete data, it falls back to the next available lower resolution. const image = ImageDefinition.create({ originalSize: [1920, 1080], }); image["1920x1080"] = FileStream.create(); // Empty image upload image["800x450"] = await FileStream.createFromBlob(mediumSizeBlob); const highestRes = image.highestResAvailable(); console.log(highestRes.res); // 800x450 --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/schemaunions ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/subscription-and-loading Jazz's Collaborative Values (such as CoMaps or CoLists) work like reactive state. By subscribing to them, you can react to both local and remote updates. This is the main way to consume data in your application. Subscriptions also take care of loading CoValues that are not yet loaded locally and can do so _deeply_ — by resolving nested CoValues. To make use of this, we'll show you how to specify the depth of data you need with resolve queries. With each update you can also handle loading states and inaccessible CoValues. ## Manual subscriptions You can subscribe to a CoValue from anywhere in your code (if you have its ID) by using `CoValue.subscribe()`. class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Task.title: co<string>title = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.description: co<string>description = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.status: co<"todo" | "in-progress" | "completed">status = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.literal<["todo", "in-progress", "completed"]>(_lit_0: "todo", _lit_1: "in-progress", _lit_2: "completed"): co<"todo" | "in-progress" | "completed">literal("todo", "in-progress", "completed"); Task.assignedTo: co<string | undefined>assignedTo = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }optional.string: co<string | undefined>string; } // ... // Subscribe to a Task by ID const const unsubscribe: () => voidunsubscribe = class TaskTask.CoMap.subscribe<Task, true>(this: CoValueClass<Task>, id: ID<Task>, listener: (value: Task, unsubscribe: () => void) => void): () => void (+1 overload)Load and subscribe to a `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`/`CoFeed`/`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.@example```ts const unsub = Person.subscribe( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} }, (person) => console.log(person) ); ```@categorySubscription & Loadingsubscribe(const taskId: ID<Task>taskId, (updatedTask: TaskupdatedTask) => { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task updated:", updatedTask: TaskupdatedTask.Task.title: co<string>title); var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("New status:", updatedTask: TaskupdatedTask.Task.status: co<"todo" | "in-progress" | "completed">status); }); // Clean up when you're done const unsubscribe: () => voidunsubscribe(); If you already have a CoValue instance, you can subscribe to it by calling its `subscribe` method. const const task: Tasktask = class TaskTask.CoMap.create<Task>(this: CoValueClass<...>, init: { title: co<string> & (co<string> | undefined); description: co<string> & (co<string> | undefined); status: co<"todo" | "in-progress" | "completed"> & (co<...> | undefined); assignedTo?: string | ... 2 more ... | undefined; }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): TaskCreate a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.@example```ts const person = Person.create({ name: "Alice", age: 42, pet: cat, }, { owner: friendGroup }); ```@categoryCreationcreate({ title: co<string> & (co<string> | undefined)title: "Cut the grass", ...const otherProps: anyotherProps }); const const unsubscribe: () => voidunsubscribe = const task: Tasktask.CoMap.subscribe<Task, true>(this: Task, listener: (value: Task, unsubscribe: () => void) => void): () => void (+1 overload)Given an already loaded `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.@categorySubscription & Loadingsubscribe((updatedTask: TaskupdatedTask) => { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task updated:", updatedTask: TaskupdatedTask.Task.title: co<string>title); }); // Clean up when you're done const unsubscribe: () => voidunsubscribe(); ## Loading States and Permission Checking When subscribing to or loading a CoValue, you need to handle three possible states: * `undefined`: The initial loading state, indicating the value is being fetched * `null`: The CoValue was not found or is not accessible (e.g., due to permissions) * `Value`: The successfully loaded CoValue instance This allows you to handle loading, error, and success states in your application: class TaskTask.CoMap.subscribe<Task, true>(this: CoValueClass<Task>, id: ID<Task>, listener: (value: Task, unsubscribe: () => void) => void): () => void (+1 overload)Load and subscribe to a `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`/`CoFeed`/`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.@example```ts const unsub = Person.subscribe( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} }, (person) => console.log(person) ); ```@categorySubscription & Loadingsubscribe(const taskId: ID<Task>taskId, (task: Tasktask) => { if (task: Tasktask === var undefinedundefined) { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task is loading..."); } else if (task: Tasktask === null) { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task not found or not accessible"); } else { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```@see[source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.@sincev0.1.100log("Task loaded:", task: Tasktask.Task.title: co<string>title); } }); ## Deep Loading When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. This is especially important when working with CoMaps that contain references to other CoValues or with CoLists that contain multiple items. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load. ### Resolve queries Resolve queries let you declare exactly which references to load and how deep to go using the `resolve` property: class class ProjectProject extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Project.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Project.tasks: co<ListOfTasks | null>tasks = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof ListOfTasks>(arg: typeof ListOfTasks | ((_raw: RawCoList<JsonValue, JsonObject | null>) => typeof ListOfTasks), options?: never) => co<...> (+1 overload)ref(class ListOfTasksListOfTasks); Project.owner: co<TeamMember | null>owner = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof TeamMember>(arg: typeof TeamMember | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof TeamMember), options?: never) => co<...> (+1 overload)ref(class TeamMemberTeamMember); } class class TaskTask extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { Task.title: co<string>title = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; Task.subtasks: co<ListOfTasks | null>subtasks = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof ListOfTasks>(arg: typeof ListOfTasks | ((_raw: RawCoList<JsonValue, JsonObject | null>) => typeof ListOfTasks), options?: never) => co<...> (+1 overload)ref(class ListOfTasksListOfTasks); Task.assignee: co<TeamMember | null | undefined>assignee = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }optional.ref: <typeof TeamMember>(arg: typeof TeamMember | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof TeamMember)) => co<...>ref(class TeamMemberTeamMember); } class class TeamMemberTeamMember extends class CoMapCoMaps are collaborative versions of plain objects, mapping string-like keys to values.@categoryDescriptionDeclaration Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`. Optional `co.ref(...)` fields must be marked with `{ optional: true }`. ```ts 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 }); } ```@categoryDescriptionContent You can access properties you declare on a `CoMap` (using `co`) as if they were normal properties on a plain object, using dot notation, `Object.keys()`, etc. ```ts person.name; person["age"]; person.age = 42; person.pet?.name; Object.keys(person); // => ["name", "age", "pet"] ```@categoryCoValuesCoMap { TeamMember.name: co<string>name = const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.string: co<string>string; } class class ListOfTasksListOfTasks extends class CoList<Item = any>CoLists are collaborative versions of plain arrays.@categoryDescriptionContent You can access items on a `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. ```ts colorList[0]; colorList[3] = "yellow"; colorList.push("Kawazaki Green"); colorList.splice(1, 1); ```@categoryCoValuesCoList.CoList<Item = any>.Of<co<Task | null>>(item: co<Task | null>): { new (options: { fromRaw: RawCoList; } | undefined): CoList<co<Task | null>>; ... 12 more ...; fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>; fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>; }Declare a `CoList` by subclassing `CoList.Of(...)` and passing the item schema using `co`.@example```ts class ColorList extends CoList.Of( co.string ) {} class AnimalList extends CoList.Of( co.ref(Animal) ) {} ```@categoryDeclarationOf(const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }@categorySchema definition@categorySchema definitionco.ref: <typeof Task>(arg: typeof Task | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof Task), options?: never) => co<...> (+1 overload)ref(class TaskTask)) {} // Load just the project, not its references const const project: Project | nullproject = await class ProjectProject.CoMap.load<Project, true>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId); if (!const project: Project | nullproject) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project not found or not accessible"); } // string - primitive fields are always loaded const project: Projectproject.Project.name: co<string>name; // undefined | null | ListOfTasks - non-requested references might not be loaded, or inaccessible const project: Projectproject.Project.tasks: co<ListOfTasks | null>tasks; // Load the project and shallowly load its list of tasks const const projectWithTasksShallow: ({ tasks: ListOfTasks; } & Project) | nullprojectWithTasksShallow = await class ProjectProject.CoMap.load<Project, { tasks: boolean; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: true } }); if (!const projectWithTasksShallow: ({ tasks: ListOfTasks; } & Project) | nullprojectWithTasksShallow) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project or required references not found or not accessible"); } // ListOfTasks - shallowly loaded const projectWithTasksShallow: { tasks: ListOfTasks; } & ProjectprojectWithTasksShallow.Project.tasks: ListOfTasks & co<ListOfTasks | null>tasks; // number - length of the list const projectWithTasksShallow: { tasks: ListOfTasks; } & ProjectprojectWithTasksShallow.Project.tasks: ListOfTasks & co<ListOfTasks | null>tasks.Array<T>.length: numberGets or sets the length of the array. This is a number one higher than the highest index in the array.length; // undefined | null | Task - items might not be loaded, or inaccessible const projectWithTasksShallow: { tasks: ListOfTasks; } & ProjectprojectWithTasksShallow.Project.tasks: ListOfTasks & co<ListOfTasks | null>tasks[0]; // Load the project and its tasks const const projectWithTasks: ({ tasks: Task[] & ListOfTasks; } & Project) | nullprojectWithTasks = await class ProjectProject.CoMap.load<Project, { tasks: { $each: boolean; }; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true } } }); if (!const projectWithTasks: ({ tasks: Task[] & ListOfTasks; } & Project) | nullprojectWithTasks) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project or required references not found or not accessible"); } // ListOfTasks - fully loaded const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks; // Task - fully loaded const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks[0]; // string - primitive fields are always loaded const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.title: co<string>title; // undefined | null | ListOfTasks - subtasks might not be loaded, or inaccessible const projectWithTasks: { tasks: Task[] & ListOfTasks; } & ProjectprojectWithTasks.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.subtasks: co<ListOfTasks | null>subtasks; // Load the project, its tasks, and their subtasks const const projectDeep: ({ tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & Project) | nullprojectDeep = await class ProjectProject.CoMap.load<Project, { tasks: { $each: { subtasks: { $each: boolean; }; assignee: boolean; }; }; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: { subtasks?: RefsToResolve<ListOfTasks, 10, [0, 0, 0]> | undefinedsubtasks: { $each: RefsToResolve<Task, 10, [0, 0, 0, 0]>$each: true }, assignee?: RefsToResolve<TeamMember, 10, [0, 0, 0]> | undefinedassignee: true } } } }); if (!const projectDeep: ({ tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & Project) | nullprojectDeep) { throw new var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)Error("Project or required references not found or not accessible"); } // string - primitive fields are always loaded const projectDeep: { tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & ProjectprojectDeep.Project.tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.subtasks: co<ListOfTasks | null> & Task[] & ListOfTaskssubtasks[0].Task.title: co<string>title; // undefined | null | TeamMember - since assignee is optional: // TeamMember - set and definitely loaded // null - set but unavailable/inaccessible // undefined - not set, or loading (in case of subscription) const projectDeep: { tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks; } & ProjectprojectDeep.Project.tasks: (Task & { subtasks: Task[] & ListOfTasks; assignee: TeamMember | undefined; })[] & ListOfTasks & co<ListOfTasks | null>tasks[0].Task.assignee: TeamMember | (TeamMember & CoMarker) | undefinedassignee; The resolve query defines which parts of the graph you want to load, making it intuitive to express complex loading patterns. ### Loading states and permissions When loading data with references, the load operation will fail if one of the references is unavailable or if the user doesn't have read access to it. Let's explore what happens in various scenarios: #### Resolved References When a user tries to load a reference they don't have access to: // If assignee is not accessible to the user: const const task: ({ assignee: TeamMember | undefined; } & Task) | nulltask = await class TaskTask.CoMap.load<Task, { assignee: boolean; }>(this: CoValueClass<...>, id: ID<Task>, options?: { resolve?: RefsToResolve<Task, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const taskId: ID<Task>taskId, { resolve?: RefsToResolve<Task, 10, []> | undefinedresolve: { assignee?: RefsToResolve<TeamMember, 10, [0]> | undefinedassignee: true } }); const task: ({ assignee: TeamMember | undefined; } & Task) | nulltask // => null The load operation will fail and return `null` if any requested reference is inaccessible. This maintains data consistency by ensuring all requested references are available before returning the object. The behavior is the same for optional and required references. #### List References When a list contains references to items the user can't access: // If any item in the list is not accessible: const const project: ({ tasks: Task[] & ListOfTasks; } & Project) | nullproject = await class ProjectProject.CoMap.load<Project, { tasks: { $each: boolean; }; }>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true } } }); const project: ({ tasks: Task[] & ListOfTasks; } & Project) | nullproject // => null If any item in a list is inaccessible to the user, the entire load operation will fail and return `null`. This is because lists expect all their items to be accessible - a partially loaded list could lead to data inconsistencies. #### Reading a non-resolved inaccessible reference When trying to load an object with an inaccessible reference without directly resolving it: const const project: Project | nullproject = await class ProjectProject.CoMap.load<Project, true>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(const projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: true }); const project: Project | nullproject // => Project // The user doesn't have access to the owner const project: Project | nullproject?.Project.owner: co<TeamMember | null> | undefinedowner // => always null The load operation will succeed and return the object, but the inaccessible reference will always be `null`. ## Type Safety with Resolved Type Jazz provides the `Resolved` type to help you define and enforce the structure of deeply loaded data in your application. This makes it easier to ensure that components receive the data they expect with proper TypeScript validation. The `Resolved` type is especially useful when passing data between components, as it guarantees that all necessary nested data has been loaded: Using the `Resolved` type helps catch errors at compile time rather than runtime, ensuring that your components and functions receive data with the proper resolution depth. This is especially useful for larger applications where data is passed between many components. ## Ensuring Data is Loaded Sometimes you need to make sure data is loaded before proceeding with an operation. The `ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth: async function function completeAllTasks(projectId: ID<Project>): Promise<void>completeAllTasks(projectId: ID<Project>projectId: type ID<T> = `co_z${string}` & IDMarker<T>IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.@example```ts type AccountID = ID<Account>; ```@categoryCoValuesID<class ProjectProject>) { // Ensure the project is loaded const const project: Project | nullproject = await class ProjectProject.CoMap.load<Project, true>(this: CoValueClass<...>, id: ID<Project>, options?: { resolve?: RefsToResolve<Project, 10, []> | undefined; loadAs?: Account | AnonymousJazzAgent; } | undefined): Promise<...>Load a `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`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.@example```ts const person = await Person.load( "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax", { pet: {} } ); ```@categorySubscription & Loadingload(projectId: ID<Project>projectId, { resolve?: RefsToResolve<Project, 10, []> | undefinedresolve: true }); if (!const project: Project | nullproject) return; // Ensure tasks are loaded const const loadedProject: { tasks: Task[] & ListOfTasks; } & ProjectloadedProject = await const project: Projectproject.CoMap.ensureLoaded<Project, { tasks: { $each: boolean; }; }>(this: Project, options: { resolve: RefsToResolve<Project, 10, []>; }): Promise<{ tasks: Task[] & ListOfTasks; } & Project>Given an already loaded `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.@categorySubscription & LoadingensureLoaded({ resolve: RefsToResolve<Project, 10, []>resolve: { tasks?: RefsToResolve<ListOfTasks, 10, [0]> | undefinedtasks: { $each: RefsToResolve<Task, 10, [0, 0]>$each: true } } }); // Now we can safely access and modify tasks const loadedProject: { tasks: Task[] & ListOfTasks; } & ProjectloadedProject.Project.tasks: Task[] & ListOfTasks & co<ListOfTasks | null>tasks.Array<T>.forEach(callbackfn: (value: Task, index: number, array: Task[]) => void, thisArg?: any): void (+1 overload)Performs the specified action for each element in an array.@paramcallbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.forEach(task: Tasktask => { task: Tasktask.Task.status: co<"todo" | "in-progress" | "completed">status = "completed"; }); } ## Best Practices 1. **Be explicit about resolution depths**: Always specify exactly what you need 2. **Use framework integrations**: They handle subscription lifecycle automatically 3. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done 4. **Handle all loading states**: Check for undefined (loading), null (not found), and success states 5. **Use the Resolved type**: Add compile-time type safety for components that require specific resolution patterns --- ## Page: https://jazz.tools/docs/react-native-expo/using-covalues/history ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native-expo/groups/intro Every CoValue has an owner, which can be a `Group` or an `Account`. You can use a `Group` to grant access to a CoValue to multiple users. These users can have different roles, such as "writer", "reader" or "admin". ## Creating a Group Here's how you can create a `Group`. import { Group } from "jazz-tools"; const group = Group.create(); The `Group` itself is a CoValue, and whoever owns it is the initial admin. You typically add members using public sharing or invites. But if you already know their ID, you can add them directly (see below). ## Adding group members by ID You can add group members by ID by using `Account.load` and `Group.addMember`. import { Group, Account } from "jazz-tools"; const group = Group.create(); const bob = await Account.load(bobsID, []); group.addMember(bob, "writer"); Note: if the account ID is of type `string`, because it comes from a URL parameter or something similar, you need to cast it to `ID<Account>` first: import { Group, Account, ID } from "jazz-tools"; const bob = await Account.load(bobsID as ID<Account>, []); group.addMember(bob, "writer"); ## Getting the Group of an existing CoValue You can get the group of an existing CoValue by using `coValue._owner`. const group = existingCoValue._owner; const newValue = MyCoMap.create( { color: "red"}, { owner: group } ); Because `._owner` can be an `Account` or a `Group`, in cases where you specifically need to use `Group` methods (such as for adding members or getting your own role), you can cast it to assert it to be a Group: import { Group } from "jazz-tools"; const group = existingCoValue._owner.castAs(Group); group.addMember(bob, "writer"); const role = group.getRoleOf(bob); ## Checking the permissions You can check the permissions of an account on a CoValue by using the `canRead`, `canWrite` and `canAdmin` methods. const value = await MyCoMap.load(valueID, {}); const me = Account.getMe(); if (me.canAdmin(value)) { console.log("I can share value with others"); } else if (me.canWrite(value)) { console.log("I can edit value"); } else if (me.canRead(value)) { console.log("I can view value"); } else { console.log("I cannot access value"); } To check the permissions of another account, you need to load it first: const value = await MyCoMap.load(valueID, {}); const bob = await Account.load(accountID, []); if (bob.canAdmin(value)) { console.log("Bob can share value with others"); } else if (bob.canWrite(value)) { console.log("Bob can edit value"); } else if (bob.canRead(value)) { console.log("Bob can view value"); } else { console.log("Bob cannot access value"); } --- ## Page: https://jazz.tools/docs/react-native-expo/groups/sharing You can share CoValues publicly by setting the `owner` to a `Group`, and granting access to "everyone". const group = Group.create(); group.addMember("everyone", "writer"); // *highlight* This is done in the chat example where anyone can join the chat, and send messages. You can grant users access to a CoValue by sending them an invite link. import { createInviteLink } from "jazz-expo"; createInviteLink(organization, "writer"); // or reader, or admin In your app, you need to handle this route, and let the user accept the invitation, as done here. useAcceptInvite({ invitedObjectSchema: PetPost, onAccept: (petPostID) => navigate("/pet/" + petPostID), }); --- ## Page: https://jazz.tools/docs/react-native-expo/groups/inheritance Groups can inherit members from other groups using the `extend` method. When a group extends another group, members of the parent group will become automatically part of the child group. ## Basic Usage Here's how to extend a group: const playlistGroup = Group.create(); const trackGroup = Group.create(); // This way track becomes visible to the members of playlist trackGroup.extend(playlistGroup); When you extend a group: * Members of the parent group get access to the child group * Their roles are inherited (with some exceptions, see below) * Removing a member from the parent group also removes their access to child groups ## Inheriting members but overriding their role In some cases you might want to inherit all members from a parent group but override/flatten their roles to the same specific role in the child group. You can do so by passing an "override role" as a second argument to `extend`: const organizationGroup = Group.create(); organizationGroup.addMember(bob, "admin"); const billingGroup = Group.create(); // This way the members of the organization can only read the billing data billingGroup.extend(organizationGroup, "reader"); The "override role" works in both directions: const parentGroup = Group.create(); parentGroup.addMember(bob, "reader"); parentGroup.addMember(alice, "admin"); const childGroup = Group.create(); childGroup.extend(parentGroup, "writer"); // Bob and Alice are now writers in the child group ## Multiple Levels of Inheritance Groups can be extended multiple levels deep: const grandParentGroup = Group.create(); const parentGroup = Group.create(); const childGroup = Group.create(); childGroup.extend(parentGroup); parentGroup.extend(grandParentGroup); Members of the grandparent group will get access to all descendant groups based on their roles. ## Permission Changes When you remove a member from a parent group, they automatically lose access to all child groups. We handle key rotation automatically to ensure security. // Remove member from parent await parentGroup.removeMember(bob); // Bob loses access to both parent and child groups ## Role Inheritance Rules If the account is already a member of the child group, it will get the more permissive role: const parentGroup = Group.create(); parentGroup.addMember(bob, "reader"); const childGroup = Group.create(); parentGroup.addMember(bob, "writer"); childGroup.extend(parentGroup); // Bob stays a writer because his role is higher // than the inherited reader role. When extending groups, only admin, writer and reader roles are inherited: const parentGroup = Group.create(); parentGroup.addMember(bob, "writeOnly"); const childGroup = Group.create(); childGroup.extend(parentGroup); // Bob does not become a member of the child group To extend a group: 1. The current account must be an admin in the child group 2. The current account must be a member of the parent group const companyGroup = company._owner.castAs(Group) const teamGroup = Group.create(); // Works only if I'm a member of companyGroup teamGroup.extend(companyGroup); ## Revoking a group extension You can revoke a group extension by using the `revokeExtend` method: const parentGroup = Group.create(); const childGroup = Group.create(); childGroup.extend(parentGroup); // Revoke the extension await childGroup.revokeExtend(parentGroup); ## Getting all parent groups You can get all the parent groups of a group by calling the `getParentGroups` method: const childGroup = Group.create(); const parentGroup = Group.create(); childGroup.extend(parentGroup); console.log(childGroup.getParentGroups()); // [parentGroup] ## Example: Team Hierarchy Here's a practical example of using group inheritance for team permissions: // Company-wide group const companyGroup = Group.create(); companyGroup.addMember(CEO, "admin"); // Team group with elevated permissions const teamGroup = Group.create(); teamGroup.extend(companyGroup); // Inherits company-wide access teamGroup.addMember(teamLead, "admin"); teamGroup.addMember(developer, "writer"); // Project group with specific permissions const projectGroup = Group.create(); projectGroup.extend(teamGroup); // Inherits team permissions projectGroup.addMember(client, "reader"); // Client can only read project items This creates a hierarchy where: * The CEO has admin access to everything * Team members get writer access to team and project content * Team leads get admin access to team and project content * The client can only read project content --- ## Page: https://jazz.tools/docs/react-native-expo/authentication/overview Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user. When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally. Without any additional steps the user can use Jazz normally, but they would be limited to use on only one device. To make Accounts work across devices, you can store/retrieve the account keys from an authentication method by using the corresponding hooks and providers. ## Passphrase-based authentication Passphrase authentication lets users log into any device using a Bitcoin-style passphrase. This means users are themselves responsible for storing the passphrase safely. The passphrase is generated from the local account certificate using a wordlist of your choice. You can find a set of ready-to-use wordlists in the bip39 repository. For example: import { View, TextInput, Button, Text } from 'react-native'; export function AuthModal({ open, onOpenChange }: AuthModalProps) { const [loginPassphrase, setLoginPassphrase] = useState(""); const auth = usePassphraseAuth({ wordlist: englishWordlist, }); if (auth.state === "signedIn") { return <Text>You are already signed in</Text>; } const handleSignUp = async () => { await auth.signUp(); onOpenChange(false); }; const handleLogIn = async () => { await auth.logIn(loginPassphrase); onOpenChange(false); }; return ( <View> <View> <Text>Your current passphrase</Text> <TextInput editable={false} value={auth.passphrase} multiline numberOfLines={5} /> </View> <Button title="I have stored my passphrase" onPress={handleSignUp} /> <View> <Text>Log in with your passphrase</Text> <TextInput value={loginPassphrase} onChangeText={setLoginPassphrase} placeholder="Enter your passphrase" multiline numberOfLines={5} required /> </View> <Button title="Log in" onPress={handleLogIn} /> </View> ); } You can try our passphrase authentication using our passphrase example or the todo list demo. ## Integration with Clerk Jazz can be used with Clerk to authenticate users. This authentication method is not fully local-first, because the login and signup need to be done while online. Clerk and anyone who is an admin in the app's Clerk org are trusted with the user's key secret and could impersonate them. However, once authenticated, your users won't need to interact with Clerk anymore, and are able to use all of Jazz's features without needing to be online. You can use the `JazzProviderWithClerk` component to wrap your app. Note the `__experimental_resourceCache` option. This helps render Clerk components when offline. import { JazzProviderWithClerk } from "jazz-expo/auth/clerk"; import { secureStore } from "@clerk/clerk-expo/secure-store"; function JazzAndAuth({ children }: { children: React.ReactNode }) { const clerk = useClerk(); return ( <JazzProviderWithClerk clerk={clerk} sync={{ peer: `wss://cloud.jazz.tools/?key=${apiKey}`, }} > {children} </JazzProviderWithClerk> ); } export default function RootLayout() { const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY; if (!publishableKey) { throw new Error( "Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env", ); } return ( <ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey} __experimental_resourceCache={secureStore} > <ClerkLoaded> <JazzAndAuth> <Slot /> </JazzAndAuth> </ClerkLoaded> </ClerkProvider> ); } Then you can use the Clerk auth methods to log in and sign up: import { SignInButton } from "@clerk/clerk-expo"; import { useAccount, useIsAuthenticated } from "jazz-expo"; export function AuthButton() { const { logOut } = useAccount(); const { signIn, setActive, isLoaded } = useSignIn(); const isAuthenticated = useIsAuthenticated(); if (isAuthenticated) { return <button onClick={() => logOut()}>Logout</button>; } // Login code with Clerk Expo } ## Migrating data from anonymous to authenticated account You may want allow your users to use your app without authenticating (a poll response for example). When _signing up_ their anonymous account is transparently upgraded using the provided auth method, keeping the data stored in the account intact. However, a user may realise that they already have an existing account _after using the app anonymously and having already stored data in the anonymous account_. When they now _log in_, by default the anonymous account will be discarded and this could lead to unexpected data loss. To avoid this situation we provide the `onAnonymousAccountDiscarded` handler to migrate the data from the anonymous account to the existing authenticated one. This is an example from our music player: export async function onAnonymousAccountDiscarded( anonymousAccount: MusicaAccount, ) { const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({ root: { rootPlaylist: { tracks: [{}], }, }, }); const me = await MusicaAccount.getMe().ensureLoaded({ root: { rootPlaylist: { tracks: [], }, }, }); for (const track of anonymousAccountRoot.rootPlaylist.tracks) { if (track.isExampleTrack) continue; const trackGroup = track._owner.castAs(Group); trackGroup.addMember(me, "admin"); me.root.rootPlaylist.tracks.push(track); } } To see how this works in reality we suggest you to try to upload a song in the music player demo and then try to log in with an existing account. ## Disable network sync for anonymous users You can disable network sync to make your app local-only under specific circumstances. For example, you may want to give the opportunity to non-authenticated users to try your app locally-only (incurring no sync traffic), then enable the network sync only when the user is authenticated: <JazzProvider sync={{ peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // This makes the app work in local mode when the user is anonymous when: "signedUp", }} > <App /> </JazzProvider> For more complex behaviours, you can manually control sync by statefully switching when between `"always"` and `"never"`. --- ## Page: https://jazz.tools/docs/react-native-expo/authentication/writing-your-own ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started. --- ## Page: https://jazz.tools/docs/react-native-expo/design-patterns/form Normally, we implement forms using the onSubmit handler, or by making a controlled form with useState, or by using special libraries like react-hook-form. In Jazz, we can do something simpler and more powerful, because CoValues give us reactive, persisted state which we can use to directly edit live objects, and represent auto-saved drafts. See the full example here. ## Updating a CoValue To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server, so we don't need to handle form submissions either. <input type="text" value={order.name} onChange={(e) => order.name = e.target.value} /> This means we can write update forms in fewer lines of code. ## Creating a CoValue However, when creating a CoValue, the CoValue does not exist yet, so we don't have the advantages previously mentioned. There's a way around this, and it provides unexpected benefits too. ### Using a Draft CoValue Let's say we have a CoValue called `BubbleTeaOrder`. We can create a "draft" CoValue, which is an empty version of a `BubbleTeaOrder`, that we can then modify when we are "creating" a new CoValue. A `DraftBubbleTeaOrder` is essentially a copy of `BubbleTeaOrder`, but with all the fields made optional. // schema.ts export class BubbleTeaOrder extends CoMap { name = co.string; } export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; } ## Writing the components in React Let's write the form component that will be used for both create and update. // OrderForm.tsx export function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }) { return ( <form onSubmit={onSave}> <label> Name <input type="text" value={order.name} onChange={(e) => (order.name = e.target.value)} required /> </label> {onSave && <button type="submit">Submit</button>} </form> ); } ### Writing the edit form To make the edit form, simply pass the `BubbleTeaOrder`. // EditOrder.tsx export function EditOrder(props: { id: ID<BubbleTeaOrder> }) { const order = useCoState(BubbleTeaOrder, props.id, []); if (!order) return; return <OrderForm order={order} />; } ### Writing the create form For the create form, we need to: 1. Create a draft order. 2. Edit the draft order. 3. Convert the draft order to a "real" order on submit. Here's how that looks like: // CreateOrder.tsx export function CreateOrder() { const { me } = useAccount(); const [draft, setDraft] = useState<DraftBubbleTeaOrder>(); useEffect(() => { setDraft(DraftBubbleTeaOrder.create({})); }, [me?.id]); const onSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!draft) return; const order = draft as BubbleTeaOrder; console.log("Order created:", order); }; if (!draft) return; return <OrderForm order={draft} onSave={onSave} />; } ## Validation In a `BubbleTeaOrder`, the `name` field is required, so it would be a good idea to validate this before turning the draft into a real order. Update the schema to include a `validate` method. // schema.ts export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; validate() { const errors: string[] = []; if (!this.name) { errors.push("Please enter a name."); } return { errors }; } } Then perform the validation on submit. // CreateOrder.tsx export function CreateOrder() { const { me } = useAccount(); const [draft, setDraft] = useState<DraftBubbleTeaOrder>(); useEffect(() => { setDraft(DraftBubbleTeaOrder.create({})); }, [me?.id]); const onSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!draft) return; const validation = draft.validate(); if (validation.errors.length > 0) { console.log(validation.errors); return; } const order = draft as BubbleTeaOrder; console.log("Order created:", order); }; if (!draft) return; return <OrderForm order={draft} onSave={onSave} />; } ## Saving the user's work-in-progress It turns out that using this pattern also provides a UX improvement. By storing the draft in the user's account, they can come back to it anytime without losing their work. 🙌 // schema.ts export class BubbleTeaOrder extends CoMap { name = co.string; } export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; } export class AccountRoot extends CoMap { draft = co.ref(DraftBubbleTeaOrder); } export class JazzAccount extends Account { root = co.ref(AccountRoot); migrate(this: JazzAccount, creationProps?: { name: string }) { if (this.root === undefined) { const draft = DraftBubbleTeaOrder.create({}); this.root = AccountRoot.create({ draft }); } } } Let's not forget to update the `AccountSchema`. import { JazzProvider } from "jazz-react"; import { JazzAccount } from "./schema"; export function MyJazzProvider({ children }: { children: React.ReactNode }) { return ( <JazzProvider sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} AccountSchema={JazzAccount} > {children} </JazzProvider> ); } // Register the Account schema so `useAccount` returns our custom `JazzAccount` declare module "jazz-react" { interface Register { Account: JazzAccount; } } Instead of creating a new draft every time we use the create form, let's use the draft from the account root. // CreateOrder.tsx export function CreateOrder() { const { me } = useAccount({ root: { draft: {} } }); if (!me?.root) return; const onSave = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const draft = me.root.draft; if (!draft) return; const validation = draft.validate(); if (validation.errors.length > 0) { console.log(validation.errors); return; } const order = draft as BubbleTeaOrder; console.log("Order created:", order); // create a new empty draft me.root.draft = DraftBubbleTeaOrder.create( {}, ); }; return <CreateOrderForm id={me.root.draft.id} onSave={onSave} /> } function CreateOrderForm({ id, onSave, }: { id: ID<DraftBubbleTeaOrder>; onSave: (e: React.FormEvent<HTMLFormElement>) => void; }) { const draft = useCoState(DraftBubbleTeaOrder, id); if (!draft) return; return <OrderForm order={draft} onSave={onSave} />; } When the new draft is created, we need to call `useCoState` again, so that we are passing the new draft to `<OrderForm/>`. There you have it! Notice that when you refresh the page, you will see your unsaved changes. ## Draft indicator To improve the UX even further, in just a few more steps, we can tell the user that they currently have unsaved changes. Simply add a `hasChanges` checker to your schema. // schema.ts export class DraftBubbleTeaOrder extends CoMap { name = co.optional.string; validate() { const errors: string[] = []; if (!this.name) { errors.push("Plese enter a name."); } return { errors }; } get hasChanges() { return Object.keys(this._edits).length; } } In the UI, you can choose how you want to show the draft indicator. // DraftIndicator.tsx export function DraftIndicator() { const { me } = useAccount({ root: { draft: {} }, }); if (me?.root.draft?.hasChanges) { return ( <p>You have a draft</p> ); } } A more subtle way is to show a small dot next to the Create button. ## Handling different types of data Forms can be more complex than just a single string field, so we've put together an example app that shows you how to handle single-select, multi-select, date, and boolean inputs. See the full example here. export class BubbleTeaOrder extends CoMap { baseTea = co.literal(...BubbleTeaBaseTeaTypes); addOns = co.ref(ListOfBubbleTeaAddOns); deliveryDate = co.Date; withMilk = co.boolean; instructions = co.optional.string; } --- ## Page: https://jazz.tools/docs/react-native-expo/design-patterns/organization Organizations are a way to share a set of data between users. Different apps have different names for this concept, such as "teams" or "workspaces". We'll use the term Organization. See the full example here. ## Defining the schema for an Organization Create a CoMap shared by the users of the same organization to act as a root (or "main database") for the shared data within an organization. For this example, users within an `Organization` will be sharing `Project`s. // schema.ts export class Project extends CoMap { name = co.string; } export class ListOfProjects extends CoList.Of(co.ref(Project)) {} export class Organization extends CoMap { name = co.string; // shared data between users of each organization projects = co.ref(ListOfProjects); } export class ListOfOrganizations extends CoList.Of(co.ref(Organization)) {} Learn more about defining schemas. ## Adding a list of Organizations to the user's Account Let's add the list of `Organization`s to the user's Account `root` so they can access them. // schema.ts export class JazzAccountRoot extends CoMap { organizations = co.ref(ListOfOrganizations); } export class JazzAccount extends Account { root = co.ref(JazzAccountRoot); async migrate() { if (this.root === undefined) { // Using a Group as an owner allows you to give access to other users const organizationGroup = Group.create(); const organizations = ListOfOrganizations.create( [ // Create the first Organization so users can start right away Organization.create( { name: "My organization", projects: ListOfProjects.create([], organizationGroup), }, organizationGroup, ), ], ); this.root = JazzAccountRoot.create( { organizations }, ); } } } This schema now allows users to create `Organization`s and add `Project`s to them. See the schema for the example app here. ## Adding other users to an Organization To give users access to an `Organization`, you can either send them an invite link, or add their `Account` manually. ### Adding users through invite links Here's how you can generate an invite link. When the user accepts the invite, add the `Organization` to the user's `organizations` list. const onAccept = async (organizationId: ID<Organization>) => { const me = await MusicaAccount.getMe().ensureLoaded({ root: { organizations: [], }, }); const organization = await Organization.load(organizationId, []); if (!organization) throw new Error("Failed to load organization data"); const ids = me.root.organizations.map( (organization) => organization?.id, ); if (ids.includes(organizationId)) return; me.root.organizations.push(organization); navigate("/organizations/" + organizationId); }; useAcceptInvite({ invitedObjectSchema: Organization, onAccept, }); ### Adding users through their Account ID ...more on this coming soon --- ## Page: https://jazz.tools/docs/react-native-expo/jazz-under-the-hood ## Documentation coming soon Grayed out pages on our sidebar indicate that documentation for this feature is still in progress. We're excited to bring you comprehensive guides and tutorials as soon as possible. This feature has already been released, and we're working hard to provide top-notch support. In the meantime, if you have any questions or need assistance, please don't hesitate to reach out to us on Discord. We would love to help you get started.