Compare commits
43 Commits
jazz-nodej
...
cojson-sto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31614c0a4f | ||
|
|
57b69eb8da | ||
|
|
066676c243 | ||
|
|
e141024656 | ||
|
|
2c48ae0434 | ||
|
|
2bf974390d | ||
|
|
e123715819 | ||
|
|
0d087f3d4c | ||
|
|
e410823087 | ||
|
|
dcc11e5b60 | ||
|
|
1b05fe5b55 | ||
|
|
de8f896bf9 | ||
|
|
d63716a827 | ||
|
|
a1e7fce3b9 | ||
|
|
d5edad7ba5 | ||
|
|
559a4a223b | ||
|
|
16d5553ccd | ||
|
|
ef76c586cc | ||
|
|
8ea4b9761c | ||
|
|
a81f281079 | ||
|
|
3ecb602459 | ||
|
|
ea69ea1f67 | ||
|
|
e430bd061e | ||
|
|
a957485172 | ||
|
|
9060975dfb | ||
|
|
753ec83fdd | ||
|
|
44a9785e93 | ||
|
|
d9b390e538 | ||
|
|
b194f7831b | ||
|
|
b60be9405d | ||
|
|
84588e0798 | ||
|
|
e5b170f25e | ||
|
|
63d0b0673e | ||
|
|
2b456b5e07 | ||
|
|
e20809d314 | ||
|
|
b6de11e125 | ||
|
|
d23c71d511 | ||
|
|
b24071cf33 | ||
|
|
a5c88c08de | ||
|
|
906932db1e | ||
|
|
2033e35a41 | ||
|
|
5a2a12ccbb | ||
|
|
d288cb6954 |
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
matrix:
|
||||
project: ["tests/e2e", "examples/chat", "examples/clerk", "examples/betterauth", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/pets", "starters/react-passkey-auth"]
|
||||
project: ["tests/e2e", "examples/chat", "examples/clerk", "examples/betterauth", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/organization", "examples/pets", "starters/react-passkey-auth"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,3 +29,5 @@ test-results
|
||||
|
||||
.cursorrules
|
||||
.windsurfrules
|
||||
|
||||
playwright-report
|
||||
@@ -1,5 +1,26 @@
|
||||
# betterauth
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-betterauth-server-plugin@0.13.32
|
||||
- jazz-react@0.13.32
|
||||
- jazz-react-auth-betterauth@0.13.32
|
||||
- jazz-betterauth-client-plugin@0.13.32
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-betterauth-server-plugin@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-react-auth-betterauth@0.13.31
|
||||
- jazz-betterauth-client-plugin@0.13.31
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "betterauth",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# chat-rn-expo-clerk
|
||||
|
||||
## 1.0.123
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-expo@0.13.32
|
||||
|
||||
## 1.0.122
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-expo@0.13.31
|
||||
- jazz-react-native-media-images@0.13.31
|
||||
|
||||
## 1.0.121
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-expo-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.121",
|
||||
"version": "1.0.123",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# chat-rn-expo
|
||||
|
||||
## 1.0.110
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-expo@0.13.32
|
||||
|
||||
## 1.0.109
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-expo@0.13.31
|
||||
|
||||
## 1.0.108
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn-expo",
|
||||
"version": "1.0.108",
|
||||
"version": "1.0.110",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.13.32
|
||||
|
||||
## 1.0.117
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- jazz-tools@0.13.31
|
||||
- cojson@0.13.31
|
||||
- jazz-react-native@0.13.31
|
||||
- cojson-transport-ws@0.13.31
|
||||
|
||||
## 1.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.116",
|
||||
"version": "1.0.118",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.101
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.32
|
||||
- jazz-vue@0.13.32
|
||||
|
||||
## 0.0.100
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-browser@0.13.31
|
||||
- jazz-vue@0.13.31
|
||||
|
||||
## 0.0.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.99",
|
||||
"version": "0.0.101",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.199
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.198
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.197
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.197",
|
||||
"version": "0.0.199",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
- jazz-react-auth-clerk@0.13.32
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-react-auth-clerk@0.13.31
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.98",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.13.32
|
||||
|
||||
## 0.0.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector-element@0.13.31
|
||||
- jazz-svelte@0.13.31
|
||||
|
||||
## 0.0.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.80",
|
||||
"version": "0.0.82",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# jazz-tailwind-demo-auth-starter
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "filestream",
|
||||
"private": true,
|
||||
"version": "0.0.36",
|
||||
"version": "0.0.38",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# form
|
||||
|
||||
## 0.1.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.1.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.1.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.1.37",
|
||||
"version": "0.1.39",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.95",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.148
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- cojson-transport-ws@0.13.31
|
||||
|
||||
## 0.0.147
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.147",
|
||||
"version": "0.0.148",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# multi-cursors
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multi-cursors",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.91",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# multiauth
|
||||
|
||||
## 0.0.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
- jazz-react-auth-clerk@0.13.32
|
||||
|
||||
## 0.0.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-react-auth-clerk@0.13.31
|
||||
|
||||
## 0.0.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multiauth",
|
||||
"private": true,
|
||||
"version": "0.0.37",
|
||||
"version": "0.0.39",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.120
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.119
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.118",
|
||||
"version": "0.0.120",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
2
examples/organization/.gitignore
vendored
2
examples/organization/.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
playwright-report
|
||||
@@ -1,5 +1,19 @@
|
||||
# organization
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.89",
|
||||
"version": "0.0.91",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:*",
|
||||
@@ -20,6 +22,7 @@
|
||||
"react-router-dom": "^6.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@types/react": "^18.3.12",
|
||||
|
||||
53
examples/organization/playwright.config.ts
Normal file
53
examples/organization/playwright.config.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import isCI from "is-ci";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: isCI,
|
||||
/* Retry on CI only */
|
||||
retries: isCI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: isCI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
|
||||
/* 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/",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: "pnpm preview --port 5173",
|
||||
url: "http://localhost:5173/",
|
||||
reuseExistingServer: !isCI,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -5,7 +5,13 @@ import { Heading } from "./components/Heading.tsx";
|
||||
|
||||
export function HomePage() {
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { organizations: true } },
|
||||
resolve: {
|
||||
root: {
|
||||
organizations: {
|
||||
$each: { $onError: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!me?.root.organizations) return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserIcon } from "lucide-react";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { me, logOut } = useAccount({
|
||||
resolve: { root: { draftOrganization: true } },
|
||||
resolve: { profile: true },
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -16,7 +16,20 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<span className="bg-stone-500 pt-1 size-6 flex items-center justify-center rounded-full">
|
||||
<UserIcon size={20} className="stroke-white" />
|
||||
</span>
|
||||
{me?.profile?.name}
|
||||
<label htmlFor="profile-name" className="sr-only">
|
||||
Profile name
|
||||
</label>
|
||||
<input
|
||||
id="profile-name"
|
||||
type="text"
|
||||
value={me?.profile.name ?? ""}
|
||||
className="rounded-md shadow-sm dark:bg-transparent text-sm py-1.5 px-3"
|
||||
onChange={(e) => {
|
||||
if (me) {
|
||||
me.profile.name = e.target.value;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button
|
||||
|
||||
@@ -16,7 +16,21 @@ export function OrganizationPage() {
|
||||
resolve: { projects: true },
|
||||
});
|
||||
|
||||
if (!organization) return <p>Loading organization...</p>;
|
||||
if (organization === undefined) return <p>Loading organization...</p>;
|
||||
if (organization === null) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
You don't have access to this organization
|
||||
</h1>
|
||||
<a href="/#" className="text-blue-500">
|
||||
Go back to home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function OrganizationForm({
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Submit
|
||||
Create
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Group } from "jazz-tools";
|
||||
import { useAccount, useCoState } from "jazz-react";
|
||||
import { Account, Group, ID } from "jazz-tools";
|
||||
import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationMembers({
|
||||
@@ -9,13 +10,51 @@ export function OrganizationMembers({
|
||||
return (
|
||||
<>
|
||||
{group.members.map((member) => (
|
||||
<div key={member.id} className="px-4 py-5 sm:px-6">
|
||||
<strong className="font-medium">
|
||||
{member.account.profile?.name}
|
||||
</strong>{" "}
|
||||
({member.role})
|
||||
</div>
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
accountId={member.account.id}
|
||||
role={member.role}
|
||||
group={group}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MemberItem({
|
||||
accountId,
|
||||
role,
|
||||
group,
|
||||
}: { accountId: ID<Account>; role: string; group: Group }) {
|
||||
const account = useCoState(Account, accountId, {
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
const { me } = useAccount();
|
||||
|
||||
const canRemoveMember = group.myRole() === "admin" && accountId !== me?.id;
|
||||
|
||||
function handleRemoveMember() {
|
||||
if (canRemoveMember && account) {
|
||||
group.removeMember(account);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 py-5 sm:px-6 flex justify-between items-center">
|
||||
<div>
|
||||
<strong className="font-medium">{account?.profile.name}</strong> ({role}
|
||||
)
|
||||
</div>
|
||||
{canRemoveMember && (
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
onClick={handleRemoveMember}
|
||||
>
|
||||
Remove member
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationSelector({ className }: { className?: string }) {
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { organizations: { $each: true } } },
|
||||
resolve: { root: { organizations: { $each: { $onError: null } } } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -48,6 +48,10 @@ export function OrganizationSelector({ className }: { className?: string }) {
|
||||
className="rounded-md shadow-sm dark:bg-transparent w-full"
|
||||
>
|
||||
{me?.root.organizations.map((organization) => {
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<option key={organization.id} value={organization.id}>
|
||||
{organization.name}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Account, CoList, CoMap, Group, co } from "jazz-tools";
|
||||
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
|
||||
import { getRandomUsername } from "./util";
|
||||
|
||||
export class Project extends CoMap {
|
||||
name = co.string;
|
||||
@@ -38,43 +39,49 @@ export class JazzAccountRoot extends CoMap {
|
||||
export class JazzAccount extends Account {
|
||||
root = co.ref(JazzAccountRoot);
|
||||
|
||||
async migrate() {
|
||||
if (!this._refs.root) {
|
||||
const draftOrganizationOwnership = {
|
||||
owner: Group.create({ owner: this }),
|
||||
};
|
||||
async migrate(this: JazzAccount) {
|
||||
if (this.profile === undefined) {
|
||||
const group = Group.create();
|
||||
this.profile = Profile.create(
|
||||
{
|
||||
name: getRandomUsername(),
|
||||
},
|
||||
group,
|
||||
);
|
||||
group.addMember("everyone", "reader");
|
||||
}
|
||||
|
||||
if (this.root === undefined) {
|
||||
const draftOrgGroup = Group.create();
|
||||
const draftOrganization = DraftOrganization.create(
|
||||
{
|
||||
projects: ListOfProjects.create([], draftOrganizationOwnership),
|
||||
projects: ListOfProjects.create([], draftOrgGroup),
|
||||
},
|
||||
draftOrganizationOwnership,
|
||||
draftOrgGroup,
|
||||
);
|
||||
|
||||
const initialOrganizationOwnership = {
|
||||
owner: Group.create({ owner: this }),
|
||||
};
|
||||
const organizations = ListOfOrganizations.create(
|
||||
[
|
||||
Organization.create(
|
||||
{
|
||||
name: this.profile?.name
|
||||
? `${this.profile.name}'s projects`
|
||||
: "Your projects",
|
||||
projects: ListOfProjects.create([], initialOrganizationOwnership),
|
||||
},
|
||||
initialOrganizationOwnership,
|
||||
),
|
||||
],
|
||||
{ owner: this },
|
||||
);
|
||||
const defaultOrgGroup = Group.create();
|
||||
|
||||
this.root = JazzAccountRoot.create(
|
||||
{
|
||||
draftOrganization,
|
||||
organizations,
|
||||
const { profile } = await this.ensureLoaded({
|
||||
resolve: {
|
||||
profile: true,
|
||||
},
|
||||
{ owner: this },
|
||||
);
|
||||
});
|
||||
|
||||
const organizations = ListOfOrganizations.create([
|
||||
Organization.create(
|
||||
{
|
||||
name: profile.name ? `${profile.name}'s projects` : "Your projects",
|
||||
projects: ListOfProjects.create([], defaultOrgGroup),
|
||||
},
|
||||
defaultOrgGroup,
|
||||
),
|
||||
]);
|
||||
|
||||
this.root = JazzAccountRoot.create({
|
||||
draftOrganization,
|
||||
organizations,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
examples/organization/src/util.ts
Normal file
16
examples/organization/src/util.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
const animals = [
|
||||
"elephant",
|
||||
"penguin",
|
||||
"giraffe",
|
||||
"octopus",
|
||||
"kangaroo",
|
||||
"dolphin",
|
||||
"cheetah",
|
||||
"koala",
|
||||
"platypus",
|
||||
"pangolin",
|
||||
];
|
||||
|
||||
export function getRandomUsername() {
|
||||
return `Anonymous ${animals[Math.floor(Math.random() * animals.length)]}`;
|
||||
}
|
||||
65
examples/organization/tests/createAndShare.spec.ts
Normal file
65
examples/organization/tests/createAndShare.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { BrowserContext, expect, test } from "@playwright/test";
|
||||
|
||||
test("create a new organization and share", async ({
|
||||
page: marioPage,
|
||||
browser,
|
||||
}) => {
|
||||
await marioPage.goto("/");
|
||||
|
||||
const luigiContext = await browser.newContext();
|
||||
const luigiPage = await luigiContext.newPage();
|
||||
await luigiPage.goto("/");
|
||||
|
||||
await test.step("Set the profile names", async () => {
|
||||
await marioPage
|
||||
.getByRole("textbox", { name: "Profile name" })
|
||||
.fill("Mario");
|
||||
await luigiPage
|
||||
.getByRole("textbox", { name: "Profile name" })
|
||||
.fill("Luigi");
|
||||
});
|
||||
|
||||
await test.step("Create a new organization", async () => {
|
||||
await marioPage
|
||||
.getByRole("textbox", { name: "Organization name" })
|
||||
.fill("Mario's organization");
|
||||
await marioPage.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
await expect(
|
||||
marioPage.getByRole("heading", { name: "Mario's organization" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Invite Luigi to the organization", async () => {
|
||||
await marioPage.getByRole("button", { name: "Copy invite link" }).click();
|
||||
|
||||
const inviteUrl = await marioPage.evaluate(() =>
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
|
||||
await luigiPage.goto(inviteUrl);
|
||||
|
||||
await expect(
|
||||
luigiPage.getByRole("heading", { name: "Mario's organization" }),
|
||||
).toBeVisible();
|
||||
await expect(marioPage.getByText("Luigi")).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Kick out Luigi from the organization", async () => {
|
||||
await marioPage.getByRole("button", { name: "Remove" }).click();
|
||||
|
||||
await expect(marioPage.getByText("Luigi")).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
luigiPage.getByRole("heading", {
|
||||
name: "You don't have access to this organization",
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await luigiPage.getByRole("link", { name: "Go back to home" }).click();
|
||||
|
||||
await expect(
|
||||
luigiPage.getByRole("heading", { name: "Organizations example app" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,19 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.13.32
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-svelte@0.13.31
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.84",
|
||||
"version": "0.0.86",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<JazzProvider
|
||||
sync={{
|
||||
peer: `wss://cloud.jazz.tools/?key={apiKey}`,
|
||||
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
|
||||
}}
|
||||
>
|
||||
<PasskeyAuthBasicUI appName="minimal-svelte-auth-passkey">
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.96
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.94",
|
||||
"version": "0.0.96",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# passphrase
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.91",
|
||||
"version": "0.0.93",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.117
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.115
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.115",
|
||||
"version": "0.0.117",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.215
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.214
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.213
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.213",
|
||||
"version": "0.0.215",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.95
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.94
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.93",
|
||||
"version": "0.0.95",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# richtext-tiptap
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
- jazz-richtext-tiptap@0.1.8
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-richtext-tiptap@0.1.7
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext-tiptap",
|
||||
"private": true,
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# richtext
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
- jazz-richtext-prosemirror@0.1.19
|
||||
|
||||
## 0.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
- jazz-richtext-prosemirror@0.1.18
|
||||
|
||||
## 0.0.83
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "richtext",
|
||||
"private": true,
|
||||
"version": "0.0.83",
|
||||
"version": "0.0.85",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.13.32
|
||||
- jazz-vue@0.13.32
|
||||
|
||||
## 0.0.98
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-browser@0.13.31
|
||||
- jazz-vue@0.13.31
|
||||
|
||||
## 0.0.97
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.99",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.214
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.213
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.212
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.212",
|
||||
"version": "0.0.214",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.93
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.13.32
|
||||
|
||||
## 0.0.92
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e5b170f]
|
||||
- jazz-tools@0.13.31
|
||||
- jazz-inspector@0.13.31
|
||||
- jazz-react@0.13.31
|
||||
|
||||
## 0.0.91
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.91",
|
||||
"version": "0.0.93",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -4,8 +4,6 @@ import { clsx } from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
|
||||
// TODO: add tabs feature, and remove CodeExampleTabs
|
||||
|
||||
export function CopyButton({
|
||||
code,
|
||||
size,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExampleCard } from "@/components/examples/ExampleCard";
|
||||
import { ExampleDemo } from "@/components/examples/ExampleDemo";
|
||||
import { ClerkFullLogo } from "@/components/icons/ClerkFullLogo";
|
||||
import { ReactLogo } from "@/components/icons/ReactLogo";
|
||||
import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
|
||||
@@ -508,16 +507,16 @@ const reactExamples: Example[] = [
|
||||
demoUrl: "https://music-demo.jazz.tools",
|
||||
illustration: <MusicIllustration />,
|
||||
},
|
||||
{
|
||||
name: "Jazz paper scissors",
|
||||
slug: "jazz-paper-scissors",
|
||||
description:
|
||||
"A game that shows how to communicate with other accounts through the experimental Inbox API.",
|
||||
tech: [tech.react],
|
||||
features: [features.serverWorker, features.inbox],
|
||||
illustration: <JazzPaperScissorsIllustration />,
|
||||
demoUrl: "https://jazz-paper-scissors.vercel.app",
|
||||
},
|
||||
// {
|
||||
// name: "Jazz paper scissors",
|
||||
// slug: "jazz-paper-scissors",
|
||||
// description:
|
||||
// "A game that shows how to communicate with other accounts through the experimental Inbox API.",
|
||||
// tech: [tech.react],
|
||||
// features: [features.serverWorker, features.inbox],
|
||||
// illustration: <JazzPaperScissorsIllustration />,
|
||||
// demoUrl: "https://jazz-paper-scissors.vercel.app",
|
||||
// },
|
||||
{
|
||||
name: "Clerk",
|
||||
slug: "clerk",
|
||||
@@ -681,15 +680,11 @@ export default function Page() {
|
||||
|
||||
<GappedGrid>
|
||||
{category.examples.map((example) =>
|
||||
example.showDemo ? (
|
||||
<ExampleDemo key={example.slug} example={example} />
|
||||
) : (
|
||||
<ExampleCard
|
||||
className="border bg-stone-50 shadow-sm p-3 rounded-lg dark:bg-stone-950"
|
||||
key={example.slug}
|
||||
example={example}
|
||||
/>
|
||||
),
|
||||
<ExampleCard
|
||||
className="border bg-stone-50 shadow-sm p-3 rounded-lg dark:bg-stone-950"
|
||||
key={example.slug}
|
||||
example={example}
|
||||
/>
|
||||
)}
|
||||
</GappedGrid>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { HelpLinks } from "@/components/docs/HelpLinks";
|
||||
import { TableOfContents } from "@/components/docs/TableOfContents";
|
||||
import { JazzMobileNav } from "@/components/nav";
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CodeExampleTab {
|
||||
name: string;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface CodeExampleTabsProps {
|
||||
tabs: Array<CodeExampleTab>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// TODO: handle tabs in CodeGroup component
|
||||
|
||||
export function CodeExampleTabs({
|
||||
tabs,
|
||||
className = "",
|
||||
}: CodeExampleTabsProps) {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-white h-full flex flex-col",
|
||||
"dark:bg-stone-925",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex border-b overflow-x-auto overflow-y-hidden dark:bg-stone-950">
|
||||
{tabs.map((tab, index) => (
|
||||
<div key={index}>
|
||||
<button
|
||||
key={index}
|
||||
className={clsx(
|
||||
activeTab === index
|
||||
? "border-blue-700 bg-white text-stone-700 dark:bg-stone-925 dark:text-blue-500 dark:border-blue-500"
|
||||
: "border-transparent text-stone-500 hover:text-stone-700 dark:hover:text-blue-500",
|
||||
"flex items-center -mb-px transition-colors px-3 pb-2 pt-2.5 block text-xs border-b-2 ",
|
||||
)}
|
||||
onClick={() => setActiveTab(index)}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">{tabs[activeTab].content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { CodeExampleTabs } from "@/components/examples/CodeExampleTabs";
|
||||
import { ExampleLinks } from "@/components/examples/ExampleLinks";
|
||||
import { ExampleTags } from "@/components/examples/ExampleTags";
|
||||
import { Example } from "@/content/example";
|
||||
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
|
||||
|
||||
export function ExampleDemo({ example }: { example: Example }) {
|
||||
const { name, demoUrl, illustration } = example;
|
||||
|
||||
return (
|
||||
<GappedGrid
|
||||
gap="none"
|
||||
className="border bg-stone-50 shadow-sm rounded-lg dark:bg-stone-950 overflow-hidden"
|
||||
>
|
||||
<div className="p-3 col-span-full flex flex-col gap-2 justify-between items-baseline border-b sm:flex-row">
|
||||
<div className="flex flex-col gap-2 items-baseline sm:flex-row">
|
||||
<h2 className="font-medium text-stone-900 dark:text-white leading-none">
|
||||
{name}
|
||||
</h2>
|
||||
<ExampleTags example={example} />
|
||||
</div>
|
||||
|
||||
<ExampleLinks example={example} />
|
||||
</div>
|
||||
<div className="h-[25rem] lg:h-[30rem] border-t overflow-auto col-span-full md:col-span-2 lg:col-span-3 order-last md:order-none md:border-r md:border-t-0">
|
||||
{example.codeSamples && (
|
||||
<CodeExampleTabs tabs={example.codeSamples}></CodeExampleTabs>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-full md:p-8 md:col-span-2 lg:col-span-3 h-[25rem] lg:h-[30rem] lg:p-12">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="md:rounded-lg md:shadow-lg"
|
||||
src={demoUrl}
|
||||
title={name}
|
||||
/>
|
||||
</div>
|
||||
</GappedGrid>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
CodeExampleTabs as CodeExampleTabsClient,
|
||||
CodeExampleTabsProps,
|
||||
} from "@/components/examples/CodeExampleTabs";
|
||||
|
||||
import {
|
||||
ContentByFramework as ContentByFrameworkClient,
|
||||
ContentByFrameworkProps,
|
||||
@@ -14,10 +9,6 @@ import { FileDownloadLink as FileDownloadLinkClient } from "./FileDownloadLink";
|
||||
import { Framework as FrameworkClient } from "./docs/Framework";
|
||||
import { IssueTrackerPreview as IssueTrackerPreviewClient } from "./docs/IssueTrackerPreview";
|
||||
|
||||
export function CodeExampleTabs(props: CodeExampleTabsProps) {
|
||||
return <CodeExampleTabsClient {...props} />;
|
||||
}
|
||||
|
||||
export function CodeGroup(props: { children: React.ReactNode }) {
|
||||
return <CodeGroupClient {...props}></CodeGroupClient>;
|
||||
}
|
||||
|
||||
@@ -150,10 +150,12 @@ declare module "jazz-react" {
|
||||
}
|
||||
// ---cut---
|
||||
export function AcceptInvitePage() {
|
||||
const { me } = useAccount({ resolve: { root: { organizations: true } } });
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { organizations: { $each: { $onError: null } } } },
|
||||
});
|
||||
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
if (me?.root?.organizations) {
|
||||
if (me) {
|
||||
Organization.load(organizationId).then((organization) => {
|
||||
if (organization) {
|
||||
// avoid duplicates
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
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
|
||||
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
|
||||
@@ -43,7 +43,7 @@ group.addMember(bob, "writer");
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
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:
|
||||
**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:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
@@ -66,7 +66,7 @@ group.addMember(bob, "reader");
|
||||
|
||||
Bob just went from a writer to a reader.
|
||||
|
||||
Note: only admins can change a member's role.
|
||||
**Note:** only admins can change a member's role.
|
||||
|
||||
## Removing a member
|
||||
|
||||
@@ -78,9 +78,11 @@ group.removeMember(bob);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
This only works if you are an admin, and Bob is not an admin.
|
||||
|
||||
Admins cannot remove other admins, but they can remove themselves, as long as there is another admin present.
|
||||
Rules:
|
||||
- All roles can remove themselves.
|
||||
- Only admins can remove other users.
|
||||
- An admin cannot remove other admins.
|
||||
- As an admin, you cannot remove yourself if you are the only admin in the Group, because there has to be at least one admin present.
|
||||
|
||||
## Getting the Group of an existing CoValue
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { ContentByFramework, CodeGroup } from '@/components/forMdx'
|
||||
|
||||
# Public sharing and invites
|
||||
|
||||
...more docs coming soon
|
||||
|
||||
## Public sharing
|
||||
|
||||
You can share CoValues publicly by setting the `owner` to a `Group`, and granting
|
||||
@@ -94,3 +92,104 @@ useAcceptInvite({
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### Requesting Invites
|
||||
|
||||
To allow a non-group member to request an invitation to a group you can use the `writeOnly` role.
|
||||
This means that users only have write access to a specific requests list (they can't read other requests).
|
||||
However, Administrators can review and approve these requests.
|
||||
|
||||
Create the data models.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { CoMap, co, CoValue, Account, CoList } from "jazz-tools";
|
||||
// ---cut-before---
|
||||
class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.literal("pending", "approved", "rejected");
|
||||
}
|
||||
|
||||
class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Set up the request system with appropriate access controls.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { Group, co, CoList, CoMap, Account } from "jazz-tools";
|
||||
import { createInviteLink } from "jazz-react";
|
||||
|
||||
export class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.literal("pending", "approved", "rejected");
|
||||
}
|
||||
|
||||
export class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
|
||||
// ---cut-before---
|
||||
function createRequestsToJoin() {
|
||||
const requestsGroup = Group.create();
|
||||
requestsGroup.addMember("everyone", "writeOnly");
|
||||
|
||||
return RequestsList.create([], requestsGroup);
|
||||
}
|
||||
|
||||
async function sendJoinRequest(
|
||||
requestsList: RequestsList,
|
||||
account: Account,
|
||||
) {
|
||||
const request = JoinRequest.create(
|
||||
{
|
||||
account,
|
||||
status: "pending",
|
||||
},
|
||||
requestsList._owner // Inherit the access controls of the requestsList
|
||||
);
|
||||
|
||||
requestsList.push(request);
|
||||
|
||||
return request;
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Using the write-only access users can submit requests that only administrators can review and approve.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { co, CoMap, CoList, ID, Account, Group, Resolved } from "jazz-tools";
|
||||
|
||||
class JoinRequest extends CoMap {
|
||||
account = co.ref(Account);
|
||||
status = co.string; // Can be "pending", "approved", "rejected"
|
||||
}
|
||||
|
||||
export class RequestsList extends CoList.Of(co.ref(JoinRequest)) {};
|
||||
|
||||
export class RequestsToJoin extends CoMap {
|
||||
writeOnlyInvite = co.string;
|
||||
requests = co.ref(RequestsList);
|
||||
}
|
||||
|
||||
// ---cut-before---
|
||||
async function approveJoinRequest(
|
||||
joinRequest: JoinRequest,
|
||||
targetGroup: Group,
|
||||
) {
|
||||
const account = await Account.load(joinRequest._refs.account.id);
|
||||
|
||||
if (account) {
|
||||
targetGroup.addMember(account, "reader");
|
||||
joinRequest.status = "approved";
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -465,6 +465,191 @@ project?.owner // => always null
|
||||
The load operation will succeed and return the object, but the inaccessible reference will always be `null`.
|
||||
|
||||
|
||||
#### Deep loading lists with shared items
|
||||
|
||||
When loading a list with shared items, you can use the `$onError` option to safely load the list skipping any inaccessible items.
|
||||
|
||||
This is especially useful when in your app access to these items might be revoked.
|
||||
|
||||
This way the inaccessible items are replaced with `null` in the returned list.
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, CoList, co, Group, Account } from "jazz-tools";
|
||||
import { assert } from "vitest";
|
||||
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
class Friends extends CoList.Of(co.ref(Person)) {}
|
||||
|
||||
const privateGroup = Group.create();
|
||||
const publicGroup = Group.create();
|
||||
const me = {} as unknown as Account;
|
||||
|
||||
// ---cut-before---
|
||||
const source = Friends.create(
|
||||
[
|
||||
Person.create(
|
||||
{
|
||||
name: "Jane",
|
||||
},
|
||||
privateGroup, // We don't have access to Jane
|
||||
),
|
||||
Person.create(
|
||||
{
|
||||
name: "Alice",
|
||||
},
|
||||
publicGroup, // We have access to Alice
|
||||
),
|
||||
],
|
||||
publicGroup,
|
||||
);
|
||||
|
||||
const friends = await Friends.load(source.id, {
|
||||
resolve: {
|
||||
$each: { $onError: null }
|
||||
},
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
// Thanks to $onError catching the errors, the list is loaded
|
||||
// because we have access to friends
|
||||
friends // => Friends
|
||||
|
||||
assert(friends);
|
||||
|
||||
// Jane is null because we lack access rights
|
||||
// and we have used $onError to catch the error on the list items
|
||||
friends[0] // => null
|
||||
|
||||
// Alice is not null because we have access
|
||||
// the type is nullable because we have used $onError
|
||||
friends[1] // => Person
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The `$onError` works as a "catch" clause option to block any error in the resolved childs.
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, CoList, co, Group, Account } from "jazz-tools";
|
||||
import { assert } from "vitest";
|
||||
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
dog = co.ref(Dog);
|
||||
}
|
||||
class Dog extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
class Friends extends CoList.Of(co.ref(Person)) {}
|
||||
|
||||
class User extends CoMap {
|
||||
name = co.string;
|
||||
friends = co.ref(Friends);
|
||||
}
|
||||
|
||||
const privateGroup = Group.create();
|
||||
const publicGroup = Group.create();
|
||||
const me = {} as unknown as Account;
|
||||
|
||||
// ---cut-before---
|
||||
const source = Friends.create(
|
||||
[
|
||||
Person.create(
|
||||
{
|
||||
name: "Jane",
|
||||
dog: Dog.create(
|
||||
{ name: "Rex" },
|
||||
privateGroup,
|
||||
), // We don't have access to Rex
|
||||
},
|
||||
publicGroup,
|
||||
),
|
||||
],
|
||||
publicGroup,
|
||||
);
|
||||
|
||||
const friends = await Friends.load(source.id, {
|
||||
resolve: {
|
||||
$each: { dog: true, $onError: null }
|
||||
},
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
assert(friends);
|
||||
|
||||
// Jane is null because we don't have access to Rex
|
||||
// and we have used $onError to catch the error on the list items
|
||||
friends[0] // => null
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
We can actually use `$onError` everywhere in the resolve query, so we can use it to catch the error on dog:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, CoList, co, Group, Account } from "jazz-tools";
|
||||
import { assert } from "vitest";
|
||||
|
||||
class Person extends CoMap {
|
||||
name = co.string;
|
||||
dog = co.ref(Dog);
|
||||
}
|
||||
class Dog extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
class Friends extends CoList.Of(co.ref(Person)) {}
|
||||
|
||||
class User extends CoMap {
|
||||
name = co.string;
|
||||
friends = co.ref(Friends);
|
||||
}
|
||||
|
||||
const privateGroup = Group.create();
|
||||
const publicGroup = Group.create();
|
||||
const me = {} as unknown as Account;
|
||||
|
||||
const source = Friends.create(
|
||||
[
|
||||
Person.create(
|
||||
{
|
||||
name: "Jane",
|
||||
dog: Dog.create(
|
||||
{ name: "Rex" },
|
||||
privateGroup,
|
||||
), // We don't have access to Rex
|
||||
},
|
||||
publicGroup,
|
||||
),
|
||||
],
|
||||
publicGroup,
|
||||
);
|
||||
|
||||
// ---cut-before---
|
||||
const friends = await Friends.load(source.id, {
|
||||
resolve: {
|
||||
$each: { dog: { $onError: null } }
|
||||
},
|
||||
loadAs: me,
|
||||
});
|
||||
|
||||
assert(friends);
|
||||
|
||||
// Jane now is not-nullable at type level because
|
||||
// we have moved $onError down to the dog field
|
||||
//
|
||||
// This also means that if we don't have access to Jane
|
||||
// the entire friends list will be null
|
||||
friends[0] // => Person
|
||||
|
||||
// Jane's dog is null because we don't have access to Rex
|
||||
// and we have used $onError to catch the error
|
||||
friends[0].dog // => null
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -6,7 +6,6 @@ export type Example = {
|
||||
tech?: string[];
|
||||
features?: string[];
|
||||
demoUrl?: string;
|
||||
showDemo?: boolean;
|
||||
imageUrl?: string;
|
||||
codeSamples?: { name: string; content: React.ReactNode }[];
|
||||
};
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage-indexeddb
|
||||
|
||||
## 0.13.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2bf9743]
|
||||
- cojson-storage@0.13.32
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
- cojson-storage@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage-indexeddb",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.32",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -67,16 +67,14 @@ export class IDBClient implements DBClientInterfaceAsync {
|
||||
|
||||
async getNewTransactionInSession(
|
||||
sessionRowId: number,
|
||||
firstNewTxIdx: number,
|
||||
fromIdx: number,
|
||||
toIdx: number,
|
||||
): Promise<TransactionRow[]> {
|
||||
return this.makeRequest<TransactionRow[]>((tx) =>
|
||||
tx
|
||||
.getObjectStore("transactions")
|
||||
.getAll(
|
||||
IDBKeyRange.bound(
|
||||
[sessionRowId, firstNewTxIdx],
|
||||
[sessionRowId, Number.POSITIVE_INFINITY],
|
||||
),
|
||||
IDBKeyRange.bound([sessionRowId, fromIdx], [sessionRowId, toIdx]),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -161,7 +159,7 @@ export class IDBClient implements DBClientInterfaceAsync {
|
||||
ses: sessionRowID,
|
||||
idx,
|
||||
signature,
|
||||
} satisfies SignatureAfterRow),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { IDBStorage } from "../index.js";
|
||||
import { toSimplifiedMessages } from "./messagesTestUtils.js";
|
||||
import { trackMessages } from "./testUtils.js";
|
||||
import { trackMessages, waitFor } from "./testUtils.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -96,11 +96,84 @@ test("should sync and load data from storage", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> KNOWN Map sessions: header/1",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
]
|
||||
`);
|
||||
|
||||
node2Sync.restore();
|
||||
});
|
||||
|
||||
test("should send an empty content message if there is no content", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const node1 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node1Sync = trackMessages(node1);
|
||||
|
||||
const peer = await IDBStorage.asPeer();
|
||||
|
||||
node1.syncManager.addPeer(peer);
|
||||
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: map.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node1Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> KNOWN Group sessions: header/3",
|
||||
"client -> CONTENT Map header: true new: ",
|
||||
"storage -> KNOWN Map sessions: header/0",
|
||||
]
|
||||
`);
|
||||
|
||||
node1Sync.restore();
|
||||
|
||||
const node2 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node2Sync = trackMessages(node2);
|
||||
|
||||
const peer2 = await IDBStorage.asPeer();
|
||||
|
||||
node2.syncManager.addPeer(peer2);
|
||||
|
||||
const map2 = await node2.load(map.id);
|
||||
if (map2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: map.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node2Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> CONTENT Map header: true new: ",
|
||||
]
|
||||
`);
|
||||
|
||||
@@ -185,14 +258,9 @@ test("should load dependencies correctly (group inheritance)", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
|
||||
"storage -> KNOWN Group sessions: header/5",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 5",
|
||||
"client -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> KNOWN Map sessions: header/1",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN Group sessions: header/5",
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -260,19 +328,14 @@ test("should not send the same dependency value twice", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
|
||||
"storage -> KNOWN Group sessions: header/5",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 5",
|
||||
"client -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> KNOWN Map sessions: header/1",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN ParentGroup sessions: header/4",
|
||||
"client -> KNOWN Group sessions: header/5",
|
||||
"client -> KNOWN Map sessions: header/1",
|
||||
"client -> LOAD MapFromParent sessions: empty",
|
||||
"storage -> KNOWN MapFromParent sessions: header/1",
|
||||
"storage -> CONTENT MapFromParent header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN MapFromParent sessions: header/1",
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -374,11 +437,164 @@ test("should recover from data loss", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> KNOWN Map sessions: header/4",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 4",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("should sync multiple sessions in a single content message", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const node1 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
node1.syncManager.addPeer(await IDBStorage.asPeer());
|
||||
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("hello", "world");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
node1.gracefulShutdown();
|
||||
|
||||
const node2 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
node2.syncManager.addPeer(await IDBStorage.asPeer());
|
||||
|
||||
const map2 = await node2.load(map.id);
|
||||
if (map2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(map2.get("hello")).toBe("world");
|
||||
|
||||
map2.set("hello", "world2");
|
||||
|
||||
await map2.core.waitForSync();
|
||||
|
||||
node2.gracefulShutdown();
|
||||
|
||||
const node3 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node3Sync = trackMessages(node3);
|
||||
|
||||
node3.syncManager.addPeer(await IDBStorage.asPeer());
|
||||
|
||||
const map3 = await node3.load(map.id);
|
||||
if (map3 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(map3.get("hello")).toBe("world2");
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: map.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node3Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
|
||||
]
|
||||
`);
|
||||
|
||||
node3Sync.restore();
|
||||
});
|
||||
|
||||
test("large coValue upload streaming", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const node1 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
node1.syncManager.addPeer(await IDBStorage.asPeer());
|
||||
|
||||
const group = node1.createGroup();
|
||||
const largeMap = group.createMap();
|
||||
|
||||
// Generate a large amount of data (about 100MB)
|
||||
const dataSize = 1 * 1024 * 200;
|
||||
const chunkSize = 1024; // 1KB chunks
|
||||
const chunks = dataSize / chunkSize;
|
||||
|
||||
const value = "a".repeat(chunkSize);
|
||||
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
const key = `key${i}`;
|
||||
largeMap.set(key, value, "trusting");
|
||||
}
|
||||
|
||||
await largeMap.core.waitForSync();
|
||||
|
||||
const knownState = largeMap.core.knownState();
|
||||
|
||||
node1.gracefulShutdown();
|
||||
|
||||
const node2 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node2Sync = trackMessages(node2);
|
||||
|
||||
node2.syncManager.addPeer(await IDBStorage.asPeer());
|
||||
|
||||
const largeMapOnNode2 = await node2.load(largeMap.id);
|
||||
|
||||
if (largeMapOnNode2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(largeMapOnNode2.core.knownState()).toEqual(knownState);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: largeMap.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node2Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN Map sessions: header/200",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 97",
|
||||
"storage -> CONTENT Map header: true new: After: 97 New: 97",
|
||||
"storage -> CONTENT Map header: true new: After: 194 New: 6",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
"client -> KNOWN Map sessions: header/97",
|
||||
"client -> KNOWN Map sessions: header/194",
|
||||
"client -> KNOWN Map sessions: header/200",
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -42,3 +42,33 @@ export function trackMessages(node: LocalNode) {
|
||||
restore,
|
||||
};
|
||||
}
|
||||
|
||||
export function waitFor(
|
||||
callback: () => boolean | undefined | Promise<boolean | undefined>,
|
||||
) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const checkPassed = async () => {
|
||||
try {
|
||||
return { ok: await callback(), error: null };
|
||||
} catch (error) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
let retries = 0;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
const { ok, error } = await checkPassed();
|
||||
|
||||
if (ok !== false) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
|
||||
if (++retries > 10) {
|
||||
clearInterval(interval);
|
||||
reject(error);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# cojson-storage-sqlite
|
||||
|
||||
## 0.13.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2bf9743]
|
||||
- cojson-storage@0.13.32
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
- cojson-storage@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "cojson-storage-sqlite",
|
||||
"type": "module",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.32",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cojson": "workspace:0.13.30",
|
||||
"cojson": "workspace:0.13.31",
|
||||
"cojson-storage": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -86,13 +86,14 @@ export class SQLiteClient implements DBClientInterfaceSync {
|
||||
|
||||
getNewTransactionInSession(
|
||||
sessionRowId: number,
|
||||
firstNewTxIdx: number,
|
||||
fromIdx: number,
|
||||
toIdx: number,
|
||||
): TransactionRow[] {
|
||||
const txs = this.db
|
||||
.prepare<[number, number]>(
|
||||
"SELECT * FROM transactions WHERE ses = ? AND idx >= ?",
|
||||
.prepare<[number, number, number]>(
|
||||
"SELECT * FROM transactions WHERE ses = ? AND idx >= ? AND idx <= ?",
|
||||
)
|
||||
.all(sessionRowId, firstNewTxIdx) as RawTransactionRow[];
|
||||
.all(sessionRowId, fromIdx, toIdx) as RawTransactionRow[];
|
||||
|
||||
try {
|
||||
return txs.map((transactionRow) => ({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
||||
import { expect, onTestFinished, test, vi } from "vitest";
|
||||
import { SQLiteNode } from "../index.js";
|
||||
import { toSimplifiedMessages } from "./messagesTestUtils.js";
|
||||
import { trackMessages } from "./testUtils.js";
|
||||
import { trackMessages, waitFor } from "./testUtils.js";
|
||||
|
||||
const Crypto = await WasmCrypto.create();
|
||||
|
||||
@@ -117,10 +117,8 @@ test("should sync and load data from storage", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
"storage -> KNOWN Map sessions: header/1",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN Map sessions: header/1",
|
||||
]
|
||||
@@ -129,6 +127,84 @@ test("should sync and load data from storage", async () => {
|
||||
node2Sync.restore();
|
||||
});
|
||||
|
||||
test("should send an empty content message if there is no content", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const node1 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node1Sync = trackMessages(node1);
|
||||
|
||||
const { peer, dbPath } = await createSQLiteStorage();
|
||||
|
||||
node1.syncManager.addPeer(peer);
|
||||
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: map.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node1Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"storage -> KNOWN Group sessions: header/3",
|
||||
"client -> CONTENT Map header: true new: ",
|
||||
"storage -> KNOWN Map sessions: header/0",
|
||||
]
|
||||
`);
|
||||
|
||||
node1Sync.restore();
|
||||
|
||||
const node2 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node2Sync = trackMessages(node2);
|
||||
|
||||
const { peer: peer2 } = await createSQLiteStorage(dbPath);
|
||||
|
||||
node2.syncManager.addPeer(peer2);
|
||||
|
||||
const map2 = await node2.load(map.id);
|
||||
if (map2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: map.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node2Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Map header: true new: ",
|
||||
"client -> KNOWN Map sessions: header/0",
|
||||
]
|
||||
`);
|
||||
|
||||
node2Sync.restore();
|
||||
});
|
||||
|
||||
test("should load dependencies correctly (group inheritance)", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
@@ -207,13 +283,10 @@ test("should load dependencies correctly (group inheritance)", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
|
||||
"client -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> KNOWN Group sessions: header/5",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 5",
|
||||
"client -> KNOWN Group sessions: header/5",
|
||||
"storage -> KNOWN Map sessions: header/1",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN Map sessions: header/1",
|
||||
]
|
||||
@@ -283,17 +356,13 @@ test("should not send the same dependency value twice", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
|
||||
"client -> KNOWN ParentGroup sessions: header/4",
|
||||
"storage -> KNOWN Group sessions: header/5",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 5",
|
||||
"client -> KNOWN Group sessions: header/5",
|
||||
"storage -> KNOWN Map sessions: header/1",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN Map sessions: header/1",
|
||||
"client -> LOAD MapFromParent sessions: empty",
|
||||
"storage -> KNOWN MapFromParent sessions: header/1",
|
||||
"storage -> CONTENT MapFromParent header: true new: After: 0 New: 1",
|
||||
"client -> KNOWN MapFromParent sessions: header/1",
|
||||
]
|
||||
@@ -397,10 +466,8 @@ test("should recover from data loss", async () => {
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
"storage -> KNOWN Map sessions: header/4",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 4",
|
||||
"client -> KNOWN Map sessions: header/4",
|
||||
]
|
||||
@@ -495,3 +562,166 @@ test("should recover missing dependencies from storage", async () => {
|
||||
"0": 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("should sync multiple sessions in a single content message", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const node1 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const { peer, dbPath } = await createSQLiteStorage();
|
||||
|
||||
node1.syncManager.addPeer(peer);
|
||||
|
||||
const group = node1.createGroup();
|
||||
|
||||
const map = group.createMap();
|
||||
|
||||
map.set("hello", "world");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
node1.gracefulShutdown();
|
||||
|
||||
const node2 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
node2.syncManager.addPeer((await createSQLiteStorage(dbPath)).peer);
|
||||
|
||||
const map2 = await node2.load(map.id);
|
||||
if (map2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(map2.get("hello")).toBe("world");
|
||||
|
||||
map2.set("hello", "world2");
|
||||
|
||||
await map2.core.waitForSync();
|
||||
|
||||
node2.gracefulShutdown();
|
||||
|
||||
const node3 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node3Sync = trackMessages(node3);
|
||||
|
||||
node3.syncManager.addPeer((await createSQLiteStorage(dbPath)).peer);
|
||||
|
||||
const map3 = await node3.load(map.id);
|
||||
if (map3 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
expect(map3.get("hello")).toBe("world2");
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: map.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node3Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
|
||||
"client -> KNOWN Map sessions: header/2",
|
||||
]
|
||||
`);
|
||||
|
||||
node3Sync.restore();
|
||||
});
|
||||
|
||||
test("large coValue upload streaming", async () => {
|
||||
const agentSecret = Crypto.newRandomAgentSecret();
|
||||
|
||||
const node1 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const { peer, dbPath } = await createSQLiteStorage();
|
||||
|
||||
node1.syncManager.addPeer(peer);
|
||||
|
||||
const group = node1.createGroup();
|
||||
const largeMap = group.createMap();
|
||||
|
||||
const dataSize = 1 * 1024 * 200;
|
||||
const chunkSize = 1024; // 1KB chunks
|
||||
const chunks = dataSize / chunkSize;
|
||||
|
||||
const value = "a".repeat(chunkSize);
|
||||
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
const key = `key${i}`;
|
||||
largeMap.set(key, value, "trusting");
|
||||
}
|
||||
|
||||
await largeMap.core.waitForSync();
|
||||
|
||||
node1.gracefulShutdown();
|
||||
|
||||
const node2 = new LocalNode(
|
||||
agentSecret,
|
||||
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
||||
Crypto,
|
||||
);
|
||||
|
||||
const node2Sync = trackMessages(node2);
|
||||
|
||||
const { peer: peer2 } = await createSQLiteStorage(dbPath);
|
||||
|
||||
node2.syncManager.addPeer(peer2);
|
||||
|
||||
const largeMapOnNode2 = await node2.load(largeMap.id);
|
||||
|
||||
if (largeMapOnNode2 === "unavailable") {
|
||||
throw new Error("Map is unavailable");
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(largeMapOnNode2.core.knownState()).toEqual(
|
||||
largeMap.core.knownState(),
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(
|
||||
toSimplifiedMessages(
|
||||
{
|
||||
Map: largeMap.core,
|
||||
Group: group.core,
|
||||
},
|
||||
node2Sync.messages,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
"client -> LOAD Map sessions: empty",
|
||||
"storage -> KNOWN Map sessions: header/200",
|
||||
"storage -> CONTENT Group header: true new: After: 0 New: 3",
|
||||
"client -> KNOWN Group sessions: header/3",
|
||||
"storage -> CONTENT Map header: true new: After: 0 New: 97",
|
||||
"client -> KNOWN Map sessions: header/97",
|
||||
"storage -> CONTENT Map header: true new: After: 97 New: 97",
|
||||
"client -> KNOWN Map sessions: header/194",
|
||||
"storage -> CONTENT Map header: true new: After: 194 New: 6",
|
||||
"client -> KNOWN Map sessions: header/200",
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -42,3 +42,32 @@ export function trackMessages(node: LocalNode) {
|
||||
restore,
|
||||
};
|
||||
}
|
||||
export function waitFor(
|
||||
callback: () => boolean | undefined | Promise<boolean | undefined>,
|
||||
) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const checkPassed = async () => {
|
||||
try {
|
||||
return { ok: await callback(), error: null };
|
||||
} catch (error) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
let retries = 0;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
const { ok, error } = await checkPassed();
|
||||
|
||||
if (ok !== false) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
|
||||
if (++retries > 10) {
|
||||
clearInterval(interval);
|
||||
reject(error);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# cojson-storage
|
||||
|
||||
## 0.13.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2bf9743: Implement content streaming for large CoValues on storage
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cojson-storage",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.32",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
logger,
|
||||
} from "cojson";
|
||||
import { collectNewTxs, getDependedOnCoValues } from "./syncUtils.js";
|
||||
import type { DBClientInterfaceAsync, StoredSessionRow } from "./types.js";
|
||||
import type {
|
||||
DBClientInterfaceAsync,
|
||||
SignatureAfterRow,
|
||||
StoredCoValueRow,
|
||||
StoredSessionRow,
|
||||
} from "./types.js";
|
||||
import NewContentMessage = CojsonInternalTypes.NewContentMessage;
|
||||
import KnownStateMessage = CojsonInternalTypes.KnownStateMessage;
|
||||
import RawCoID = CojsonInternalTypes.RawCoID;
|
||||
@@ -42,156 +47,156 @@ export class StorageManagerAsync {
|
||||
await this.handleContent(msg);
|
||||
break;
|
||||
case "known":
|
||||
await this.handleKnown(msg);
|
||||
this.handleKnown(msg);
|
||||
break;
|
||||
case "done":
|
||||
await this.handleDone(msg);
|
||||
this.handleDone(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async handleSessionUpdate({
|
||||
sessionRow,
|
||||
peerKnownState,
|
||||
newContentMessages,
|
||||
}: {
|
||||
sessionRow: StoredSessionRow;
|
||||
peerKnownState: CojsonInternalTypes.CoValueKnownState;
|
||||
newContentMessages: CojsonInternalTypes.NewContentMessage[];
|
||||
}) {
|
||||
if (
|
||||
sessionRow.lastIdx <= (peerKnownState.sessions[sessionRow.sessionID] || 0)
|
||||
)
|
||||
return;
|
||||
|
||||
const firstNewTxIdx = peerKnownState.sessions[sessionRow.sessionID] || 0;
|
||||
|
||||
const newTxsInSession = await this.dbClient.getNewTransactionInSession(
|
||||
sessionRow.rowID,
|
||||
firstNewTxIdx,
|
||||
);
|
||||
|
||||
collectNewTxs({
|
||||
newTxsInSession,
|
||||
newContentMessages,
|
||||
sessionRow,
|
||||
firstNewTxIdx,
|
||||
});
|
||||
}
|
||||
|
||||
async sendNewContent(
|
||||
coValueKnownState: CojsonInternalTypes.CoValueKnownState,
|
||||
): Promise<void> {
|
||||
const outputMessages: OutputMessageMap =
|
||||
await this.collectCoValueData(coValueKnownState);
|
||||
|
||||
// reverse it to send the top level id the last in the order
|
||||
const collectedMessages = Object.values(outputMessages).reverse();
|
||||
for (const { knownMessage, contentMessages } of collectedMessages) {
|
||||
this.sendStateMessage(knownMessage);
|
||||
|
||||
if (contentMessages?.length) {
|
||||
for (const msg of contentMessages) {
|
||||
this.sendStateMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async collectCoValueData(
|
||||
peerKnownState: CojsonInternalTypes.CoValueKnownState,
|
||||
messageMap: OutputMessageMap = {},
|
||||
asDependencyOf?: CojsonInternalTypes.RawCoID,
|
||||
) {
|
||||
if (messageMap[peerKnownState.id]) {
|
||||
return messageMap;
|
||||
}
|
||||
|
||||
const coValueRow = await this.dbClient.getCoValue(peerKnownState.id);
|
||||
const coValueRow = await this.dbClient.getCoValue(coValueKnownState.id);
|
||||
|
||||
if (!coValueRow) {
|
||||
const emptyKnownMessage: KnownStateMessage = {
|
||||
action: "known",
|
||||
...emptyKnownState(peerKnownState.id),
|
||||
...emptyKnownState(coValueKnownState.id),
|
||||
};
|
||||
if (asDependencyOf) {
|
||||
emptyKnownMessage.asDependencyOf = asDependencyOf;
|
||||
}
|
||||
messageMap[peerKnownState.id] = { knownMessage: emptyKnownMessage };
|
||||
return messageMap;
|
||||
|
||||
this.sendStateMessage(emptyKnownMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const allCoValueSessions = await this.dbClient.getCoValueSessions(
|
||||
coValueRow.rowID,
|
||||
);
|
||||
|
||||
const newCoValueKnownState: CojsonInternalTypes.CoValueKnownState = {
|
||||
id: coValueRow.id,
|
||||
header: true,
|
||||
sessions: {},
|
||||
};
|
||||
const signaturesBySession = new Map<
|
||||
SessionID,
|
||||
Pick<SignatureAfterRow, "idx" | "signature">[]
|
||||
>();
|
||||
|
||||
const newContentMessages: CojsonInternalTypes.NewContentMessage[] = [
|
||||
{
|
||||
action: "content",
|
||||
let contentStreaming = false;
|
||||
for (const sessionRow of allCoValueSessions) {
|
||||
const signatures = await this.dbClient.getSignatures(sessionRow.rowID, 0);
|
||||
|
||||
if (signatures.length > 0) {
|
||||
contentStreaming = true;
|
||||
signaturesBySession.set(sessionRow.sessionID, signatures);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are going to send the content in streaming, we send before a known state message
|
||||
* to let the peer know how many transactions we are going to send.
|
||||
*/
|
||||
if (contentStreaming) {
|
||||
const newCoValueKnownState: CojsonInternalTypes.CoValueKnownState = {
|
||||
id: coValueRow.id,
|
||||
header: coValueRow.header,
|
||||
new: {},
|
||||
priority: cojsonInternals.getPriorityFromHeader(coValueRow.header),
|
||||
},
|
||||
];
|
||||
header: true,
|
||||
sessions: {},
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
allCoValueSessions.map((sessionRow) => {
|
||||
for (const sessionRow of allCoValueSessions) {
|
||||
newCoValueKnownState.sessions[sessionRow.sessionID] =
|
||||
sessionRow.lastIdx;
|
||||
// Collect new sessions data into newContentMessages
|
||||
return this.handleSessionUpdate({
|
||||
sessionRow,
|
||||
peerKnownState,
|
||||
newContentMessages,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.sendStateMessage({
|
||||
action: "known",
|
||||
...newCoValueKnownState,
|
||||
});
|
||||
}
|
||||
|
||||
this.loadedCoValues.add(coValueRow.id);
|
||||
|
||||
let contentMessage = {
|
||||
action: "content",
|
||||
id: coValueRow.id,
|
||||
header: coValueRow.header,
|
||||
new: {},
|
||||
priority: cojsonInternals.getPriorityFromHeader(coValueRow.header),
|
||||
} satisfies CojsonInternalTypes.NewContentMessage;
|
||||
|
||||
for (const sessionRow of allCoValueSessions) {
|
||||
if (
|
||||
sessionRow.lastIdx <=
|
||||
(coValueKnownState.sessions[sessionRow.sessionID] || 0)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const signatures = signaturesBySession.get(sessionRow.sessionID) || [];
|
||||
|
||||
let idx = 0;
|
||||
|
||||
signatures.push({
|
||||
idx: sessionRow.lastIdx,
|
||||
signature: sessionRow.lastSignature,
|
||||
});
|
||||
|
||||
for (const signature of signatures) {
|
||||
const newTxsInSession = await this.dbClient.getNewTransactionInSession(
|
||||
sessionRow.rowID,
|
||||
idx,
|
||||
signature.idx,
|
||||
);
|
||||
|
||||
collectNewTxs({
|
||||
newTxsInSession,
|
||||
contentMessage,
|
||||
sessionRow,
|
||||
firstNewTxIdx: idx,
|
||||
signature: signature.signature,
|
||||
});
|
||||
|
||||
idx = signature.idx + 1;
|
||||
|
||||
if (signatures.length > 1) {
|
||||
await this.sendContentMessage(coValueRow, contentMessage);
|
||||
contentMessage = {
|
||||
action: "content",
|
||||
id: coValueRow.id,
|
||||
header: coValueRow.header,
|
||||
new: {},
|
||||
priority: cojsonInternals.getPriorityFromHeader(coValueRow.header),
|
||||
} satisfies CojsonInternalTypes.NewContentMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(contentMessage.new).length === 0 && contentStreaming) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sendContentMessage(coValueRow, contentMessage);
|
||||
}
|
||||
|
||||
async sendContentMessage(
|
||||
coValueRow: StoredCoValueRow,
|
||||
contentMessage: CojsonInternalTypes.NewContentMessage,
|
||||
) {
|
||||
const dependedOnCoValuesList = getDependedOnCoValues({
|
||||
coValueRow,
|
||||
newContentMessages,
|
||||
newContentMessages: [contentMessage],
|
||||
});
|
||||
|
||||
const knownMessage: KnownStateMessage = {
|
||||
action: "known",
|
||||
...newCoValueKnownState,
|
||||
};
|
||||
if (asDependencyOf) {
|
||||
knownMessage.asDependencyOf = asDependencyOf;
|
||||
for (const dependedOnCoValue of dependedOnCoValuesList) {
|
||||
if (this.loadedCoValues.has(dependedOnCoValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.sendNewContent({
|
||||
id: dependedOnCoValue,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
}
|
||||
messageMap[newCoValueKnownState.id] = {
|
||||
knownMessage: knownMessage,
|
||||
contentMessages: newContentMessages,
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
dependedOnCoValuesList.map((dependedOnCoValue) => {
|
||||
if (this.loadedCoValues.has(dependedOnCoValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.collectCoValueData(
|
||||
{
|
||||
id: dependedOnCoValue,
|
||||
header: false,
|
||||
sessions: {},
|
||||
},
|
||||
messageMap,
|
||||
asDependencyOf || coValueRow.id,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return messageMap;
|
||||
this.sendStateMessage(contentMessage);
|
||||
}
|
||||
|
||||
handleLoad(msg: CojsonInternalTypes.LoadMessage) {
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
logger,
|
||||
} from "cojson";
|
||||
import { collectNewTxs, getDependedOnCoValues } from "./syncUtils.js";
|
||||
import type { DBClientInterfaceSync, StoredSessionRow } from "./types.js";
|
||||
import type {
|
||||
DBClientInterfaceSync,
|
||||
SignatureAfterRow,
|
||||
StoredCoValueRow,
|
||||
StoredSessionRow,
|
||||
} from "./types.js";
|
||||
import NewContentMessage = CojsonInternalTypes.NewContentMessage;
|
||||
import KnownStateMessage = CojsonInternalTypes.KnownStateMessage;
|
||||
import RawCoID = CojsonInternalTypes.RawCoID;
|
||||
@@ -47,145 +52,161 @@ export class StorageManagerSync {
|
||||
}
|
||||
}
|
||||
|
||||
async handleSessionUpdate({
|
||||
sessionRow,
|
||||
peerKnownState,
|
||||
newContentMessages,
|
||||
}: {
|
||||
sessionRow: StoredSessionRow;
|
||||
peerKnownState: CojsonInternalTypes.CoValueKnownState;
|
||||
newContentMessages: CojsonInternalTypes.NewContentMessage[];
|
||||
}) {
|
||||
if (
|
||||
sessionRow.lastIdx <= (peerKnownState.sessions[sessionRow.sessionID] || 0)
|
||||
)
|
||||
return;
|
||||
|
||||
const firstNewTxIdx = peerKnownState.sessions[sessionRow.sessionID] || 0;
|
||||
|
||||
const newTxsInSession = this.dbClient.getNewTransactionInSession(
|
||||
sessionRow.rowID,
|
||||
firstNewTxIdx,
|
||||
);
|
||||
|
||||
collectNewTxs({
|
||||
newTxsInSession,
|
||||
newContentMessages,
|
||||
sessionRow,
|
||||
firstNewTxIdx,
|
||||
});
|
||||
}
|
||||
|
||||
sendNewContent(coValueKnownState: CojsonInternalTypes.CoValueKnownState) {
|
||||
const outputMessages: OutputMessageMap =
|
||||
this.collectCoValueData(coValueKnownState);
|
||||
|
||||
// reverse it to send the top level id the last in the order
|
||||
const collectedMessages = Object.values(outputMessages).reverse();
|
||||
for (const { knownMessage, contentMessages } of collectedMessages) {
|
||||
this.sendStateMessage(knownMessage);
|
||||
|
||||
if (contentMessages?.length) {
|
||||
for (const msg of contentMessages) {
|
||||
this.sendStateMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private collectCoValueData(
|
||||
peerKnownState: CojsonInternalTypes.CoValueKnownState,
|
||||
messageMap: OutputMessageMap = {},
|
||||
asDependencyOf?: CojsonInternalTypes.RawCoID,
|
||||
async sendNewContent(
|
||||
coValueKnownState: CojsonInternalTypes.CoValueKnownState,
|
||||
) {
|
||||
if (messageMap[peerKnownState.id]) {
|
||||
return messageMap;
|
||||
}
|
||||
|
||||
const coValueRow = this.dbClient.getCoValue(peerKnownState.id);
|
||||
const coValueRow = this.dbClient.getCoValue(coValueKnownState.id);
|
||||
|
||||
if (!coValueRow) {
|
||||
const emptyKnownMessage: KnownStateMessage = {
|
||||
action: "known",
|
||||
...emptyKnownState(peerKnownState.id),
|
||||
...emptyKnownState(coValueKnownState.id),
|
||||
};
|
||||
if (asDependencyOf) {
|
||||
emptyKnownMessage.asDependencyOf = asDependencyOf;
|
||||
}
|
||||
messageMap[peerKnownState.id] = { knownMessage: emptyKnownMessage };
|
||||
return messageMap;
|
||||
|
||||
this.sendStateMessage(emptyKnownMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const allCoValueSessions = this.dbClient.getCoValueSessions(
|
||||
coValueRow.rowID,
|
||||
);
|
||||
|
||||
const newCoValueKnownState: CojsonInternalTypes.CoValueKnownState = {
|
||||
id: coValueRow.id,
|
||||
header: true,
|
||||
sessions: {},
|
||||
};
|
||||
const signaturesBySession = new Map<
|
||||
SessionID,
|
||||
Pick<SignatureAfterRow, "idx" | "signature">[]
|
||||
>();
|
||||
|
||||
const newContentMessages: CojsonInternalTypes.NewContentMessage[] = [
|
||||
{
|
||||
action: "content",
|
||||
let contentStreaming = false;
|
||||
for (const sessionRow of allCoValueSessions) {
|
||||
const signatures = this.dbClient.getSignatures(sessionRow.rowID, 0);
|
||||
|
||||
if (signatures.length > 0) {
|
||||
contentStreaming = true;
|
||||
signaturesBySession.set(sessionRow.sessionID, signatures);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are going to send the content in streaming, we send before a known state message
|
||||
* to let the peer know how many transactions we are going to send.
|
||||
*/
|
||||
if (contentStreaming) {
|
||||
const newCoValueKnownState: CojsonInternalTypes.CoValueKnownState = {
|
||||
id: coValueRow.id,
|
||||
header: coValueRow.header,
|
||||
new: {},
|
||||
priority: cojsonInternals.getPriorityFromHeader(coValueRow.header),
|
||||
},
|
||||
];
|
||||
header: true,
|
||||
sessions: {},
|
||||
};
|
||||
|
||||
allCoValueSessions.map((sessionRow) => {
|
||||
newCoValueKnownState.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
|
||||
// Collect new sessions data into newContentMessages
|
||||
this.handleSessionUpdate({
|
||||
sessionRow,
|
||||
peerKnownState,
|
||||
newContentMessages,
|
||||
for (const sessionRow of allCoValueSessions) {
|
||||
newCoValueKnownState.sessions[sessionRow.sessionID] =
|
||||
sessionRow.lastIdx;
|
||||
}
|
||||
|
||||
this.sendStateMessage({
|
||||
action: "known",
|
||||
...newCoValueKnownState,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.loadedCoValues.add(coValueRow.id);
|
||||
|
||||
const dependedOnCoValuesList = getDependedOnCoValues({
|
||||
coValueRow,
|
||||
newContentMessages,
|
||||
});
|
||||
let contentMessage = {
|
||||
action: "content",
|
||||
id: coValueRow.id,
|
||||
header: coValueRow.header,
|
||||
new: {},
|
||||
priority: cojsonInternals.getPriorityFromHeader(coValueRow.header),
|
||||
} satisfies CojsonInternalTypes.NewContentMessage;
|
||||
|
||||
const knownMessage: KnownStateMessage = {
|
||||
action: "known",
|
||||
...newCoValueKnownState,
|
||||
};
|
||||
if (asDependencyOf) {
|
||||
knownMessage.asDependencyOf = asDependencyOf;
|
||||
}
|
||||
messageMap[newCoValueKnownState.id] = {
|
||||
knownMessage: knownMessage,
|
||||
contentMessages: newContentMessages,
|
||||
};
|
||||
|
||||
dependedOnCoValuesList.map((dependedOnCoValue) => {
|
||||
if (this.loadedCoValues.has(dependedOnCoValue)) {
|
||||
return;
|
||||
for (const sessionRow of allCoValueSessions) {
|
||||
if (
|
||||
sessionRow.lastIdx <=
|
||||
(coValueKnownState.sessions[sessionRow.sessionID] || 0)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return this.collectCoValueData(
|
||||
{
|
||||
id: dependedOnCoValue,
|
||||
header: false,
|
||||
sessions: {},
|
||||
},
|
||||
messageMap,
|
||||
asDependencyOf || coValueRow.id,
|
||||
);
|
||||
const signatures = signaturesBySession.get(sessionRow.sessionID) || [];
|
||||
|
||||
let idx = 0;
|
||||
|
||||
signatures.push({
|
||||
idx: sessionRow.lastIdx,
|
||||
signature: sessionRow.lastSignature,
|
||||
});
|
||||
|
||||
for (const signature of signatures) {
|
||||
const newTxsInSession = this.dbClient.getNewTransactionInSession(
|
||||
sessionRow.rowID,
|
||||
idx,
|
||||
signature.idx,
|
||||
);
|
||||
|
||||
collectNewTxs({
|
||||
newTxsInSession,
|
||||
contentMessage,
|
||||
sessionRow,
|
||||
firstNewTxIdx: idx,
|
||||
signature: signature.signature,
|
||||
});
|
||||
|
||||
idx = signature.idx + 1;
|
||||
|
||||
if (signatures.length > 1) {
|
||||
await this.sendContentMessage(coValueRow, contentMessage);
|
||||
contentMessage = {
|
||||
action: "content",
|
||||
id: coValueRow.id,
|
||||
header: coValueRow.header,
|
||||
new: {},
|
||||
priority: cojsonInternals.getPriorityFromHeader(coValueRow.header),
|
||||
} satisfies CojsonInternalTypes.NewContentMessage;
|
||||
|
||||
// Introduce a delay to not block the main thread
|
||||
// for the entire content processing
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(contentMessage.new).length === 0 && contentStreaming) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sendContentMessage(coValueRow, contentMessage);
|
||||
}
|
||||
|
||||
async sendContentMessage(
|
||||
coValueRow: StoredCoValueRow,
|
||||
contentMessage: CojsonInternalTypes.NewContentMessage,
|
||||
) {
|
||||
const dependedOnCoValuesList = getDependedOnCoValues({
|
||||
coValueRow,
|
||||
newContentMessages: [contentMessage],
|
||||
});
|
||||
|
||||
return messageMap;
|
||||
for (const dependedOnCoValue of dependedOnCoValuesList) {
|
||||
if (this.loadedCoValues.has(dependedOnCoValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.sendNewContent({
|
||||
id: dependedOnCoValue,
|
||||
header: false,
|
||||
sessions: {},
|
||||
});
|
||||
}
|
||||
|
||||
this.sendStateMessage(contentMessage);
|
||||
}
|
||||
|
||||
handleLoad(msg: CojsonInternalTypes.LoadMessage) {
|
||||
return this.sendNewContent(msg);
|
||||
this.sendNewContent(msg).catch((e) =>
|
||||
logger.error("Error sending new content", {
|
||||
id: msg.id,
|
||||
err: e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
handleContent(msg: CojsonInternalTypes.NewContentMessage) {
|
||||
|
||||
@@ -13,32 +13,33 @@ import type {
|
||||
|
||||
export function collectNewTxs({
|
||||
newTxsInSession,
|
||||
newContentMessages,
|
||||
contentMessage,
|
||||
sessionRow,
|
||||
firstNewTxIdx,
|
||||
signature,
|
||||
}: {
|
||||
newTxsInSession: TransactionRow[];
|
||||
newContentMessages: CojsonInternalTypes.NewContentMessage[];
|
||||
contentMessage: CojsonInternalTypes.NewContentMessage;
|
||||
sessionRow: StoredSessionRow;
|
||||
signature: CojsonInternalTypes.Signature;
|
||||
firstNewTxIdx: number;
|
||||
}) {
|
||||
for (const tx of newTxsInSession) {
|
||||
const lastMessage = newContentMessages[newContentMessages.length - 1];
|
||||
if (!lastMessage) return;
|
||||
let sessionEntry = contentMessage.new[sessionRow.sessionID];
|
||||
|
||||
let sessionEntry = lastMessage.new[sessionRow.sessionID];
|
||||
if (!sessionEntry) {
|
||||
sessionEntry = {
|
||||
after: firstNewTxIdx,
|
||||
lastSignature: "WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
|
||||
newTransactions: [],
|
||||
};
|
||||
lastMessage.new[sessionRow.sessionID] = sessionEntry;
|
||||
}
|
||||
|
||||
sessionEntry.newTransactions.push(tx.tx);
|
||||
sessionEntry.lastSignature = sessionRow.lastSignature;
|
||||
if (!sessionEntry) {
|
||||
sessionEntry = {
|
||||
after: firstNewTxIdx,
|
||||
lastSignature: "WILL_BE_REPLACED" as CojsonInternalTypes.Signature,
|
||||
newTransactions: [],
|
||||
};
|
||||
contentMessage.new[sessionRow.sessionID] = sessionEntry;
|
||||
}
|
||||
|
||||
for (const tx of newTxsInSession) {
|
||||
sessionEntry.newTransactions.push(tx.tx);
|
||||
}
|
||||
|
||||
sessionEntry.lastSignature = signature;
|
||||
}
|
||||
|
||||
export function getDependedOnCoValues({
|
||||
|
||||
@@ -98,13 +98,7 @@ describe("DB sync manager", () => {
|
||||
|
||||
await syncManager.handleSyncMessage(loadMsg);
|
||||
|
||||
expect(syncManager.sendStateMessage).toBeCalledTimes(2);
|
||||
expect(syncManager.sendStateMessage).toBeCalledWith({
|
||||
action: "known",
|
||||
header: true,
|
||||
id: coValueIdToLoad,
|
||||
sessions: {},
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toBeCalledTimes(1);
|
||||
expect(syncManager.sendStateMessage).toBeCalledWith({
|
||||
action: "content",
|
||||
header: expect.objectContaining({
|
||||
@@ -117,78 +111,6 @@ describe("DB sync manager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Sends both known and content messages when we have new sessions info for the requested coValue ", async () => {
|
||||
const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
|
||||
|
||||
DBClient.prototype.getCoValue.mockResolvedValueOnce({
|
||||
id: coValueIdToLoad,
|
||||
header: coValueHeader,
|
||||
rowID: 3,
|
||||
});
|
||||
DBClient.prototype.getCoValueSessions.mockResolvedValueOnce(sessionsData);
|
||||
|
||||
const newTxData = {
|
||||
newTransactions: [
|
||||
{
|
||||
privacy: "trusting",
|
||||
madeAt: 1732368535089,
|
||||
changes: "",
|
||||
} as CojsonInternalTypes.Transaction,
|
||||
],
|
||||
after: 0,
|
||||
lastSignature: "signature_z111",
|
||||
} satisfies CojsonInternalTypes.SessionNewContent;
|
||||
|
||||
// mock content data combined with session updates
|
||||
syncManager.handleSessionUpdate = vi.fn(
|
||||
async ({ sessionRow, newContentMessages }) => {
|
||||
newContentMessages[0]!.new[sessionRow.sessionID] = newTxData;
|
||||
},
|
||||
);
|
||||
|
||||
await syncManager.handleSyncMessage(loadMsg);
|
||||
|
||||
expect(syncManager.sendStateMessage).toBeCalledTimes(2);
|
||||
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
|
||||
action: "known",
|
||||
header: true,
|
||||
id: coValueIdToLoad,
|
||||
sessions: sessionsData.reduce(
|
||||
(acc, sessionRow) => {
|
||||
acc[sessionRow.sessionID] = sessionRow.lastIdx;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
});
|
||||
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
|
||||
action: "content",
|
||||
header: coValueHeader,
|
||||
id: coValueIdToLoad,
|
||||
new: sessionsData.reduce(
|
||||
(acc, sessionRow) => {
|
||||
acc[sessionRow.sessionID] = {
|
||||
after: expect.any(Number),
|
||||
lastSignature: expect.any(String),
|
||||
newTransactions: expect.any(Array),
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
after: number;
|
||||
lastSignature: string;
|
||||
newTransactions: Transaction[];
|
||||
}
|
||||
>,
|
||||
),
|
||||
priority: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("Sends messages for unique coValue dependencies only, leaving out circular dependencies", async () => {
|
||||
const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
|
||||
const dependency1 = "co_zMKhQJs5rAeGjta3JX2qEdBS6hS";
|
||||
@@ -220,13 +142,7 @@ describe("DB sync manager", () => {
|
||||
|
||||
// We send out pairs (known + content) messages only FOUR times - as many as the coValues number
|
||||
// and less than amount of interconnected dependencies to loop through in dependenciesTreeWithLoop
|
||||
expect(syncManager.sendStateMessage).toBeCalledTimes(4 * 2);
|
||||
|
||||
const knownExpected = {
|
||||
action: "known",
|
||||
header: true,
|
||||
sessions: {},
|
||||
};
|
||||
expect(syncManager.sendStateMessage).toBeCalledTimes(4);
|
||||
|
||||
const contentExpected = {
|
||||
action: "content",
|
||||
@@ -236,37 +152,18 @@ describe("DB sync manager", () => {
|
||||
};
|
||||
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
|
||||
...knownExpected,
|
||||
id: dependency3,
|
||||
asDependencyOf: coValueIdToLoad,
|
||||
...contentExpected,
|
||||
id: dependency1,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
|
||||
...contentExpected,
|
||||
id: dependency3,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(3, {
|
||||
...knownExpected,
|
||||
...contentExpected,
|
||||
id: dependency2,
|
||||
asDependencyOf: coValueIdToLoad,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(4, {
|
||||
...contentExpected,
|
||||
id: dependency2,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(5, {
|
||||
...knownExpected,
|
||||
id: dependency1,
|
||||
asDependencyOf: coValueIdToLoad,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(6, {
|
||||
...contentExpected,
|
||||
id: dependency1,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(7, {
|
||||
...knownExpected,
|
||||
id: coValueIdToLoad,
|
||||
});
|
||||
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(8, {
|
||||
...contentExpected,
|
||||
id: coValueIdToLoad,
|
||||
});
|
||||
|
||||
@@ -47,7 +47,8 @@ export interface DBClientInterfaceAsync {
|
||||
|
||||
getNewTransactionInSession(
|
||||
sessionRowId: number,
|
||||
firstNewTxIdx: number,
|
||||
fromIdx: number,
|
||||
toIdx: number,
|
||||
): Promise<TransactionRow[]>;
|
||||
|
||||
getSignatures(
|
||||
@@ -96,13 +97,14 @@ export interface DBClientInterfaceSync {
|
||||
|
||||
getNewTransactionInSession(
|
||||
sessionRowId: number,
|
||||
firstNewTxIdx: number,
|
||||
fromIdx: number,
|
||||
toIdx: number,
|
||||
): TransactionRow[];
|
||||
|
||||
getSignatures(
|
||||
sessionRowId: number,
|
||||
firstNewTxIdx: number,
|
||||
): SignatureAfterRow[];
|
||||
): Pick<SignatureAfterRow, "idx" | "signature">[];
|
||||
|
||||
addCoValue(msg: CojsonInternalTypes.NewContentMessage): number;
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# cojson-transport-nodejs-ws
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d63716a]
|
||||
- Updated dependencies [d5edad7]
|
||||
- cojson@0.13.31
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cojson-transport-ws",
|
||||
"type": "module",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# cojson
|
||||
|
||||
## 0.13.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d63716a: Fix removing members when the admin doesn't have access to the parent group readkeys
|
||||
- d5edad7: Group invites: restore support for role upgrades and inviting revoked members
|
||||
|
||||
## 0.13.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "0.13.30",
|
||||
"version": "0.13.31",
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-metrics": "^2.0.0",
|
||||
"typescript": "catalog:"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user