Compare commits

..

102 Commits

Author SHA1 Message Date
Guido D'Orsi
867cb6b7a5 Merge pull request #2659 from garden-co/changeset-release/main
Version Packages
2025-07-23 18:30:51 +02:00
github-actions[bot]
0401fcf2a8 Version Packages 2025-07-23 12:43:56 +00:00
Nico Rainhart
139a649279 Merge pull request #2663 from garden-co/fix/export-WithHelpers-type
fix: Export `WithHelpers` type
2025-07-23 09:41:20 -03:00
NicoR
9acccb5df2 Add changeset 2025-07-23 09:23:12 -03:00
NicoR
fd90cdb49a fix: Export WithHelpers type 2025-07-23 09:15:09 -03:00
Giordano Ricci
df487d5335 Merge pull request #2658 from garden-co/gio/quint 2025-07-23 12:35:06 +01:00
Giordano Ricci
1efe84c691 Merge branch 'main' into gio/quint 2025-07-23 12:17:02 +01:00
Guido D'Orsi
063553090e docs: fix type errors on co.profile 2025-07-22 18:29:18 +02:00
Guido D'Orsi
6dffe73bd2 chore: rename jazz-paper-scissors in server-worker-inbox 2025-07-22 16:32:08 +02:00
Guido D'Orsi
68cb357a94 Merge pull request #2660 from garden-co/feat/persistent-peers
feat: introduce persistent peers
2025-07-22 16:18:56 +02:00
Guido D'Orsi
4f7bc91502 chore: apply suggestions from code review
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-07-22 14:40:45 +02:00
Giordano Ricci
f61a120560 changeset 2025-07-22 12:22:22 +01:00
Giordano Ricci
2f1307a0ba rename package 2025-07-22 12:21:19 +01:00
Giordano Ricci
fa15ea56d1 build & dev workflows 2025-07-22 12:03:21 +01:00
Guido D'Orsi
1e58ecb3ac test: integration tests for browser and workers on offline loading & sync 2025-07-22 12:42:49 +02:00
Giordano Ricci
ceeabfaf89 strip down PR 2025-07-22 11:07:52 +01:00
Guido D'Orsi
70ce7c5736 feat: introduce persistent peers 2025-07-22 11:12:48 +02:00
Nico Rainhart
6afdb16739 Merge pull request #2656 from garden-co/feat/prevent-resolving-discriminated-union-fields
feat: Prevent resolving discriminated union fields
2025-07-21 14:45:51 -03:00
Giordano Ricci
b0b2b85a6f fix lint issues 2025-07-21 18:13:18 +01:00
Giordano Ricci
28c19c134f Merge branch 'main' into gio/quint 2025-07-21 18:09:42 +01:00
Giordano Ricci
0924c9baaa revert changes to example app 2025-07-21 18:04:17 +01:00
Giordano Ricci
b2712e18a2 fix imports 2025-07-21 17:46:15 +01:00
Giordano Ricci
66894b63d7 more cleanup 2025-07-21 17:43:11 +01:00
Giordano Ricci
b1a05143e3 cleanup 2025-07-21 15:26:49 +01:00
Giordano Ricci
fb761ce66d cleanup 2025-07-21 15:25:09 +01:00
Giordano Ricci
07a6c340dc some cleanup 2025-07-21 15:23:02 +01:00
Giordano Ricci
0fea904dd0 tailwind class source 2025-07-21 14:04:39 +01:00
Giordano Ricci
373aef313f wip: quint 2025-07-21 13:55:21 +01:00
NicoR
a584590ed8 Add changeset 2025-07-18 18:00:50 -03:00
NicoR
0a830e29a9 Prevent resolving discriminated union fields 2025-07-18 17:57:07 -03:00
Guido D'Orsi
efff4d0f4f Merge pull request #2654 from garden-co/fix/vitest-type-tests
fix: restore type tests on Vitest and upgrade Vitest to v3.2.4
2025-07-18 14:13:01 +02:00
Guido D'Orsi
ea2b01d8a2 fix: restore type tests on Vitest and upgrade Vitest to v3.2.4 2025-07-18 12:51:02 +02:00
Guido D'Orsi
55cb83e6e0 Merge pull request #2652 from garden-co/changeset-release/main
Version Packages
2025-07-17 15:26:20 +02:00
github-actions[bot]
6290088fec Version Packages 2025-07-17 13:06:49 +00:00
Guido D'Orsi
b9c17b37db Merge pull request #2651 from garden-co/fix/optional-load
fix: load failures when loading a missing ref declared with z.optional and Schema.optional
2025-07-17 15:04:36 +02:00
Guido D'Orsi
6c76ff8fbf fix: load of missing z.optional and Schema.optional doesn't fail 2025-07-17 14:54:33 +02:00
Guido D'Orsi
3c6a2a6092 Merge pull request #2649 from garden-co/changeset-release/main
Version Packages
2025-07-16 13:16:14 +02:00
github-actions[bot]
e8a950e61a Version Packages 2025-07-16 10:15:20 +00:00
Guido D'Orsi
e2cbf035de Merge pull request #2648 from garden-co/fix/onAnonymous-stuck
fix: fix stuck authentication when using onAnonymousAccountDiscarded with a storage
2025-07-16 12:13:11 +02:00
Matteo Manchi
47599b6307 chore(cojson): new removeStorage method exposed on LocalNode 2025-07-16 11:19:35 +02:00
Guido D'Orsi
901d0762ee Merge pull request #2647 from garden-co/fix/GCO-646-uncaught-from-load-promise
fix: catch errors from CoValue loading and treat it as "unavailable"
2025-07-16 09:18:43 +02:00
Guido D'Orsi
d1c1b0c5cc fix: fix stuck authentication when using onAnonymousAccountDiscarded with a storage 2025-07-15 22:17:51 +02:00
Matteo Manchi
cf4ad7285d fix(jazz-tools/tools): catch errors from CoValue loading and treat it as "unavailable" 2025-07-15 21:12:21 +02:00
Guido D'Orsi
2983c7bd58 fix: ignore builds for server-side-validation 2025-07-15 18:41:12 +02:00
Nico Rainhart
ab6328f767 Merge pull request #2645 from garden-co/changeset-release/main
Version Packages
2025-07-15 12:23:00 -03:00
github-actions[bot]
e0555debde Version Packages 2025-07-15 15:12:46 +00:00
Guido D'Orsi
247f4556e7 change the changeset to a patch 2025-07-15 17:10:44 +02:00
Nico Rainhart
7903c737f4 Merge pull request #2627 from garden-co/feat/co-optional-and-discriminatedUnion
feat: add `co.optional` and `co.discriminatedUnion`
2025-07-15 12:01:47 -03:00
NicoR
6145da5525 Update changeset 2025-07-15 11:50:55 -03:00
Nico Rainhart
fc0a2e77a3 Merge pull request #2629 from garden-co/feat/improve-zod-functions-type-safety
feat: prevent using Zod functions with CoValue schemas
2025-07-15 09:21:38 -03:00
NicoR
334fbbbb7f Comment back z.record and z.intersection until we support them 2025-07-15 09:12:18 -03:00
NicoR
eaac1e6580 Keep z.optional and z.discriminatedUnion compatible with covalues 2025-07-14 22:44:02 -03:00
NicoR
cbc3f0cc65 Improve changeset message 2025-07-14 15:25:04 -03:00
NicoR
29c487e288 Remove new usages of zodSchemaToCoSchema 2025-07-14 15:13:51 -03:00
NicoR
0b0590a364 Merge remote-tracking branch 'origin/main' into feat/co-optional-and-discriminatedUnion 2025-07-14 15:11:31 -03:00
NicoR
1eb01997d8 Remove unused imports 2025-07-14 14:57:42 -03:00
Nico Rainhart
0dc8d511a1 Merge pull request #2636 from garden-co/feat/zod-jazz-schema-boundary
refactor: consolidate boundary between Zod schemas, CoValue schemas & CoValue classes
2025-07-14 14:43:36 -03:00
Guido D'Orsi
2b043abffa Merge pull request #2637 from garden-co/feat/discriminated-union-load-subscribe
feat: add load & subscribe to Discriminated union schemas
2025-07-14 10:53:33 +02:00
NicoR
e136e1b696 Remove unnecessary comment 2025-07-11 16:01:38 -03:00
NicoR
2475a46578 Remove resolve type for when loading/subscribing to discriminated unions 2025-07-11 15:45:23 -03:00
NicoR
44f653a64b Format and reorder imports 2025-07-11 14:00:55 -03:00
NicoR
f8437042a6 Add CoDiscriminatedUnionSchema.subscribe 2025-07-11 13:38:08 -03:00
NicoR
db23582b4c Fix "Expression produces a union type that is too complex to represent" error 2025-07-11 12:27:31 -03:00
NicoR
4b0b6d8a69 Use Add CoDiscriminatedUnionSchema.load in existing tests 2025-07-11 12:21:31 -03:00
NicoR
d450b394fa Tighten CoDiscriminatedUnionSchema type 2025-07-11 12:12:56 -03:00
NicoR
0abc96e400 Add CoDiscriminatedUnionSchema.load 2025-07-11 11:17:44 -03:00
NicoR
7562354b29 Fix e2e test error 2025-07-10 21:44:54 -03:00
NicoR
6c085a3919 Fix imports 2025-07-10 17:29:33 -03:00
NicoR
6afff848bc Clean up CoValue schema definitions 2025-07-10 16:49:19 -03:00
NicoR
47059845cc Make co.discriminatedUnion tests type-tests 2025-07-10 16:09:41 -03:00
NicoR
a1735a8232 Update tests that expected CoValue class instead of schema 2025-07-10 16:00:56 -03:00
NicoR
1f5750d8c4 Support nesting CoValue classes inside CoValue schemas 2025-07-10 15:24:57 -03:00
NicoR
f756ce26b5 Fix CoListSchema 2025-07-10 14:46:22 -03:00
NicoR
84f5bdda74 Rename AnyCoSchema.getCoSchema to getCoValueClass 2025-07-10 14:41:54 -03:00
NicoR
ee7aefa97c Tighten Account schema type when inferring from Zod schema 2025-07-10 14:32:27 -03:00
NicoR
b0895981ba Add CoDiscriminatedUnionSchema 2025-07-10 14:31:58 -03:00
NicoR
94f636b2ee Fix CoOptionalSchema 2025-07-10 14:05:30 -03:00
NicoR
331ab070f6 Fix CoMapSchema.catchall 2025-07-10 13:38:42 -03:00
NicoR
13e73adfb9 Refactor zodSchemaToCoSchema to return CoValue schema instead of class 2025-07-10 12:38:23 -03:00
NicoR
f1552b8262 [WIP] FieldSchema refactor 2025-07-09 17:20:51 -03:00
NicoR
6826ad8e45 Add CoOptionalSchema 2025-07-09 17:09:26 -03:00
NicoR
7c1b757b62 Reuse DefaultProfileShape wherever possible 2025-07-09 17:09:26 -03:00
NicoR
326e1734a4 Fix import order 2025-07-09 17:09:10 -03:00
NicoR
0cf027c91b Revert changes on z.object and z.strictObject 2025-07-09 17:09:10 -03:00
NicoR
e358881b76 Prevent using z.record with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
cee8010918 Prevent using z.intersection with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
cc877139ef Prevent using z.strictObject with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
56a9b89538 Prevent using z.tuple with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
199c463e28 Prevent using z.array with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
50e523d19c Prevent using z.union with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
796ea24288 Prevent using z.discriminatedUnion with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
c8be86e823 Add tests 2025-07-09 17:09:10 -03:00
NicoR
41b7054aab Prevent using z.optional and z.object with CoValue schemas 2025-07-09 17:09:10 -03:00
NicoR
101adcd024 More updates to docs and examples 2025-07-09 17:08:59 -03:00
NicoR
a2854aeec9 Fix import order 2025-07-09 16:24:45 -03:00
NicoR
bdc9aee689 Add changeset 2025-07-09 16:11:55 -03:00
NicoR
2f53ae0ab8 Update docs 2025-07-09 16:09:03 -03:00
NicoR
f76c05448c Update examples 2025-07-09 16:08:54 -03:00
NicoR
585e7e8177 Add tests for co.optional and co.discriminatedUnion 2025-07-09 15:30:05 -03:00
NicoR
82d8d1d873 Add co.discriminatedUnion 2025-07-09 15:30:05 -03:00
NicoR
2c523c86ff Replace usages of z.optional with CoSchemas with a new co.optional 2025-07-09 15:30:05 -03:00
NicoR
6616668d4a Add pnpm check command to jazz-tools 2025-07-09 15:30:05 -03:00
159 changed files with 4379 additions and 1444 deletions

View File

@@ -14,7 +14,7 @@
"tests/jazz-svelte/src/**",
"examples/*svelte*/**",
"starters/*svelte*/**",
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
"examples/server-worker-inbox/src/routeTree.gen.ts",
"homepage/homepage/**",
"**/package.json"
]

View File

@@ -1,5 +1,35 @@
# passkey-svelte
## 0.0.103
### Patch Changes
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- jazz-tools@0.15.14
## 0.0.102
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
## 0.0.101
### Patch Changes
- Updated dependencies [d1c1b0c]
- Updated dependencies [cf4ad72]
- jazz-tools@0.15.12
## 0.0.100
### Patch Changes
- Updated dependencies [bdc9aee]
- jazz-tools@0.15.11
## 0.0.99
### Patch Changes

View File

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

View File

@@ -1,8 +1,8 @@
import { co, z } from "jazz-tools";
import { co } from "jazz-tools";
export const Message = co.map({
text: co.plainText(),
image: z.optional(co.image()),
image: co.optional(co.image()),
});
export type Message = co.loaded<typeof Message>;

View File

@@ -1,7 +1,7 @@
import { co, z } from "jazz-tools";
import { co } from "jazz-tools";
export const JazzProfile = co.profile({
file: z.optional(co.fileStream()),
file: co.optional(co.fileStream()),
});
export const JazzAccount = co.account({

View File

@@ -28,16 +28,16 @@ export const BubbleTeaOrder = co.map({
addOns: ListOfBubbleTeaAddOns,
deliveryDate: z.date(),
withMilk: z.boolean(),
instructions: z.optional(co.plainText()),
instructions: co.optional(co.plainText()),
});
export const DraftBubbleTeaOrder = co
.map({
baseTea: z.optional(z.literal([...BubbleTeaBaseTeaTypes])),
addOns: z.optional(ListOfBubbleTeaAddOns),
addOns: co.optional(ListOfBubbleTeaAddOns),
deliveryDate: z.optional(z.date()),
withMilk: z.optional(z.boolean()),
instructions: z.optional(co.plainText()),
instructions: co.optional(co.plainText()),
})
.withHelpers((Self) => ({
hasChanges(order: Loaded<typeof Self> | undefined) {

View File

@@ -1,7 +1,7 @@
import { co, z } from "jazz-tools";
import { co } from "jazz-tools";
export const JazzProfile = co.profile({
image: z.optional(co.image()),
image: co.optional(co.image()),
});
export const JazzAccount = co.account({

View File

@@ -66,7 +66,7 @@ export const MusicaAccountRoot = co.map({
// track and playlist
// You can also add the position in time if you want make it possible
// to resume the song
activeTrack: z.optional(MusicTrack),
activeTrack: co.optional(MusicTrack),
activePlaylist: Playlist,
exampleDataLoaded: z.optional(z.boolean()),

View File

@@ -0,0 +1,3 @@
{
"ignoreCommand": "echo true"
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -8,7 +8,7 @@ export type Player = co.loaded<typeof Player>;
export const Game = co.map({
player1: Player,
player2: z.optional(Player),
player2: co.optional(Player),
outcome: z.optional(z.literal(["player1", "player2", "draw"])),
player1Score: z.number(),
player2Score: z.number(),
@@ -17,8 +17,8 @@ export type Game = co.loaded<typeof Game>;
export const WaitingRoom = co.map({
account1: co.account(),
account2: z.optional(co.account()),
game: z.optional(Game),
account2: co.optional(co.account()),
game: co.optional(Game),
});
export type WaitingRoom = co.loaded<typeof WaitingRoom>;
@@ -47,7 +47,7 @@ export const JoinGameRequest = co.map({
});
export type JoinGameRequest = co.loaded<typeof JoinGameRequest>;
export const InboxMessage = z.discriminatedUnion("type", [
export const InboxMessage = co.discriminatedUnion("type", [
PlayIntent,
NewGameIntent,
CreateGameRequest,

View File

@@ -94,7 +94,7 @@ const MusicaAccountRoot = co.map({
const MusicaAccount = co.account({
root: MusicaAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
type MusicaAccount = co.loaded<typeof MusicaAccount>

View File

@@ -67,7 +67,7 @@ export const JazzAccountRoot = co.map({
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
})
.withMigration((account) => {
if (account.root === undefined) {
@@ -130,7 +130,7 @@ const JazzAccountRoot = co.map({
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
// ---cut---

View File

@@ -11,7 +11,7 @@ The main detail to understand when using Jazz server-side is that Server Workers
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/jazz-paper-scissors)
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/server-worker-inbox)
<Alert variant="info" className="mt-4 flex gap-2 items-center">Requires at least Node.js v20.</Alert>

View File

@@ -202,7 +202,7 @@ See the corresponding sections for [creating](/docs/using-covalues/filestreams#c
### Unions of CoMaps (declaration)
You can declare unions of CoMaps that have discriminating fields, using `z.discriminatedUnion()`.
You can declare unions of CoMaps that have discriminating fields, using `co.discriminatedUnion()`.
<CodeGroup>
```ts twoslash
@@ -220,7 +220,7 @@ const SliderWidget = co.map({
max: z.number(),
});
const WidgetUnion = z.discriminatedUnion("type", [ButtonWidget, SliderWidget]);
const WidgetUnion = co.discriminatedUnion("type", [ButtonWidget, SliderWidget]);
```
</CodeGroup>
@@ -321,7 +321,7 @@ const Company = co.map({
#### Optional References
You can make references optional with `z.optional()`:
You can make references optional with `co.optional()`:
<CodeGroup>
```ts twoslash
@@ -331,7 +331,7 @@ const Pet = co.map({
});
// ---cut---
const Person = co.map({
pet: z.optional(Pet),
pet: co.optional(Pet),
});
```
</CodeGroup>
@@ -372,7 +372,7 @@ const ListOfPeople = co.list(Person);
</CodeGroup>
Note: similarly, if you use modifiers like `z.optional()` you'll need to help TypeScript along:
Note: similarly, if you use modifiers like `co.optional()` you'll need to help TypeScript along:
<CodeGroup>
```ts twoslash
@@ -381,7 +381,7 @@ import { co, z } from "jazz-tools";
const Person = co.map({
name: z.string(),
get bestFriend(): z.ZodOptional<typeof Person> {
return z.optional(Person);
return co.optional(Person);
}
});
```

View File

@@ -24,7 +24,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
export type Project = co.loaded<typeof Project>;
```
@@ -54,7 +54,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
// ---cut---
@@ -90,7 +90,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
// ---cut---
@@ -134,7 +134,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
const project = Project.create(
{
@@ -165,7 +165,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
const project = Project.create(
{
@@ -197,7 +197,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
get subProject() {
return Project.optional();
}
@@ -219,9 +219,9 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
get subProjects(): z.ZodOptional<CoListSchema<typeof Project>> {
return z.optional(co.list(Project));
return co.optional(co.list(Project));
}
});
export type Project = co.loaded<typeof Project>;
@@ -265,7 +265,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
const project = Project.create(
@@ -297,7 +297,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
const project = Project.create(
@@ -347,7 +347,7 @@ const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: z.optional(Member),
coordinator: co.optional(Member),
});
const Inventory = co.record(z.string(), z.number());
const project = Project.create(
@@ -445,7 +445,7 @@ const TaskV2 = co.map({
// Export the discriminated union; because some users might
// not be able to run the migration
export const Task = z.discriminatedUnion("version", [
export const Task = co.discriminatedUnion("version", [
TaskV1,
TaskV2,
]);

View File

@@ -1,5 +1,30 @@
# cojson-storage-indexeddb
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes
- cojson@0.15.13
## 0.15.12
### Patch Changes
- cojson@0.15.12
## 0.15.11
### Patch Changes
- cojson@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.15.10",
"version": "0.15.14",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,30 @@
# cojson-storage-sqlite
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes
- cojson@0.15.13
## 0.15.12
### Patch Changes
- cojson@0.15.12
## 0.15.11
### Patch Changes
- cojson@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.15.10",
"version": "0.15.14",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -1,5 +1,31 @@
# cojson-transport-nodejs-ws
## 0.15.14
### Patch Changes
- 70ce7c5: Introduce the persistent peers. Used to mark the WebSocket connections to the server as persistent, and wait for reconnection before failing load.
- Updated dependencies [70ce7c5]
- cojson@0.15.14
## 0.15.13
### Patch Changes
- cojson@0.15.13
## 0.15.12
### Patch Changes
- cojson@0.15.12
## 0.15.11
### Patch Changes
- cojson@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.15.10",
"version": "0.15.14",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -165,6 +165,6 @@ export function createWebSocketPeer({
incoming,
outgoing,
role,
deletePeerStateOnClose,
persistent: !deletePeerStateOnClose,
};
}

View File

@@ -1,5 +1,17 @@
# cojson
## 0.15.14
### Patch Changes
- 70ce7c5: Introduce the persistent peers. Used to mark the WebSocket connections to the server as persistent, and wait for reconnection before failing load.
## 0.15.13
## 0.15.12
## 0.15.11
## 0.15.10
## 0.15.9

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.15.10",
"version": "0.15.14",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"libsql": "^0.5.13",

View File

@@ -97,6 +97,10 @@ export class PeerState {
return this.peer.incoming;
}
get persistent() {
return this.peer.persistent;
}
pushOutgoingMessage(msg: SyncMessage) {
this.peer.outgoing.push(msg);
}

View File

@@ -1056,37 +1056,33 @@ export class CoValueCore {
return;
}
const peersToActuallyLoadFrom = [] as PeerState[];
for (const peer of peers) {
const currentState = this.peers.get(peer.id)?.type;
const currentState = this.peers.get(peer.id)?.type ?? "unknown";
if (
!currentState ||
currentState === "unknown" ||
currentState === "unavailable"
) {
peersToActuallyLoadFrom.push(peer);
if (currentState === "unknown" || currentState === "unavailable") {
this.markPending(peer.id);
this.internalLoadFromPeer(peer);
}
}
for (const peer of peersToActuallyLoadFrom) {
this.internalLoadFromPeer(peer);
}
}
internalLoadFromPeer(peer: PeerState) {
if (peer.closed) {
if (peer.closed && !peer.persistent) {
this.markNotFoundInPeer(peer.id);
return;
}
peer.pushOutgoingMessage({
action: "load",
...this.knownState(),
});
peer.trackLoadRequestSent(this.id);
/**
* On reconnection persistent peers will automatically fire the load request
* as part of the reconnection process.
*/
if (!peer.closed) {
peer.pushOutgoingMessage({
action: "load",
...this.knownState(),
});
peer.trackLoadRequestSent(this.id);
}
return new Promise<void>((resolve) => {
const markNotFound = () => {
@@ -1100,7 +1096,9 @@ export class CoValueCore {
};
const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
const removeCloseListener = peer.addCloseListener(markNotFound);
const removeCloseListener = peer.persistent
? undefined
: peer.addCloseListener(markNotFound);
const listener = (state: CoValueCore) => {
const peerState = state.peers.get(peer.id);
@@ -1111,7 +1109,7 @@ export class CoValueCore {
peerState?.type === "unavailable"
) {
this.listeners.delete(listener);
removeCloseListener();
removeCloseListener?.();
clearTimeout(timeout);
resolve();
}

View File

@@ -13,6 +13,14 @@ export const CO_VALUE_LOADING_CONFIG = {
RETRY_DELAY: 3000,
};
export function setCoValueLoadingMaxRetries(maxRetries: number) {
CO_VALUE_LOADING_CONFIG.MAX_RETRIES = maxRetries;
}
export function setCoValueLoadingTimeout(timeout: number) {
CO_VALUE_LOADING_CONFIG.TIMEOUT = timeout;
}
export function setCoValueLoadingRetryDelay(delay: number) {
CO_VALUE_LOADING_CONFIG.RETRY_DELAY = delay;
}

View File

@@ -82,6 +82,11 @@ export class LocalNode {
this.storage = storage;
}
removeStorage() {
this.storage?.close();
this.storage = undefined;
}
getCoValue(id: RawCoID) {
let entry = this.coValues.get(id);
@@ -348,12 +353,10 @@ export class LocalNode {
skipLoadingFromPeer?: PeerID,
skipRetry?: boolean,
): Promise<CoValueCore> {
if (!id) {
throw new Error("Trying to load CoValue with undefined id");
}
if (!id.startsWith("co_z")) {
throw new Error(`Trying to load CoValue with invalid id ${id}`);
if (typeof id !== "string" || !id.startsWith("co_z")) {
throw new TypeError(
`Trying to load CoValue with invalid id ${Array.isArray(id) ? JSON.stringify(id) : id}`,
);
}
if (this.crashed) {

View File

@@ -13,9 +13,11 @@ export function connectedPeers(
{
peer1role = "client",
peer2role = "client",
persistent = false,
}: {
peer1role?: Peer["role"];
peer2role?: Peer["role"];
persistent?: boolean;
} = {},
): [Peer, Peer] {
const from1to2 = new ConnectedPeerChannel();
@@ -26,6 +28,7 @@ export function connectedPeers(
incoming: from2to1,
outgoing: from1to2,
role: peer2role,
persistent,
};
const peer1AsPeer: Peer = {
@@ -33,6 +36,7 @@ export function connectedPeers(
incoming: from1to2,
outgoing: from2to1,
role: peer1role,
persistent,
};
return [peer1AsPeer, peer2AsPeer];

View File

@@ -88,7 +88,7 @@ export interface Peer {
outgoing: OutgoingPeerChannel;
role: "server" | "client";
priority?: number;
deletePeerStateOnClose?: boolean;
persistent?: boolean;
}
export function combinedKnownStates(
@@ -157,8 +157,7 @@ export class SyncManager {
getServerPeers(excludePeerId?: PeerID): PeerState[] {
return this.getPeers().filter(
(peer) =>
peer.role === "server" && peer.id !== excludePeerId && !peer.closed,
(peer) => peer.role === "server" && peer.id !== excludePeerId,
);
}
@@ -353,7 +352,7 @@ export class SyncManager {
unsubscribeFromKnownStatesUpdates();
this.peersCounter.add(-1, { role: peer.role });
if (peer.deletePeerStateOnClose && this.peers[peer.id] === peerState) {
if (!peer.persistent && this.peers[peer.id] === peerState) {
delete this.peers[peer.id];
}
});
@@ -794,8 +793,8 @@ export class SyncManager {
const peerState = this.peers[peerId];
// The peer has been closed, so it isn't possible to sync
if (!peerState || peerState.closed) {
// The peer has been closed and is not persistent, so it isn't possible to sync
if (!peerState) {
return;
}
@@ -830,7 +829,7 @@ export class SyncManager {
return this.local.storage?.waitForSync(id, this.local.getCoValue(id));
}
waitForSync(id: RawCoID, timeout = 30_000) {
waitForSync(id: RawCoID, timeout = 60_000) {
const peers = this.getPeers();
return Promise.all(

View File

@@ -251,9 +251,11 @@ describe("SyncStateManager", () => {
).toEqual({ uploaded: true });
});
test("should skip closed peers", async () => {
test("should skip non-persistent closed peers", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer();
const { peerState } = client.connectToSyncServer({
persistent: false,
});
peerState.gracefulShutdown();
@@ -263,6 +265,42 @@ describe("SyncStateManager", () => {
await map.core.waitForSync();
});
test("should wait for persistent closed peers to reconnect", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer({
persistent: true,
});
peerState.gracefulShutdown();
const group = client.node.createGroup();
const map = group.createMap();
const promise = map.core.waitForSync().then(() => "waitForSync");
const result = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => resolve("timeout"), 10);
}),
]);
expect(result).toBe("timeout");
client.connectToSyncServer({
persistent: true,
});
const result2 = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => resolve("timeout"), 10);
}),
]);
expect(result2).toBe("waitForSync");
});
test("should skip client peers that are not subscribed to the coValue", async () => {
const server = setupTestNode({ isSyncServer: true });
const client = setupTestNode();

View File

@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { expectMap } from "../coValue";
import { setCoValueLoadingRetryDelay } from "../config";
import {
CO_VALUE_LOADING_CONFIG,
setCoValueLoadingRetryDelay,
} from "../config";
import { RawCoMap } from "../exports";
import {
SyncMessagesLog,
@@ -54,6 +57,43 @@ describe("loading coValues from server", () => {
`);
});
test("coValue load throws on invalid id", async () => {
const { node } = setupTestNode({
connected: true,
});
await expect(async () => await node.load("test" as any)).rejects.toThrow(
"Trying to load CoValue with invalid id test",
);
await expect(async () => await node.load(null as any)).rejects.toThrow(
"Trying to load CoValue with invalid id null",
);
await expect(async () => await node.load(undefined as any)).rejects.toThrow(
"Trying to load CoValue with invalid id undefined",
);
await expect(async () => await node.load(1 as any)).rejects.toThrow(
"Trying to load CoValue with invalid id 1",
);
await expect(async () => await node.load({} as any)).rejects.toThrow(
"Trying to load CoValue with invalid id [object Object]",
);
await expect(async () => await node.load([] as any)).rejects.toThrow(
"Trying to load CoValue with invalid id []",
);
await expect(async () => await node.load(["test"] as any)).rejects.toThrow(
'Trying to load CoValue with invalid id ["test"]',
);
await expect(
async () => await node.load((() => {}) as any),
).rejects.toMatchInlineSnapshot(`
[TypeError: Trying to load CoValue with invalid id () => {
}]
`);
await expect(
async () => await node.load(new Date() as any),
).rejects.toThrow();
});
test("unavailable coValue retry with skipRetry set to true", async () => {
const client = setupTestNode();
const client2 = setupTestNode();
@@ -470,9 +510,12 @@ describe("loading coValues from server", () => {
`);
});
test("should mark the coValue as unavailable if the peer is closed", async () => {
test("should wait for a persistent peer to reconnect before marking the coValue as unavailable", async () => {
const client = setupTestNode();
const { peerState } = client.connectToSyncServer();
const connection1 = client.connectToSyncServer({
persistent: true,
});
connection1.peerState.gracefulShutdown();
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "writer");
@@ -480,13 +523,15 @@ describe("loading coValues from server", () => {
const map = group.createMap({
test: "value",
});
const promise = client.node.load(map.id);
// Close the peer connection
peerState.gracefulShutdown();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(await promise).toEqual("unavailable");
client.connectToSyncServer();
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
@@ -496,6 +541,60 @@ describe("loading coValues from server", () => {
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | KNOWN Map sessions: header/1",
]
`);
});
test("should handle reconnections in the middle of a load with a persistent peer", async () => {
const client = setupTestNode();
const connection1 = client.connectToSyncServer({
persistent: true,
});
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "writer");
const map = group.createMap({
test: "value",
});
blockMessageTypeOnOutgoingPeer(connection1.peerOnServer, "content", {
id: map.id,
once: true,
});
const promise = client.node.load(map.id);
await new Promise((resolve) => setTimeout(resolve, 10));
// Close the peer connection
connection1.peerState.gracefulShutdown();
client.connectToSyncServer();
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
Group: group.core,
Map: map.core,
}),
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | LOAD Map sessions: empty",
"client -> server | LOAD Group sessions: header/5",
"server -> client | KNOWN Group sessions: header/5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Map sessions: header/1",
]
`);
});
@@ -872,4 +971,62 @@ describe("loading coValues from server", () => {
]
`);
});
test("should retry loading from a closed persistent peer after a timeout", async () => {
vi.useFakeTimers();
const client = setupTestNode();
const connection1 = client.connectToSyncServer({
persistent: true,
});
// Close the peer connection
connection1.peerState.gracefulShutdown();
const group = jazzCloud.node.createGroup();
group.addMember("everyone", "reader");
const map = group.createMap();
map.set("hello", "world");
const promise = loadCoValueOrFail(client.node, map.id);
await vi.advanceTimersByTimeAsync(
CO_VALUE_LOADING_CONFIG.TIMEOUT +
CO_VALUE_LOADING_CONFIG.RETRY_DELAY +
10,
);
client.connectToSyncServer({
persistent: true,
});
await vi.advanceTimersByTimeAsync(
CO_VALUE_LOADING_CONFIG.TIMEOUT +
CO_VALUE_LOADING_CONFIG.RETRY_DELAY +
10,
);
const coValue = await promise;
expect(coValue).not.toBe("unavailable");
expect(
SyncMessagesLog.getMessages({
Group: group.core,
Map: map.core,
}),
).toMatchInlineSnapshot(`
[
"client -> server | LOAD Map sessions: empty",
"server -> client | CONTENT Group header: true new: After: 0 New: 5",
"server -> client | CONTENT Map header: true new: After: 0 New: 1",
"client -> server | KNOWN Group sessions: header/5",
"client -> server | KNOWN Map sessions: header/1",
]
`);
vi.useRealTimers();
});
});

View File

@@ -26,6 +26,7 @@ function setupMesh() {
ourName: "edge-italy",
syncServerName: "core",
syncServer: coreServer.node,
persistent: true,
});
edgeItaly.addStorage({
ourName: "edge-italy",
@@ -36,6 +37,7 @@ function setupMesh() {
ourName: "edge-france",
syncServerName: "core",
syncServer: coreServer.node,
persistent: true,
});
edgeFrance.addStorage({
ourName: "edge-france",

View File

@@ -111,10 +111,12 @@ test("Can sync a coValue with private transactions through a server to another c
expect(mapOnClient2.get("hello")).toEqual("world");
});
test("should keep the peer state when the peer closes", async () => {
test("should keep the peer state when the peer closes if persistent is true", async () => {
const client = setupTestNode();
const { peer, peerState, peerOnServer } = client.connectToSyncServer();
const { peer, peerState, peerOnServer } = client.connectToSyncServer({
persistent: true,
});
const group = jazzCloud.node.createGroup();
const map = group.createMap();
@@ -133,12 +135,12 @@ test("should keep the peer state when the peer closes", async () => {
expect(syncManager.peers[peer.id]).not.toBeUndefined();
});
test("should delete the peer state when the peer closes if deletePeerStateOnClose is true", async () => {
test("should delete the peer state when the peer closes if persistent is false", async () => {
const client = setupTestNode();
const { peer, peerState, peerOnServer } = client.connectToSyncServer();
peer.deletePeerStateOnClose = true;
const { peer, peerState, peerOnServer } = client.connectToSyncServer({
persistent: false,
});
const group = jazzCloud.node.createGroup();
const map = group.createMap();
@@ -991,7 +993,7 @@ describe("LocalNode.load", () => {
// @ts-expect-error Testing with undefined ID
await expect(client.node.load(undefined)).rejects.toThrow(
"Trying to load CoValue with undefined id",
"Trying to load CoValue with invalid id undefined",
);
});

View File

@@ -429,6 +429,7 @@ export function getSyncServerConnectedPeer(opts: {
ourName?: string;
syncServer?: LocalNode;
peerId: string;
persistent?: boolean;
}) {
const currentSyncServer = opts?.syncServer ?? syncServer.current;
@@ -451,6 +452,7 @@ export function getSyncServerConnectedPeer(opts: {
role: "client",
name: opts.ourName,
},
persistent: opts?.persistent,
});
currentSyncServer.syncManager.addPeer(peer2);
@@ -483,6 +485,7 @@ export function setupTestNode(
syncServerName?: string;
ourName?: string;
syncServer?: LocalNode;
persistent?: boolean;
}) {
const { peer, peerStateOnServer, peerOnServer } =
getSyncServerConnectedPeer({
@@ -490,6 +493,7 @@ export function setupTestNode(
syncServerName: opts?.syncServerName,
ourName: opts?.ourName,
syncServer: opts?.syncServer,
persistent: opts?.persistent,
});
node.syncManager.addPeer(peer);
@@ -646,12 +650,22 @@ export type SyncTestMessage = {
export function connectedPeersWithMessagesTracking(opts: {
peer1: { id: string; role: Peer["role"]; name?: string };
peer2: { id: string; role: Peer["role"]; name?: string };
persistent?: boolean;
}) {
const [peer1, peer2] = connectedPeers(opts.peer1.id, opts.peer2.id, {
peer1role: opts.peer1.role,
peer2role: opts.peer2.role,
persistent: opts.persistent,
});
// If the persistent option is not provided, we default to true for the server and false for the client
// Trying to mimic the real world behavior of the sync server
if (opts.persistent === undefined) {
peer1.persistent = opts.peer1.role === "server";
peer2.persistent = opts.peer2.role === "server";
}
const peer1Push = peer1.outgoing.push;
peer1.outgoing.push = (msg) => {
if (typeof msg !== "string") {

View File

@@ -2136,7 +2136,7 @@ See the corresponding sections for [creating](/docs/using-covalues/filestreams#c
### Unions of CoMaps (declaration)
You can declare unions of CoMaps that have discriminating fields, using `z.discriminatedUnion()`.
You can declare unions of CoMaps that have discriminating fields, using `co.discriminatedUnion()`.
<CodeGroup>
```ts twoslash
@@ -2153,7 +2153,7 @@ const SliderWidget = co.map({
max: z.number(),
});
const WidgetUnion = z.discriminatedUnion([ButtonWidget, SliderWidget]);
const WidgetUnion = co.discriminatedUnion([ButtonWidget, SliderWidget]);
```
</CodeGroup>
@@ -7890,7 +7890,7 @@ export const JazzAccountRoot = co.map({
export const JazzAccount = co
.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
})
.withMigration((account) => {
if (account.root === undefined) {
@@ -7950,7 +7950,7 @@ const JazzAccountRoot = co.map({
const JazzAccount = co.account({
root: JazzAccountRoot,
profile: co.profile({}),
profile: co.profile(),
});
// ---cut---
@@ -11911,4 +11911,4 @@ export function cn(...inputs: ClassValue[]) {
```ts
/// <reference types="vite/client" />
```
```

View File

@@ -1,5 +1,44 @@
# jazz-auth-betterauth
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
- jazz-betterauth-client-plugin@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- jazz-betterauth-client-plugin@0.15.13
- cojson@0.15.13
## 0.15.12
### Patch Changes
- Updated dependencies [d1c1b0c]
- Updated dependencies [cf4ad72]
- jazz-tools@0.15.12
- jazz-betterauth-client-plugin@0.15.12
- cojson@0.15.12
## 0.15.11
### Patch Changes
- Updated dependencies [bdc9aee]
- jazz-tools@0.15.11
- jazz-betterauth-client-plugin@0.15.11
- cojson@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-auth-betterauth",
"version": "0.15.10",
"version": "0.15.14",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,29 @@
# jazz-betterauth-client-plugin
## 0.15.14
### Patch Changes
- jazz-betterauth-server-plugin@0.15.14
## 0.15.13
### Patch Changes
- jazz-betterauth-server-plugin@0.15.13
## 0.15.12
### Patch Changes
- jazz-betterauth-server-plugin@0.15.12
## 0.15.11
### Patch Changes
- jazz-betterauth-server-plugin@0.15.11
## 0.15.10
### Patch Changes

View File

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

View File

@@ -1,5 +1,40 @@
# jazz-betterauth-server-plugin
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- cojson@0.15.13
## 0.15.12
### Patch Changes
- Updated dependencies [d1c1b0c]
- Updated dependencies [cf4ad72]
- jazz-tools@0.15.12
- cojson@0.15.12
## 0.15.11
### Patch Changes
- Updated dependencies [bdc9aee]
- jazz-tools@0.15.11
- cojson@0.15.11
## 0.15.10
### Patch Changes

View File

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

View File

@@ -1,5 +1,48 @@
# jazz-react-auth-betterauth
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson@0.15.14
- jazz-tools@0.15.14
- jazz-auth-betterauth@0.15.14
- jazz-betterauth-client-plugin@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- jazz-auth-betterauth@0.15.13
- jazz-betterauth-client-plugin@0.15.13
- cojson@0.15.13
## 0.15.12
### Patch Changes
- Updated dependencies [d1c1b0c]
- Updated dependencies [cf4ad72]
- jazz-tools@0.15.12
- jazz-auth-betterauth@0.15.12
- jazz-betterauth-client-plugin@0.15.12
- cojson@0.15.12
## 0.15.11
### Patch Changes
- Updated dependencies [bdc9aee]
- jazz-tools@0.15.11
- jazz-auth-betterauth@0.15.11
- jazz-betterauth-client-plugin@0.15.11
- cojson@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-react-auth-betterauth",
"version": "0.15.10",
"version": "0.15.14",
"type": "module",
"main": "dist/index.js",
"types": "src/index.tsx",

View File

@@ -1,5 +1,48 @@
# jazz-run
## 0.15.14
### Patch Changes
- Updated dependencies [70ce7c5]
- Updated dependencies [a584590]
- Updated dependencies [9acccb5]
- cojson-transport-ws@0.15.14
- cojson@0.15.14
- jazz-tools@0.15.14
- cojson-storage-sqlite@0.15.14
## 0.15.13
### Patch Changes
- Updated dependencies [6c76ff8]
- jazz-tools@0.15.13
- cojson@0.15.13
- cojson-storage-sqlite@0.15.13
- cojson-transport-ws@0.15.13
## 0.15.12
### Patch Changes
- Updated dependencies [d1c1b0c]
- Updated dependencies [cf4ad72]
- jazz-tools@0.15.12
- cojson@0.15.12
- cojson-storage-sqlite@0.15.12
- cojson-transport-ws@0.15.12
## 0.15.11
### Patch Changes
- Updated dependencies [bdc9aee]
- jazz-tools@0.15.11
- cojson@0.15.11
- cojson-storage-sqlite@0.15.11
- cojson-transport-ws@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.15.10",
"version": "0.15.14",
"exports": {
"./startSyncServer": {
"import": "./dist/startSyncServer.js",
@@ -28,11 +28,11 @@
"@effect/printer-ansi": "^0.34.5",
"@effect/schema": "^0.71.1",
"@effect/typeclass": "^0.25.5",
"cojson": "workspace:0.15.10",
"cojson-storage-sqlite": "workspace:0.15.10",
"cojson-transport-ws": "workspace:0.15.10",
"cojson": "workspace:0.15.14",
"cojson-storage-sqlite": "workspace:0.15.14",
"cojson-transport-ws": "workspace:0.15.14",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.15.10",
"jazz-tools": "workspace:0.15.14",
"ws": "^8.14.2"
},
"devDependencies": {

View File

@@ -230,8 +230,7 @@ describe("startWorker integration", () => {
await worker2.done();
});
// Flaky test, fails randomly on CI
test.skip("worker reconnects when sync server is closed and reopened", async () => {
test("worker reconnects when sync server is closed and reopened", async () => {
const worker1 = await setup();
const worker2 = await setupWorker(worker1.syncServer);
@@ -267,10 +266,6 @@ describe("startWorker integration", () => {
db: "",
});
// Wait for reconnection
await worker1.waitForConnection();
await worker2.waitForConnection();
await worker1.worker.waitForAllCoValuesSync();
// Verify both old and new values are synced

View File

@@ -1,5 +1,45 @@
# jazz-tools
## 0.15.14
### Patch Changes
- a584590: Prevent resolving discriminated union fields
- 9acccb5: Export `WithHelpers` type used in CoValue schemas
- Updated dependencies [70ce7c5]
- cojson-transport-ws@0.15.14
- cojson@0.15.14
- cojson-storage-indexeddb@0.15.14
## 0.15.13
### Patch Changes
- 6c76ff8: Fix load failures when loading a missing ref declared with z.optional and Schema.optional
- cojson@0.15.13
- cojson-storage-indexeddb@0.15.13
- cojson-transport-ws@0.15.13
## 0.15.12
### Patch Changes
- d1c1b0c: Fix stuck authentication when using onAnonymousAccountDiscarded with a storage
- cf4ad72: fix unhandled rejection on CoValue.load
- cojson@0.15.12
- cojson-storage-indexeddb@0.15.12
- cojson-transport-ws@0.15.12
## 0.15.11
### Patch Changes
- bdc9aee: - Add `co.optional` and `co.discriminatedUnion`. You can now `load` and `subcribe` to schemas created with `co.discriminatedUnion`.
- Improved type-checking around `z.` schemas to prevent invalid combinations with `co.` schemas.
- cojson@0.15.11
- cojson-storage-indexeddb@0.15.11
- cojson-transport-ws@0.15.11
## 0.15.10
### Patch Changes

View File

@@ -139,7 +139,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.15.10",
"version": "0.15.14",
"dependencies": {
"@manuscripts/prosemirror-recreate-steps": "^0.1.4",
"@scure/base": "1.2.1",
@@ -163,6 +163,7 @@
"zod": "3.25.28"
},
"scripts": {
"check": "tsc --noEmit",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"dev": "tsup --watch --dts",

View File

@@ -1,16 +1,6 @@
// @vitest-environment happy-dom
import {
Account,
CoMap,
Loaded,
RefsToResolve,
Resolved,
co,
coField,
z,
zodSchemaToCoSchema,
} from "jazz-tools";
import { RefsToResolve, co, z, zodSchemaToCoSchema } from "jazz-tools";
import { beforeEach, describe, expect, it } from "vitest";
import { useAccount, useJazzContextManager } from "../hooks.js";
import { useIsAuthenticated } from "../index.js";

View File

@@ -47,6 +47,26 @@ describe("useCoState", () => {
expect(result.current?.value).toBe("123");
});
it("should return null on invalid id", async () => {
const TestMap = co.map({
value: z.string(),
});
const account = await createJazzTestAccount({
isCurrentActiveAccount: true,
});
const { result } = renderHook(() => useCoState(TestMap, "test", {}), {
account,
});
expect(result.current).toBeUndefined();
await waitFor(() => {
expect(result.current).toBeNull();
});
});
it("should update the value when the coValue changes", async () => {
const TestMap = co.map({
value: z.string(),

View File

@@ -11,10 +11,10 @@ import {
anySchemaToCoSchema,
coValuesCache,
inspect,
isCoValueSchema,
} from "../internal.js";
import type {
Account,
CoValueClassFromZodSchema,
Group,
InstanceOfSchemaCoValuesNullable,
} from "../internal.js";
@@ -101,7 +101,7 @@ export class CoValueBase implements CoValue {
: S extends z.core.$ZodType
? NonNullable<InstanceOfSchemaCoValuesNullable<S>>
: never {
const cl = "getCoSchema" in schema ? (schema as any).getCoSchema() : schema;
const cl = isCoValueSchema(schema) ? schema.getCoValueClass() : schema;
if (this.constructor === cl) {
return this as any;

View File

@@ -2,11 +2,23 @@ import { SessionID } from "cojson";
import { ItemsSym } from "../internal.js";
import { type Account } from "./account.js";
import { CoFeedEntry } from "./coFeed.js";
import { type CoKeys, type CoMap } from "./coMap.js";
import { type CoKeys } from "./coMap.js";
import { type CoValue, type ID } from "./interfaces.js";
type NotNull<T> = Exclude<T, null>;
/**
* Used to check if T is a union type.
*
* If T is a union type, the left hand side of the extends becomes a union of function types.
* The right hand side is always a single function type.
*/
type IsUnion<T, U = T> = (T extends any ? (x: T) => void : never) extends (
x: U,
) => void
? false
: true;
export type RefsToResolve<
V,
DepthLimit extends number = 10,
@@ -16,56 +28,58 @@ export type RefsToResolve<
| (DepthLimit extends CurrentDepth["length"]
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
any
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
V extends Array<infer Item>
?
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
| boolean
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
V extends { _type: "CoMap" | "Group" | "Account" }
: IsUnion<NonNullable<V>> extends true
? true
: // Basically V extends CoList - but if we used that we'd introduce circularity into the definition of CoList itself
V extends Array<infer Item>
?
| ({
[Key in CoKeys<V> as NonNullable<V[Key]> extends CoValue
? Key
: never]?: RefsToResolve<
NonNullable<V[Key]>,
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
} & { $onError?: null })
| (ItemsSym extends keyof V
? {
$onError?: null;
}
| boolean
: // Basically V extends CoMap | Group | Account - but if we used that we'd introduce circularity into the definition of CoMap itself
V extends { _type: "CoMap" | "Group" | "Account" }
?
| ({
[Key in CoKeys<V> as NonNullable<V[Key]> extends CoValue
? Key
: never]?: RefsToResolve<
NonNullable<V[Key]>,
DepthLimit,
[0, ...CurrentDepth]
>;
} & { $onError?: null })
| (ItemsSym extends keyof V
? {
$each: RefsToResolve<
NonNullable<V[ItemsSym]>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
: never)
| boolean
: V extends {
_type: "CoStream";
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| {
$each: RefsToResolve<
NonNullable<V[ItemsSym]>,
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
: never)
| boolean
: V extends {
_type: "CoStream";
byMe: CoFeedEntry<infer Item> | undefined;
}
?
| {
$each: RefsToResolve<
NotNull<Item>,
DepthLimit,
[0, ...CurrentDepth]
>;
$onError?: null;
}
| boolean
: boolean);
| boolean
: boolean);
export type RefsToResolveStrict<T, V> = V extends RefsToResolve<T>
? RefsToResolve<T>

View File

@@ -3,14 +3,12 @@ import { CoStreamItem, RawCoStream } from "cojson";
import {
type Account,
CoValue,
CoValueClass,
CoValueOrZodSchema,
ID,
InstanceOfSchema,
activeAccountContext,
anySchemaToCoSchema,
loadCoValue,
zodSchemaToCoSchema,
} from "../internal.js";
export type InboxInvite = `${CoID<MessagesStream>}/${InviteSecret}`;

View File

@@ -1,8 +1,19 @@
import {
Account,
AnonymousJazzAgent,
CoValue,
CoValueBase,
CoValueClass,
CoValueFromRaw,
ID,
RefsToResolve,
RefsToResolveStrict,
Resolved,
SubscribeListenerOptions,
SubscribeRestArgs,
loadCoValueWithoutMe,
parseSubscribeRestArgs,
subscribeToCoValueWithoutMe,
} from "../internal.js";
/**
@@ -99,4 +110,55 @@ export abstract class SchemaUnion extends CoValueBase implements CoValue {
static fromRaw<V extends CoValue>(this: CoValueClass<V>, raw: V["_raw"]): V {
throw new Error("Not implemented");
}
/**
* Load a `SchemaUnion` with a given ID, as a given account.
*
* Note: The `resolve` option is not supported for `SchemaUnion`s due to https://github.com/garden-co/jazz/issues/2639
*
* @category Subscription & Loading
*/
static load<M extends SchemaUnion>(
this: CoValueClass<M>,
id: ID<M>,
options?: {
loadAs?: Account | AnonymousJazzAgent;
skipRetry?: boolean;
},
): Promise<Resolved<M, true> | null> {
return loadCoValueWithoutMe(this, id, options);
}
/**
* Load and subscribe to a `CoMap` with a given ID, as a given account.
*
* Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
*
* Returns an unsubscribe function that you should call when you no longer need updates.
*
* Also see the `useCoState` hook to reactively subscribe to a CoValue in a React component.
*
* Note: The `resolve` option is not supported for `SchemaUnion`s due to https://github.com/garden-co/jazz/issues/2639
*
* @category Subscription & Loading
*/
static subscribe<M extends SchemaUnion>(
this: CoValueClass<M>,
id: ID<M>,
listener: (value: Resolved<M, true>, unsubscribe: () => void) => void,
): () => void;
static subscribe<M extends SchemaUnion>(
this: CoValueClass<M>,
id: ID<M>,
options: SubscribeListenerOptions<M, true>,
listener: (value: Resolved<M, true>, unsubscribe: () => void) => void,
): () => void;
static subscribe<M extends SchemaUnion>(
this: CoValueClass<M>,
id: ID<M>,
...args: SubscribeRestArgs<M, true>
): () => void {
const { options, listener } = parseSubscribeRestArgs(args);
return subscribeToCoValueWithoutMe<M, true>(this, id, options, listener);
}
}

View File

@@ -35,6 +35,7 @@ export type {
TextPos,
AccountClass,
AccountCreationProps,
WithHelpers,
} from "./internal.js";
export {

View File

@@ -279,8 +279,12 @@ export class JazzContextManager<
},
);
prevContext.node.syncManager.addPeer(currentAccountAsPeer);
// Closing storage on the prevContext to avoid conflicting transactions and getting stuck on waitForAllCoValuesSync
// The storage is reachable through currentContext using the connectedPeers
prevContext.node.removeStorage();
currentContext.node.syncManager.addPeer(prevAccountAsPeer);
prevContext.node.syncManager.addPeer(currentAccountAsPeer);
try {
await this.props.onAnonymousAccountDiscarded?.(prevContext.me);

View File

@@ -10,4 +10,6 @@ export {
coImageDefiner as image,
coAccountDefiner as account,
coProfileDefiner as profile,
coOptionalDefiner as optional,
coDiscriminatedUnionDefiner as discriminatedUnion,
} from "./zodCo.js";

View File

@@ -1,5 +1,6 @@
import { CoMap, CoValueClass, isCoValueClass } from "../../../internal.js";
import { coField } from "../../schema.js";
import { isAnyCoOptionalSchema } from "../schemaTypes/CoOptionalSchema.js";
import {
isUnionOfCoMapsDeeply,
isUnionOfPrimitivesDeeply,
@@ -12,10 +13,19 @@ import {
ZodReadonly,
z,
} from "../zodReExport.js";
import { ZodPrimitiveSchema } from "../zodSchema.js";
import { zodSchemaToCoSchemaOrKeepPrimitive } from "./zodSchemaToCoSchema.js";
import { AnyCoSchema, ZodPrimitiveSchema } from "../zodSchema.js";
import {
isCoValueSchema,
zodSchemaToCoSchemaOrKeepPrimitive,
} from "./zodSchemaToCoSchema.js";
type FieldSchema =
/**
* Types of objects that can be nested inside CoValue schema containers
*/
type SchemaField =
// Schemas created with co.map(), co.record(), co.list(), etc.
| AnyCoSchema
// CoValue classes created with class syntax, or framework-provided classes like Group
| CoValueClass
| ZodPrimitiveSchema
| z.core.$ZodOptional<z.core.$ZodType>
@@ -33,20 +43,35 @@ type FieldSchema =
| z.core.$ZodCatch<z.core.$ZodType>
| (z.core.$ZodCustom<any, any> & { builtin: any });
export function zodFieldToCoFieldDef(schema: FieldSchema) {
export function schemaFieldToCoFieldDef(
schema: SchemaField,
isOptional = false,
) {
if (isCoValueClass(schema)) {
return coField.ref(schema);
if (isOptional) {
return coField.ref(schema, { optional: true });
} else {
return coField.ref(schema);
}
} else if (isCoValueSchema(schema)) {
if (isAnyCoOptionalSchema(schema)) {
return coField.ref(schema.getCoValueClass(), {
optional: true,
});
}
if (isOptional) {
return coField.ref(schema.getCoValueClass(), { optional: true });
} else {
return coField.ref(schema.getCoValueClass());
}
} else {
if ("_zod" in schema) {
if (schema._zod.def.type === "optional") {
const inner = zodSchemaToCoSchemaOrKeepPrimitive(
schema._zod.def.innerType,
);
if (isCoValueClass(inner)) {
return coField.ref(inner, { optional: true });
} else {
return zodFieldToCoFieldDef(inner);
}
return schemaFieldToCoFieldDef(inner, true);
} else if (schema._zod.def.type === "string") {
return coField.string;
} else if (schema._zod.def.type === "number") {
@@ -58,8 +83,9 @@ export function zodFieldToCoFieldDef(schema: FieldSchema) {
} else if (schema._zod.def.type === "enum") {
return coField.string;
} else if (schema._zod.def.type === "readonly") {
return zodFieldToCoFieldDef(
(schema as unknown as ZodReadonly).def.innerType as FieldSchema,
return schemaFieldToCoFieldDef(
(schema as unknown as ZodReadonly).def.innerType as SchemaField,
isOptional,
);
} else if (schema._zod.def.type === "date") {
return coField.optional.Date;
@@ -67,8 +93,9 @@ export function zodFieldToCoFieldDef(schema: FieldSchema) {
return coField.string;
} else if (schema._zod.def.type === "lazy") {
// Mostly to support z.json()
return zodFieldToCoFieldDef(
(schema as unknown as ZodLazy).unwrap() as FieldSchema,
return schemaFieldToCoFieldDef(
(schema as unknown as ZodLazy).unwrap() as SchemaField,
isOptional,
);
} else if (
schema._zod.def.type === "default" ||
@@ -78,9 +105,10 @@ export function zodFieldToCoFieldDef(schema: FieldSchema) {
"z.default()/z.catch() are not supported in collaborative schemas. They will be ignored.",
);
return zodFieldToCoFieldDef(
return schemaFieldToCoFieldDef(
(schema as unknown as ZodDefault | ZodCatch).def
.innerType as FieldSchema,
.innerType as SchemaField,
isOptional,
);
} else if (schema._zod.def.type === "literal") {
if (
@@ -112,7 +140,7 @@ export function zodFieldToCoFieldDef(schema: FieldSchema) {
return coField.json();
} else if (schema._zod.def.type === "custom") {
if ("builtin" in schema) {
return zodFieldToCoFieldDef(schema.builtin);
return schemaFieldToCoFieldDef(schema.builtin, isOptional);
} else {
throw new Error(`Unsupported custom zod type`);
}
@@ -120,9 +148,13 @@ export function zodFieldToCoFieldDef(schema: FieldSchema) {
if (isUnionOfPrimitivesDeeply(schema)) {
return coField.json();
} else if (isUnionOfCoMapsDeeply(schema)) {
return coField.ref<CoValueClass<CoMap>>(
schemaUnionDiscriminatorFor(schema),
);
const result = schemaUnionDiscriminatorFor(schema);
if (isOptional) {
return coField.ref<CoValueClass<CoMap>>(result, { optional: true });
} else {
return coField.ref<CoValueClass<CoMap>>(result);
}
} else {
throw new Error(
"z.union()/z.discriminatedUnion() of mixed collaborative and non-collaborative types is not supported",

View File

@@ -9,89 +9,140 @@ import {
CoValueClass,
FileStream,
SchemaUnion,
enrichAccountSchema,
enrichCoDiscriminatedUnionSchema,
enrichCoFeedSchema,
enrichCoListSchema,
enrichCoMapSchema,
enrichFileStreamSchema,
enrichPlainTextSchema,
isCoValueClass,
} from "../../../internal.js";
import { coField } from "../../schema.js";
import { isAnyCoOptionalSchema } from "../schemaTypes/CoOptionalSchema.js";
import { enrichRichTextSchema } from "../schemaTypes/RichTextSchema.js";
import {
isUnionOfCoMapsDeeply,
schemaUnionDiscriminatorFor,
} from "../unionUtils.js";
import { z } from "../zodReExport.js";
import {
CoValueClassFromZodSchema,
AnyCoSchema,
CoValueClassFromAnySchema,
CoValueOrZodSchema,
CoValueSchemaFromZodSchema,
ZodPrimitiveSchema,
getDef,
isZodArray,
isZodCustom,
isZodObject,
} from "../zodSchema.js";
import { zodFieldToCoFieldDef } from "./zodFieldToCoFieldDef.js";
import { schemaFieldToCoFieldDef } from "./zodFieldToCoFieldDef.js";
let coSchemasForZodSchemas = new Map<z.core.$ZodType, CoValueClass>();
let coSchemasForZodSchemas = new Map<z.core.$ZodType, AnyCoSchema>();
export function tryZodSchemaToCoSchema<S extends z.core.$ZodType>(
export function isAnyCoValueSchema(
schema: z.core.$ZodType | CoValueClass,
): schema is AnyCoSchema {
return "collaborative" in schema && schema.collaborative === true;
}
export function isCoValueSchema(
schema: z.core.$ZodType | CoValueClass,
): schema is CoValueSchemaFromZodSchema<AnyCoSchema> {
return isAnyCoValueSchema(schema) && "getCoValueClass" in schema;
}
/**
* Convert a Zod schema into a CoValue schema.
*
* @param schema A Zod schema that may represent a CoValue schema
* @returns The CoValue schema matching the provided ProtoCoSchema, or `null` if the Zod schema
* does not match a CoValue schema.
*/
function tryZodSchemaToCoSchema<S extends z.core.$ZodType>(
schema: S,
): CoValueClassFromZodSchema<S> | null {
if ("collaborative" in schema && schema.collaborative) {
): CoValueSchemaFromZodSchema<S> | null {
if (isAnyCoValueSchema(schema)) {
if (coSchemasForZodSchemas.has(schema)) {
return coSchemasForZodSchemas.get(schema) as CoValueClassFromZodSchema<S>;
return coSchemasForZodSchemas.get(
schema,
) as CoValueSchemaFromZodSchema<S>;
}
if (isZodObject(schema)) {
if (isAnyCoOptionalSchema(schema)) {
// Optional schemas are not supported as top-level schemas
return null;
} else if (isZodObject(schema)) {
const def = getDef(schema);
const ClassToExtend =
"builtin" in schema && schema.builtin === "Account" ? Account : CoMap;
const coSchema = class ZCoMap extends ClassToExtend {
const coValueClass = class ZCoMap extends ClassToExtend {
constructor(options: { fromRaw: RawCoMap } | undefined) {
super(options);
for (const [field, fieldType] of Object.entries(
def.shape as z.core.$ZodShape,
)) {
(this as any)[field] = zodFieldToCoFieldDef(
(this as any)[field] = schemaFieldToCoFieldDef(
zodSchemaToCoSchemaOrKeepPrimitive(fieldType),
);
}
if (def.catchall) {
(this as any)[coField.items] = zodFieldToCoFieldDef(
(this as any)[coField.items] = schemaFieldToCoFieldDef(
zodSchemaToCoSchemaOrKeepPrimitive(def.catchall),
);
}
}
};
coSchemasForZodSchemas.set(schema, coSchema as unknown as CoValueClass);
return coSchema as unknown as CoValueClassFromZodSchema<S>;
const coValueSchema =
ClassToExtend === Account
? enrichAccountSchema(schema as any, coValueClass as any)
: enrichCoMapSchema(schema as any, coValueClass as any);
coSchemasForZodSchemas.set(schema, coValueSchema);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else if (isZodArray(schema)) {
const def = getDef(schema);
const coSchema = class ZCoList extends CoList {
const coValueClass = class ZCoList extends CoList {
constructor(options: { fromRaw: RawCoList } | undefined) {
super(options);
(this as any)[coField.items] = zodFieldToCoFieldDef(
(this as any)[coField.items] = schemaFieldToCoFieldDef(
zodSchemaToCoSchemaOrKeepPrimitive(def.element),
);
}
};
coSchemasForZodSchemas.set(schema, coSchema);
return coSchema as unknown as CoValueClassFromZodSchema<S>;
const coValueSchema = enrichCoListSchema(schema, coValueClass as any);
coSchemasForZodSchemas.set(schema, coValueSchema);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else if (isZodCustom(schema)) {
if ("builtin" in schema) {
if (schema.builtin === "CoFeed" && "element" in schema) {
return CoFeed.Of(
zodFieldToCoFieldDef(
const coValueClass = CoFeed.Of(
schemaFieldToCoFieldDef(
zodSchemaToCoSchemaOrKeepPrimitive(
schema.element as z.core.$ZodType,
),
),
) as unknown as CoValueClassFromZodSchema<S>;
);
const coValueSchema = enrichCoFeedSchema(schema, coValueClass as any);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else if (schema.builtin === "FileStream") {
return FileStream as unknown as CoValueClassFromZodSchema<S>;
const coValueClass = FileStream;
const coValueSchema = enrichFileStreamSchema(schema, coValueClass);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else if (schema.builtin === "CoPlainText") {
return CoPlainText as unknown as CoValueClassFromZodSchema<S>;
const coValueClass = CoPlainText;
const coValueSchema = enrichPlainTextSchema(schema, coValueClass);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else if (schema.builtin === "CoRichText") {
return CoRichText as unknown as CoValueClassFromZodSchema<S>;
const coValueClass = CoRichText;
const coValueSchema = enrichRichTextSchema(schema, coValueClass);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else {
throw new Error(`Unsupported builtin type: ${schema.builtin}`);
}
@@ -105,9 +156,12 @@ export function tryZodSchemaToCoSchema<S extends z.core.$ZodType>(
}
} else if (schema instanceof z.core.$ZodDiscriminatedUnion) {
if (isUnionOfCoMapsDeeply(schema)) {
return SchemaUnion.Of(
schemaUnionDiscriminatorFor(schema),
) as unknown as CoValueClassFromZodSchema<S>;
const coValueClass = SchemaUnion.Of(schemaUnionDiscriminatorFor(schema));
const coValueSchema = enrichCoDiscriminatedUnionSchema(
schema as any,
coValueClass as any,
);
return coValueSchema as unknown as CoValueSchemaFromZodSchema<S>;
} else {
throw new Error(
"z.discriminatedUnion() of non-collaborative types is not supported as a top-level schema",
@@ -118,19 +172,9 @@ export function tryZodSchemaToCoSchema<S extends z.core.$ZodType>(
}
}
export function zodSchemaToCoSchema<
S extends
| z.core.$ZodType
| (z.core.$ZodObject<any, any> & {
builtin: "Account";
migration?: (account: any, creationProps?: { name: string }) => void;
})
| (z.core.$ZodCustom<any, any> & { builtin: "FileStream" })
| (z.core.$ZodCustom<any, any> & {
builtin: "CoFeed";
element: z.core.$ZodType;
}),
>(schema: S): CoValueClassFromZodSchema<S> {
export function zodSchemaToCoSchema<S extends z.core.$ZodType | AnyCoSchema>(
schema: S,
): CoValueSchemaFromZodSchema<S> {
const coSchema = tryZodSchemaToCoSchema(schema);
if (!coSchema) {
throw new Error(
@@ -140,38 +184,24 @@ export function zodSchemaToCoSchema<
return coSchema;
}
export function anySchemaToCoSchema<
S extends
| CoValueClass
| z.core.$ZodType
| (z.core.$ZodObject<any, any> & {
builtin: "Account";
migration?: (account: any, creationProps?: { name: string }) => void;
})
| (z.core.$ZodCustom<any, any> & { builtin: "FileStream" })
| (z.core.$ZodCustom<any, any> & {
builtin: "CoFeed";
element: z.core.$ZodType;
}),
>(
// TODO this should be coValueClassOrAnySchemaToCoValueClass
export function anySchemaToCoSchema<S extends CoValueOrZodSchema>(
schema: S,
): S extends CoValueClass
? S
: S extends z.core.$ZodType
? CoValueClassFromZodSchema<S>
: never {
): CoValueClassFromAnySchema<S> {
if (isCoValueClass(schema)) {
return schema as any;
} else if ("getCoSchema" in schema) {
return (schema as any).getCoSchema() as any;
} else if (isCoValueSchema(schema)) {
return schema.getCoValueClass() as any;
} else if ("def" in schema) {
const coSchema = tryZodSchemaToCoSchema(schema as z.core.$ZodType);
const coSchema = tryZodSchemaToCoSchema(
schema as z.core.$ZodType | AnyCoSchema,
);
if (!coSchema) {
throw new Error(
`Unsupported zod type: ${(schema.def as any)?.type || JSON.stringify(schema)}`,
);
}
return coSchema as any;
return coSchema.getCoValueClass() as any;
}
throw new Error(`Unsupported schema: ${JSON.stringify(schema)}`);
@@ -179,7 +209,7 @@ export function anySchemaToCoSchema<
export function zodSchemaToCoSchemaOrKeepPrimitive<S extends z.core.$ZodType>(
schema: S,
): CoValueClassFromZodSchema<S> | ZodPrimitiveSchema {
): CoValueSchemaFromZodSchema<S> | ZodPrimitiveSchema {
const coSchema = tryZodSchemaToCoSchema(schema);
if (!coSchema) {
return schema as any;

View File

@@ -1,5 +1,10 @@
import { CryptoProvider } from "cojson";
import { Account, Group, RefsToResolveStrict } from "../../../internal.js";
import {
Account,
AccountCreationProps,
Group,
RefsToResolveStrict,
} from "../../../internal.js";
import { AnonymousJazzAgent } from "../../anonymousJazzAgent.js";
import { InstanceOrPrimitiveOfSchema } from "../typeConverters/InstanceOrPrimitiveOfSchema.js";
import { InstanceOrPrimitiveOfSchemaCoValuesNullable } from "../typeConverters/InstanceOrPrimitiveOfSchemaCoValuesNullable.js";
@@ -57,9 +62,63 @@ export type AccountSchema<
) => void,
): AccountSchema<Shape>;
getCoSchema: () => typeof Account;
getCoValueClass: () => typeof Account;
};
export function enrichAccountSchema<Shape extends BaseAccountShape>(
schema: AnyAccountSchema<Shape>,
coValueClass: typeof Account,
): AccountSchema<Shape> {
const enrichedSchema = Object.assign(schema, {
create: (...args: any[]) => {
// @ts-expect-error
return coValueClass.create(...args);
},
createAs: (...args: any[]) => {
// @ts-expect-error
return coValueClass.createAs(...args);
},
getMe: (...args: any[]) => {
// @ts-expect-error
return coValueClass.getMe(...args);
},
load: (...args: any[]) => {
// @ts-expect-error
return coValueClass.load(...args);
},
subscribe: (...args: any[]) => {
// @ts-expect-error
return coValueClass.subscribe(...args);
},
withHelpers: (helpers: (Self: z.core.$ZodType) => object) => {
return Object.assign(schema, helpers(schema));
},
fromRaw: (...args: any[]) => {
// @ts-expect-error
return coValueClass.fromRaw(...args);
},
withMigration: (
migration: (
value: any,
creationProps?: AccountCreationProps,
) => void | Promise<void>,
) => {
(coValueClass.prototype as Account).migrate = async function (
this,
creationProps,
) {
await migration(this, creationProps);
};
return enrichedSchema;
},
getCoValueClass: () => {
return coValueClass;
},
}) as unknown as AccountSchema<Shape>;
return enrichedSchema;
}
export type DefaultProfileShape = {
name: z.core.$ZodString<string>;
inbox: z.core.$ZodOptional<z.core.$ZodString>;
@@ -71,7 +130,7 @@ export type CoProfileSchema<
Config extends z.core.$ZodObjectConfig = z.core.$ZodObjectConfig,
> = CoMapSchema<Shape & DefaultProfileShape, Config, Group>;
// less precise verion to avoid circularity issues and allow matching against
// less precise version to avoid circularity issues and allow matching against
export type AnyAccountSchema<
Shape extends z.core.$ZodLooseShape = z.core.$ZodLooseShape,
> = z.core.$ZodObject<Shape> & {

View File

@@ -0,0 +1,90 @@
import {
Account,
AnonymousJazzAgent,
AnyCoSchema,
InstanceOrPrimitiveOfSchemaCoValuesNullable,
RefsToResolve,
RefsToResolveStrict,
Resolved,
SchemaUnion,
SubscribeListenerOptions,
} from "../../../internal.js";
import { z } from "../zodReExport.js";
export type AnyDiscriminableCoSchema = AnyCoSchema &
z.core.$ZodTypeDiscriminable;
export type AnyCoDiscriminatedUnionSchema<
Types extends readonly [
AnyDiscriminableCoSchema,
...AnyDiscriminableCoSchema[],
],
> = z.ZodDiscriminatedUnion<Types> & {
collaborative: true;
};
export type CoDiscriminatedUnionSchema<
Types extends readonly [
AnyDiscriminableCoSchema,
...AnyDiscriminableCoSchema[],
],
> = AnyCoDiscriminatedUnionSchema<Types> & {
load(
id: string,
options?: {
loadAs?: Account | AnonymousJazzAgent;
skipRetry?: boolean;
},
): Promise<Resolved<
CoDiscriminatedUnionInstanceCoValuesNullable<Types> & SchemaUnion,
true
> | null>;
subscribe(
id: string,
options: SubscribeListenerOptions<
CoDiscriminatedUnionInstanceCoValuesNullable<Types> & SchemaUnion,
true
>,
listener: (
value: Resolved<
CoDiscriminatedUnionInstanceCoValuesNullable<Types> & SchemaUnion,
true
>,
unsubscribe: () => void,
) => void,
): () => void;
getCoValueClass: () => typeof SchemaUnion;
};
export function enrichCoDiscriminatedUnionSchema<
Types extends readonly [
AnyDiscriminableCoSchema,
...AnyDiscriminableCoSchema[],
],
>(
schema: z.ZodDiscriminatedUnion<Types>,
coValueClass: typeof SchemaUnion,
): CoDiscriminatedUnionSchema<Types> {
return Object.assign(schema, {
load: (...args: [any, ...any]) => {
// @ts-expect-error
return coValueClass.load(...args);
},
subscribe: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.subscribe(...args);
},
getCoValueClass: () => {
return coValueClass;
},
}) as unknown as CoDiscriminatedUnionSchema<Types>;
}
type CoDiscriminatedUnionInstanceCoValuesNullable<
Types extends readonly [
AnyDiscriminableCoSchema,
...AnyDiscriminableCoSchema[],
],
> = NonNullable<InstanceOrPrimitiveOfSchemaCoValuesNullable<Types[number]>>;

View File

@@ -58,10 +58,35 @@ export type CoFeedSchema<T extends z.core.$ZodType> = z.core.$ZodCustom<
) => void,
): () => void;
getCoSchema: () => typeof CoFeed;
getCoValueClass: () => typeof CoFeed;
};
// less precise verion to avoid circularity issues and allow matching against
export function enrichCoFeedSchema<T extends z.core.$ZodType>(
schema: AnyCoFeedSchema<T>,
coValueClass: typeof CoFeed,
): CoFeedSchema<T> {
return Object.assign(schema, {
create: (...args: [any, ...any[]]) => {
return coValueClass.create(...args);
},
load: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.load(...args);
},
subscribe: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.subscribe(...args);
},
withHelpers: (helpers: (Self: z.core.$ZodType) => object) => {
return Object.assign(schema, helpers(schema));
},
getCoValueClass: () => {
return coValueClass;
},
}) as unknown as CoFeedSchema<T>;
}
// less precise version to avoid circularity issues and allow matching against
export type AnyCoFeedSchema<T extends z.core.$ZodType = z.core.$ZodType> =
z.core.$ZodCustom<any, unknown> & {
collaborative: true;

View File

@@ -19,9 +19,7 @@ type CoListInit<T extends z.core.$ZodType> = Array<
: NonNullable<InstanceOrPrimitiveOfSchemaCoValuesNullable<T>>
>;
export type CoListSchema<T extends z.core.$ZodType> = z.core.$ZodArray<T> & {
collaborative: true;
export type CoListSchema<T extends z.core.$ZodType> = AnyCoListSchema<T> & {
create: (
items: CoListInit<T>,
options?: { owner: Account | Group } | Account | Group,
@@ -52,10 +50,34 @@ export type CoListSchema<T extends z.core.$ZodType> = z.core.$ZodArray<T> & {
helpers: (Self: S) => T,
): WithHelpers<S, T>;
getCoSchema: () => typeof CoList;
getCoValueClass: () => typeof CoList;
};
// less precise verion to avoid circularity issues and allow matching against
export function enrichCoListSchema<T extends z.core.$ZodType>(
schema: AnyCoListSchema<T>,
coValueClass: typeof CoList,
): CoListSchema<T> {
return Object.assign(schema, {
create: (...args: [any, ...any[]]) => {
return coValueClass.create(...args);
},
load: (...args: [any, ...any[]]) => {
return coValueClass.load(...args);
},
subscribe: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.subscribe(...args);
},
withHelpers: (helpers: (Self: z.core.$ZodType) => object) => {
return Object.assign(schema, helpers(schema));
},
getCoValueClass: () => {
return coValueClass;
},
}) as unknown as CoListSchema<T>;
}
// less precise version to avoid circularity issues and allow matching against
export type AnyCoListSchema<T extends z.core.$ZodType = z.core.$ZodType> =
z.core.$ZodArray<T> & { collaborative: true };

View File

@@ -8,6 +8,7 @@ import {
Resolved,
Simplify,
SubscribeListenerOptions,
zodSchemaToCoSchema,
} from "../../../internal.js";
import { AnonymousJazzAgent } from "../../anonymousJazzAgent.js";
import { InstanceOrPrimitiveOfSchema } from "../typeConverters/InstanceOrPrimitiveOfSchema.js";
@@ -19,10 +20,8 @@ export type CoMapSchema<
Shape extends z.core.$ZodLooseShape,
Config extends z.core.$ZodObjectConfig = z.core.$ZodObjectConfig,
Owner extends Account | Group = Account | Group,
> = z.core.$ZodObject<Shape, Config> &
> = AnyCoMapSchema<Shape, Config> &
z.$ZodTypeDiscriminable & {
collaborative: true;
create: (
init: Simplify<CoMapInitZod<Shape>>,
options?:
@@ -145,9 +144,65 @@ export type CoMapSchema<
) => undefined,
): CoMapSchema<Shape, Config, Owner>;
getCoSchema: () => typeof CoMap;
getCoValueClass: () => typeof CoMap;
};
export function enrichCoMapSchema<
Shape extends z.core.$ZodLooseShape,
Config extends z.core.$ZodObjectConfig,
>(
schema: AnyCoMapSchema<Shape, Config>,
coValueClass: typeof CoMap,
): CoMapSchema<Shape, Config> {
// @ts-expect-error schema is actually a z.ZodObject, but we need to use z.core.$ZodObject to avoid circularity issues
const baseCatchall = schema.catchall;
const coValueSchema = Object.assign(schema, {
create: (...args: [any, ...any[]]) => {
return coValueClass.create(...args);
},
load: (...args: [any, ...any[]]) => {
return coValueClass.load(...args);
},
subscribe: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.subscribe(...args);
},
findUnique: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.findUnique(...args);
},
upsertUnique: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.upsertUnique(...args);
},
loadUnique: (...args: [any, ...any[]]) => {
// @ts-expect-error
return coValueClass.loadUnique(...args);
},
catchall: (index: z.core.$ZodType) => {
const newSchema = baseCatchall(index);
// TODO avoid repeating this with coMapDefiner
const enrichedSchema = Object.assign(newSchema, {
collaborative: true,
}) as AnyCoMapSchema<Shape, Config>;
return zodSchemaToCoSchema(enrichedSchema);
},
withHelpers: (helpers: (Self: z.core.$ZodType) => object) => {
return Object.assign(schema, helpers(schema));
},
withMigration: (migration: (value: any) => undefined) => {
// @ts-expect-error TODO check
coValueClass.prototype.migrate = migration;
return coValueSchema;
},
getCoValueClass: () => {
return coValueClass;
},
}) as unknown as CoMapSchema<Shape, Config>;
return coValueSchema;
}
export type optionalKeys<Shape extends z.core.$ZodLooseShape> = {
[key in keyof Shape]: Shape[key] extends z.core.$ZodOptional<any>
? key
@@ -170,7 +225,7 @@ export type CoMapInitZod<Shape extends z.core.$ZodLooseShape> = {
>;
} & { [key in keyof Shape]?: unknown };
// less precise verion to avoid circularity issues and allow matching against
// less precise version to avoid circularity issues and allow matching against
export type AnyCoMapSchema<
Shape extends z.core.$ZodLooseShape = z.core.$ZodLooseShape,
Config extends z.core.$ZodObjectConfig = z.core.$ZodObjectConfig,

View File

@@ -0,0 +1,33 @@
import { isAnyCoValueSchema } from "../runtimeConverters/zodSchemaToCoSchema.js";
import { z } from "../zodReExport.js";
import { AnyCoSchema, CoValueSchemaFromZodSchema } from "../zodSchema.js";
export type AnyCoOptionalSchema<
Shape extends z.core.$ZodType = z.core.$ZodType,
> = z.ZodOptional<Shape> & {
collaborative: true;
};
export type CoOptionalSchema<Shape extends z.core.$ZodType = z.core.$ZodType> =
AnyCoOptionalSchema<Shape> & {
getCoValueClass: () => CoValueSchemaFromZodSchema<AnyCoSchema>["getCoValueClass"];
};
export function createCoOptionalSchema<T extends AnyCoSchema>(
schema: T,
): CoOptionalSchema<T> {
return Object.assign(z.optional(schema), {
collaborative: true,
getCoValueClass: () => {
return (
schema as CoValueSchemaFromZodSchema<AnyCoSchema>
).getCoValueClass();
},
}) as unknown as CoOptionalSchema<T>;
}
export function isAnyCoOptionalSchema(
schema: z.core.$ZodType,
): schema is CoOptionalSchema<z.core.$ZodType> {
return isAnyCoValueSchema(schema) && schema._zod.def.type === "optional";
}

View File

@@ -28,9 +28,7 @@ type CoRecordInit<
export type CoRecordSchema<
K extends z.core.$ZodString<string>,
V extends z.core.$ZodType,
> = z.core.$ZodRecord<K, V> & {
collaborative: true;
> = AnyCoRecordSchema<K, V> & {
create: (
init: Simplify<CoRecordInit<K, V>>,
options?:
@@ -83,10 +81,10 @@ export type CoRecordSchema<
this: S,
helpers: (Self: S) => T,
): WithHelpers<S, T>;
getCoSchema: () => typeof CoMap;
getCoValueClass: () => typeof CoMap;
};
// less precise verion to avoid circularity issues and allow matching against
// less precise version to avoid circularity issues and allow matching against
export type AnyCoRecordSchema<
K extends z.core.$ZodString<string> = z.core.$ZodString<string>,
V extends z.core.$ZodType = z.core.$ZodType,

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