Compare commits

..

34 Commits

Author SHA1 Message Date
Guido D'Orsi
0966a90f3d Merge pull request #2768 from garden-co/changeset-release/main
Version Packages
2025-08-19 08:44:03 +02:00
github-actions[bot]
cd2f0846db Version Packages 2025-08-18 20:33:16 +00:00
Guido D'Orsi
c2e411d056 Merge pull request #2759 from 0x100101/feat/sync-server-host-opt
Add host option to the jazz-run sync command
2025-08-18 22:31:01 +02:00
Guido D'Orsi
be5211d088 Merge pull request #2765 from garden-co/changeset-release/main
Version Packages
2025-08-18 20:00:59 +02:00
github-actions[bot]
747f73d168 Version Packages 2025-08-18 17:48:48 +00:00
Guido D'Orsi
7501702f7b Merge pull request #2761 from garden-co/fix/GCO-726
fix(jazz-tools/media): get resized file's id without triggering shallow load
2025-08-18 19:44:23 +02:00
Guido D'Orsi
16fb9fab5f Merge pull request #2764 from garden-co/fix/create-from-json-without-active-account
fix: create CoValues from JSON without an active account
2025-08-18 19:43:39 +02:00
NicoR
82de51c93d chore: add changeset 2025-08-18 13:49:25 -03:00
NicoR
5d96991981 fix: create CoValues from JSON without an active account 2025-08-18 13:42:28 -03:00
Matteo Manchi
694b168fb4 fix(jazz-tools/media): get resized file's id without triggering shallow load 2025-08-18 17:54:48 +02:00
0x100101
feaa69ebdd Add patch file 2025-08-18 10:04:04 -05:00
0x100101
d5fa172b17 Update docs 2025-08-17 20:46:12 -05:00
0x100101
96de15593b Add and update tests. Tweak sync server return. 2025-08-17 20:32:22 -05:00
0x100101
5ba03ebc70 Add host option to startSyncServerCommand command 2025-08-17 16:10:04 -05:00
Guido D'Orsi
4609cebed6 Merge pull request #2757 from garden-co/feat/music-player-welcome
fix: avatar permissions
2025-08-16 18:12:54 +02:00
Guido D'Orsi
06d21b9529 fix: avatar permissions 2025-08-16 18:09:24 +02:00
Guido D'Orsi
f3426beaf5 Merge pull request #2756 from garden-co/feat/music-player-welcome
feat(music): show the welcome screen, add playlist members list
2025-08-16 17:47:53 +02:00
Guido D'Orsi
8b3e038a98 feat(music): show the welcome screen, add playlist members list 2025-08-16 17:36:15 +02:00
Guido D'Orsi
4002d6afb9 Merge pull request #2754 from garden-co/fix/replace-catalog
fix: remove catalog: deps
2025-08-16 11:59:49 +02:00
Guido D'Orsi
7dd128962d fix: remove catalog: deps 2025-08-16 11:53:44 +02:00
Guido D'Orsi
8fb1748433 Merge pull request #2750 from garden-co/changeset-release/main
Version Packages
2025-08-15 20:36:35 +02:00
github-actions[bot]
c8644bf678 Version Packages 2025-08-15 16:30:37 +00:00
Guido D'Orsi
269ee94338 test: skip flaky e2e test 2025-08-15 18:26:41 +02:00
Guido D'Orsi
dae80eeba8 Merge pull request #2751 from garden-co/feat/unmount
fix: remove unnecessary content sent as dependency
2025-08-15 18:25:51 +02:00
Guido D'Orsi
ce54667b4d Merge pull request #2752 from garden-co/more-unique-static-methods
Implement/expose loadUnique and upsertUnique on co.list and co.record
2025-08-15 18:25:33 +02:00
Anselm
5963658e28 Implement/expose loadUnique and upsertUnique on co.list and co.record 2025-08-15 17:20:48 +01:00
Guido D'Orsi
71c1411bbd fix: remove unnecessary content sent as dependency 2025-08-15 18:05:42 +02:00
Guido D'Orsi
71b221dc79 Merge pull request #2749 from garden-co/feat/unmount
feat: make the unmount function detach the CoValue from the localNode
2025-08-15 17:48:01 +02:00
Guido D'Orsi
2d11d448dc feat: make the unmount function detach the CoValue from the localNode 2025-08-15 17:41:18 +02:00
Guido D'Orsi
2d42fc9b34 Merge pull request #2748 from garden-co/changeset-release/main
Version Packages
2025-08-15 17:27:17 +02:00
github-actions[bot]
c9bda7e1e3 Version Packages 2025-08-15 15:26:39 +00:00
Guido D'Orsi
476f2d7eee Merge pull request #2747 from garden-co/export-ref-from-jazz-tools
Export Ref class from jazz-tools package
2025-08-15 17:23:08 +02:00
Guido D'Orsi
1ba3a2ca34 test: add waitForSync to fix flaky test on mesh 2025-08-15 17:22:31 +02:00
Anselm
7dd3d005a3 Export Ref class from jazz-tools package 2025-08-15 16:19:51 +01:00
108 changed files with 2614 additions and 617 deletions

View File

@@ -1,48 +1,48 @@
{
"name": "betterauth",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"email": "email dev --dir src/components/emails"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^12.8.0",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-slot": "^1.2.2",
"better-auth": "^1.2.4",
"better-sqlite3": "^11.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jazz-react-auth-betterauth": "workspace:*",
"jazz-betterauth-client-plugin": "workspace:*",
"jazz-betterauth-server-plugin": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "catalog:react",
"react-dom": "catalog:react",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^20",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"react-email": "^4.0.11",
"tailwindcss": "^4",
"typescript": "catalog:default"
}
"name": "betterauth",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"email": "email dev --dir src/components/emails"
},
"dependencies": {
"@icons-pack/react-simple-icons": "^12.8.0",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-slot": "^1.2.2",
"better-auth": "^1.2.4",
"better-sqlite3": "^11.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jazz-react-auth-betterauth": "workspace:*",
"jazz-betterauth-client-plugin": "workspace:*",
"jazz-betterauth-server-plugin": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@biomejs/biome": "2.1.3",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^20",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"react-email": "^4.0.11",
"tailwindcss": "^4",
"typescript": "5.6.2"
}
}

View File

@@ -13,13 +13,13 @@
"@bacons/text-decoder": "^0.0.0",
"@bam.tech/react-native-image-resizer": "^3.0.11",
"@react-native-community/netinfo": "11.4.1",
"expo": "catalog:expo",
"expo-clipboard": "catalog:expo",
"expo-secure-store": "catalog:expo",
"expo-sqlite": "catalog:expo",
"expo": "54.0.0-canary-20250701-6a945c5",
"expo-clipboard": "^7.1.4",
"expo-secure-store": "~14.2.3",
"expo-sqlite": "~15.2.10",
"jazz-tools": "workspace:*",
"react": "catalog:expo",
"react-native": "catalog:expo",
"react": "19.1.0",
"react-native": "0.80.0",
"react-native-get-random-values": "^1.11.0",
"readable-stream": "^4.7.0"
},
@@ -29,4 +29,4 @@
"typescript": "~5.8.3"
},
"private": true
}
}

View File

@@ -18,8 +18,8 @@
"@react-navigation/native": "7.1.14",
"@react-navigation/native-stack": "7.3.19",
"jazz-tools": "workspace:*",
"react": "catalog:rn",
"react-native": "catalog:rn",
"react": "19.1.0",
"react-native": "0.80.0",
"react-native-get-random-values": "^1.11.0",
"react-native-mmkv": "3.3.0",
"react-native-safe-area-context": "5.5.0",
@@ -31,16 +31,16 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "catalog:rn",
"@react-native-community/cli-platform-android": "catalog:rn",
"@react-native-community/cli-platform-ios": "catalog:rn",
"@react-native/babel-preset": "catalog:rn",
"@react-native/eslint-config": "catalog:rn",
"@react-native/metro-config": "catalog:rn",
"@react-native/typescript-config": "catalog:rn",
"@react-native-community/cli": "19.0.0",
"@react-native-community/cli-platform-android": "19.0.0",
"@react-native-community/cli-platform-ios": "19.0.0",
"@react-native/babel-preset": "0.80.0",
"@react-native/eslint-config": "0.80.0",
"@react-native/metro-config": "0.80.0",
"@react-native/typescript-config": "0.80.0",
"@rnx-kit/metro-config": "^2.0.1",
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
"@types/react": "catalog:rn",
"@types/react": "^19.1.0",
"eslint": "^8.19.0",
"pod-install": "^0.3.5",
"prettier": "2.8.8",

View File

@@ -1,5 +1,33 @@
# passkey-svelte
## 0.0.120
### Patch Changes
- jazz-tools@0.17.7
## 0.0.119
### Patch Changes
- Updated dependencies [82de51c]
- Updated dependencies [694b168]
- jazz-tools@0.17.6
## 0.0.118
### Patch Changes
- Updated dependencies [5963658]
- jazz-tools@0.17.5
## 0.0.117
### Patch Changes
- Updated dependencies [7dd3d00]
- jazz-tools@0.17.4
## 0.0.116
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-svelte",
"version": "0.0.116",
"version": "0.0.120",
"type": "module",
"private": true,
"scripts": {

View File

@@ -16,20 +16,20 @@
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.536.0",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "3.25.76"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"is-ci": "^3.0.1",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}
}

View File

@@ -14,22 +14,22 @@
"@bam.tech/react-native-image-resizer": "^3.0.11",
"@clerk/clerk-expo": "^2.13.1",
"@react-native-community/netinfo": "11.4.1",
"expo": "catalog:expo",
"expo-crypto": "catalog:expo",
"expo-linking": "catalog:expo",
"expo-secure-store": "catalog:expo",
"expo-sqlite": "catalog:expo",
"expo-web-browser": "catalog:expo",
"expo": "54.0.0-canary-20250701-6a945c5",
"expo-crypto": "~14.1.5",
"expo-linking": "~7.1.5",
"expo-secure-store": "~14.2.3",
"expo-sqlite": "~15.2.10",
"expo-web-browser": "~14.2.0",
"jazz-tools": "workspace:*",
"react": "catalog:expo",
"react-native": "catalog:expo",
"react": "19.1.0",
"react-native": "0.80.0",
"react-native-get-random-values": "^1.11.0",
"readable-stream": "^4.7.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "catalog:expo",
"@types/react": "^19.0.10",
"typescript": "~5.8.3"
},
"private": true
}
}

View File

@@ -14,17 +14,17 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@biomejs/biome": "catalog:default",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@biomejs/biome": "2.1.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}
}

View File

@@ -1,7 +1,8 @@
import { clerk } from "@clerk/testing/playwright";
import { expect, test } from "@playwright/test";
test("login & expiration", async ({ page, context }) => {
// Flaky on CI
test.skip("login & expiration", async ({ page, context }) => {
// Clear cookies first
await context.clearCookies();

View File

@@ -11,20 +11,20 @@
},
"dependencies": {
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -12,22 +12,22 @@
"dependencies": {
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@playwright/test": "^1.50.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -11,18 +11,18 @@
},
"dependencies": {
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "catalog:default",
"vite": "catalog:default",
"typescript": "5.6.2",
"vite": "6.3.5",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10"
}

View File

@@ -17,19 +17,19 @@
"cojson-transport-ws": "workspace:*",
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-use": "^17.4.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -13,22 +13,22 @@
"dependencies": {
"@react-spring/web": "^9.7.5",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "3.25.76"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@biomejs/biome": "2.1.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"postcss": "^8.4.40",
"@tailwindcss/postcss": "^4.1.10",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default",
"vitest": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5",
"vitest": "3.2.4"
}
}
}

View File

@@ -12,17 +12,17 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@biomejs/biome": "2.1.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -23,8 +23,8 @@
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.536.0",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0"
@@ -32,13 +32,13 @@
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"postcss": "^8.4.27",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default",
"typescript": "5.6.2",
"vite": "6.3.5",
"vite-plugin-pwa": "^1.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import { co, z } from "jazz-tools";
import { co, Group, z } from "jazz-tools";
/** Walkthrough: Defining the data model with CoJSON
*
@@ -70,15 +70,22 @@ export const MusicaAccountRoot = co.map({
activePlaylist: Playlist,
exampleDataLoaded: z.optional(z.boolean()),
accountSetupCompleted: z.optional(z.boolean()),
});
export type MusicaAccountRoot = co.loaded<typeof MusicaAccountRoot>;
export const MusicaAccountProfile = co.profile({
avatar: co.optional(co.image()),
});
export type MusicaAccountProfile = co.loaded<typeof MusicaAccountProfile>;
export const MusicaAccount = co
.account({
/** the default user profile with a name */
profile: co.profile(),
profile: MusicaAccountProfile,
root: MusicaAccountRoot,
})
.withMigration((account) => {
.withMigration(async (account) => {
/**
* The account migration is run on account creation and on every log-in.
* You can use it to set up the account root and any other initial CoValues you need.
@@ -97,6 +104,32 @@ export const MusicaAccount = co
exampleDataLoaded: false,
});
}
if (account.profile === undefined) {
account.profile = MusicaAccountProfile.create({
name: "",
});
}
// Load the profile and root in memory, to have them ready
const { profile, root } = await account.ensureLoaded({
resolve: {
profile: {
avatar: true,
},
root: true,
},
});
// Clean up the private avatars (were created using the account as owner)
if (profile.avatar) {
const group = profile.avatar._owner.castAs(Group);
if (group.getRoleOf("everyone") !== "reader") {
root.accountSetupCompleted = false;
profile.avatar = undefined;
}
}
});
export type MusicaAccount = co.loaded<typeof MusicaAccount>;

View File

@@ -7,12 +7,13 @@ import { RouterProvider, createHashRouter } from "react-router-dom";
import { HomePage } from "./3_HomePage";
import { useMediaPlayer } from "./5_useMediaPlayer";
import { InvitePage } from "./6_InvitePage";
import { WelcomeScreen } from "./components/WelcomeScreen";
import "./index.css";
import { MusicaAccount } from "@/1_schema";
import { apiKey } from "@/apiKey.ts";
import { SidebarProvider } from "@/components/ui/sidebar";
import { JazzReactProvider } from "jazz-tools/react";
import { JazzReactProvider, useAccount } from "jazz-tools/react";
import { onAnonymousAccountDiscarded } from "./4_actions";
import { KeyboardListener } from "./components/PlayerControls";
import { usePrepareAppState } from "./lib/usePrepareAppState";
@@ -28,11 +29,22 @@ import { usePrepareAppState } from "./lib/usePrepareAppState";
* `<JazzReactProvider/>` also runs our account migration
*/
function Main() {
const mediaPlayer = useMediaPlayer();
function AppContent({
mediaPlayer,
}: {
mediaPlayer: ReturnType<typeof useMediaPlayer>;
}) {
const { me } = useAccount(MusicaAccount, {
resolve: { root: true },
});
const isReady = usePrepareAppState(mediaPlayer);
// Show welcome screen if account setup is not completed
if (me && !me.root.accountSetupCompleted) {
return <WelcomeScreen />;
}
const router = createHashRouter([
{
path: "/",
@@ -59,6 +71,17 @@ function Main() {
);
}
function Main() {
const mediaPlayer = useMediaPlayer();
return (
<SidebarProvider>
<AppContent mediaPlayer={mediaPlayer} />
<JazzInspector />
</SidebarProvider>
);
}
const peer =
(new URL(window.location.href).searchParams.get(
"peer",

View File

@@ -12,11 +12,13 @@ import { MediaPlayer } from "./5_useMediaPlayer";
import { FileUploadButton } from "./components/FileUploadButton";
import { MusicTrackRow } from "./components/MusicTrackRow";
import { PlayerControls } from "./components/PlayerControls";
import { PlaylistTitleInput } from "./components/PlaylistTitleInput";
import { EditPlaylistModal } from "./components/EditPlaylistModal";
import { PlaylistMembers } from "./components/PlaylistMembers";
import { SidePanel } from "./components/SidePanel";
import { Button } from "./components/ui/button";
import { SidebarInset, SidebarTrigger } from "./components/ui/sidebar";
import { usePlayState } from "./lib/audio/usePlayState";
import { useState } from "react";
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
/**
@@ -30,6 +32,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const playState = usePlayState();
const isPlaying = playState.value === "play";
const { toast } = useToast();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
async function handleFileLoad(files: FileList) {
/**
@@ -50,6 +53,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
},
});
const membersIds = playlist?._owner.members.map((member) => member.id);
const isRootPlaylist = !params.playlistId;
const isPlaylistOwner = playlist?._owner.myRole() === "admin";
const isActivePlaylist = playlistId === me?.root.activePlaylist?.id;
@@ -66,6 +70,10 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
});
};
const handleEditClick = () => {
setIsEditModalOpen(true);
};
const isAuthenticated = useIsAuthenticated();
return (
@@ -74,11 +82,17 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
<SidePanel />
<main className="flex-1 px-2 py-4 md:px-6 overflow-y-auto overflow-x-hidden relative sm:h-[calc(100vh-80px)] bg-white h-[calc(100vh-165px)]">
<SidebarTrigger className="md:hidden" />
<div className="flex flex-row items-center justify-between mb-4 pl-1 md:pl-10 pr-2 md:pr-0 mt-2 md:mt-0 w-full">
{isRootPlaylist ? (
<h1 className="text-2xl font-bold text-blue-800">All tracks</h1>
) : (
<PlaylistTitleInput className="w-full" playlistId={playlistId} />
<div className="flex items-center space-x-4">
<h1 className="text-2xl font-bold text-blue-800">
{playlist?.title}
</h1>
{membersIds && <PlaylistMembers memberIds={membersIds} />}
</div>
)}
<div className="flex items-center space-x-4">
{isRootPlaylist && (
@@ -89,9 +103,12 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
</>
)}
{!isRootPlaylist && isAuthenticated && (
<Button onClick={handlePlaylistShareClick}>
Share playlist
</Button>
<>
<Button onClick={handleEditClick} variant="outline">
Edit
</Button>
<Button onClick={handlePlaylistShareClick}>Share</Button>
</>
)}
</div>
</div>
@@ -118,6 +135,13 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
</main>
<PlayerControls mediaPlayer={mediaPlayer} />
</div>
{/* Playlist Title Edit Modal */}
<EditPlaylistModal
playlistId={playlistId}
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
/>
</SidebarInset>
);
}

View File

@@ -59,7 +59,7 @@ export async function uploadMusicTracks(
}
}
export async function createNewPlaylist() {
export async function createNewPlaylist(title: string = "New Playlist") {
const { root } = await MusicaAccount.getMe().ensureLoaded({
resolve: {
root: {
@@ -69,7 +69,7 @@ export async function createNewPlaylist() {
});
const playlist = Playlist.create({
title: "New Playlist",
title,
tracks: [],
});

View File

@@ -24,7 +24,7 @@ export function useMediaPlayer() {
// Reference used to avoid out-of-order track loads
const lastLoadedTrackId = useRef<string | null>(null);
async function loadTrack(track: MusicTrack) {
async function loadTrack(track: MusicTrack, autoPlay = true) {
lastLoadedTrackId.current = track.id;
audioManager.unloadCurrentAudio();
@@ -44,7 +44,7 @@ export function useMediaPlayer() {
return;
}
await playMedia(file);
await playMedia(file, autoPlay);
setLoading(null);
}

View File

@@ -7,8 +7,6 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAccount, usePasskeyAuth } from "jazz-tools/react";
import { useState } from "react";
@@ -18,7 +16,6 @@ interface AuthModalProps {
}
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const [isSignUp, setIsSignUp] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -31,6 +28,7 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
},
},
},
profile: true,
},
});
@@ -48,7 +46,7 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
try {
if (isSignUp) {
await auth.signUp(username);
await auth.signUp(me?.profile.name || "");
} else {
await auth.logIn();
}
@@ -84,18 +82,6 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{isSignUp && (
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
required
/>
</div>
)}
{error && <div className="text-sm text-red-500">{error}</div>}
{shouldShowTransferRootPlaylist && (
<div className="text-sm text-red-500">

View File

@@ -0,0 +1,105 @@
import { createNewPlaylist } from "@/4_actions";
import { useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
interface CreatePlaylistModalProps {
isOpen: boolean;
onClose: () => void;
onPlaylistCreated: (playlistId: string) => void;
}
export function CreatePlaylistModal({
isOpen,
onClose,
onPlaylistCreated,
}: CreatePlaylistModalProps) {
const [playlistTitle, setPlaylistTitle] = useState("");
const [isCreating, setIsCreating] = useState(false);
function handleTitleChange(evt: React.ChangeEvent<HTMLInputElement>) {
setPlaylistTitle(evt.target.value);
}
async function handleCreate() {
if (!playlistTitle.trim()) return;
setIsCreating(true);
try {
const playlist = await createNewPlaylist(playlistTitle.trim());
onPlaylistCreated(playlist.id);
onClose();
} catch (error) {
console.error("Failed to create playlist:", error);
} finally {
setIsCreating(false);
}
}
function handleCancel() {
setPlaylistTitle("");
onClose();
}
function handleKeyDown(evt: React.KeyboardEvent) {
if (evt.key === "Enter") {
handleCreate();
} else if (evt.key === "Escape") {
handleCancel();
}
}
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Create New Playlist
</h2>
<p className="text-sm text-gray-600">Give your new playlist a name</p>
</div>
<div className="space-y-4">
<div>
<Label
htmlFor="playlist-title"
className="text-sm font-medium text-gray-700"
>
Playlist Title
</Label>
<Input
id="playlist-title"
value={playlistTitle}
onChange={handleTitleChange}
onKeyDown={handleKeyDown}
placeholder="Enter playlist title"
className="mt-1"
autoFocus
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={handleCancel}
className="px-4 py-2"
disabled={isCreating}
>
Cancel
</Button>
<Button
onClick={handleCreate}
disabled={!playlistTitle.trim() || isCreating}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{isCreating ? "Creating..." : "Create Playlist"}
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { Playlist } from "@/1_schema";
import { updatePlaylistTitle } from "@/4_actions";
import { useCoState } from "jazz-tools/react";
import { ChangeEvent, useState, useEffect } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
interface EditPlaylistModalProps {
playlistId: string | undefined;
isOpen: boolean;
onClose: () => void;
}
export function EditPlaylistModal({
playlistId,
isOpen,
onClose,
}: EditPlaylistModalProps) {
const playlist = useCoState(Playlist, playlistId);
const [localPlaylistTitle, setLocalPlaylistTitle] = useState("");
// Reset local title when modal opens or playlist changes
useEffect(() => {
if (isOpen && playlist) {
setLocalPlaylistTitle(playlist.title ?? "");
}
}, [isOpen, playlist]);
function handleTitleChange(evt: ChangeEvent<HTMLInputElement>) {
setLocalPlaylistTitle(evt.target.value);
}
function handleSave() {
if (playlist && localPlaylistTitle.trim()) {
updatePlaylistTitle(playlist, localPlaylistTitle.trim());
onClose();
}
}
function handleCancel() {
setLocalPlaylistTitle(playlist?.title ?? "");
onClose();
}
function handleKeyDown(evt: React.KeyboardEvent) {
if (evt.key === "Enter") {
handleSave();
} else if (evt.key === "Escape") {
handleCancel();
}
}
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Edit Playlist
</h2>
</div>
<div className="space-y-4">
<div>
<Label
htmlFor="playlist-title"
className="text-sm font-medium text-gray-700"
>
Playlist Title
</Label>
<Input
id="playlist-title"
value={localPlaylistTitle}
onChange={handleTitleChange}
onKeyDown={handleKeyDown}
placeholder="Enter playlist title"
className="mt-1"
autoFocus
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button
variant="outline"
onClick={handleCancel}
className="px-4 py-2"
>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={!localPlaylistTitle.trim()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
Save Changes
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import { useCoState } from "jazz-tools/react";
import { MusicaAccount } from "@/1_schema";
import { Image } from "jazz-tools/react";
interface MemberProps {
accountId: string;
size?: "sm" | "md" | "lg";
showTooltip?: boolean;
className?: string;
}
export function Member({
accountId,
size = "md",
showTooltip = true,
className = "",
}: MemberProps) {
const account = useCoState(MusicaAccount, accountId, {
resolve: { profile: true },
});
if (!account) {
return (
<div
className={`rounded-full bg-gray-200 border-2 border-white flex items-center justify-center ${getSizeClasses(size)} ${className}`}
>
<span className="text-gray-500 text-xs">👤</span>
</div>
);
}
const avatar = account.profile?.avatar;
const name = account.profile?.name || "Unknown User";
return (
<div
className={`rounded-full border-2 border-white overflow-hidden ${getSizeClasses(size)} ${className}`}
title={showTooltip ? name : undefined}
>
{avatar ? (
<Image
imageId={avatar.id}
width={getSizePx(size)}
height={getSizePx(size)}
alt={`${name}'s avatar`}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<span className="text-gray-500 text-sm">
{name.charAt(0).toUpperCase()}
</span>
</div>
)}
</div>
);
}
function getSizeClasses(size: "sm" | "md" | "lg"): string {
switch (size) {
case "sm":
return "w-6 h-6";
case "md":
return "w-8 h-8";
case "lg":
return "w-10 h-10";
default:
return "w-8 h-8";
}
}
function getSizePx(size: "sm" | "md" | "lg"): number {
switch (size) {
case "sm":
return 24;
case "md":
return 32;
case "lg":
return 40;
default:
return 32;
}
}

View File

@@ -152,7 +152,7 @@ export function MusicTrackRow({
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={handleEdit}>Edit</DropdownMenuItem>
{playlists.map((playlist, playlistIndex) => (
{playlists.filter(Boolean).map((playlist, playlistIndex) => (
<Fragment key={playlistIndex}>
{isPartOfThePlaylist(trackId, playlist) ? (
<DropdownMenuItem

View File

@@ -0,0 +1,30 @@
import { Member } from "./Member";
interface PlaylistMembersProps {
memberIds: string[];
size?: "sm" | "md" | "lg";
className?: string;
}
export function PlaylistMembers({
memberIds,
size = "md",
className = "",
}: PlaylistMembersProps) {
if (!memberIds || memberIds.length === 0) return null;
return (
<div className={`flex items-center space-x-2 ${className}`}>
<div className="flex -space-x-2">
{memberIds.map((memberId) => (
<Member
key={memberId}
accountId={memberId}
size={size}
showTooltip={true}
/>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,213 @@
import React, { useState, useRef } from "react";
import { Image, useAccount } from "jazz-tools/react";
import { createImage } from "jazz-tools/media";
import { MusicaAccount } from "../1_schema";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Separator } from "./ui/separator";
import { Group } from "jazz-tools";
interface ProfileFormProps {
onSubmit?: (data: { username: string; avatar?: any }) => void;
submitButtonText?: string;
showHeader?: boolean;
headerTitle?: string;
headerDescription?: string;
initialUsername?: string;
initialAvatar?: any;
onCancel?: () => void;
showCancelButton?: boolean;
cancelButtonText?: string;
className?: string;
}
export function ProfileForm({
onSubmit,
submitButtonText = "Save Changes",
showHeader = false,
headerTitle = "Profile Settings",
headerDescription = "Update your profile information",
initialUsername = "",
initialAvatar,
onCancel,
showCancelButton = false,
cancelButtonText = "Cancel",
className = "",
}: ProfileFormProps) {
const { me } = useAccount(MusicaAccount, {
resolve: { profile: true, root: true },
});
const [username, setUsername] = useState(
initialUsername || me?.profile?.name || "",
);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
if (!me) return null;
const handleAvatarUpload = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file) return;
setIsUploading(true);
try {
// Create image using the Image API from jazz-tools/media
const image = await createImage(file, {
owner: Group.create().makePublic(),
maxSize: 256, // Good size for avatars
placeholder: "blur",
progressive: true,
});
// Update the profile with the new avatar
me.profile.avatar = image;
} catch (error) {
console.error("Failed to upload avatar:", error);
} finally {
setIsUploading(false);
}
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!username.trim()) return;
// Update username
me.profile.name = username.trim();
// Call custom onSubmit if provided
if (onSubmit) {
onSubmit({ username: username.trim(), avatar: me.profile.avatar });
}
};
const currentAvatar = initialAvatar || me.profile.avatar;
const canSubmit = username.trim();
return (
<div className={className}>
{showHeader && (
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
{headerTitle}
</h1>
<p className="text-gray-600">{headerDescription}</p>
</div>
)}
<form className="space-y-6" onSubmit={handleSubmit}>
{/* Avatar Section */}
<div className="space-y-3">
<Label
htmlFor="avatar"
className="text-sm font-medium text-gray-700 sr-only"
>
Profile Picture
</Label>
<div className="flex flex-col items-center space-y-3">
{/* Current Avatar Display */}
<div className="relative">
<div className="w-24 h-24 rounded-full overflow-hidden border-4 border-white shadow-lg">
{currentAvatar ? (
<Image
imageId={currentAvatar.id}
width={96}
height={96}
alt="Profile"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<span className="text-gray-400 text-2xl">👤</span>
</div>
)}
</div>
{/* Upload Overlay */}
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="absolute -bottom-1 -right-1 w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white hover:bg-blue-700 disabled:opacity-50 transition-colors cursor-pointer"
title="Change avatar"
>
{isUploading ? (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<span className="text-sm">📷</span>
)}
</button>
</div>
<input
ref={fileInputRef}
type="file"
id="avatar"
accept="image/*"
onChange={handleAvatarUpload}
className="hidden"
disabled={isUploading}
/>
<p className="text-xs text-gray-500 text-center">
Click the camera icon to upload a profile picture
</p>
</div>
</div>
<Separator />
{/* Username Section */}
<div className="space-y-3">
<Label
htmlFor="username"
className="text-sm font-medium text-gray-700"
>
Username
</Label>
<Input
id="username"
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full"
maxLength={30}
/>
<p className="text-xs text-gray-500">
This will be displayed to other users
</p>
</div>
{/* Action Buttons */}
<div className="flex space-x-3">
{showCancelButton && (
<Button
type="button"
variant="outline"
onClick={onCancel}
className="flex-1"
size="lg"
>
{cancelButtonText}
</Button>
)}
<Button
type="submit"
disabled={!canSubmit}
className={`${showCancelButton ? "flex-1" : "w-full"} bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed`}
size="lg"
>
{submitButtonText}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,117 @@
import { useState } from "react";
import { useAccount } from "jazz-tools/react";
import { MusicaAccount } from "../1_schema";
import { ProfileForm } from "./ProfileForm";
import { Button } from "./ui/button";
export function ProfileSettings() {
const { me } = useAccount(MusicaAccount, {
resolve: { profile: true, root: true },
});
const [isEditing, setIsEditing] = useState(false);
if (!me) return null;
const handleSaveProfile = (data: { username: string; avatar?: any }) => {
// Profile is automatically updated by the ProfileForm component
// You can add additional logic here if needed
console.log("Profile updated:", data);
setIsEditing(false);
};
const handleCancel = () => {
setIsEditing(false);
};
if (isEditing) {
return (
<div className="max-w-2xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg p-6">
<ProfileForm
onSubmit={handleSaveProfile}
onCancel={handleCancel}
showCancelButton={true}
submitButtonText="Save Changes"
showHeader={true}
headerTitle="Edit Profile"
headerDescription="Update your profile information"
initialUsername={me.profile.name || ""}
initialAvatar={me.profile.avatar}
/>
</div>
</div>
);
}
return (
<div className="max-w-2xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg p-6">
{/* Profile Display */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Profile Settings
</h1>
{/* Avatar Display */}
<div className="mb-6">
<div className="w-32 h-32 rounded-full overflow-hidden border-4 border-white shadow-lg mx-auto">
{me.profile.avatar ? (
<img
src={`/api/images/${me.profile.avatar.id}`}
alt="Profile"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<span className="text-gray-400 text-4xl">👤</span>
</div>
)}
</div>
</div>
{/* Username Display */}
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-800">
{me.profile.name || "No username set"}
</h2>
<p className="text-gray-600 text-sm">
{me.profile.name
? "Your display name"
: "Set a username to get started"}
</p>
</div>
{/* Edit Button */}
<Button
onClick={() => setIsEditing(true)}
className="bg-blue-600 hover:bg-blue-700"
size="lg"
>
Edit Profile
</Button>
</div>
{/* Additional Profile Information */}
<div className="border-t pt-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">
Account Information
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b border-gray-100">
<span className="text-gray-600">Account ID:</span>
<span className="text-sm font-mono text-gray-800">{me.id}</span>
</div>
<div className="flex justify-between items-center py-2">
<span className="text-gray-600">Setup Completed:</span>
<span
className={`text-sm ${me.root.accountSetupCompleted ? "text-green-600" : "text-red-600"}`}
>
{me.root.accountSetupCompleted ? "Yes" : "No"}
</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { MusicaAccount } from "@/1_schema";
import { createNewPlaylist, deletePlaylist } from "@/4_actions";
import { deletePlaylist } from "@/4_actions";
import {
Sidebar,
SidebarContent,
@@ -15,7 +15,9 @@ import {
import { useAccount } from "jazz-tools/react";
import { Home, Music, Plus, Trash2 } from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { useState } from "react";
import { AuthButton } from "./AuthButton";
import { CreatePlaylistModal } from "./CreatePlaylistModal";
export function SidePanel() {
const { playlistId } = useParams();
@@ -23,6 +25,7 @@ export function SidePanel() {
const { me } = useAccount(MusicaAccount, {
resolve: { root: { playlists: { $each: true } } },
});
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
function handleAllTracksClick() {
navigate(`/`);
@@ -39,92 +42,104 @@ export function SidePanel() {
}
}
async function handleCreatePlaylist() {
const playlist = await createNewPlaylist();
navigate(`/playlist/${playlist.id}`);
function handleCreatePlaylistClick() {
setIsCreateModalOpen(true);
}
function handlePlaylistCreated(playlistId: string) {
navigate(`/playlist/${playlistId}`);
}
return (
<Sidebar>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-blue-600 text-white">
<svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 18V5l12-2v13"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6 15H3c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h3c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zM18 13h-3c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h3c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2z"
fill="currentColor"
/>
</svg>
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Music Player</span>
</div>
<AuthButton />
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={handleAllTracksClick}>
<Home className="size-4" />
<span>Go to all tracks</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Playlists</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={handleCreatePlaylist}>
<Plus className="size-4" />
<span>Add a new playlist</span>
</SidebarMenuButton>
</SidebarMenuItem>
{me?.root.playlists.map((playlist) => (
<SidebarMenuItem key={playlist.id}>
<SidebarMenuButton
onClick={() => handlePlaylistClick(playlist.id)}
isActive={playlist.id === playlistId}
>
<div className="flex items-center gap-2">
<Music className="size-4" />
<span>{playlist.title}</span>
</div>
<>
<Sidebar>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-blue-600 text-white">
<svg
className="size-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 18V5l12-2v13"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6 15H3c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h3c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2zM18 13h-3c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h3c1.1 0 2-.9 2-2v-4c0-1.1-.9-2-2-2z"
fill="currentColor"
/>
</svg>
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Music Player</span>
</div>
<AuthButton />
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={handleAllTracksClick}>
<Home className="size-4" />
<span>Go to all tracks</span>
</SidebarMenuButton>
{playlist.id === playlistId && (
<SidebarMenuAction
onClick={() => handleDeletePlaylist(playlist.id)}
className="text-red-600 hover:text-red-800"
>
<Trash2 className="size-4" />
<span className="sr-only">Delete {playlist.title}</span>
</SidebarMenuAction>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Playlists</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={handleCreatePlaylistClick}>
<Plus className="size-4" />
<span>Add a new playlist</span>
</SidebarMenuButton>
</SidebarMenuItem>
{me?.root.playlists.map((playlist) => (
<SidebarMenuItem key={playlist.id}>
<SidebarMenuButton
onClick={() => handlePlaylistClick(playlist.id)}
isActive={playlist.id === playlistId}
>
<div className="flex items-center gap-2">
<Music className="size-4" />
<span>{playlist.title}</span>
</div>
</SidebarMenuButton>
{playlist.id === playlistId && (
<SidebarMenuAction
onClick={() => handleDeletePlaylist(playlist.id)}
className="text-red-600 hover:text-red-800"
>
<Trash2 className="size-4" />
<span className="sr-only">Delete {playlist.title}</span>
</SidebarMenuAction>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
{/* Create Playlist Modal */}
<CreatePlaylistModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onPlaylistCreated={handlePlaylistCreated}
/>
</>
);
}

View File

@@ -0,0 +1,95 @@
import { useAccount, usePasskeyAuth } from "jazz-tools/react";
import { MusicaAccount } from "../1_schema";
import { ProfileForm } from "./ProfileForm";
import { Button } from "./ui/button";
export function WelcomeScreen() {
const { me } = useAccount(MusicaAccount, {
resolve: { profile: true, root: true },
});
const auth = usePasskeyAuth({
appName: "Jazz Music Player",
});
if (!me) return null;
const handleCompleteSetup = () => {
// Mark account setup as completed
me.root.accountSetupCompleted = true;
};
const handleLogin = () => {
auth.logIn();
};
return (
<div className="w-full lg:w-auto min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="flex flex-col lg:flex-row gap-8 lg:gap-16 items-center">
{/* Form Panel */}
<div className="w-full max-w-md bg-white rounded-lg shadow-xl p-8">
<ProfileForm
onSubmit={handleCompleteSetup}
submitButtonText="Continue"
showHeader={true}
headerTitle="Welcome to Music Player! 🎵"
headerDescription="Let's set up your profile to get started"
initialUsername={me?.profile?.name || ""}
initialAvatar={me?.profile?.avatar}
/>
</div>
<div className="lg:hidden pt-4 flex justify-end items-center w-full gap-2">
<div className="text-sm font-semibold text-gray-600">
Already a user?
</div>
<Button onClick={handleLogin} size="sm">
Login
</Button>
</div>
{/* Title Section - Hidden on mobile, shown on right side for larger screens */}
<div className="hidden lg:flex flex-col justify-center items-start max-w-md">
<div className="space-y-6">
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 leading-tight">
Your Music at your fingertips.
</h1>
<div className="space-y-4">
<p className="text-xl lg:text-2xl text-gray-700 font-medium">
Offline, Collaborative, Fast
</p>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-500 font-medium">
Powered by
</span>
<a
href="https://jazz.tools"
target="_blank"
rel="noopener noreferrer"
className="text-lg font-bold text-blue-600 hover:underline"
>
Jazz
</a>
</div>
{/* Login Button */}
<div className="pt-4">
<p className="text-sm font-semibold text-gray-600 mb-2">
Already a user?
</p>
<Button
onClick={handleLogin}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 text-lg font-medium rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
size="lg"
>
Login
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -294,7 +294,7 @@ const SidebarTrigger = React.forwardRef<
}}
{...props}
>
<PanelLeft />
<PanelLeft className="size-4" />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);

View File

@@ -6,7 +6,7 @@ export function usePlayMedia() {
const previousMediaLoad = useRef<Promise<unknown> | undefined>(undefined);
async function playMedia(file: Blob) {
async function playMedia(file: Blob, autoPlay = true) {
// Wait for the previous load to finish
// to avoid to incur into concurrency issues
await previousMediaLoad.current;
@@ -17,7 +17,9 @@ export function usePlayMedia() {
await promise;
audioManager.play();
if (autoPlay) {
audioManager.play();
}
}
return playMedia;

View File

@@ -34,7 +34,7 @@ async function loadInitialData(mediaPlayer: MediaPlayer) {
// Load the active track in the AudioManager
if (me.root.activeTrack) {
mediaPlayer.loadTrack(me.root.activeTrack);
mediaPlayer.loadTrack(me.root.activeTrack, false);
}
}

View File

@@ -44,7 +44,10 @@ test("sign up and log out", async ({ page: marioPage }) => {
const marioHome = new HomePage(marioPage);
await marioHome.signUp("Mario");
await marioHome.fillUsername("Mario");
await marioPage.keyboard.press("Enter");
await marioHome.signUp();
await marioHome.logoutButton.waitFor({
state: "visible",

View File

@@ -47,12 +47,14 @@ test("create a new playlist and share", async ({
const marioHome = new HomePage(marioPage);
await marioHome.fillUsername("Mario");
await marioPage.keyboard.press("Enter");
// The example song should be loaded
await marioHome.expectMusicTrack("Example song");
await marioHome.editTrackTitle("Example song", "Super Mario World");
await marioHome.createPlaylist();
await marioHome.editPlaylistTitle("Save the princess");
await marioHome.createPlaylist("Save the princess");
await marioHome.navigateToPlaylist("All tracks");
await marioHome.addTrackToPlaylist("Super Mario World", "Save the princess");
@@ -60,7 +62,7 @@ test("create a new playlist and share", async ({
await marioHome.navigateToPlaylist("Save the princess");
await marioHome.expectMusicTrack("Super Mario World");
await marioHome.signUp("Mario");
await marioHome.signUp();
const url = await marioHome.getShareLink();
@@ -74,7 +76,10 @@ test("create a new playlist and share", async ({
const luigiHome = new HomePage(luigiPage);
await luigiHome.signUp("Luigi");
await luigiHome.fillUsername("Luigi");
await luigiPage.keyboard.press("Enter");
await luigiHome.signUp();
await luigiPage.goto(url);
@@ -90,15 +95,18 @@ test("create a new playlist, share, then remove track", async ({
// Create playlist with a song and share
await marioPage.goto("/");
const marioHome = new HomePage(marioPage);
await marioHome.fillUsername("Mario");
await marioPage.keyboard.press("Enter");
await marioHome.expectMusicTrack("Example song");
await marioHome.editTrackTitle("Example song", "Super Mario World");
await marioHome.createPlaylist();
await marioHome.editPlaylistTitle("Save the princess");
await marioHome.createPlaylist("Save the princess");
await marioHome.navigateToPlaylist("All tracks");
await marioHome.addTrackToPlaylist("Super Mario World", "Save the princess");
await marioHome.navigateToPlaylist("Save the princess");
await marioHome.expectMusicTrack("Super Mario World");
await marioHome.signUp("Mario");
await marioHome.signUp();
const url = await marioHome.getShareLink();
await sleep(4000); // Wait for the sync to complete
@@ -109,7 +117,12 @@ test("create a new playlist, share, then remove track", async ({
const luigiPage = await luigiContext.newPage();
await luigiPage.goto("/");
const luigiHome = new HomePage(luigiPage);
await luigiHome.signUp("Luigi");
await luigiHome.fillUsername("Luigi");
await luigiPage.keyboard.press("Enter");
await luigiHome.signUp();
await luigiPage.goto(url);
await luigiHome.expectMusicTrack("Super Mario World");

View File

@@ -19,6 +19,10 @@ export class HomePage {
name: "Sign out",
});
async fillUsername(username: string) {
await this.page.getByRole("textbox", { name: "Username" }).fill(username);
}
async expectActiveTrackPlaying() {
await expect(
this.page.getByRole("button", {
@@ -71,12 +75,10 @@ export class HomePage {
await this.page.getByRole("button", { name: "Save" }).click();
}
async createPlaylist() {
async createPlaylist(playlistTitle: string) {
await this.newPlaylistButton.click();
}
async editPlaylistTitle(playlistTitle: string) {
await this.playlistTitleInput.fill(playlistTitle);
await this.page.getByRole("button", { name: "Create Playlist" }).click();
}
async navigateToPlaylist(playlistTitle: string) {
@@ -98,7 +100,7 @@ export class HomePage {
async getShareLink() {
await this.page
.getByRole("button", {
name: "Share playlist",
name: "Share",
})
.click();
@@ -139,9 +141,8 @@ export class HomePage {
.click();
}
async signUp(name: string) {
async signUp() {
await this.page.getByRole("button", { name: "Sign up" }).click();
await this.page.getByRole("textbox", { name: "Username" }).fill(name);
await this.page
.getByRole("button", { name: "Sign up with passkey" })
.click();
@@ -156,10 +157,12 @@ export class HomePage {
async logOut() {
await this.logoutButton.click();
await this.loginButton.waitFor({
await this.page.getByRole("textbox", { name: "Username" }).waitFor({
state: "visible",
});
await expect(this.loginButton).toBeVisible();
await expect(
this.page.getByRole("textbox", { name: "Username" }),
).toBeVisible();
}
}

View File

@@ -14,23 +14,23 @@
"dependencies": {
"jazz-tools": "workspace:*",
"lucide-react": "^0.536.0",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@playwright/test": "^1.50.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -11,17 +11,17 @@
},
"dependencies": {
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@biomejs/biome": "2.1.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -12,17 +12,17 @@
"dependencies": {
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@biomejs/biome": "2.1.3",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -19,21 +19,21 @@
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.39.1",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -22,21 +22,21 @@
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.509.0",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",
"postcss": "^8.4.40",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -24,20 +24,20 @@
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.485.0",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.17",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.3.4",
"jazz-run": "workspace:*",
"npm-run-all": "^4.1.5",
"tsx": "^4.19.3",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -18,8 +18,8 @@
"jazz-tools": "workspace:*",
"lucide-react": "^0.536.0",
"qrcode": "^1.5.3",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0",
@@ -29,12 +29,12 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.1.10",
"@types/qrcode": "^1.5.1",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"postcss": "^8.4.27",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -12,18 +12,18 @@
"dependencies": {
"@tailwindcss/forms": "^0.5.9",
"jazz-tools": "workspace:*",
"react": "catalog:react",
"react-dom": "catalog:react"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"tailwindcss": "^4.1.10",
"typescript": "catalog:default",
"vite": "catalog:default"
"typescript": "5.6.2",
"vite": "6.3.5"
}
}

View File

@@ -46,6 +46,7 @@ In this case, provide the WebSocket endpoint your proxy exposes as the sync serv
### Command line options:
- `--host` / `-h` - the host to run the sync server on. Defaults to 127.0.0.1.
- `--port` / `-p` - the port to run the sync server on. Defaults to 4200.
- `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default.
- `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`.

View File

@@ -12,14 +12,14 @@
"node": ">=22.0.0"
},
"devDependencies": {
"@biomejs/biome": "catalog:default",
"@biomejs/biome": "2.1.3",
"@changesets/cli": "^2.27.10",
"@playwright/test": "^1.50.1",
"@vitejs/plugin-react": "^4.5.1",
"@vitest/browser": "catalog:default",
"@vitest/coverage-istanbul": "catalog:default",
"@vitest/coverage-v8": "catalog:default",
"@vitest/ui": "catalog:default",
"@vitest/browser": "3.2.4",
"@vitest/coverage-istanbul": "3.2.4",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"happy-dom": "^17.4.4",
"jazz-run": "workspace:*",
"jazz-tools": "workspace:*",
@@ -28,7 +28,7 @@
"playwright": "^1.50.1",
"turbo": "^2.3.1",
"typedoc": "^0.25.13",
"vitest": "catalog:default"
"vitest": "3.2.4"
},
"scripts": {
"dev": "turbo dev",
@@ -59,11 +59,11 @@
]
},
"overrides": {
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"react": "catalog:react",
"react-dom": "catalog:react",
"vite": "catalog:default",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"vite": "6.3.5",
"esbuild": "0.24.0"
}
}

View File

@@ -1,5 +1,31 @@
# cojson-storage-indexeddb
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6
### Patch Changes
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
## 0.17.4
### Patch Changes
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.17.3",
"version": "0.17.7",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
@@ -9,7 +9,7 @@
"cojson": "workspace:*"
},
"devDependencies": {
"typescript": "catalog:default",
"typescript": "5.6.2",
"webdriverio": "^8.15.0"
},
"scripts": {

View File

@@ -1,5 +1,31 @@
# cojson-storage-sqlite
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6
### Patch Changes
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
## 0.17.4
### Patch Changes
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.17.3",
"version": "0.17.7",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
@@ -11,7 +11,7 @@
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.12",
"typescript": "catalog:default"
"typescript": "5.6.2"
},
"scripts": {
"dev": "tsc --watch --sourceMap --outDir dist",

View File

@@ -1,5 +1,31 @@
# cojson-transport-nodejs-ws
## 0.17.7
### Patch Changes
- cojson@0.17.7
## 0.17.6
### Patch Changes
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
## 0.17.4
### Patch Changes
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.17.3",
"version": "0.17.7",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
@@ -20,7 +20,7 @@
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"@types/ws": "8.5.10",
"typescript": "catalog:default",
"typescript": "5.6.2",
"ws": "^8.14.2"
}
}

View File

@@ -1,5 +1,18 @@
# cojson
## 0.17.7
## 0.17.6
## 0.17.5
### Patch Changes
- 71c1411: Removed some unnecessary content messages sent after a local transaction when sending a value as dependency before the ack response
- 2d11d44: Make the CoValueCore.unmount function detach the CoValue from LocalNode
## 0.17.4
## 0.17.3
### Patch Changes

View File

@@ -25,11 +25,11 @@
},
"type": "module",
"license": "MIT",
"version": "0.17.3",
"version": "0.17.7",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"libsql": "^0.5.13",
"typescript": "catalog:default"
"typescript": "5.6.2"
},
"dependencies": {
"@noble/ciphers": "^1.3.0",

View File

@@ -33,11 +33,7 @@ export class GarbageCollector {
const timeSinceLastAccessed = currentTime - verified.lastAccessed;
if (timeSinceLastAccessed > GARBAGE_COLLECTOR_CONFIG.MAX_AGE) {
const unmounted = coValue.unmount();
if (unmounted) {
this.coValues.delete(coValue.id);
}
coValue.unmount();
}
}
}

View File

@@ -1,8 +1,4 @@
import {
CoValueHeader,
Transaction,
VerifiedState,
} from "./coValueCore/verifiedState.js";
import { CoValueHeader, Transaction } from "./coValueCore/verifiedState.js";
import { TRANSACTION_CONFIG } from "./config.js";
import { Signature } from "./crypto/crypto.js";
import { RawCoID, SessionID } from "./ids.js";
@@ -65,6 +61,7 @@ export function exceedsRecommendedSize(
export function knownStateFromContent(content: NewContentMessage) {
const knownState = emptyKnownState(content.id);
knownState.header = Boolean(content.header);
for (const [sessionID, session] of Object.entries(content.new)) {
knownState.sessions[sessionID as SessionID] =

View File

@@ -219,6 +219,8 @@ export class CoValueCore {
this.groupInvalidationSubscription = undefined;
}
this.node.internalDeleteCoValue(this.id);
return true;
}

View File

@@ -0,0 +1,215 @@
import { describe, expect, test } from "vitest";
import { knownStateFromContent } from "../coValueContentMessage.js";
import { emptyKnownState } from "../sync.js";
import type { NewContentMessage } from "../sync.js";
import type { RawCoID, SessionID } from "../ids.js";
import { stableStringify } from "../jsonStringify.js";
import { CO_VALUE_PRIORITY } from "../priority.js";
describe("knownStateFromContent", () => {
const mockCoID: RawCoID = "co_z1234567890abcdef";
const mockSessionID1: SessionID = "sealer_z123/signer_z456_session_z789";
const mockSessionID2: SessionID = "sealer_zabc/signer_zdef_session_zghi";
test("returns empty known state for content with no header and no sessions", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
header: undefined,
priority: CO_VALUE_PRIORITY.HIGH,
new: {},
};
const result = knownStateFromContent(content);
const expected = emptyKnownState(mockCoID);
expect(result).toEqual(expected);
expect(result.id).toBe(mockCoID);
expect(result.header).toBe(false);
expect(result.sessions).toEqual({});
});
test("sets header to true when content has header", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
header: {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
uniqueness: null,
createdAt: null,
},
priority: CO_VALUE_PRIORITY.HIGH,
new: {},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(true);
expect(result.id).toBe(mockCoID);
expect(result.sessions).toEqual({});
});
test("sets header to false when content has no header", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(false);
expect(result.id).toBe(mockCoID);
expect(result.sessions).toEqual({});
});
test("calculates session states correctly for single session", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
header: {
type: "comap",
ruleset: { type: "unsafeAllowAll" },
meta: null,
uniqueness: null,
createdAt: null,
},
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 5,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(true);
expect(result.sessions[mockSessionID1]).toBe(8); // 5 + 3
expect(Object.keys(result.sessions)).toHaveLength(1);
});
test("calculates session states correctly for multiple sessions", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 3,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
[mockSessionID2]: {
after: 7,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.header).toBe(false);
expect(result.sessions[mockSessionID1]).toBe(5); // 3 + 2
expect(result.sessions[mockSessionID2]).toBe(11); // 7 + 4
expect(Object.keys(result.sessions)).toHaveLength(2);
});
test("handles session with no transactions", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 10,
newTransactions: [],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.sessions[mockSessionID1]).toBe(10); // 10 + 0
});
test("handles session with after index 0", () => {
const content: NewContentMessage = {
action: "content",
id: mockCoID,
priority: CO_VALUE_PRIORITY.HIGH,
new: {
[mockSessionID1]: {
after: 0,
newTransactions: [
{
privacy: "trusting",
madeAt: Date.now(),
changes: stableStringify([]),
},
],
lastSignature: "signature_z1234",
},
},
};
const result = knownStateFromContent(content);
expect(result.sessions[mockSessionID1]).toBe(1); // 0 + 1
});
});

View File

@@ -163,13 +163,10 @@ describe("sync after the garbage collector has run", () => {
"edge -> storage | LOAD Map sessions: empty",
"storage -> edge | CONTENT Group header: true new: After: 0 New: 5",
"storage -> edge | CONTENT Map header: true new: After: 0 New: 1",
"edge -> server | CONTENT Map header: true new: ",
"edge -> client | CONTENT Group header: true new: After: 0 New: 5",
"edge -> client | CONTENT Map header: true new: After: 0 New: 1",
"server -> edge | KNOWN Map sessions: header/1",
"server -> storage | CONTENT Map header: true new: After: 0 New: 1",
"server -> edge | KNOWN Map sessions: header/1",
"server -> storage | CONTENT Map header: true new: ",
"client -> edge | KNOWN Group sessions: header/5",
"client -> edge | KNOWN Map sessions: header/1",
]

View File

@@ -973,7 +973,6 @@ describe("loading coValues from server", () => {
"server -> client | KNOWN Group sessions: header/6",
"server -> client | KNOWN ParentGroup sessions: header/8",
"server -> client | KNOWN Map sessions: header/1",
"client -> server | CONTENT ParentGroup header: true new: ",
]
`);
});

View File

@@ -132,6 +132,10 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
group.extend(parentGroup);
// We wait for sync here to avoid flakiness on CI
await parentGroup.core.waitForSync();
await group.core.waitForSync();
const map = group.createMap();
map.set("hello", "world");
@@ -154,20 +158,17 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
"edge-france -> core | CONTENT ParentGroup header: true new: After: 0 New: 6",
"edge-france -> storage | CONTENT Group header: false new: After: 3 New: 2",
"edge-france -> core | CONTENT Group header: false new: After: 3 New: 2",
"edge-france -> storage | CONTENT Map header: true new: After: 0 New: 1",
"edge-france -> core | CONTENT Map header: true new: After: 0 New: 1",
"core -> edge-france | KNOWN Group sessions: header/3",
"core -> storage | CONTENT Group header: true new: After: 0 New: 3",
"core -> edge-france | KNOWN ParentGroup sessions: header/6",
"core -> storage | CONTENT ParentGroup header: true new: After: 0 New: 6",
"core -> edge-france | KNOWN Group sessions: header/5",
"core -> storage | CONTENT Group header: false new: After: 3 New: 2",
"edge-france -> storage | CONTENT Map header: true new: After: 0 New: 1",
"edge-france -> core | CONTENT Map header: true new: After: 0 New: 1",
"core -> edge-france | KNOWN Map sessions: header/1",
"core -> storage | CONTENT Map header: true new: After: 0 New: 1",
"edge-france -> core | CONTENT ParentGroup header: true new: ",
"client -> edge-italy | LOAD Map sessions: empty",
"core -> edge-france | KNOWN ParentGroup sessions: header/6",
"core -> storage | CONTENT ParentGroup header: true new: ",
"edge-italy -> storage | LOAD Map sessions: empty",
"storage -> edge-italy | KNOWN Map sessions: empty",
"edge-italy -> core | LOAD Map sessions: empty",

View File

@@ -47,8 +47,6 @@ describe("peer reconciliation", () => {
"server -> client | KNOWN Map sessions: empty",
"server -> client | KNOWN Group sessions: header/3",
"server -> client | KNOWN Map sessions: header/1",
"client -> server | CONTENT Group header: true new: ",
"client -> server | CONTENT Map header: true new: ",
]
`);
});

View File

@@ -86,7 +86,6 @@ describe("client to server upload", () => {
"server -> client | KNOWN ParentGroup sessions: header/6",
"server -> client | KNOWN Group sessions: header/5",
"server -> client | KNOWN Map sessions: header/1",
"client -> server | CONTENT ParentGroup header: true new: ",
]
`);
});

View File

@@ -1,5 +1,39 @@
# jazz-react
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-tools@0.17.7
## 0.17.6
### Patch Changes
- Updated dependencies [82de51c]
- Updated dependencies [694b168]
- jazz-tools@0.17.6
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
## 0.17.4
### Patch Changes
- Updated dependencies [7dd3d00]
- jazz-tools@0.17.4
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "community-jazz-vue",
"version": "0.17.3",
"version": "0.17.7",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -21,8 +21,8 @@
"@types/degit": "^2.8.3",
"@types/gradient-string": "^1.1.2",
"@types/inquirer": "^9.0.3",
"typescript": "catalog:default",
"vitest": "catalog:default"
"typescript": "5.6.2",
"vitest": "3.2.4"
},
"scripts": {
"dev": "tsc --watch",

View File

@@ -8,7 +8,7 @@
"devDependencies": {
"@types/react": "^18.3.12",
"react": "^18.3.1",
"typescript": "catalog:default"
"typescript": "5.6.2"
},
"peerDependencies": {
"react": "*"

View File

@@ -1,5 +1,43 @@
# jazz-auth-betterauth
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-betterauth-client-plugin@0.17.7
- jazz-tools@0.17.7
## 0.17.6
### Patch Changes
- Updated dependencies [82de51c]
- Updated dependencies [694b168]
- jazz-tools@0.17.6
- jazz-betterauth-client-plugin@0.17.6
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
- jazz-betterauth-client-plugin@0.17.5
## 0.17.4
### Patch Changes
- Updated dependencies [7dd3d00]
- jazz-tools@0.17.4
- jazz-betterauth-client-plugin@0.17.4
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-auth-betterauth",
"version": "0.17.3",
"version": "0.17.7",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
@@ -19,6 +19,6 @@
"test:watch": "vitest --watch --root ../../ --project jazz-auth-betterauth"
},
"devDependencies": {
"typescript": "catalog:default"
"typescript": "5.6.2"
}
}

View File

@@ -1,5 +1,29 @@
# jazz-betterauth-client-plugin
## 0.17.7
### Patch Changes
- jazz-betterauth-server-plugin@0.17.7
## 0.17.6
### Patch Changes
- jazz-betterauth-server-plugin@0.17.6
## 0.17.5
### Patch Changes
- jazz-betterauth-server-plugin@0.17.5
## 0.17.4
### Patch Changes
- jazz-betterauth-server-plugin@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-client-plugin",
"version": "0.17.3",
"version": "0.17.7",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,39 @@
# jazz-betterauth-server-plugin
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-tools@0.17.7
## 0.17.6
### Patch Changes
- Updated dependencies [82de51c]
- Updated dependencies [694b168]
- jazz-tools@0.17.6
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
## 0.17.4
### Patch Changes
- Updated dependencies [7dd3d00]
- jazz-tools@0.17.4
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-server-plugin",
"version": "0.17.3",
"version": "0.17.7",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,47 @@
# jazz-react-auth-betterauth
## 0.17.7
### Patch Changes
- cojson@0.17.7
- jazz-auth-betterauth@0.17.7
- jazz-betterauth-client-plugin@0.17.7
- jazz-tools@0.17.7
## 0.17.6
### Patch Changes
- Updated dependencies [82de51c]
- Updated dependencies [694b168]
- jazz-tools@0.17.6
- jazz-auth-betterauth@0.17.6
- jazz-betterauth-client-plugin@0.17.6
- cojson@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
- jazz-auth-betterauth@0.17.5
- jazz-betterauth-client-plugin@0.17.5
## 0.17.4
### Patch Changes
- Updated dependencies [7dd3d00]
- jazz-tools@0.17.4
- jazz-auth-betterauth@0.17.4
- jazz-betterauth-client-plugin@0.17.4
- cojson@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-react-auth-betterauth",
"version": "0.17.3",
"version": "0.17.7",
"type": "module",
"main": "dist/index.js",
"types": "src/index.tsx",
@@ -23,6 +23,6 @@
"devDependencies": {
"@testing-library/react": "^16.1.0",
"@types/react": "^17 || ^18 || ^19",
"typescript": "catalog:default"
"typescript": "5.6.2"
}
}

View File

@@ -1,5 +1,48 @@
# jazz-run
## 0.17.7
### Patch Changes
- feaa69e: Add host option to the jazz-run sync command
- cojson@0.17.7
- cojson-storage-sqlite@0.17.7
- cojson-transport-ws@0.17.7
- jazz-tools@0.17.7
## 0.17.6
### Patch Changes
- Updated dependencies [82de51c]
- Updated dependencies [694b168]
- jazz-tools@0.17.6
- cojson@0.17.6
- cojson-storage-sqlite@0.17.6
- cojson-transport-ws@0.17.6
## 0.17.5
### Patch Changes
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- Updated dependencies [5963658]
- cojson@0.17.5
- jazz-tools@0.17.5
- cojson-storage-sqlite@0.17.5
- cojson-transport-ws@0.17.5
## 0.17.4
### Patch Changes
- Updated dependencies [7dd3d00]
- jazz-tools@0.17.4
- cojson@0.17.4
- cojson-storage-sqlite@0.17.4
- cojson-transport-ws@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.17.3",
"version": "0.17.7",
"exports": {
"./startSyncServer": {
"types": "./dist/startSyncServer.d.ts",
@@ -28,15 +28,15 @@
"@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.17.3",
"cojson-storage-sqlite": "workspace:0.17.3",
"cojson-transport-ws": "workspace:0.17.3",
"cojson": "workspace:0.17.7",
"cojson-storage-sqlite": "workspace:0.17.7",
"cojson-transport-ws": "workspace:0.17.7",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.17.3",
"jazz-tools": "workspace:0.17.7",
"ws": "^8.14.2"
},
"devDependencies": {
"@types/ws": "8.5.10",
"typescript": "catalog:default"
"typescript": "5.6.2"
}
}

View File

@@ -0,0 +1,6 @@
export const serverDefaults = {
host: "127.0.0.1",
port: 4200,
inMemory: false,
db: "sync-db/storage.db",
};

View File

@@ -5,6 +5,7 @@ import { NodeContext, NodeRuntime } from "@effect/platform-node";
import { Console, Effect } from "effect";
import { createWorkerAccount } from "./createWorkerAccount.js";
import { startSyncServer } from "./startSyncServer.js";
import { serverDefaults } from "./config.js";
const jazzTools = Command.make("jazz-tools");
@@ -39,36 +40,63 @@ const accountCommand = Command.make("account").pipe(
Command.withSubcommands([createAccountCommand]),
);
const hostOption = Options.text("host")
.pipe(Options.withAlias("h"))
.pipe(
Options.withDescription(
`The host to listen on. Default is ${serverDefaults.host}`,
),
)
.pipe(Options.withDefault(serverDefaults.host));
const portOption = Options.text("port")
.pipe(Options.withAlias("p"))
.pipe(
Options.withDescription(
"Select a different port for the WebSocket server. Default is 4200",
`Select a different port for the WebSocket server. Default is ${serverDefaults.port}`,
),
)
.pipe(Options.withDefault("4200"));
.pipe(Options.withDefault(serverDefaults.port.toString()));
const inMemoryOption = Options.boolean("in-memory").pipe(
Options.withDescription("Use an in-memory storage instead of file-based"),
Options.withDescription("Use an in-memory storage instead of file-based."),
);
const dbOption = Options.file("db")
.pipe(
Options.withDescription(
"The path to the file where to store the data. Default is 'sync-db/storage.db'",
`The path to the file where to store the data. Default is '${serverDefaults.db}'`,
),
)
.pipe(Options.withDefault("sync-db/storage.db"));
.pipe(Options.withDefault(serverDefaults.db));
const startSyncServerCommand = Command.make(
"sync",
{ port: portOption, inMemory: inMemoryOption, db: dbOption },
({ port, inMemory, db }) => {
{
host: hostOption,
port: portOption,
inMemory: inMemoryOption,
db: dbOption,
},
({ host, port, inMemory, db }) => {
return Effect.gen(function* () {
yield* Effect.promise(() => startSyncServer({ port, inMemory, db }));
const server = yield* Effect.promise(() =>
startSyncServer({ host, port, inMemory, db }),
);
const serverAddress = server.address();
if (!serverAddress) {
return yield* Effect.fail(new Error("Failed to start sync server."));
}
const socketAddress =
typeof serverAddress === "object"
? `${serverAddress.address}:${serverAddress.port}`
: serverAddress;
yield* Console.log(
`COJSON sync server listening on ws://127.0.0.1:${port}`,
`COJSON sync server listening on ws://${socketAddress}`,
);
// Keep the server up

View File

@@ -1,4 +1,4 @@
import { createServer, type Server } from "node:http";
import { createServer } from "node:http";
import { mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import { LocalNode } from "cojson";
@@ -6,16 +6,19 @@ import { getBetterSqliteStorage } from "cojson-storage-sqlite";
import { createWebSocketPeer } from "cojson-transport-ws";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { WebSocketServer } from "ws";
import { type SyncServer } from "./types.js";
export const startSyncServer = async ({
host,
port,
inMemory,
db,
}: {
host: string | undefined;
port: string | undefined;
inMemory: boolean;
db: string;
}) => {
}): Promise<SyncServer> => {
const crypto = await WasmCrypto.create();
const server = createServer((req, res) => {
@@ -94,8 +97,6 @@ export const startSyncServer = async ({
localNode.gracefulShutdown();
});
server.listen(port ? parseInt(port) : undefined);
const _close = server.close;
server.close = () => {
@@ -106,5 +107,11 @@ export const startSyncServer = async ({
Object.defineProperty(server, "localNode", { value: localNode });
return server as Server & { localNode: LocalNode };
server.listen(port ? parseInt(port) : undefined, host);
return new Promise((resolve) => {
server.once("listening", () => {
resolve(server as SyncServer);
});
});
};

View File

@@ -5,11 +5,13 @@ import { describe, expect, it, onTestFinished } from "vitest";
import { WebSocket } from "ws";
import { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js";
import { serverDefaults } from "../config.js";
describe("createWorkerAccount - integration tests", () => {
it("should create a worker account using the local sync server", async () => {
// Pass port: undefined to let the server choose a random port
const server = await startSyncServer({
host: serverDefaults.host,
port: undefined,
inMemory: true,
db: "",

View File

@@ -1,24 +1,29 @@
import { randomUUID } from "crypto";
import { tmpdir } from "os";
import { join } from "path";
import { LocalNode } from "cojson";
import { co, z } from "jazz-tools";
import { startWorker } from "jazz-tools/worker";
import { describe, expect, test } from "vitest";
import { describe, expect, test, afterAll } from "vitest";
import { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js";
import { serverDefaults } from "../config.js";
import { unlinkSync } from "node:fs";
const TestMap = co.map({
value: z.string(),
});
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
afterAll(() => {
unlinkSync(dbPath);
});
describe("startSyncServer", () => {
test("persists values in storage and loads them after restart", async () => {
// Create a temporary database file
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
// Start first server instance
const server1 = await startSyncServer({
host: serverDefaults.host,
port: "0", // Random available port
inMemory: false,
db: dbPath,
@@ -48,6 +53,7 @@ describe("startSyncServer", () => {
// Start second server instance with same DB
const server2 = await startSyncServer({
host: serverDefaults.host,
port: "0",
inMemory: false,
db: dbPath,
@@ -74,4 +80,21 @@ describe("startSyncServer", () => {
await worker2.done();
server2.close();
});
test("starts a sync server with a specific host and port", async () => {
const server = await startSyncServer({
host: "0.0.0.0",
port: "4900",
inMemory: false,
db: dbPath,
});
expect(server.address()).toEqual({
address: "0.0.0.0",
port: 4900,
family: "IPv4",
});
server.close();
});
});

View File

@@ -18,6 +18,7 @@ import { afterAll, describe, expect, onTestFinished, test } from "vitest";
import { createWorkerAccount } from "../createWorkerAccount.js";
import { startSyncServer } from "../startSyncServer.js";
import { waitFor } from "./utils.js";
import { serverDefaults } from "../config.js";
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
@@ -30,9 +31,9 @@ async function setup<
| (AccountClass<Account> & CoValueFromRaw<Account>)
| AnyAccountSchema,
>(AccountSchema?: S) {
const { server, port } = await setupSyncServer();
const { server, port, host } = await setupSyncServer();
const syncServer = `ws://localhost:${port}`;
const syncServer = `ws://${host}:${port}`;
const { worker, done, waitForConnection, subscribeToConnectionChange } =
await setupWorker(syncServer, AccountSchema);
@@ -43,13 +44,18 @@ async function setup<
syncServer,
server,
port,
host,
waitForConnection,
subscribeToConnectionChange,
};
}
async function setupSyncServer(defaultPort = "0") {
async function setupSyncServer(
defaultHost = serverDefaults.host,
defaultPort = "0",
) {
const server = await startSyncServer({
host: defaultHost,
port: defaultPort,
inMemory: false,
db: dbPath,
@@ -61,7 +67,7 @@ async function setupSyncServer(defaultPort = "0") {
server.close();
});
return { server, port };
return { server, port, host: defaultHost };
}
async function setupWorker<
@@ -258,6 +264,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port
const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port,
inMemory: true,
db: "",
@@ -290,6 +297,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port
const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port,
inMemory: true,
db: "",
@@ -326,6 +334,7 @@ describe("startWorker integration", () => {
// Start a new sync server on the same port
const newServer = await startSyncServer({
host: worker1.host,
port: worker1.port,
inMemory: true,
db: "",

View File

@@ -0,0 +1,4 @@
import { type Server } from "node:http";
import { type LocalNode } from "cojson";
export type SyncServer = Server & { localNode: LocalNode };

View File

@@ -1,5 +1,43 @@
# jazz-tools
## 0.17.7
### Patch Changes
- cojson@0.17.7
- cojson-storage-indexeddb@0.17.7
- cojson-transport-ws@0.17.7
## 0.17.6
### Patch Changes
- 82de51c: allow creating CoValues from JSON without an active account
- 694b168: get resized image's id without triggering shallow load in `loadImageBySize`
- cojson@0.17.6
- cojson-storage-indexeddb@0.17.6
- cojson-transport-ws@0.17.6
## 0.17.5
### Patch Changes
- 5963658: Implement/expose loadUnique and upsertUnique on co.list and co.record
- Updated dependencies [71c1411]
- Updated dependencies [2d11d44]
- cojson@0.17.5
- cojson-storage-indexeddb@0.17.5
- cojson-transport-ws@0.17.5
## 0.17.4
### Patch Changes
- 7dd3d00: Export `Ref` class from jazz-tools package
- cojson@0.17.4
- cojson-storage-indexeddb@0.17.4
- cojson-transport-ws@0.17.4
## 0.17.3
### Patch Changes

View File

@@ -140,7 +140,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.17.3",
"version": "0.17.7",
"dependencies": {
"@manuscripts/prosemirror-recreate-steps": "^0.1.4",
"@scure/base": "1.2.1",
@@ -186,8 +186,8 @@
"playwright": "^1.50.1",
"queueueue": "^4.1.2",
"tsup": "8.5.0",
"typescript": "catalog:default",
"vitest": "catalog:default",
"typescript": "5.6.2",
"vitest": "3.2.4",
"ws": "^8.14.2"
},
"peerDependencies": {

View File

@@ -347,7 +347,6 @@ describe("loadImageBySize", async () => {
it("returns the image already loaded", async () => {
const account = await setupJazzTestSync({ asyncPeers: true });
const account2 = await createJazzTestAccount();
setActiveAccount(account);
@@ -363,9 +362,11 @@ describe("loadImageBySize", async () => {
group,
);
const account2 = await createJazzTestAccount();
setActiveAccount(account2);
const result = await loadImageBySize(imageDef, 1024, 1024);
const result = await loadImageBySize(imageDef.id, 1024, 1024);
expect(result).not.toBeNull();
expect(result?.image.id).toBe(imageDef["1024x1024"]!.id);
expect(result?.image.isBinaryStreamEnded()).toBe(true);
expect(result?.image.asBase64()).toStrictEqual(expect.any(String));

View File

@@ -185,7 +185,11 @@ export async function loadImageBySize(
const bestTarget =
sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1)!;
const file = image[bestTarget.size[2]];
// The image's `wxh` keys reference FileStream.
// image[bestTarget.size[2]] returns undefined if FileStream hasn't loaded yet.
// Since we only need the file's ID to fetch it later, we check the raw _refs
// which contain only the linked covalue's ID.
const file = image._refs[bestTarget.size[2]];
if (!file) {
return null;

View File

@@ -1,5 +1,5 @@
import type { JsonValue, RawCoList } from "cojson";
import { ControlledAccount, RawAccount } from "cojson";
import type { JsonValue, RawCoList, CoValueUniqueness, RawCoID } from "cojson";
import { ControlledAccount, RawAccount, cojsonInternals } from "cojson";
import { calcPatch } from "fast-myers-diff";
import type {
Account,
@@ -24,6 +24,7 @@ import {
RegisteredSchemas,
SchemaInit,
accessChildByKey,
activeAccountContext,
coField,
coValueClassFromCoValueClassOrSchema,
coValuesCache,
@@ -236,12 +237,21 @@ export class CoList<out Item = any> extends Array<Item> implements CoValue {
static create<L extends CoList>(
this: CoValueClass<L>,
items: L[number][],
options?: { owner: Account | Group } | Account | Group,
options?:
| {
owner: Account | Group;
unique?: CoValueUniqueness["uniqueness"];
}
| Account
| Group,
) {
const { owner } = parseCoValueCreateOptions(options);
const { owner, uniqueness } = parseCoValueCreateOptions(options);
const instance = new this({ init: items, owner });
const raw = owner._raw.createList(
toRawItems(items, instance._schema[ItemsSym], owner),
null,
"private",
uniqueness,
);
Object.defineProperties(instance, {
@@ -546,6 +556,116 @@ export class CoList<out Item = any> extends Array<Item> implements CoValue {
return cl.fromRaw(this._raw) as InstanceType<Cl>;
}
/** @deprecated Use `CoList.upsertUnique` and `CoList.loadUnique` instead. */
static findUnique<L extends CoList>(
this: CoValueClass<L>,
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
) {
return CoList._findUnique(unique, ownerID, as);
}
/** @internal */
static _findUnique<L extends CoList>(
this: CoValueClass<L>,
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
) {
as ||= activeAccountContext.get();
const header = {
type: "colist" as const,
ruleset: {
type: "ownedByGroup" as const,
group: ownerID as RawCoID,
},
meta: null,
uniqueness: unique,
};
const crypto =
as._type === "Anonymous" ? as.node.crypto : as._raw.core.node.crypto;
return cojsonInternals.idforHeader(header, crypto) as ID<L>;
}
/**
* Given some data, updates an existing CoList or initialises a new one if none exists.
*
* Note: This method respects resolve options, and thus can return `null` if the references cannot be resolved.
*
* @example
* ```ts
* const activeItems = await ItemList.upsertUnique(
* {
* value: [item1, item2, item3],
* unique: sourceData.identifier,
* owner: workspace,
* }
* );
* ```
*
* @param options The options for creating or loading the CoList. This includes the intended state of the CoList, its unique identifier, its owner, and the references to resolve.
* @returns Either an existing & modified CoList, or a new initialised CoList if none exists.
* @category Subscription & Loading
*/
static async upsertUnique<
L extends CoList,
const R extends RefsToResolve<L> = true,
>(
this: CoValueClass<L>,
options: {
value: L[number][];
unique: CoValueUniqueness["uniqueness"];
owner: Account | Group;
resolve?: RefsToResolveStrict<L, R>;
},
): Promise<Resolved<L, R> | null> {
let listId = CoList._findUnique(options.unique, options.owner.id);
let list: Resolved<L, R> | null = await loadCoValueWithoutMe(this, listId, {
...options,
loadAs: options.owner._loadedAs,
skipRetry: true,
});
if (!list) {
list = (this as any).create(options.value, {
owner: options.owner,
unique: options.unique,
}) as Resolved<L, R>;
} else {
(list as L).applyDiff(options.value);
}
return await loadCoValueWithoutMe(this, listId, {
...options,
loadAs: options.owner._loadedAs,
skipRetry: true,
});
}
/**
* Loads a CoList by its unique identifier and owner's ID.
* @param unique The unique identifier of the CoList to load.
* @param ownerID The ID of the owner of the CoList.
* @param options Additional options for loading the CoList.
* @returns The loaded CoList, or null if unavailable.
*/
static loadUnique<L extends CoList, const R extends RefsToResolve<L> = true>(
this: CoValueClass<L>,
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
options?: {
resolve?: RefsToResolveStrict<L, R>;
loadAs?: Account | AnonymousJazzAgent;
},
): Promise<Resolved<L, R> | null> {
return loadCoValueWithoutMe(
this,
CoList._findUnique(unique, ownerID, options?.loadAs),
{ ...options, skipRetry: true },
);
}
/**
* Wait for the `CoList` to be uploaded to the other peers.
*

View File

@@ -54,6 +54,7 @@ export {
SubscriptionScope,
exportCoValue,
importContentPieces,
Ref,
} from "./internal.js";
export {

View File

@@ -171,7 +171,9 @@ export function instantiateRefEncodedWithInit<V extends CoValue>(
`Cannot automatically create CoValue from value: ${JSON.stringify(init)}. Use the CoValue schema's create() method instead.`,
);
}
const owner = Group.create();
const node = parentOwner._raw.core.node;
const rawGroup = node.createGroup();
const owner = new Group({ fromRaw: rawGroup });
owner.addMember(parentOwner.castAs(Group));
// @ts-expect-error - create is a static method in all CoValue classes
return schema.ref.create(init, owner);

View File

@@ -2,12 +2,14 @@ import {
Account,
CoList,
Group,
ID,
RefsToResolve,
RefsToResolveStrict,
Resolved,
SubscribeListenerOptions,
coOptionalDefiner,
} from "../../../internal.js";
import { CoValueUniqueness } from "cojson";
import { AnonymousJazzAgent } from "../../anonymousJazzAgent.js";
import { CoListInit } from "../typeConverters/CoFieldInit.js";
import { InstanceOrPrimitiveOfSchema } from "../typeConverters/InstanceOrPrimitiveOfSchema.js";
@@ -29,7 +31,13 @@ export class CoListSchema<T extends AnyZodOrCoValueSchema>
create(
items: CoListInit<T>,
options?: { owner: Account | Group } | Account | Group,
options?:
| {
owner: Account | Group;
unique?: CoValueUniqueness["uniqueness"];
}
| Account
| Group,
): CoListInstance<T> {
return this.coValueClass.create(items as any, options) as CoListInstance<T>;
}
@@ -62,6 +70,41 @@ export class CoListSchema<T extends AnyZodOrCoValueSchema>
return this.coValueClass;
}
/** @deprecated Use `CoList.upsertUnique` and `CoList.loadUnique` instead. */
findUnique(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
): ID<CoListInstanceCoValuesNullable<T>> {
return this.coValueClass.findUnique(unique, ownerID, as);
}
upsertUnique<
const R extends RefsToResolve<CoListInstanceCoValuesNullable<T>> = true,
>(options: {
value: CoListInit<T>;
unique: CoValueUniqueness["uniqueness"];
owner: Account | Group;
resolve?: RefsToResolveStrict<CoListInstanceCoValuesNullable<T>, R>;
}): Promise<Resolved<CoListInstanceCoValuesNullable<T>, R> | null> {
// @ts-expect-error
return this.coValueClass.upsertUnique(options);
}
loadUnique<
const R extends RefsToResolve<CoListInstanceCoValuesNullable<T>> = true,
>(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
options?: {
resolve?: RefsToResolveStrict<CoListInstanceCoValuesNullable<T>, R>;
loadAs?: Account | AnonymousJazzAgent;
},
): Promise<Resolved<CoListInstanceCoValuesNullable<T>, R> | null> {
// @ts-expect-error
return this.coValueClass.loadUnique(unique, ownerID, options);
}
optional(): CoOptionalSchema<this> {
return coOptionalDefiner(this);
}

View File

@@ -72,12 +72,37 @@ export interface CoRecordSchema<
) => void,
): () => void;
/** @deprecated Use `CoMap.upsertUnique` and `CoMap.loadUnique` instead. */
findUnique(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
as?: Account | Group | AnonymousJazzAgent,
): ID<CoRecordInstanceCoValuesNullable<K, V>>;
upsertUnique<
const R extends RefsToResolve<
CoRecordInstanceCoValuesNullable<K, V>
> = true,
>(options: {
value: Simplify<CoRecordInit<K, V>>;
unique: CoValueUniqueness["uniqueness"];
owner: Account | Group;
resolve?: RefsToResolveStrict<CoRecordInstanceCoValuesNullable<K, V>, R>;
}): Promise<Resolved<CoRecordInstanceCoValuesNullable<K, V>, R> | null>;
loadUnique<
const R extends RefsToResolve<
CoRecordInstanceCoValuesNullable<K, V>
> = true,
>(
unique: CoValueUniqueness["uniqueness"],
ownerID: ID<Account> | ID<Group>,
options?: {
resolve?: RefsToResolveStrict<CoRecordInstanceCoValuesNullable<K, V>, R>;
loadAs?: Account | AnonymousJazzAgent;
},
): Promise<Resolved<CoRecordInstanceCoValuesNullable<K, V>, R> | null>;
getCoValueClass: () => typeof CoMap;
optional(): CoOptionalSchema<this>;

View File

@@ -154,6 +154,24 @@ export function setActiveAccount(account: Account) {
activeAccountContext.set(account);
}
/**
* Run a callback without an active account.
*
* Takes care of restoring the active account after the callback is run.
*
* @param callback - The callback to run.
* @returns The result of the callback.
*/
export function runWithoutActiveAccount<Result>(
callback: () => Result,
): Result {
const me = Account.getMe();
activeAccountContext.set(null);
const result = callback();
activeAccountContext.set(me);
return result;
}
export async function createJazzTestGuest() {
const ctx = await createAnonymousJazzContext({
crypto: await PureJSCrypto.create(),

View File

@@ -863,6 +863,173 @@ describe("CoList subscription", async () => {
});
});
describe("CoList unique methods", () => {
test("loadUnique returns existing list", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const originalList = ItemList.create(["item1", "item2", "item3"], {
owner: group,
unique: "test-list",
});
const foundList = await ItemList.loadUnique("test-list", group.id);
expect(foundList).toEqual(originalList);
expect(foundList?.length).toBe(3);
expect(foundList?.[0]).toBe("item1");
});
test("loadUnique returns null for non-existent list", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const foundList = await ItemList.loadUnique("non-existent", group.id);
expect(foundList).toBeNull();
});
test("upsertUnique creates new list when none exists", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const sourceData = ["item1", "item2", "item3"];
const result = await ItemList.upsertUnique({
value: sourceData,
unique: "new-list",
owner: group,
});
expect(result).not.toBeNull();
expect(result?.length).toBe(3);
expect(result?.[0]).toBe("item1");
expect(result?.[1]).toBe("item2");
expect(result?.[2]).toBe("item3");
});
test("upsertUnique updates existing list", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
// Create initial list
const originalList = ItemList.create(["original1", "original2"], {
owner: group,
unique: "update-list",
});
// Upsert with new data
const updatedList = await ItemList.upsertUnique({
value: ["updated1", "updated2", "updated3"],
unique: "update-list",
owner: group,
});
expect(updatedList).toEqual(originalList); // Should be the same instance
expect(updatedList?.length).toBe(3);
expect(updatedList?.[0]).toBe("updated1");
expect(updatedList?.[1]).toBe("updated2");
expect(updatedList?.[2]).toBe("updated3");
});
test("upsertUnique with CoValue items", async () => {
const Item = co.map({
name: z.string(),
value: z.number(),
});
const ItemList = co.list(Item);
const group = Group.create();
const items = [
Item.create({ name: "First", value: 1 }, group),
Item.create({ name: "Second", value: 2 }, group),
];
const result = await ItemList.upsertUnique({
value: items,
unique: "item-list",
owner: group,
resolve: { $each: true },
});
expect(result).not.toBeNull();
expect(result?.length).toBe(2);
expect(result?.[0]?.name).toBe("First");
expect(result?.[1]?.name).toBe("Second");
});
test("upsertUnique updates list with CoValue items", async () => {
const Item = co.map({
name: z.string(),
value: z.number(),
});
const ItemList = co.list(Item);
const group = Group.create();
// Create initial list
const initialItems = [Item.create({ name: "Initial", value: 0 }, group)];
const originalList = ItemList.create(initialItems, {
owner: group,
unique: "updateable-item-list",
});
// Upsert with new items
const newItems = [
Item.create({ name: "Updated", value: 1 }, group),
Item.create({ name: "Added", value: 2 }, group),
];
const updatedList = await ItemList.upsertUnique({
value: newItems,
unique: "updateable-item-list",
owner: group,
resolve: { $each: true },
});
expect(updatedList).toEqual(originalList); // Should be the same instance
expect(updatedList?.length).toBe(2);
expect(updatedList?.[0]?.name).toBe("Updated");
expect(updatedList?.[1]?.name).toBe("Added");
});
test("findUnique returns correct ID", async () => {
const ItemList = co.list(z.string());
const group = Group.create();
const originalList = ItemList.create(["test"], {
owner: group,
unique: "find-test",
});
const foundId = ItemList.findUnique("find-test", group.id);
expect(foundId).toBe(originalList.id);
});
test("upsertUnique with resolve options", async () => {
const Category = co.map({ title: z.string() });
const Item = co.map({
name: z.string(),
category: Category,
});
const ItemList = co.list(Item);
const group = Group.create();
const category = Category.create({ title: "Category 1" }, group);
const items = [Item.create({ name: "Item 1", category }, group)];
const result = await ItemList.upsertUnique({
value: items,
unique: "resolved-list",
owner: group,
resolve: { $each: { category: true } },
});
expect(result).not.toBeNull();
expect(result?.length).toBe(1);
expect(result?.[0]?.name).toBe("Item 1");
expect(result?.[0]?.category?.title).toBe("Category 1");
});
});
describe("co.list schema", () => {
test("can access the inner schema of a co.list", () => {
const Keywords = co.list(co.plainText());

View File

@@ -460,3 +460,107 @@ describe("CoMap.Record", async () => {
}
});
});
describe("CoRecord unique methods", () => {
test("loadUnique returns existing record", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
const originalRecord = ItemRecord.create(
{ item1: 1, item2: 2, item3: 3 },
{ owner: group, unique: "test-record" },
);
const foundRecord = await ItemRecord.loadUnique("test-record", group.id);
expect(foundRecord).toEqual(originalRecord);
expect(foundRecord?.item1).toBe(1);
expect(foundRecord?.item2).toBe(2);
});
test("loadUnique returns null for non-existent record", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
const foundRecord = await ItemRecord.loadUnique("non-existent", group.id);
expect(foundRecord).toBeNull();
});
test("upsertUnique creates new record when none exists", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
const sourceData = { item1: 1, item2: 2, item3: 3 };
const result = await ItemRecord.upsertUnique({
value: sourceData,
unique: "new-record",
owner: group,
});
expect(result).not.toBeNull();
expect(result?.item1).toBe(1);
expect(result?.item2).toBe(2);
expect(result?.item3).toBe(3);
});
test("upsertUnique updates existing record", async () => {
const ItemRecord = co.record(z.string(), z.number());
const group = Group.create();
// Create initial record
const originalRecord = ItemRecord.create(
{ original1: 1, original2: 2 },
{ owner: group, unique: "update-record" },
);
// Upsert with new data
const updatedRecord = await ItemRecord.upsertUnique({
value: { updated1: 10, updated2: 20, updated3: 30 },
unique: "update-record",
owner: group,
});
expect(updatedRecord).toEqual(originalRecord); // Should be the same instance
expect(updatedRecord?.updated1).toBe(10);
expect(updatedRecord?.updated2).toBe(20);
expect(updatedRecord?.updated3).toBe(30);
});
test("upsertUnique with CoValue items", async () => {
const Item = co.map({
name: z.string(),
value: z.number(),
});
const ItemRecord = co.record(z.string(), Item);
const group = Group.create();
const items = {
first: Item.create({ name: "First", value: 1 }, group),
second: Item.create({ name: "Second", value: 2 }, group),
};
const result = await ItemRecord.upsertUnique({
value: items,
unique: "item-record",
owner: group,
resolve: { first: true, second: true },
});
expect(result).not.toBeNull();
expect(result?.first?.name).toBe("First");
expect(result?.second?.name).toBe("Second");
});
test("findUnique returns correct ID", async () => {
const ItemRecord = co.record(z.string(), z.string());
const group = Group.create();
const originalRecord = ItemRecord.create(
{ test: "value" },
{ owner: group, unique: "find-test" },
);
const foundId = ItemRecord.findUnique("find-test", group.id);
expect(foundId).toBe(originalRecord.id);
});
});

View File

@@ -12,10 +12,15 @@ import {
} from "vitest";
import { Group, co, subscribeToCoValue, z } from "../exports.js";
import { Account } from "../index.js";
import { Loaded, coValueClassFromCoValueClassOrSchema } from "../internal.js";
import {
Loaded,
activeAccountContext,
coValueClassFromCoValueClassOrSchema,
} from "../internal.js";
import {
createJazzTestAccount,
getPeerConnectedToTestSyncServer,
runWithoutActiveAccount,
setupJazzTestSync,
} from "../testing.js";
import { setupTwoNodes, waitFor } from "./utils.js";
@@ -214,6 +219,19 @@ describe("CoMap", async () => {
const map = Schema.create({ text: "" });
expect(map.text.toString()).toBe("");
});
it("creates a group for the new CoValue when there is no active account", () => {
const Schema = co.map({ text: co.plainText() });
const parentGroup = Group.create();
runWithoutActiveAccount(() => {
const map = Schema.create({ text: "Hello" }, parentGroup);
expect(
map.text._owner.getParentGroups().map((group: Group) => group.id),
).toContain(parentGroup.id);
});
});
});
test("CoMap with self reference", () => {

View File

@@ -31,14 +31,14 @@
},
"devDependencies": {
"next": "15.4.2",
"react": "catalog:react",
"react-dom": "catalog:react",
"react": "19.1.0",
"react-dom": "19.1.0",
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^22.16.5",
"@types/react": "catalog:react",
"@types/react-dom": "catalog:react",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"lucide-react": "^0.525.0",
"tailwindcss": "^4.1.11",
"typescript": "catalog:default"
"typescript": "5.6.2"
}
}

280
pnpm-lock.yaml generated
View File

@@ -6,77 +6,9 @@ settings:
catalogs:
default:
'@biomejs/biome':
specifier: 2.1.3
version: 2.1.3
'@vitest/browser':
specifier: 3.2.4
version: 3.2.4
'@vitest/coverage-istanbul':
specifier: 3.2.4
version: 3.2.4
'@vitest/coverage-v8':
specifier: 3.2.4
version: 3.2.4
'@vitest/ui':
specifier: 3.2.4
version: 3.2.4
typescript:
specifier: 5.6.2
version: 5.6.2
vitest:
specifier: 3.2.4
version: 3.2.4
expo:
expo:
specifier: 54.0.0-canary-20250701-6a945c5
version: 54.0.0-canary-20250701-6a945c5
expo-clipboard:
specifier: ^7.1.4
version: 7.1.4
expo-crypto:
specifier: ~14.1.5
version: 14.1.5
expo-linking:
specifier: ~7.1.5
version: 7.1.5
expo-secure-store:
specifier: ~14.2.3
version: 14.2.3
expo-sqlite:
specifier: ~15.2.10
version: 15.2.10
expo-web-browser:
specifier: ~14.2.0
version: 14.2.0
react-native:
specifier: 0.80.0
version: 0.80.0
rn:
'@react-native-community/cli':
specifier: 19.0.0
version: 19.0.0
'@react-native-community/cli-platform-android':
specifier: 19.0.0
version: 19.0.0
'@react-native-community/cli-platform-ios':
specifier: 19.0.0
version: 19.0.0
'@react-native/babel-preset':
specifier: 0.80.0
version: 0.80.0
'@react-native/eslint-config':
specifier: 0.80.0
version: 0.80.0
'@react-native/metro-config':
specifier: 0.80.0
version: 0.80.0
'@react-native/typescript-config':
specifier: 0.80.0
version: 0.80.0
react-native:
specifier: 0.80.0
version: 0.80.0
overrides:
'@types/react': 19.1.0
@@ -91,7 +23,7 @@ importers:
.:
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@changesets/cli':
specifier: ^2.27.10
@@ -103,16 +35,16 @@ importers:
specifier: ^4.5.1
version: 4.5.1(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1))
'@vitest/browser':
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(playwright@1.50.1)(vite@6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1))(vitest@3.2.4)(webdriverio@8.41.0)
'@vitest/coverage-istanbul':
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(vitest@3.2.4)
'@vitest/coverage-v8':
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)
'@vitest/ui':
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(vitest@3.2.4)
happy-dom:
specifier: ^17.4.4
@@ -139,7 +71,7 @@ importers:
specifier: ^0.25.13
version: 0.25.13(typescript@5.8.3)
vitest:
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.5)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
examples/betterauth:
@@ -200,7 +132,7 @@ importers:
version: 1.2.8
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -227,7 +159,7 @@ importers:
specifier: ^4
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
examples/chat:
@@ -279,7 +211,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -315,7 +247,7 @@ importers:
specifier: 19.1.0
version: 19.1.0
react-native:
specifier: catalog:rn
specifier: 0.80.0
version: 0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.6.2))(@types/react@19.1.0)(react@19.1.0)
react-native-get-random-values:
specifier: ^1.11.0
@@ -346,25 +278,25 @@ importers:
specifier: ^7.25.0
version: 7.26.0
'@react-native-community/cli':
specifier: catalog:rn
specifier: 19.0.0
version: 19.0.0(typescript@5.6.2)
'@react-native-community/cli-platform-android':
specifier: catalog:rn
specifier: 19.0.0
version: 19.0.0
'@react-native-community/cli-platform-ios':
specifier: catalog:rn
specifier: 19.0.0
version: 19.0.0
'@react-native/babel-preset':
specifier: catalog:rn
specifier: 0.80.0
version: 0.80.0(@babel/core@7.27.1)
'@react-native/eslint-config':
specifier: catalog:rn
specifier: 0.80.0
version: 0.80.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.17))(@types/node@22.16.5)(typescript@5.6.2)))(prettier@2.8.8)(typescript@5.6.2)
'@react-native/metro-config':
specifier: catalog:rn
specifier: 0.80.0
version: 0.80.0(@babel/core@7.27.1)
'@react-native/typescript-config':
specifier: catalog:rn
specifier: 0.80.0
version: 0.80.0
'@rnx-kit/metro-config':
specifier: ^2.0.1
@@ -403,16 +335,16 @@ importers:
specifier: 11.4.1
version: 11.4.1(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
expo:
specifier: catalog:expo
specifier: 54.0.0-canary-20250701-6a945c5
version: 54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
expo-clipboard:
specifier: catalog:expo
specifier: ^7.1.4
version: 7.1.4(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
expo-secure-store:
specifier: catalog:expo
specifier: ~14.2.3
version: 14.2.3(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))
expo-sqlite:
specifier: catalog:expo
specifier: ~15.2.10
version: 15.2.10(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
jazz-tools:
specifier: workspace:*
@@ -421,7 +353,7 @@ importers:
specifier: 19.1.0
version: 19.1.0
react-native:
specifier: catalog:expo
specifier: 0.80.0
version: 0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
react-native-get-random-values:
specifier: ^1.11.0
@@ -514,7 +446,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -532,7 +464,7 @@ importers:
specifier: ^15.11.0
version: 15.15.0
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -556,22 +488,22 @@ importers:
specifier: 11.4.1
version: 11.4.1(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
expo:
specifier: catalog:expo
specifier: 54.0.0-canary-20250701-6a945c5
version: 54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0)
expo-crypto:
specifier: catalog:expo
specifier: ~14.1.5
version: 14.1.5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))
expo-linking:
specifier: catalog:expo
specifier: ~7.1.5
version: 7.1.5(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
expo-secure-store:
specifier: catalog:expo
specifier: ~14.2.3
version: 14.2.3(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))
expo-sqlite:
specifier: catalog:expo
specifier: ~15.2.10
version: 15.2.10(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react@19.1.0)
expo-web-browser:
specifier: catalog:expo
specifier: ~14.2.0
version: 14.2.0(expo@54.0.0-canary-20250701-6a945c5(@babel/core@7.27.1)(graphql@16.11.0)(metro-runtime@0.82.3)(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))(react-refresh@0.17.0)(react@19.1.0))(react-native@0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0))
jazz-tools:
specifier: workspace:*
@@ -580,7 +512,7 @@ importers:
specifier: 19.1.0
version: 19.1.0
react-native:
specifier: catalog:expo
specifier: 0.80.0
version: 0.80.0(@babel/core@7.27.1)(@react-native-community/cli@19.0.0(typescript@5.8.3))(@types/react@19.1.0)(react@19.1.0)
react-native-get-random-values:
specifier: ^1.11.0
@@ -847,7 +779,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@tailwindcss/postcss':
specifier: ^4.1.10
@@ -874,7 +806,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -896,7 +828,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -929,7 +861,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -948,7 +880,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@tailwindcss/postcss':
specifier: ^4.1.10
@@ -972,7 +904,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1027,7 +959,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1086,7 +1018,7 @@ importers:
version: 3.25.76
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@tailwindcss/postcss':
specifier: ^4.1.10
@@ -1113,13 +1045,13 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
version: 6.3.5(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
vitest:
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.5)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.4(@types/node@22.16.5)(typescript@5.6.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
examples/multiauth:
@@ -1141,7 +1073,7 @@ importers:
version: 4.1.10
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@types/react':
specifier: 19.1.0
@@ -1156,7 +1088,7 @@ importers:
specifier: ^15.11.0
version: 15.15.0
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1235,7 +1167,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1266,7 +1198,7 @@ importers:
version: 6.28.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -1296,7 +1228,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1318,7 +1250,7 @@ importers:
version: 4.1.10
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@types/react':
specifier: 19.1.0
@@ -1333,7 +1265,7 @@ importers:
specifier: ^15.11.0
version: 15.15.0
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1358,7 +1290,7 @@ importers:
version: 4.1.10
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@types/react':
specifier: 19.1.0
@@ -1373,7 +1305,7 @@ importers:
specifier: ^15.11.0
version: 15.15.0
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1453,7 +1385,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -1483,7 +1415,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1529,7 +1461,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -1559,7 +1491,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1705,7 +1637,7 @@ importers:
specifier: ^4.19.3
version: 4.19.3
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1781,7 +1713,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1803,7 +1735,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@tailwindcss/postcss':
specifier: ^4.1.10
@@ -1824,7 +1756,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -1864,7 +1796,7 @@ importers:
specifier: ^0.5.13
version: 0.5.13
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
packages/cojson-storage-indexeddb:
@@ -1874,7 +1806,7 @@ importers:
version: link:../cojson
devDependencies:
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
webdriverio:
specifier: ^8.15.0
@@ -1893,7 +1825,7 @@ importers:
specifier: ^7.6.12
version: 7.6.12
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
packages/cojson-transport-ws:
@@ -1912,7 +1844,7 @@ importers:
specifier: 8.5.10
version: 8.5.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
ws:
specifier: ^8.14.2
@@ -1986,10 +1918,10 @@ importers:
specifier: ^9.0.3
version: 9.0.7
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vitest:
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.5)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.4(@types/node@22.16.5)(typescript@5.6.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
packages/cursor-docs: {}
@@ -2003,7 +1935,7 @@ importers:
specifier: 19.1.0
version: 19.1.0
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
packages/jazz-auth-betterauth:
@@ -2019,7 +1951,7 @@ importers:
version: link:../jazz-tools
devDependencies:
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
packages/jazz-betterauth-client-plugin:
@@ -2082,7 +2014,7 @@ importers:
specifier: 19.1.0
version: 19.1.0
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
packages/jazz-run:
@@ -2106,19 +2038,19 @@ importers:
specifier: ^0.25.5
version: 0.25.8(effect@3.11.9)
cojson:
specifier: workspace:0.17.3
specifier: workspace:0.17.7
version: link:../cojson
cojson-storage-sqlite:
specifier: workspace:0.17.3
specifier: workspace:0.17.7
version: link:../cojson-storage-sqlite
cojson-transport-ws:
specifier: workspace:0.17.3
specifier: workspace:0.17.7
version: link:../cojson-transport-ws
effect:
specifier: ^3.6.5
version: 3.11.9
jazz-tools:
specifier: workspace:0.17.3
specifier: workspace:0.17.7
version: link:../jazz-tools
ws:
specifier: ^8.14.2
@@ -2128,7 +2060,7 @@ importers:
specifier: 8.5.10
version: 8.5.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
packages/jazz-tools:
@@ -2267,10 +2199,10 @@ importers:
specifier: 8.5.0
version: 8.5.0(@microsoft/api-extractor@7.52.10(@types/node@22.16.5))(@swc/core@1.11.29(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.6.2)(yaml@2.6.1)
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vitest:
specifier: catalog:default
specifier: 3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.5)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.3(@types/node@22.16.5)(typescript@5.6.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.6.1)
ws:
specifier: ^8.14.2
@@ -2322,7 +2254,7 @@ importers:
specifier: ^4.1.11
version: 4.1.11
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
starters/react-passkey-auth:
@@ -2338,7 +2270,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
'@biomejs/biome':
specifier: catalog:default
specifier: 2.1.3
version: 2.1.3
'@playwright/test':
specifier: ^1.50.1
@@ -2368,7 +2300,7 @@ importers:
specifier: ^4.1.10
version: 4.1.10
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -2475,7 +2407,7 @@ importers:
version: 19.1.0(react@19.1.0)
devDependencies:
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
tests/cloudflare-workers:
@@ -2500,7 +2432,7 @@ importers:
specifier: ^9.5.2
version: 9.5.2
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
wrangler:
specifier: ^3.109.2
@@ -2555,7 +2487,7 @@ importers:
specifier: ^1.9.6
version: 1.9.6
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
vite:
specifier: 6.3.5
@@ -2601,7 +2533,7 @@ importers:
specifier: ^4.0.0
version: 4.2.1(picomatch@4.0.2)(svelte@5.34.6)(typescript@5.6.2)
typescript:
specifier: catalog:default
specifier: 5.6.2
version: 5.6.2
virtua:
specifier: ^0.41.5
@@ -14819,7 +14751,7 @@ snapshots:
'@babel/generator@7.28.0':
dependencies:
'@babel/parser': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
jsesc: 3.1.0
@@ -14830,7 +14762,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/helper-compilation-targets@7.27.2':
dependencies:
@@ -14933,14 +14865,14 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.25.9':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
'@babel/helper-member-expression-to-functions@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -14989,11 +14921,11 @@ snapshots:
'@babel/helper-optimise-call-expression@7.25.9':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/helper-plugin-utils@7.27.1': {}
@@ -15061,7 +14993,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -15075,7 +15007,7 @@ snapshots:
dependencies:
'@babel/template': 7.27.2
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -15087,7 +15019,7 @@ snapshots:
'@babel/helpers@7.27.6':
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/highlight@7.25.9':
dependencies:
@@ -15102,7 +15034,7 @@ snapshots:
'@babel/parser@7.28.0':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.27.1)':
dependencies:
@@ -16039,7 +15971,7 @@ snapshots:
'@babel/helper-module-imports': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -16050,7 +15982,7 @@ snapshots:
'@babel/helper-module-imports': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0)
'@babel/types': 7.28.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@@ -17546,7 +17478,7 @@ snapshots:
'@babel/core': 7.28.0
'@babel/generator': 7.28.0
'@babel/parser': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@expo/config': 11.0.12-canary-20250701-6a945c5
'@expo/env': 1.0.7-canary-20250701-6a945c5
'@expo/json-file': 9.1.5-canary-20250701-6a945c5
@@ -20993,16 +20925,16 @@ snapshots:
'@types/babel__generator@7.6.8':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.27.2
'@babel/types': 7.28.0
'@babel/parser': 7.28.0
'@babel/types': 7.28.2
'@types/babel__traverse@7.20.6':
dependencies:
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@types/better-sqlite3@7.6.12':
dependencies:
@@ -22148,7 +22080,7 @@ snapshots:
array-buffer-byte-length: 1.0.2
call-bind: 1.0.8
define-properties: 1.2.1
es-abstract: 1.23.9
es-abstract: 1.24.0
es-errors: 1.3.0
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
@@ -22253,7 +22185,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.28.0
'@babel/types': 7.28.2
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.20.6
@@ -25883,7 +25815,7 @@ snapshots:
magicast@0.3.5:
dependencies:
'@babel/parser': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
source-map-js: 1.2.1
make-dir@4.0.0:
@@ -26013,7 +25945,7 @@ snapshots:
dependencies:
'@babel/traverse': 7.28.0
'@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.0'
'@babel/types': 7.28.0
'@babel/types': 7.28.2
flow-enums-runtime: 0.0.6
invariant: 2.2.4
metro-symbolicate: 0.82.3
@@ -26051,7 +25983,7 @@ snapshots:
'@babel/core': 7.28.0
'@babel/generator': 7.28.0
'@babel/parser': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
flow-enums-runtime: 0.0.6
metro: 0.82.3
metro-babel-transformer: 0.82.3
@@ -26074,7 +26006,7 @@ snapshots:
'@babel/parser': 7.28.0
'@babel/template': 7.27.2
'@babel/traverse': 7.28.0
'@babel/types': 7.28.0
'@babel/types': 7.28.2
accepts: 1.3.8
chalk: 4.1.2
ci-info: 2.0.0
@@ -28639,7 +28571,7 @@ snapshots:
call-bound: 1.0.4
define-data-property: 1.1.4
define-properties: 1.2.1
es-abstract: 1.23.9
es-abstract: 1.24.0
es-object-atoms: 1.1.1
has-property-descriptors: 1.0.2
@@ -29881,7 +29813,7 @@ snapshots:
deepmerge-ts: 5.1.0
got: 12.6.1
ky: 0.33.3
ws: 8.18.1
ws: 8.18.2
transitivePeerDependencies:
- bufferutil
- supports-color

Some files were not shown because too many files have changed in this diff Show More