Compare commits

...

17 Commits

Author SHA1 Message Date
Benjamin S. Leveritt
327a7315b7 Update from comments 2024-11-18 16:50:53 +00:00
Benjamin S. Leveritt
2bf28170f7 Update homepage/homepage/app/docs/[...slug]/schemas/accounts.mdx
Co-authored-by: Anselm Eickhoff <anselm.eickhoff@gmail.com>
2024-11-18 16:34:57 +00:00
Benjamin S. Leveritt
e472d2c513 Add accounts & migration docs 2024-11-18 16:34:57 +00:00
Benjamin S. Leveritt
64e0aaba88 Add accounts & migration docs 2024-11-18 16:33:49 +00:00
Benjamin S. Leveritt
27b0d75338 Add Vue example 2024-11-18 16:30:34 +00:00
Benjamin S. Leveritt
d6175fb936 Use sentence case on title 2024-11-18 16:30:34 +00:00
Benjamin S. Leveritt
8311a61f3f Improve copy 2024-11-18 16:30:34 +00:00
Benjamin S. Leveritt
2e3b10c5f6 Add accounts & migration docs 2024-11-18 16:30:34 +00:00
Anselm Eickhoff
2dfe2158fd Merge pull request #790 from gardencmp/trishalim-jazz-474
Fix: page reloads when clicking a link inside mdx
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
2a4f9da62a Merge pull request #794 from gardencmp/benjamin-jazz-502
PassphraseAuth - Move saving credentials into `saveCredentials`
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
67b1a6ba66 Merge pull request #789 from gardencmp/docs/getting-started
Rewrite docs introduction
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
0609388899 Merge pull request #768 from gardencmp/fuzzyobject-jazz-491
Enhance onboarding test - add page context
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
b12f8e3f26 Merge pull request #777 from gardencmp/benjamin-jazz-481
Fix readme for jazz-react-auth-clerk
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
892ca34443 Merge pull request #784 from gardencmp/vscode-settings
chore: remove .vscode/settings.json and add it to .gitignore
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
3533707794 Merge pull request #792 from gardencmp/benjamin-jazz-495
Fix PasskeyAuth forgetting authentication on reload
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
2f848a8d83 Merge pull request #783 from gardencmp/benjamin-jazz-497
Tweak 'Docs coming soon' copy
2024-11-18 16:30:34 +00:00
Anselm Eickhoff
eae493b9d3 Merge pull request #757 from gardencmp/fuzzyobject-jazz-460
Rename CoStream to CoFeed
2024-11-18 16:30:34 +00:00
32 changed files with 370 additions and 218 deletions

View File

@@ -0,0 +1,5 @@
---
"jazz-browser": patch
---
Persist PasskeyAuth credentials on reload

4
.gitignore vendored
View File

@@ -15,4 +15,6 @@ coverage
# Playwright
test-results
.husky
.husky
.vscode/settings.json

View File

@@ -1,3 +0,0 @@
{
"editor.defaultFormatter": "biomejs.biome"
}

View File

@@ -20,7 +20,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173/?peer=ws://localhost:1234",
baseURL: "http://localhost:5173/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
@@ -42,10 +42,5 @@ export default defineConfig({
url: "http://localhost:5173/",
reuseExistingServer: !isCI,
},
{
command: "pnpm sync --in-memory --port 1234",
url: "http://localhost:1234/health",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -1,5 +1,5 @@
import { Button } from "@/components/Button.tsx";
import { useAccount, useCoState } from "@/main.tsx";
import { useAcceptInvite, useAccount, useCoState } from "@/main.tsx";
import { EmployeeList } from "@/pages/EmployeeList.tsx";
import { EmployeeOnboading } from "@/pages/EmployeeOnboarding.tsx";
import { NewEmployee } from "@/pages/NewEmployee.tsx";
@@ -8,7 +8,7 @@ import { ID } from "jazz-tools";
import { useEffect } from "react";
import {
RouterProvider,
createBrowserRouter,
createHashRouter,
useNavigate,
useParams,
} from "react-router-dom";
@@ -36,11 +36,24 @@ function ImportEmployee({
return <div>Importing Employee ${employeeCoId} ...</div>;
}
function AcceptInvite() {
const navigate = useNavigate();
useAcceptInvite({
invitedObjectSchema: CoEmployee,
onAccept: (employeeCoId) => {
navigate(`/import/${employeeCoId}`);
},
});
return <p>Accepting invite...</p>;
}
function App() {
const { me, logOut } = useAccount();
const employeeCoListId = me.profile?._refs.employees.id;
const router = createBrowserRouter([
const router = createHashRouter([
{
path: "/",
element: <EmployeeList employeeListCoId={employeeCoListId} />,
@@ -57,6 +70,10 @@ function App() {
path: "/import/:employeeCoId",
element: <ImportEmployee employeeListCoId={employeeCoListId} />,
},
{
path: "/invite/*",
element: <AcceptInvite />,
},
]);
return (

View File

@@ -10,15 +10,17 @@ const Jazz = createJazzReactApp({
});
export const { useAccount, useCoState, useAcceptInvite } = Jazz;
const peer =
(new URL(window.location.href).searchParams.get(
"peer",
) as `ws://${string}`) ??
"wss://cloud.jazz.tools/?key=onboarding-example-jazz@gcmp.io";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, authState] = useDemoAuth();
return (
<>
<Jazz.Provider
auth={auth}
// replace `you@example.com` with your email as a temporary API key
peer="wss://cloud.jazz.tools/?key=you@example.com"
>
<Jazz.Provider auth={auth} peer={peer}>
{children}
</Jazz.Provider>
{authState.state !== "signedIn" && (

View File

@@ -2,13 +2,12 @@ import { Button } from "@/components/Button.tsx";
import { NavigateBack } from "@/components/NavigateBack.tsx";
import { Stack } from "@/components/Stack.tsx";
import { TextInput } from "@/components/TextInput.tsx";
import { useAcceptInvite, useCoState } from "@/main.tsx";
import { useCoState } from "@/main.tsx";
import { createImage } from "jazz-browser-media-images";
import { ProgressiveImg, createInviteLink } from "jazz-react";
import { CoMap, ID } from "jazz-tools";
import { ChangeEvent, ReactNode, useCallback } from "react";
import { useParams } from "react-router";
import { useNavigate } from "react-router-dom";
import {
CoDocUploadStep,
CoEmployee,
@@ -173,17 +172,9 @@ const ConfirmationCard = ({
export function EmployeeOnboading() {
const { employeeCoId } = useParams();
const navigate = useNavigate();
const employee = useCoState(CoEmployee, employeeCoId as ID<CoEmployee>, {});
useAcceptInvite({
invitedObjectSchema: CoEmployee,
onAccept: (employeeCoId) => {
navigate(`/import/${employeeCoId}`);
},
});
const handleInviteLinkCreation = useCallback(
(role: "reader" | "writer") => {
if (!employee) return;

View File

@@ -1,4 +1,11 @@
import { Page, expect, test } from "@playwright/test";
import {
Browser,
BrowserContext,
Page,
chromium,
expect,
test,
} from "@playwright/test";
import { EmployeeOnboardingPage } from "./pages/EmployeeOnboardingPage";
import { HomePage } from "./pages/HomePage";
import { LoginPage } from "./pages/LoginPage";
@@ -29,60 +36,82 @@ const login = async ({
};
test.describe("Admin onboarding flow", () => {
test("Create and delete flow", async ({ page }) => {
await login({ page, userName: "HR specialist" });
let browser: Browser;
let adminContext: BrowserContext;
let writerContext: BrowserContext;
const homePage = new HomePage(page);
await homePage.createEmployee("Paul");
await homePage.createEmployee("Sean");
await homePage.expectEmployee(["Sean", "admin"]);
await homePage.expectEmployee(["Paul", "admin"]);
await homePage.deleteEmployee("Sean");
await homePage.expectEmployeeDeleted("Sean");
test.beforeAll(async () => {
browser = await chromium.launch();
adminContext = await browser.newContext();
writerContext = await browser.newContext();
});
test("Onboard flow", async ({ page }) => {
test.afterAll(async () => {
await adminContext.close();
await writerContext.close();
await browser.close();
});
test("Create and delete flow", async () => {
const adminPage = await adminContext.newPage();
await login({ page: adminPage, userName: "HR specialist" });
const adminHomePage = new HomePage(adminPage);
await adminHomePage.createEmployee("Paul");
await adminHomePage.createEmployee("Sean");
await adminHomePage.expectEmployee(["Sean", "admin"]);
await adminHomePage.expectEmployee(["Paul", "admin"]);
await adminHomePage.deleteEmployee("Sean");
await adminHomePage.expectEmployeeDeleted("Sean");
await adminPage.close();
});
test("Onboard flow", async () => {
const adminPage = await adminContext.newPage();
const writerPage = await writerContext.newPage();
const adminUser = "HR specialist";
const writerUser = "Invitee";
await login({ page, userName: adminUser });
await login({ page: adminPage, userName: adminUser });
await login({ page: writerPage, userName: writerUser });
const homePage = new HomePage(page);
await homePage.createEmployee("Paul");
await homePage.expectEmployee(["Paul", "admin"]);
await homePage.navigateToEmployeeOnboardingPage("Paul");
const onboardingPage = new EmployeeOnboardingPage(page);
const adminHomePage = new HomePage(adminPage);
await adminHomePage.createEmployee("Paul");
await adminHomePage.expectEmployee(["Paul", "admin"]);
await adminHomePage.navigateToEmployeeOnboardingPage("Paul");
const adminOnboardingPage = new EmployeeOnboardingPage(adminPage);
// create invitation
const invitation = await onboardingPage.getShareLink();
await onboardingPage.logout();
const invitation = await adminOnboardingPage.getShareLink();
// Wait for the invitation to be synced
await writerPage.waitForTimeout(3000);
//fill out by invitee (writer)
await login({ page, userName: writerUser });
await page.goto(invitation);
await page.waitForTimeout(1000);
await homePage.expectEmployee(["Paul", "write"]);
await homePage.navigateToEmployeeOnboardingPage("Paul");
await onboardingPage.expectEmployeeName("Paul");
await onboardingPage.fillPersonalDetailsCardAndSave(
await writerPage.goto(invitation);
const writerHomePage = new HomePage(writerPage);
await writerHomePage.expectEmployee(["Paul", "write"]);
await writerHomePage.navigateToEmployeeOnboardingPage("Paul");
const writerOnboardingPage = new EmployeeOnboardingPage(writerPage);
await writerOnboardingPage.expectEmployeeName("Paul");
await writerOnboardingPage.fillPersonalDetailsCardAndSave(
"123-45-6789",
"123 Elm Street",
);
await onboardingPage.fillUploadCardAndSave(
await writerOnboardingPage.fillUploadCardAndSave(
"./public/jazz-logo-low-res.jpg",
);
// invitee cannot confirm the onboarding completion
expect(onboardingPage.finalConfirmationButton.isDisabled()).toBeTruthy();
expect(
writerOnboardingPage.finalConfirmationButton.isDisabled(),
).toBeTruthy();
// final confirmation step by admin
await onboardingPage.logout();
await login({ page, userName: adminUser, loginAs: true });
await homePage.expectEmployee(["Paul", "admin"]);
await homePage.navigateToEmployeeOnboardingPage("Paul");
await scrollToBottom(page);
await onboardingPage.finalConfirmationButton.click();
await onboardingPage.backButton.click();
await homePage.expectOnboardingCompleteForEmployee("Paul");
await scrollToBottom(adminPage);
await adminOnboardingPage.finalConfirmationButton.click();
await adminOnboardingPage.backButton.click();
await adminHomePage.expectOnboardingCompleteForEmployee("Paul");
});
});

View File

@@ -1,8 +1,8 @@
import {
Account,
CoFeed,
CoList,
CoMap,
CoStream,
ImageDefinition,
Profile,
co,
@@ -25,7 +25,7 @@ export const ReactionTypes = [
] as const;
export type ReactionType = (typeof ReactionTypes)[number];
export class PetReactions extends CoStream.Of(co.json<ReactionType>()) {}
export class PetReactions extends CoFeed.Of(co.json<ReactionType>()) {}
export class PetPost extends CoMap {
name = co.string;

View File

@@ -1,9 +1,7 @@
# The documentation for this feature is not yet available
# Documentation coming soon
Grayed out pages on the sidebar indicate that the documentation is not yet available.
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, but the documentation is still a work in progress.
If you don't find what you're looking for, [please ask for help in the Discord](https://discord.gg/utDMjHYg42).
We have a whole team of developers who are ready to help you get started.
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](https://discord.gg/utDMjHYg42).
We would love to help you get started.

View File

@@ -1,10 +1,5 @@
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export const metadata = {
title: "Docs",
description: "Jazz Guide & Docs.",
};
export default function DocsLayout({
children,
}: {

View File

@@ -0,0 +1,44 @@
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import { JazzLogo } from 'gcmp-design-system/src/app/components/atoms/logos/JazzLogo';
<div className="not-prose">
<h1 className="sr-only">Getting started</h1>
<HeroHeader
title={<>Learn some <JazzLogo className="h-[1.3em] relative -top-0.5 inline-block -ml-[0.1em] -mr-[0.1em]"/></>}
slogan=""
pt={false}
/>
</div>
Welcome to the Jazz documentation!
The Jazz docs are currently heavily work in progress, sorry about that!
## Quickstart
To get started, set up Jazz in your project. Here's how you can do that on your framework of choice:
- [React](/docs/project-setup/react)
- [Next.js](/docs/project-setup/next)
- [React Native](/docs/project-setup/react-native)
- [Vue](/docs/project-setup/vue)
Or you can follow this [React step-by-step guide](/docs/guide) where we walk you through building an issue tracker app.
## Example apps
You can also find [example apps](/examples) 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](/docs/sync-and-storage) using Jazz Cloud, or do it yourself.
## Collaborative values
Learn how to structure your data using [collaborative values](/docs/schemas/covalues).
## Get support
If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42).
We would love to help you get started.

View File

@@ -1,55 +0,0 @@
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
export default function Page() {
return (
<Prose>
<h1>Welcome to the Jazz documentation.</h1>
<p>
The Jazz docs are currently heavily work in progress, sorry about that!
</p>
<p>The best ways to get started are:</p>
<ul>
<li>
Quickstart (work in progress)
<ol>
<li>
<a href="/docs/sync-and-storage">Sync & Storage Setup</a>
</li>
<li>
<a href="/docs/project-setup/react">React Project Setup</a>
</li>
<li>
<a href="/docs/schemas/covalues">
CoValue Basics & Schema Definition
</a>
</li>
<li>
<span className="opacity-50">Creating Covalues</span>
</li>
<li>
<span className="opacity-50">Using Covalues</span>
</li>
</ol>
</li>
<li>
The step-by-step <a href="/docs/guide">Guide</a> (work in progress)
</li>
</ul>
<p>Also make sure to:</p>
<ul>
<li>
Find an <a href="/examples">example app with code</a> most similar to
what you want to build
</li>
<li>
Check out the <a href="/docs/api-reference">API Reference</a> (work in
progress)
</li>
</ul>
<p>
And the best way to get help is to join the{" "}
<a href="https://discord.gg/utDMjHYg42">Discord</a>!
</p>
</Prose>
);
}

View File

@@ -1,26 +1,11 @@
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import { CodeGroup } from "@/components/forMdx";
import { JazzLogo } from 'gcmp-design-system/src/app/components/atoms/logos/JazzLogo'
<div className="not-prose">
<HeroHeader
title={<>Learn some <JazzLogo className="h-[1.3em] relative -top-0.5 inline-block -ml-[0.1em] -mr-[0.1em]"/></>}
slogan="Build an issue tracker with distributed state."
pt={false}
/>
</div>
# React Guide
## About this guide
This is a step-by-step tutorial where we'll build an issue tracker app using React.
You might notice that right now, this guide is the only form of documentation there is for Jazz.
Over time, we're hoping to introduce independent doc sections for every concept in Jazz, but right now this works as:
- a quickstart guide
- a reference for the concepts in Jazz (ordered from simple & most important to more advanced)
- a tutorial that makes you build a full app (that you can use as a base)
Plus, if you get stuck or you have questions, [ask us on Discord](https://discord.gg/utDMjHYg42)
and we'll know exactly where you're at.
You'll learn how to set up a Jazz app, use Jazz Cloud for sync and storage, create and manipulate data using
Collaborative Values (CoValues), build a UI and subscribe to changes, set permissions, and send invites.
## Project Setup

View File

@@ -0,0 +1,87 @@
import { CodeGroup } from "@/components/forMdx";
# Accounts & migrations
Accounts are a fundamental building block for user identity and data organization in Jazz applications, providing a structured way to manage both public and private user data while maintaining type-safety and access control.
Account data and metadata are publicly visible to other users.
For private data, we suggest nesting privately owned CoValues within the account root.
## Accounts
To create a custom account type, extend the `Account` class and register it with the Jazz runtime.
<CodeGroup>
```ts
import { Account, CoMap } from "jazz-tools";
class MyAccountRoot extends CoMap {
privateString = co.string; // Readable by the account owner
}
class MyProfile extends Profile {
publicString = co.string; // Readable by everyone
}
class MyAccount extends Account {
root = co.ref(MyAccountRoot);
profile = co.ref(MyProfile);
}
```
</CodeGroup>
### Schema
When passing the account class to the Jazz runtime, the schema is used to type account data.
<CodeGroup>
```ts
// React
const Jazz = createJazzReactApp({
AccountSchema: MyAccount,
});
```
</CodeGroup>
<CodeGroup>
```ts
// React Native
const Jazz = createJazzReactNativeApp({
AccountSchema: MyAccount,
});
```
</CodeGroup>
<CodeGroup>
```ts
// Vue
const Jazz = createJazzVueApp({
AccountSchema: MyAccount,
});
```
</CodeGroup>
## Migrations
Accounts can be migrated to add new data or update schemas. The `migrate()` method runs automatically when an account is created and each time a user logs in.
You can use migrations to initialize the account root and set up any other required CoValues.
<CodeGroup>
```ts
class MyAccount extends Account {
async migrate() {
super.migrate(creationProps);
if (!this._refs.root) {
this.root = MyAccountRoot.create({
privateString: "Keep it hidden, keep it safe.",
},
{ owner: this });
}
}
}
```
</CodeGroup>

View File

@@ -13,7 +13,7 @@ Jazz Cloud will
- 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](https://jazz.tools/cloud#pricing) for more details
- See [Jazz Cloud pricing](/cloud#pricing) for more details
- ⚠️ Please use a valid email address as your API key.
Your full sync server URL should look something like

View File

@@ -2,8 +2,8 @@ import { DocNav } from "@/components/docs/nav";
import { clsx } from "clsx";
export const metadata = {
title: "Docs",
description: "Jazz Guide & Docs.",
title: "Documentation",
description: "Jazz guide and documentation.",
};
export default function DocsLayout({

View File

@@ -8,7 +8,6 @@ import { JazzFooter } from "@/components/footer";
import { JazzNav } from "@/components/nav";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { clsx } from "clsx";
import { ThemeProvider } from "gcmp-design-system/src/app/components/molecules/ThemeProvider";
// If loading a variable font, you don't need to specify the font weight

View File

@@ -39,13 +39,13 @@ export const docNavigationItems = [
href: "/docs/project-setup/next",
},
{
name: "Node.JS / Server Workers",
href: "/docs/project-setup/server-side",
name: "VueJS",
href: "/docs/project-setup/vue",
done: 80,
},
{
name: "VueJS",
href: "/docs/project-setup/vue",
name: "Node.JS / Server Workers",
href: "/docs/project-setup/server-side",
done: 80,
},
],
@@ -56,12 +56,12 @@ export const docNavigationItems = [
{
name: "CoValues",
href: "/docs/schemas/covalues",
done: 20,
done: 50,
},
{
name: "Accounts & Migrations",
href: "/docs/schemas/accounts",
done: 0,
done: 20,
},
],
},

View File

@@ -1,7 +1,14 @@
import type { MDXComponents } from "mdx/types";
import Link from "next/link";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
a: (props) =>
props.href ? (
<Link href={props.href}>{props.children}</Link>
) : (
props.children
),
...components,
};
}

View File

@@ -1,5 +1,41 @@
# `jazz-browser-media-images`
# `jazz-browser-auth-clerk`
This is an optional add-on for `jazz-browser` or `jazz-react` that provides support for creating `ImageDefinition`-compatible image sets from images provided as `File` or `Blob` objects.
This package provides a [Clerk-based](https://clerk.com/) authentication strategy for Jazz.
In particular, it implements multi-resolution resizing based on `pica`.
## Usage
`useJazzClerkAuth` is a hook that returns a `JazzAuth` object and a `JazzAuthState` object. Provide a Clerk instance to `useJazzClerkAuth`, and it will return the appropriate `JazzAuth` object. Once authenticated, authentication will persist across page reloads, even if the device is offline.
From [the example chat app](https://github.com/gardencmp/jazz/tree/main/examples/chat-clerk):
```typescript
import { ClerkProvider, SignInButton, useClerk } from "@clerk/clerk-react";
import { useJazzClerkAuth } from "jazz-react-auth-clerk";
const Jazz = createJazzReactApp();
export const { useAccount, useCoState } = Jazz;
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
const [auth, state] = useJazzClerkAuth(clerk);
return (
<>
{state.errors.map((error) => (
<div key={error}>{error}</div>
))}
{auth ? (
<Jazz.Provider
auth={auth}
peer="wss://cloud.jazz.tools/?key=chat-example-jazz-clerk@gcmp.io"
>
{children}
</Jazz.Provider>
) : (
<SignInButton />
)}
</>
);
}
```

View File

@@ -148,6 +148,12 @@ export class BrowserPasskeyAuth implements AuthMethod {
resolve({
type: "existing",
credentials: { accountID, secret },
saveCredentials: async ({ accountID, secret }) => {
localStorage[localStorageKey] = JSON.stringify({
accountID,
accountSecret: secret,
} satisfies LocalStorageData);
},
onSuccess: () => {
this.driver.onSignedIn({ logOut });
},

View File

@@ -98,11 +98,13 @@ export class BrowserPassphraseAuth implements AuthMethod {
resolve({
type: "existing",
credentials: { accountID, secret: accountSecret },
onSuccess: () => {
saveCredentials: async ({ accountID, secret }) => {
localStorage[localStorageKey] = JSON.stringify({
accountID,
accountSecret,
accountSecret: secret,
} satisfies LocalStorageData);
},
onSuccess: () => {
this.driver.onSignedIn({ logOut });
},
onError: (error: string | Error) => {

View File

@@ -38,11 +38,17 @@ import {
subscribeToExistingCoValue,
} from "../internal.js";
export type CoStreamEntry<Item> = SingleCoStreamEntry<Item> & {
all: IterableIterator<SingleCoStreamEntry<Item>>;
/** @deprecated Use CoFeedEntry instead */
export type CoStreamEntry<Item> = CoFeedEntry<Item>;
export type CoFeedEntry<Item> = SingleCoFeedEntry<Item> & {
all: IterableIterator<SingleCoFeedEntry<Item>>;
};
export type SingleCoStreamEntry<Item> = {
/** @deprecated Use SingleCoFeedEntry instead */
export type SingleCoStreamEntry<Item> = SingleCoFeedEntry<Item>;
export type SingleCoFeedEntry<Item> = {
value: NonNullable<Item> extends CoValue ? NonNullable<Item> | null : Item;
ref: NonNullable<Item> extends CoValue ? Ref<NonNullable<Item>> : never;
by?: Account | null;
@@ -50,11 +56,14 @@ export type SingleCoStreamEntry<Item> = {
tx: CojsonInternalTypes.TransactionID;
};
/** @deprecated Use CoFeed instead */
export { CoFeed as CoStream };
/** @category CoValues */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class CoStream<Item = any> extends CoValueBase implements CoValue {
static Of<Item>(item: IfCo<Item, Item>): typeof CoStream<Item> {
return class CoStreamOf extends CoStream<Item> {
export class CoFeed<Item = any> extends CoValueBase implements CoValue {
static Of<Item>(item: IfCo<Item, Item>): typeof CoFeed<Item> {
return class CoFeedOf extends CoFeed<Item> {
[co.items] = item;
};
}
@@ -73,12 +82,12 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
get _schema(): {
[ItemsSym]: SchemaFor<Item>;
} {
return (this.constructor as typeof CoStream)._schema;
return (this.constructor as typeof CoFeed)._schema;
}
[key: ID<Account>]: CoStreamEntry<Item>;
[key: ID<Account>]: CoFeedEntry<Item>;
get byMe(): CoStreamEntry<Item> | undefined {
get byMe(): CoFeedEntry<Item> | undefined {
if (this._loadedAs._type === "Account") {
return this[this._loadedAs.id];
} else {
@@ -86,9 +95,9 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
}
}
perSession!: {
[key: SessionID]: CoStreamEntry<Item>;
[key: SessionID]: CoFeedEntry<Item>;
};
get inCurrentSession(): CoStreamEntry<Item> | undefined {
get inCurrentSession(): CoFeedEntry<Item> | undefined {
if (this._loadedAs._type === "Account") {
return this.perSession[this._loadedAs.sessionID!];
} else {
@@ -116,9 +125,9 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
return new Proxy(this, CoStreamProxyHandler as ProxyHandler<this>);
}
static create<S extends CoStream>(
static create<S extends CoFeed>(
this: CoValueClass<S>,
init: S extends CoStream<infer Item> ? UnCo<Item>[] : never,
init: S extends CoFeed<infer Item> ? UnCo<Item>[] : never,
options: { owner: Account | Group },
) {
const instance = new this({ init, owner: options.owner });
@@ -197,9 +206,9 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
return this.toJSON();
}
static schema<V extends CoStream>(
static schema<V extends CoFeed>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: { new (...args: any): V } & typeof CoStream,
this: { new (...args: any): V } & typeof CoFeed,
def: { [ItemsSym]: V["_schema"][ItemsSym] },
) {
this._schema ||= {};
@@ -207,7 +216,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
}
/** @category Subscription & Loading */
static load<S extends CoStream, Depth>(
static load<S extends CoFeed, Depth>(
this: CoValueClass<S>,
id: ID<S>,
as: Account,
@@ -217,7 +226,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
}
/** @category Subscription & Loading */
static subscribe<S extends CoStream, Depth>(
static subscribe<S extends CoFeed, Depth>(
this: CoValueClass<S>,
id: ID<S>,
as: Account,
@@ -228,7 +237,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
}
/** @category Subscription & Loading */
ensureLoaded<S extends CoStream, Depth>(
ensureLoaded<S extends CoFeed, Depth>(
this: S,
depth: Depth & DepthsIn<S>,
): Promise<DeeplyLoaded<S, Depth> | undefined> {
@@ -236,7 +245,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
}
/** @category Subscription & Loading */
subscribe<S extends CoStream, Depth>(
subscribe<S extends CoFeed, Depth>(
this: S,
depth: Depth & DepthsIn<S>,
listener: (value: DeeplyLoaded<S, Depth>) => void,
@@ -256,7 +265,7 @@ function entryFromRawEntry<Item>(
loadedAs: Account | AnonymousJazzAgent,
accountID: ID<Account> | undefined,
itemField: Schema,
): Omit<CoStreamEntry<Item>, "all"> {
): Omit<CoFeedEntry<Item>, "all"> {
return {
get value(): NonNullable<Item> extends CoValue
? (CoValue & Item) | null
@@ -307,7 +316,7 @@ function entryFromRawEntry<Item>(
};
}
export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
export const CoStreamProxyHandler: ProxyHandler<CoFeed> = {
get(target, key, receiver) {
if (typeof key === "string" && key.startsWith("co_")) {
const rawEntry = target._raw.lastItemBy(key as RawAccountID);
@@ -337,7 +346,7 @@ export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})() satisfies IterableIterator<SingleCoStreamEntry<any>>;
})() satisfies IterableIterator<SingleCoFeedEntry<any>>;
},
});
@@ -350,8 +359,8 @@ export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
},
set(target, key, value, receiver) {
if (key === ItemsSym && typeof value === "object" && SchemaInit in value) {
(target.constructor as typeof CoStream)._schema ||= {};
(target.constructor as typeof CoStream)._schema[ItemsSym] =
(target.constructor as typeof CoFeed)._schema ||= {};
(target.constructor as typeof CoFeed)._schema[ItemsSym] =
value[SchemaInit];
return true;
} else {
@@ -365,8 +374,8 @@ export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
typeof descriptor.value === "object" &&
SchemaInit in descriptor.value
) {
(target.constructor as typeof CoStream)._schema ||= {};
(target.constructor as typeof CoStream)._schema[ItemsSym] =
(target.constructor as typeof CoFeed)._schema ||= {};
(target.constructor as typeof CoFeed)._schema[ItemsSym] =
descriptor.value[SchemaInit];
return true;
} else {
@@ -396,8 +405,8 @@ export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
};
const CoStreamPerSessionProxyHandler = (
innerTarget: CoStream,
accessFrom: CoStream,
innerTarget: CoFeed,
accessFrom: CoFeed,
): ProxyHandler<Record<string, never>> => ({
get(_target, key, receiver) {
if (typeof key === "string" && key.includes("session")) {
@@ -435,7 +444,7 @@ const CoStreamPerSessionProxyHandler = (
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
})() satisfies IterableIterator<SingleCoStreamEntry<any>>;
})() satisfies IterableIterator<SingleCoFeedEntry<any>>;
},
});

View File

@@ -339,7 +339,7 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
*
* You can pass `[]` or for shallowly loading only this CoList, or `[itemDepth]` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
* Check out the `load` methods on `CoMap`/`CoList`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.
*
* @example
* ```ts
@@ -372,7 +372,7 @@ export class CoList<Item = any> extends Array<Item> implements CoValue {
*
* You can pass `[]` or for shallowly loading only this CoList, or `[itemDepth]` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
* 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.
*

View File

@@ -366,7 +366,7 @@ export class CoMap extends CoValueBase implements CoValue {
*
* You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
* Check out the `load` methods on `CoMap`/`CoList`/`CoFeed`/`Group`/`Account` to see which depth structures are valid to nest.
*
* @example
* ```ts
@@ -398,7 +398,7 @@ export class CoMap extends CoValueBase implements CoValue {
*
* You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
*
* Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
* 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.
*

View File

@@ -1,9 +1,9 @@
import { SessionID } from "cojson";
import {
Account,
CoFeed,
CoFeedEntry,
CoList,
CoStream,
CoStreamEntry,
ItemsSym,
Ref,
RefEncoded,
@@ -66,10 +66,10 @@ export function fulfillsDepth(depth: any, value: CoValue): boolean {
return true;
} else {
const itemDepth = depth[0];
return Object.values((value as CoStream).perSession).every((entry) =>
return Object.values((value as CoFeed).perSession).every((entry) =>
entry.ref
? entry.value && fulfillsDepth(itemDepth, entry.value)
: ((value as CoStream)._schema[ItemsSym] as RefEncoded<CoValue>)
: ((value as CoFeed)._schema[ItemsSym] as RefEncoded<CoValue>)
.optional,
);
}
@@ -121,7 +121,7 @@ export type DepthsIn<
| never[]
: V extends {
_type: "CoStream";
byMe: CoStreamEntry<infer Item> | undefined;
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| [
@@ -192,7 +192,7 @@ export type DeeplyLoaded<
: [V] extends [
{
_type: "CoStream";
byMe: CoStreamEntry<infer Item> | undefined;
byMe: CoFeedEntry<infer Item> | undefined;
},
]
? Depth extends never[]

View File

@@ -17,6 +17,7 @@ export {
BinaryCoStream,
CoList,
CoMap,
CoFeed,
CoStream,
CoValueBase,
Group,

View File

@@ -5,7 +5,7 @@ export * from "./coValues/interfaces.js";
export * from "./coValues/coMap.js";
export * from "./coValues/account.js";
export * from "./coValues/coList.js";
export * from "./coValues/coStream.js";
export * from "./coValues/coFeed.js";
export * from "./coValues/group.js";
export * from "./implementation/errors.js";

View File

@@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
import {
Account,
BinaryCoStream,
CoStream,
CoFeed,
ID,
WasmCrypto,
co,
@@ -16,7 +16,7 @@ import { randomSessionProvider } from "../internal.js";
const Crypto = await WasmCrypto.create();
describe("Simple CoStream operations", async () => {
describe("Simple CoFeed operations", async () => {
const me = await Account.create({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto,
@@ -24,7 +24,7 @@ describe("Simple CoStream operations", async () => {
if (!isControlledAccount(me)) {
throw "me is not a controlled account";
}
class TestStream extends CoStream.Of(co.string) {}
class TestStream extends CoFeed.Of(co.string) {}
const stream = TestStream.create(["milk"], { owner: me });
@@ -46,16 +46,16 @@ describe("Simple CoStream operations", async () => {
});
});
describe("CoStream resolution", async () => {
class TwiceNestedStream extends CoStream.Of(co.string) {
describe("CoFeed resolution", async () => {
class TwiceNestedStream extends CoFeed.Of(co.string) {
fancyValueOf(account: ID<Account>) {
return "Sir " + this[account]?.value;
}
}
class NestedStream extends CoStream.Of(co.ref(TwiceNestedStream)) {}
class NestedStream extends CoFeed.Of(co.ref(TwiceNestedStream)) {}
class TestStream extends CoStream.Of(co.ref(NestedStream)) {}
class TestStream extends CoFeed.Of(co.ref(NestedStream)) {}
const initNodeAndStream = async () => {
const me = await Account.create({

View File

@@ -3,9 +3,9 @@ import { connectedPeers } from "cojson/src/streamUtils.ts";
import { describe, expect, expectTypeOf, test } from "vitest";
import {
Account,
CoFeed,
CoList,
CoMap,
CoStream,
ID,
Profile,
SessionID,
@@ -28,7 +28,7 @@ class InnerMap extends CoMap {
stream = co.ref(TestStream);
}
class TestStream extends CoStream.Of(co.ref(() => InnermostMap)) {}
class TestStream extends CoFeed.Of(co.ref(() => InnermostMap)) {}
class InnermostMap extends CoMap {
value = co.string;

View File

@@ -3,9 +3,9 @@ import { connectedPeers } from "cojson/src/streamUtils.js";
import { describe, expect, it, onTestFinished, vi } from "vitest";
import {
Account,
CoFeed,
CoList,
CoMap,
CoStream,
WasmCrypto,
co,
createJazzContext,
@@ -31,7 +31,7 @@ class Message extends CoMap {
}
class MessagesList extends CoList.Of(co.ref(Message)) {}
class ReactionsStream extends CoStream.Of(co.string) {}
class ReactionsStream extends CoFeed.Of(co.string) {}
async function setupAccount() {
const me = await Account.create({