Compare commits

...

161 Commits

Author SHA1 Message Date
Nico Rainhart
e21cbccd4b Merge pull request #2674 from garden-co/changeset-release/main
Version Packages
2025-07-25 11:07:46 -03:00
github-actions[bot]
a66ab7d174 Version Packages 2025-07-25 13:34:52 +00:00
Nico Rainhart
78e91f4030 Merge pull request #2667 from garden-co/0-16
Jazz 0.16 upgrade
2025-07-25 10:32:40 -03:00
Guido D'Orsi
7a915c198e Merge pull request #2657 from garden-co/fix/root-trusting
feat: store the root id unencrypted in account
2025-07-25 14:39:59 +02:00
Guido D'Orsi
c9b0420746 Merge pull request #2672 from garden-co/chore/fix-conflicts-between-http-api-and-schema-refactoring
chore: Fix conflicts between HTTP requests and CoValue schema refactor
2025-07-25 14:39:16 +02:00
NicoR
2303f3e70a fix: schema definition error in server-http-worker example 2025-07-25 09:31:47 -03:00
NicoR
a7bc9569a3 chore: Fix conflicts between HTTP requests and CoValue schema refactor 2025-07-25 09:19:15 -03:00
NicoR
f351ba0fcd Merge remote-tracking branch 'origin/main' into chore/fix-conflicts-between-http-api-and-schema-refactoring 2025-07-25 09:12:05 -03:00
Guido D'Orsi
d3e554f491 Merge pull request #2671 from garden-co/chore/queues
chore: move cojson queues in a dedicated directory
2025-07-25 13:03:33 +02:00
Guido D'Orsi
b5e31456ad chore: trigger deploy 2025-07-25 12:25:05 +02:00
Guido D'Orsi
42d07ba7b4 docs: upgrade docs for account root id 2025-07-25 11:13:40 +02:00
Guido D'Orsi
b81b6ba69b Merge remote-tracking branch 'origin/0-16' into fix/root-trusting 2025-07-25 10:54:21 +02:00
Guido D'Orsi
1bc1759bb4 Merge remote-tracking branch 'origin/main' into chore/queues 2025-07-25 10:51:43 +02:00
Guido D'Orsi
225bc1f63f Merge pull request #2670 from garden-co/changeset-release/main
Version Packages
2025-07-25 10:37:04 +02:00
github-actions[bot]
5d94564f99 Version Packages 2025-07-25 08:30:50 +00:00
Guido D'Orsi
9633d0187f chore: changeset 2025-07-25 10:28:31 +02:00
Guido D'Orsi
b82ecaa3ca docs: add server worker http example card 2025-07-25 10:23:16 +02:00
Guido D'Orsi
111ec8d351 Merge pull request #2626 from garden-co/feat/http-requests
RPC style HTTP requests with CoValues
2025-07-25 10:18:10 +02:00
Nico Rainhart
512aacdbc2 Merge pull request #2669 from garden-co/fix/simplify-circular-constraint
fix: circular constraint type check error with `Simplify`
2025-07-24 17:07:53 -03:00
NicoR
7ad843aa3e fix: circular constraint type check error with Simplify 2025-07-24 16:18:21 -03:00
Guido D'Orsi
071128339b docs: fix typo 2025-07-24 21:03:55 +02:00
Guido D'Orsi
688ced499d feat: ux improvments on http example 2025-07-24 18:59:33 +02:00
Guido D'Orsi
ac91c8e7c2 Merge remote-tracking branch 'origin/main' into feat/http-requests 2025-07-24 18:23:00 +02:00
Guido D'Orsi
c3b303c310 feat: track handled messages in the request envelope 2025-07-24 18:21:23 +02:00
Guido D'Orsi
fc027a56db Merge pull request #2650 from garden-co/refactor/covalue-zod-schema-boundary
refactor: CoValue schemas are no longer Zod schemas
2025-07-24 17:41:09 +02:00
Guido D'Orsi
959a7a3927 Merge branch '0-16' into refactor/covalue-zod-schema-boundary 2025-07-24 17:40:51 +02:00
NicoR
2548085b59 Update upgrade guide and docs with improved support for recursive refs 2025-07-24 12:24:33 -03:00
Guido D'Orsi
b27bb3e65b test: remove .only 2025-07-24 17:21:04 +02:00
Guido D'Orsi
937284f7e9 Merge pull request #2666 from garden-co/changeset-release/main
Version Packages
2025-07-24 17:16:01 +02:00
github-actions[bot]
e999727c70 Version Packages 2025-07-24 14:58:34 +00:00
Guido D'Orsi
2197766624 Merge pull request #2665 from garden-co/fix/optional-ref-assign
fix: property update when assigning an optional reference on CoMap
2025-07-24 16:56:05 +02:00
Guido D'Orsi
d1efde468f Merge remote-tracking branch 'origin/main' into fix/root-trusting 2025-07-24 16:55:31 +02:00
Guido D'Orsi
4d4fd0beaa chore: use import content instead of copy on acceptInvite 2025-07-24 16:55:19 +02:00
Guido D'Orsi
2b61e853a7 test: cover recursive references without explicit return type 2025-07-24 16:43:35 +02:00
Guido D'Orsi
6f79b45544 chore: move cojson queues in a dedicated directory 2025-07-24 16:23:58 +02:00
Guido D'Orsi
2e1ff99579 chore: use import content instead of copy on acceptInvite 2025-07-24 16:04:43 +02:00
Guido D'Orsi
7361854ee4 chore: add isCoValueId check 2025-07-24 16:03:41 +02:00
Guido D'Orsi
4a775fada3 chore: remove unused import 2025-07-24 15:57:20 +02:00
Guido D'Orsi
3fe53a3a4a fix: property update when assigning an optional reference on CoMap 2025-07-24 12:19:48 +02:00
Guido D'Orsi
fe37516786 chore: remove TODO from play route 2025-07-24 12:18:43 +02:00
Guido D'Orsi
4beafb7cf3 fix: property update when assigning an optional reference on CoMap 2025-07-24 12:16:35 +02:00
Guido D'Orsi
82a592e08a feat(ci): add server-worker-http to ci tests 2025-07-24 11:45:15 +02:00
Guido D'Orsi
4c6926153a chore: simplify groups in createPlayer 2025-07-24 11:45:15 +02:00
Guido D'Orsi
c51b088243 Update examples/server-worker-http/src/apiKey.ts
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-07-24 11:36:55 +02:00
Guido D'Orsi
3a1fdd7600 docs: remove per-user limits exception 2025-07-23 18:28:30 +02:00
Guido D'Orsi
3fdbb43b54 docs: added the security safeguards provided by Jazz 2025-07-23 18:20:04 +02:00
Guido D'Orsi
02969ee89b docs: pr feedback 2025-07-23 17:49:39 +02:00
Guido D'Orsi
9b4988a514 docs: improve deploy requirements docs 2025-07-23 17:27:15 +02:00
Guido D'Orsi
8aa4b59d49 chore: fix typo
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-07-23 16:39:27 +02:00
NicoR
ac782674de One more Upgrade guide tweak 2025-07-23 11:24:14 -03:00
NicoR
5eb406d54d Upgrade guide adjustments 2025-07-23 11:03:47 -03:00
NicoR
a3be832414 Remove WithHelpers export 2025-07-23 10:55:35 -03:00
NicoR
7ca8dd960e Merge branch 'main' into refactor/covalue-zod-schema-boundary 2025-07-23 10:50:08 -03:00
NicoR
62c8aff73f Fix Upgrade guide navigation 2025-07-23 10:24:53 -03:00
NicoR
7731109a28 Address Upgrade guide comments 2025-07-23 10:08:14 -03:00
Guido D'Orsi
4a29999c6a feat: support worker groups 2025-07-23 14:07:09 +02:00
Guido D'Orsi
73b99c6c1a docs: http request jsDoc 2025-07-23 13:07:51 +02:00
Guido D'Orsi
039b92c839 feat: export API and jsdocs 2025-07-23 13:03:58 +02:00
Guido D'Orsi
618af5f1e3 feat: e2e tests and improve UX on server-worker-http 2025-07-23 12:50:48 +02:00
NicoR
dfc4286694 Return resolved resized images on createImage 2025-07-22 15:15:02 -03:00
NicoR
970ff0d813 Fix docs 2025-07-22 14:57:57 -03:00
Guido D'Orsi
65eee0ef01 docs: update the server-worker-http readme 2025-07-22 18:39:11 +02:00
NicoR
eee221f563 Merge branch 'main' into refactor/covalue-zod-schema-boundary 2025-07-22 13:38:47 -03:00
Guido D'Orsi
97f6bcedbd test: remove failing test 2025-07-22 18:24:07 +02:00
Guido D'Orsi
7c63e6bb0f docs: mention jazz-run account create defaults 2025-07-22 18:07:06 +02:00
Guido D'Orsi
08aedcf517 docs: fix typos 2025-07-22 17:53:08 +02:00
Guido D'Orsi
3e12ee127f docs: cover the inbox API 2025-07-22 17:52:06 +02:00
NicoR
2283d375ef Update docs 2025-07-22 12:17:49 -03:00
NicoR
202e763380 Merge branch 'main' into refactor/covalue-zod-schema-boundary 2025-07-22 12:07:57 -03:00
NicoR
52bbdb37a9 Add .optional() to all CoValue schemas 2025-07-22 12:06:21 -03:00
Guido D'Orsi
96f743b2f4 Merge remote-tracking branch 'origin/main' into feat/http-requests 2025-07-22 16:38:26 +02:00
NicoR
f5c47feeb6 Upgrade Zod to 3.25.76 instead 2025-07-22 11:33:01 -03:00
Guido D'Orsi
6f9ee31179 feat: rename serve-worker examples 2025-07-22 16:23:05 +02:00
Guido D'Orsi
52f324ffc4 chore: update lockfile 2025-07-22 16:21:34 +02:00
Guido D'Orsi
2d86f53575 Merge remote-tracking branch 'origin/main' into feat/http-requests 2025-07-22 16:20:49 +02:00
Guido D'Orsi
56ccf9ab9d feat: make it possible to define API with empty responses 2025-07-22 15:57:57 +02:00
NicoR
b8b0851433 Add upgrade guide 2025-07-22 10:42:46 -03:00
NicoR
2bbb07b0bf Add changeset 2025-07-22 10:01:00 -03:00
NicoR
d3053955d8 Update docs 2025-07-22 09:34:40 -03:00
NicoR
f40484eca9 Migrate plain text, rich text, file stream and optional schemas to classes 2025-07-21 17:00:57 -03:00
NicoR
d581a59aa1 Convert CoDiscriminatedUnionSchema into a class 2025-07-21 16:39:43 -03:00
NicoR
0ca09f75c1 Convert CoFeedSchema into a class 2025-07-21 16:30:04 -03:00
NicoR
e8fcd101f2 Convert CoListSchema into a class 2025-07-21 16:24:35 -03:00
NicoR
cf43fa7529 Remove usage of withHelpers 2025-07-21 15:23:36 -03:00
NicoR
df1cdda4e8 Update zod version in all examples 2025-07-21 15:15:51 -03:00
Guido D'Orsi
be46042cdc chore: fix typos and errors 2025-07-21 20:12:39 +02:00
NicoR
7a60d7bb76 Avoid NotNull duplication 2025-07-21 14:33:22 -03:00
NicoR
f8263a8358 Stop extending Zod schemas when creating core CoValue schemas 2025-07-21 14:25:23 -03:00
NicoR
f6da966922 Explain "core" CoValue schema / actual CoValue schema distinction 2025-07-21 14:21:44 -03:00
NicoR
8a2ab51543 Expose internal schemas for co.map, co.list, co.optional and co.account 2025-07-21 14:09:48 -03:00
Guido D'Orsi
8bfaa0a18b docs: document deployments 2025-07-21 19:00:19 +02:00
Guido D'Orsi
147be76399 test: cover deep response sharing 2025-07-21 16:51:24 +02:00
Guido D'Orsi
36770bed52 fix: limit the content pieces sent to server to the envelope 2025-07-21 16:45:40 +02:00
NicoR
466e6c44ee Remove unused imports 2025-07-21 11:43:11 -03:00
NicoR
5bd8277161 Remove withHelpers schema method 2025-07-21 11:41:28 -03:00
Guido D'Orsi
8b4261f7d8 docs: pr feedback 2025-07-21 16:13:59 +02:00
NicoR
0ec917e453 Remove non-namespaced CoValue schema exports 2025-07-21 10:42:23 -03:00
NicoR
6326d0fc45 Export co.Image type 2025-07-21 10:30:55 -03:00
NicoR
d746b1279a Remove deprecated createCoValueObservable function 2025-07-21 10:18:54 -03:00
Guido D'Orsi
c09dcdfc76 feat: make the root trusting 2025-07-21 12:06:28 +02:00
NicoR
4402c553b6 Fix z.object bug with cyclic references 2025-07-18 15:44:54 -03:00
NicoR
e76fe343da Fix co.discriminatedUnion with cyclic references 2025-07-18 15:10:53 -03:00
Guido D'Orsi
dc183a19b2 feat: show that the opponent has made their move 2025-07-18 19:51:10 +02:00
Guido D'Orsi
fef55a4cd6 fix: remove bad export on route 2025-07-18 19:34:47 +02:00
Guido D'Orsi
ddef54048f docs: fix type error 2025-07-18 19:26:40 +02:00
NicoR
a2626a0f38 Avoid rehydrating CoValue schemas 2025-07-18 14:21:27 -03:00
NicoR
ec579bcaf7 Go back to using tuple for discriminatedUnion's options type 2025-07-18 14:19:53 -03:00
Guido D'Orsi
8aa2d2a789 docs: fix type errors on organization pattern 2025-07-18 19:12:24 +02:00
Guido D'Orsi
a39d009b87 docs: http requests & server workers 2025-07-18 19:00:36 +02:00
NicoR
e9af90c841 Fix Zod type messing up Zod's type inference 2025-07-17 17:06:37 -03:00
NicoR
2b7c6f5aa7 Rename anySchemaToCoSchema to coValueClassFromCoValueClassOrSchema 2025-07-17 16:55:43 -03:00
NicoR
d73a3d9d46 Rename files 2025-07-17 16:52:56 -03:00
NicoR
8af39077a3 Remove CoValueSchema.getZodSchema 2025-07-17 16:44:48 -03:00
NicoR
54bd487818 PlainText, RichText and FileStream schemas are no longer Zod schemas 2025-07-17 16:30:09 -03:00
Guido D'Orsi
f01dab5c8f test: cover requests error management 2025-07-17 20:29:02 +02:00
Guido D'Orsi
a7f6870048 chore: update package json 2025-07-17 19:43:37 +02:00
Guido D'Orsi
3b294f6994 Merge remote-tracking branch 'origin/main' into feat/http-requests 2025-07-17 19:42:56 +02:00
NicoR
a420b43029 Add CoDiscriminatedUnionSchema.optional() 2025-07-17 14:29:52 -03:00
NicoR
a57268de32 Add runtime check to prevent using z.object with coValues as values 2025-07-17 14:12:41 -03:00
NicoR
6b2c4ed280 Remove no longer necessary Zod re-export wrappers 2025-07-17 14:12:41 -03:00
NicoR
8d4e0027be Upgrade Zod to 4.0.5 2025-07-17 14:12:41 -03:00
NicoR
a4141da1b7 Drop support for z.optional CoValue schemas 2025-07-17 14:12:41 -03:00
NicoR
c9ca5202f9 Fix browser integration tests 2025-07-17 14:12:41 -03:00
NicoR
7b50a2e06d Avoid using core.$ZodTypeDiscriminable for CoDiscriminatedUnions 2025-07-17 14:12:40 -03:00
NicoR
43dabccb57 [WIP] Discriminable CoValue schemas no longer extend $ZodDiscriminatedUnion 2025-07-17 14:12:40 -03:00
NicoR
b6d04f56ef Preserve catchall type info in CoMapSchema 2025-07-17 14:12:40 -03:00
NicoR
628195b678 Remove unused types from test 2025-07-17 14:12:40 -03:00
NicoR
9a5d769717 Use CoValue schema types (instead of Zod's) in circular references 2025-07-17 14:12:40 -03:00
NicoR
e30a3f66bf CoValue schemas are no longer Zod schemas 2025-07-17 14:12:40 -03:00
NicoR
6327fce933 Refactor CoValue instances & Zod primitives type inference 2025-07-17 14:12:40 -03:00
NicoR
a650da4184 Fix bug with deeply nested discriminated unions 2025-07-17 14:12:40 -03:00
NicoR
6e4a94f6ce Avoid accessing CoDiscriminatedUnion Zod internals directly 2025-07-17 14:12:40 -03:00
NicoR
b73bec64bc Rename AnyCoSchemas to CoreCoValueSchemas 2025-07-17 14:12:40 -03:00
NicoR
50ae2f47c2 Remove CoValue schema cache 2025-07-17 14:12:40 -03:00
NicoR
724d8e7f30 Drop support for z.discriminatedUnion of CoValue schemas 2025-07-17 14:12:40 -03:00
NicoR
7b285ab110 Modify CoMap schema to no longer extend ZodObject 2025-07-17 14:12:40 -03:00
NicoR
01ac9b8c4c Rewrite tests that access Zod internals 2025-07-17 14:12:40 -03:00
NicoR
4e2e1ac73e Convert CoValue schemas into interfaces 2025-07-17 14:12:40 -03:00
NicoR
94960c1f65 Convert CoValue schemas into a discriminated union 2025-07-17 14:12:40 -03:00
NicoR
b5af58347b Add getZodSchema method to CoValue schemas 2025-07-17 14:12:40 -03:00
NicoR
46a84558c5 Clean up InstanceOfSchema types 2025-07-17 14:12:40 -03:00
NicoR
f93566c045 Replace references to z.core.$ZodType with AnyZodSchema 2025-07-17 14:12:39 -03:00
NicoR
d97ed603a3 Tighten CoOptionalSchema inner type 2025-07-17 14:12:39 -03:00
NicoR
8d33103182 Rename zodSchemaToCoSchema to coreSchemaToCoSchema 2025-07-17 14:12:39 -03:00
NicoR
aaa1ff978b Organize Schema Union types 2025-07-17 14:12:39 -03:00
NicoR
82655ea7a7 Stop exporting zodSchemaToCoSchema 2025-07-17 14:12:39 -03:00
NicoR
8afe3a2e02 Remove unnecessary usages of zodSchemaToCoSchema 2025-07-17 14:12:39 -03:00
NicoR
ae2adcbd15 Extract functions to create CoreCoSchemas 2025-07-17 14:12:39 -03:00
NicoR
eb0460d330 Revert https://github.com/garden-co/jazz/pull/2651 2025-07-17 14:12:30 -03:00
Guido D'Orsi
255a947ea6 feat: add error management 2025-07-15 20:22:04 +02:00
Guido D'Orsi
530a263d35 chore: add vercel.json 2025-07-15 18:42:05 +02:00
Guido D'Orsi
745020b7a8 chore: refactor code 2025-07-15 18:06:37 +02:00
Guido D'Orsi
991aebf7a7 feat: only accept init payload and improve auth checks 2025-07-14 21:00:08 +02:00
Guido D'Orsi
9b75880b10 feat: bestEffortResolution on export 2025-07-14 18:53:44 +02:00
Guido D'Orsi
98fe72ed42 fix: push players data on joinGameRequest 2025-07-14 15:00:47 +02:00
Guido D'Orsi
1f5c81c2ea feat: nicer example UI 2025-07-14 14:45:16 +02:00
Guido D'Orsi
fc41aa165b feat: support CoMapInit as payload for send/response 2025-07-14 14:29:20 +02:00
Guido D'Orsi
41466ea399 feat: simplify the API 2025-07-11 20:01:30 +02:00
Guido D'Orsi
3b91594d10 feat: migrate the jazz-paper-scissors to request & nextjs and rename it to server-side-validation 2025-07-11 18:11:47 +02:00
Guido D'Orsi
265a6405af feat: implement the payload/response schema spec 2025-07-10 16:56:30 +02:00
Guido D'Orsi
a4d23d527b feat: add content import/export to the experimental_request and move into tools 2025-07-09 18:43:53 +02:00
191 changed files with 9448 additions and 2538 deletions

View File

@@ -55,6 +55,7 @@ jobs:
"examples/inspector"
"examples/music-player"
"examples/organization"
"examples/server-worker-http"
"starters/react-passkey-auth"
"starters/svelte-passkey-auth"
"tests/jazz-svelte"

View File

@@ -1,5 +1,37 @@
# passkey-svelte
## 0.0.106
### Patch Changes
- 2bbb07b: Introduce a cleaner separation between Zod and CoValue schemas:
- Zod schemas and CoValue schemas are fully separated. Zod schemas can only be composed with other Zod schemas. CoValue schemas can be composed with either Zod or other CoValue schemas.
- `z.optional()` and `z.discriminatedUnion()` no longer work with CoValue schemas. Use `co.optional()` and `co.discriminatedUnion()` instead.
- Internal schema access is now simpler. You no longer need to use Zods `.def` to access internals. Use properties like `CoMapSchema.shape`, `CoListSchema.element`, and `CoOptionalSchema.innerType` directly.
- CoValue schema types are now namespaced under `co.`. Non-namespaced exports have been removed
- CoMap schemas no longer incorrectly inherit from Zod. Previously, methods like `.extend()` and `.partial()` appeared available but could cause unexpected behavior. These methods are now disabled. In their place, `.optional()` has been added, and more Zod-like methods will be introduced in future releases.
- Upgraded Zod from `3.25.28` to `3.25.76`.
- Removed deprecated `withHelpers` method from CoValue schemas
- Removed deprecated `createCoValueObservable` function
- Updated dependencies [c09dcdf]
- Updated dependencies [2bbb07b]
- jazz-tools@0.16.0
## 0.0.105
### Patch Changes
- Updated dependencies [9633d01]
- Updated dependencies [4beafb7]
- jazz-tools@0.15.16
## 0.0.104
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
## 0.0.103
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-svelte",
"version": "0.0.103",
"version": "0.0.106",
"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 const Chat = co.list(Message);

View File

@@ -18,7 +18,7 @@
"lucide-react": "^0.274.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "3.25.28"
"zod": "3.25.76"
},
"devDependencies": {
"@playwright/test": "^1.50.1",

View File

@@ -10,6 +10,7 @@ import {
DraftBubbleTeaOrder,
JazzAccount,
ListOfBubbleTeaAddOns,
validateDraftOrder,
} from "./schema.ts";
export function CreateOrder() {
@@ -22,8 +23,7 @@ export function CreateOrder() {
if (!me?.root) return;
const onSave = (draft: Loaded<typeof DraftBubbleTeaOrder>) => {
// validate if the draft is a valid order
const validation = DraftBubbleTeaOrder.validate(draft);
const validation = validateDraftOrder(draft);
setErrors(validation.errors);
if (validation.errors.length > 0) {
return;

View File

@@ -1,11 +1,11 @@
import { useAccount } from "jazz-tools/react";
import { DraftBubbleTeaOrder, JazzAccount } from "./schema";
import { JazzAccount, hasChanges } from "./schema";
export function DraftIndicator() {
const { me } = useAccount(JazzAccount, {
resolve: { root: { draft: true } },
});
if (DraftBubbleTeaOrder.hasChanges(me?.root.draft)) {
if (hasChanges(me?.root.draft)) {
return (
<div className="absolute -top-1 -right-1 bg-blue-500 border-2 border-white w-3 h-3 rounded-full dark:border-stone-925">
<span className="sr-only">You have a draft</span>

View File

@@ -15,13 +15,13 @@ export const BubbleTeaBaseTeaTypes = [
"Thai",
] as const;
export const ListOfBubbleTeaAddOns = co
.list(z.literal([...BubbleTeaAddOnTypes]))
.withHelpers((Self) => ({
hasChanges(list?: Loaded<typeof Self> | null) {
return list && Object.entries(list._raw.insertions).length > 0;
},
}));
export const ListOfBubbleTeaAddOns = co.list(
z.literal([...BubbleTeaAddOnTypes]),
);
function hasAddOnsChanges(list?: Loaded<typeof ListOfBubbleTeaAddOns> | null) {
return list && Object.entries(list._raw.insertions).length > 0;
}
export const BubbleTeaOrder = co.map({
baseTea: z.literal([...BubbleTeaBaseTeaTypes]),
@@ -31,36 +31,33 @@ export const BubbleTeaOrder = co.map({
instructions: co.optional(co.plainText()),
});
export const DraftBubbleTeaOrder = co
.map({
baseTea: z.optional(z.literal([...BubbleTeaBaseTeaTypes])),
addOns: co.optional(ListOfBubbleTeaAddOns),
deliveryDate: z.optional(z.date()),
withMilk: z.optional(z.boolean()),
instructions: co.optional(co.plainText()),
})
.withHelpers((Self) => ({
hasChanges(order: Loaded<typeof Self> | undefined) {
return (
!!order &&
(Object.keys(order._edits).length > 1 ||
ListOfBubbleTeaAddOns.hasChanges(order.addOns))
);
},
export const DraftBubbleTeaOrder = co.map({
baseTea: z.optional(z.literal([...BubbleTeaBaseTeaTypes])),
addOns: co.optional(ListOfBubbleTeaAddOns),
deliveryDate: z.optional(z.date()),
withMilk: z.optional(z.boolean()),
instructions: co.optional(co.plainText()),
});
validate(order: Loaded<typeof Self>) {
const errors: string[] = [];
export function validateDraftOrder(order: Loaded<typeof DraftBubbleTeaOrder>) {
const errors: string[] = [];
if (!order.baseTea) {
errors.push("Please select your preferred base tea.");
}
if (!order.deliveryDate) {
errors.push("Plese select a delivery date.");
}
if (!order.baseTea) {
errors.push("Please select your preferred base tea.");
}
if (!order.deliveryDate) {
errors.push("Plese select a delivery date.");
}
return { errors };
},
}));
return { errors };
}
export function hasChanges(order?: Loaded<typeof DraftBubbleTeaOrder> | null) {
return (
!!order &&
(Object.keys(order._edits).length > 1 || hasAddOnsChanges(order.addOns))
);
}
/** The root is an app-specific per-user private `CoMap`
* where you can store top-level objects for that user */

View File

@@ -11,9 +11,9 @@ export const Issue = co.map({
status: z.enum(["open", "closed"]),
labels: co.list(z.string()),
reactions: ReactionsList,
file: z.optional(co.fileStream()),
image: z.optional(co.image()),
lead: z.optional(co.account()),
file: co.optional(co.fileStream()),
image: co.optional(co.image()),
lead: co.optional(co.account()),
});
export const Project = co.map({

View File

@@ -0,0 +1 @@
export const apiKey = "jazz-nextjs@garden.co";

View File

@@ -1,12 +1,13 @@
import { JazzInspector } from "jazz-tools/inspector";
import { JazzReactProvider } from "jazz-tools/react";
import { apiKey } from "./apiKey";
export function Jazz({ children }: { children: React.ReactNode }) {
return (
<JazzReactProvider
enableSSR
sync={{
peer: `wss://cloud.jazz.tools/`,
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}

View File

@@ -15,7 +15,7 @@
"jazz-tools": "workspace:*",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "3.25.28"
"zod": "3.25.76"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
@@ -31,4 +31,4 @@
"vite": "^6.3.5",
"vitest": "3.1.1"
}
}
}

View File

@@ -7,6 +7,7 @@ import {
JazzAccount,
Organization,
Project,
validateDraftOrganization,
} from "../schema.ts";
import { Errors } from "./Errors.tsx";
import { OrganizationForm } from "./OrganizationForm.tsx";
@@ -21,8 +22,7 @@ export function CreateOrganization() {
if (!me?.root?.organizations) return;
const onSave = (draft: Loaded<typeof DraftOrganization>) => {
// validate if the draft is a valid organization
const validation = DraftOrganization.validate(draft);
const validation = validateDraftOrganization(draft);
setErrors(validation.errors);
if (validation.errors.length > 0) {
return;

View File

@@ -10,24 +10,24 @@ export const Organization = co.map({
projects: co.list(Project),
});
export const DraftOrganization = co
.map({
name: z.optional(z.string()),
projects: co.list(Project),
})
.withHelpers((Self) => ({
validate(org: Loaded<typeof Self>) {
const errors: string[] = [];
export const DraftOrganization = co.map({
name: z.optional(z.string()),
projects: co.list(Project),
});
if (!org.name) {
errors.push("Please enter a name.");
}
export function validateDraftOrganization(
org: Loaded<typeof DraftOrganization>,
) {
const errors: string[] = [];
return {
errors,
};
},
}));
if (!org.name) {
errors.push("Please enter a name.");
}
return {
errors,
};
}
export const JazzAccountRoot = co.map({
organizations: co.list(Organization),

47
examples/server-worker-http/.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# env files
.env
.env.*
!.env.example
!.env.test

View File

@@ -0,0 +1,105 @@
# Rock Paper Scissors with Jazz HTTP API
This example demonstrates how to use the **Jazz HTTP API** with **Next.js** to implement updates in a trusted environment. The application implements a multiplayer Rock Paper Scissors game where the server validates game actions and reveals player intentions only after both players have made their moves.
## 🎯 Key Concepts Demonstrated
### Trusted Environment Updates
- **Server-side validation**: All game actions are validated by the server before being applied
- **Secure reveal mechanism**: Player choices are hidden until both players have acted, built with Jazz group permissions
## 🚀 Getting Started
### Prerequisites
- Node.js 20+
- pnpm (recommended) or npm
### Installation
1. **Install dependencies**:
```bash
pnpm install
```
2. **Generate environment variables**:
```bash
pnpm generate-env
```
3. **Start the development server**:
```bash
pnpm dev
```
4. **Open your browser** to [http://localhost:3000](http://localhost:3000)
### Environment Setup
The `generate-env.ts` script creates the necessary environment variables for Jazz:
- `NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT`: Server worker account ID
- `JAZZ_WORKER_SECRET`: Server worker secret key
## 🔧 Key Implementation Details
### Server API Definition
```typescript
const playRequest = experimental_defineRequest({
url: "/api/play",
workerId,
request: {
schema: {
game: Game,
selection: z.literal(["rock", "paper", "scissors"]),
},
resolve: {
game: {
player1: { account: true, playSelection: { group: true } },
player2: { account: true, playSelection: { group: true } },
},
},
},
response: {
schema: { game: Game },
resolve: { game: true },
},
});
```
### Secure Move Handling
```typescript
// Create restricted group for the move
const group = Group.create({ owner: jazzServerAccount.worker });
group.addMember(madeBy, "reader");
// Store move with restricted access
const playSelection = PlaySelection.create(
{ value: selection, group },
group,
);
// Reveal moves only after both players have acted
if (player1PlaySelection && player2PlaySelection) {
player1PlaySelection.group.addMember(game.player2.account, "reader");
player2PlaySelection.group.addMember(game.player1.account, "reader");
}
```
## 🎯 Use Cases
This pattern is ideal for applications requiring:
- **Fair play guarantees**: Preventing cheating in games
- **Simultaneous reveals**: Auctions, voting, or sealed-bid systems
- **Trusted computation**: Server-side validation of complex business logic
- **Real-time collaboration**: Multi-user applications with strict rules
## 📚 Learn More
- [Jazz Documentation](https://jazz.tools/docs)
- [Next.js App Router](https://nextjs.org/docs/app)
- [Groups & Permissions](https://jazz.tools/docs/react/groups/intro)
- [HTTP API with experimental_defineRequest](https://jazz.tools/docs/react/server-side/http-requests)
## 🤝 Contributing
This example is part of the Jazz framework. Feel free to submit issues and enhancement requests!

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,26 @@
import * as fs from "fs";
import { createWorkerAccount } from "jazz-run/createWorkerAccount";
if (fs.existsSync(".env")) {
process.exit(0);
}
async function main() {
const account = await createWorkerAccount({
name: "jazz-paper-scissors-worker",
peer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co",
});
fs.writeFileSync(
".env",
`
JAZZ_WORKER_ACCOUNT=${account.accountID}
NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT=${account.accountID}
JAZZ_WORKER_SECRET=${account.agentSecret}
`,
);
process.exit(0);
}
main();

View File

@@ -0,0 +1,5 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;

View File

@@ -0,0 +1,40 @@
{
"name": "server-worker-http",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"generate-env": "tsx generate-env.ts",
"build": "pnpm generate-env && next build",
"start": "next start",
"lint": "next lint",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/postcss": "^4.1.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.525.0",
"next": "15.3.2",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"jazz-run": "workspace:*",
"tsx": "^4.19.3",
"tw-animate-css": "^1.2.5",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,55 @@
import { defineConfig, devices } from "@playwright/test";
import isCI from "is-ci";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
timeout: 20_000,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm start",
url: "http://localhost:3000/",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -0,0 +1,2 @@
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;

View File

@@ -0,0 +1 @@
export const apiKey = "server-side-validation@garden.co";

View File

@@ -0,0 +1,23 @@
import { jazzServerAccount } from "@/jazzServerAccount";
import { WaitingRoom } from "@/schema";
import { serverApi } from "@/serverApi";
import { Group } from "jazz-tools";
export async function POST(request: Request) {
const response = await serverApi.createGame.handle(
request,
jazzServerAccount.worker,
async (_, madeBy) => {
const waitingRoom = WaitingRoom.create(
{ creator: madeBy },
Group.create(jazzServerAccount.worker).makePublic(),
);
return {
waitingRoom,
};
},
);
return response;
}

View File

@@ -0,0 +1,49 @@
import { jazzServerAccount } from "@/jazzServerAccount";
import { Game, createGameState } from "@/schema";
import { serverApi } from "@/serverApi";
import { Account, Group, JazzRequestError } from "jazz-tools";
export async function POST(request: Request) {
return serverApi.joinGame.handle(
request,
jazzServerAccount.worker,
async ({ waitingRoom }, madeBy) => {
if (madeBy.id === waitingRoom.creator.id) {
throw new JazzRequestError("You can't join your own waiting room", 400);
}
waitingRoom.game = createGame({
account1: waitingRoom.creator,
account2: madeBy,
worker: jazzServerAccount.worker,
});
return {
waitingRoom,
};
},
);
}
interface CreateGameParams {
account1: Account;
account2: Account;
worker: Account;
}
function createGame({ account1, account2, worker }: CreateGameParams) {
const gameGroup = Group.create({ owner: worker });
gameGroup.addMember(account1, "reader");
gameGroup.addMember(account2, "reader");
const game = Game.create(
{
...createGameState({ account1, account2, worker }),
player1Score: 0,
player2Score: 0,
},
gameGroup,
);
return game;
}

View File

@@ -0,0 +1,35 @@
import { jazzServerAccount } from "@/jazzServerAccount";
import { createGameState } from "@/schema";
import { serverApi } from "@/serverApi";
import { JazzRequestError } from "jazz-tools";
export async function POST(request: Request) {
const response = await serverApi.newGame.handle(
request,
jazzServerAccount.worker,
async ({ game }, madeBy) => {
const isPlayer1 = game.player1.account.id === madeBy.id;
const isPlayer2 = game.player2.account.id === madeBy.id;
if (!isPlayer1 && !isPlayer2) {
throw new JazzRequestError("You are not a player in this game", 400);
}
if (game.outcome) {
game.applyDiff(
createGameState({
account1: game.player1.account,
account2: game.player2.account,
worker: jazzServerAccount.worker,
}),
);
}
return {
game,
};
},
);
return response;
}

View File

@@ -0,0 +1,89 @@
import { jazzServerAccount } from "@/jazzServerAccount";
import { PlaySelection } from "@/schema";
import { serverApi } from "@/serverApi";
import { Group, JazzRequestError } from "jazz-tools";
export async function POST(request: Request) {
const response = await serverApi.play.handle(
request,
jazzServerAccount.worker,
async ({ game, selection }, madeBy) => {
const isPlayer1 = game.player1.account.id === madeBy.id;
const isPlayer2 = game.player2.account.id === madeBy.id;
if (!isPlayer1 && !isPlayer2) {
throw new JazzRequestError("You are not a player in this game", 400);
}
const group = Group.create({ owner: jazzServerAccount.worker });
group.addMember(madeBy, "reader");
if (isPlayer1 && game.player1.playSelection) {
throw new JazzRequestError("You have already played", 400);
} else if (isPlayer2 && game.player2.playSelection) {
throw new JazzRequestError("You have already played", 400);
}
const playSelection = PlaySelection.create(
{ value: selection, group },
group,
);
if (isPlayer1) {
game.player1.playSelection = playSelection;
} else {
game.player2.playSelection = playSelection;
}
if (game.player1.playSelection && game.player2.playSelection) {
game.outcome = determineOutcome(
game.player1.playSelection.value,
game.player2.playSelection.value,
);
// Reveal the play selections to the other player
game.player1.playSelection.group.addMember(
game.player2.account,
"reader",
);
game.player2.playSelection.group.addMember(
game.player1.account,
"reader",
);
if (game.outcome === "player1") {
game.player1Score++;
} else if (game.outcome === "player2") {
game.player2Score++;
}
}
return {
game,
result: "success",
};
},
);
return response;
}
/**
* Given a player selections, returns the winner of the current game.
*/
function determineOutcome(
player1Choice: "rock" | "paper" | "scissors",
player2Choice: "rock" | "paper" | "scissors",
) {
if (player1Choice === player2Choice) {
return "draw";
} else if (
(player1Choice === "rock" && player2Choice === "scissors") ||
(player1Choice === "paper" && player2Choice === "rock") ||
(player1Choice === "scissors" && player2Choice === "paper")
) {
return "player1";
} else {
return "player2";
}
}

View File

@@ -0,0 +1,388 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Game, PlaySelection, PlayerState } from "@/schema";
import { serverApi } from "@/serverApi";
import { Group, isJazzRequestError } from "jazz-tools";
import { useAccount, useCoState } from "jazz-tools/react";
import {
CheckCircle,
Clock,
Gamepad2,
Minus,
Sparkles,
Trophy,
Users,
XCircle,
} from "lucide-react";
import { useParams } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
const playIcon = (
selection: "rock" | "paper" | "scissors" | undefined,
size: "sm" | "lg" = "sm",
) => {
const emojiSize = size === "lg" ? "text-4xl" : "text-2xl";
switch (selection) {
case "rock":
return (
<div className="flex items-center gap-2">
<span className={`${emojiSize} `} style={{ animationDelay: "0ms" }}>
🪨
</span>
<span className="font-semibold">Rock</span>
</div>
);
case "paper":
return (
<div className="flex items-center gap-2">
<span className={`${emojiSize} `} style={{ animationDelay: "150ms" }}>
📄
</span>
<span className="font-semibold">Paper</span>
</div>
);
case "scissors":
return (
<div className="flex items-center gap-2">
<span className={`${emojiSize} `} style={{ animationDelay: "300ms" }}>
</span>
<span className="font-semibold">Scissors</span>
</div>
);
default:
return (
<div className="flex items-center gap-2 text-muted-foreground">
<Clock className={size === "lg" ? "w-8 h-8" : "w-6 h-6"} />
<span>Waiting for selection</span>
</div>
);
}
};
const getOutcomeIcon = (outcome: string | undefined, player: string) => {
if (outcome === player) {
return <Trophy className="w-6 h-6 text-yellow-500" />;
} else if (outcome === "draw") {
return <Minus className="w-6 h-6 text-blue-500" />;
} else {
return <XCircle className="w-6 h-6 text-red-500" />;
}
};
export default function RouteComponent() {
const params = useParams<{ id: string }>();
const game = useCoState(Game, params.id, {
resolve: {
player1State: {
$onError: null,
},
player2State: {
$onError: null,
},
player1: {
account: true,
playSelection: {
$onError: null,
},
},
player2: {
account: true,
playSelection: {
$onError: null,
},
},
},
});
const isPlayer1 = game?.player1?.account?.isMe;
const player = isPlayer1 ? "player1" : "player2";
if (!game) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center">
<div className="animate-pulse">
<Gamepad2 className="w-12 h-12 text-muted-foreground" />
</div>
</div>
);
}
const gameComplete = game.outcome !== undefined;
const opponent = isPlayer1 ? "player2" : "player1";
const currentPlayer = game[player];
const opponentPlayer = game[opponent];
const currentPlayerState = game[isPlayer1 ? "player1State" : "player2State"];
const opponentSelection = opponentPlayer?.playSelection;
const opponentHasSelected = Boolean(opponentPlayer._refs.playSelection);
const handleSelection = (selection: "rock" | "paper" | "scissors") => {
if (!currentPlayerState) return;
if (currentPlayerState.submitted) return;
currentPlayerState.currentSelection = selection;
};
const handleSubmit = async (
playSelection: "rock" | "paper" | "scissors" | undefined,
) => {
if (!playSelection) return;
if (!currentPlayerState) return;
currentPlayerState.submitted = true;
try {
await serverApi.play.send({
game,
selection: playSelection,
});
} catch (error) {
currentPlayerState.submitted = false;
if (isJazzRequestError(error)) {
toast.error(error.message);
} else {
console.error(error);
toast.error("An unexpected error occurred");
}
}
};
const handleNewGame = async () => {
if (!currentPlayerState) return;
currentPlayerState.resetRequested = true;
try {
await serverApi.newGame.send({
game,
});
} catch (error) {
currentPlayerState.resetRequested = false;
if (isJazzRequestError(error)) {
toast.error(error.message);
} else {
console.error(error);
toast.error("An unexpected error occurred");
}
}
};
const playSelection = currentPlayerState?.currentSelection;
const submitDisabled =
!currentPlayerState?.currentSelection || currentPlayerState?.submitted;
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-8 px-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<div className="flex items-center justify-center gap-2 mb-4">
<Gamepad2 className="w-8 h-8 text-primary" />
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-purple-600 bg-clip-text text-transparent">
Rock, Paper, Scissors
</h1>
<Sparkles className="w-6 h-6 text-yellow-500" />
</div>
{/* Player Info */}
<div className="flex items-center justify-center gap-4 mb-6">
<Badge variant="secondary" className="px-4 py-2">
<Users className="w-4 h-4 mr-2" />
{isPlayer1 ? "Player 1" : "Player 2"}
</Badge>
</div>
{/* Score Board */}
<Card className="inline-block bg-white/80 backdrop-blur-sm border-0 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center gap-6 text-2xl font-bold">
<div className="flex items-center gap-2">
<span className="text-blue-600">Player 1</span>
<span className="text-3xl">{game?.player1Score ?? 0}</span>
</div>
<div className="text-muted-foreground">-</div>
<div className="flex items-center gap-2">
<span className="text-3xl">{game?.player2Score ?? 0}</span>
<span className="text-purple-600">Player 2</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Game Outcome */}
{gameComplete && (
<Card className="mb-8 bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200">
<CardContent className="p-6 text-center">
<div className="flex items-center justify-center gap-3 mb-4">
{getOutcomeIcon(game.outcome, player)}
<h2 className="text-2xl font-bold">
{game?.outcome === player
? "🎉 You Win! 🎉"
: game?.outcome === "draw"
? "🤝 It's a Draw! 🤝"
: "😔 You Lose! 😔"}
</h2>
</div>
<Button
disabled={currentPlayerState?.resetRequested}
onClick={handleNewGame}
className="bg-gradient-to-r from-primary to-purple-600 hover:from-primary/90 hover:to-purple-600/90 text-white"
size="lg"
>
<Gamepad2 className="w-5 h-5 mr-2" />
Start New Game
</Button>
</CardContent>
</Card>
)}
{/* Game Board */}
<div className="grid md:grid-cols-2 gap-8">
{/* Your Selection */}
<Card className="bg-white/80 backdrop-blur-sm border-0 shadow-lg">
<CardHeader className="text-center pb-4">
<CardTitle className="text-xl font-semibold text-blue-600">
Your Selection
</CardTitle>
</CardHeader>
<CardContent className="text-center">
<div className="mb-6">{playIcon(playSelection, "lg")}</div>
{!gameComplete && (
<>
{/* Choice Buttons */}
<div className="grid grid-cols-3 gap-4 mb-6">
<Button
variant={playSelection === "rock" ? "default" : "outline"}
size="lg"
className={`h-20 transition-all duration-200 ${
playSelection === "rock"
? "bg-gradient-to-br from-gray-400 to-gray-600 text-white shadow-lg scale-105"
: "hover:scale-105"
} text-3xl`}
onClick={() => handleSelection("rock")}
aria-label="Select Rock"
aria-selected={playSelection === "rock"}
>
🪨
</Button>
<Button
variant={
playSelection === "paper" ? "default" : "outline"
}
size="lg"
className={`h-20 transition-all duration-200 ${
playSelection === "paper"
? "bg-gradient-to-br from-blue-400 to-blue-600 text-white shadow-lg scale-105"
: "hover:scale-105"
} text-3xl`}
onClick={() => handleSelection("paper")}
aria-label="Select Paper"
aria-selected={playSelection === "paper"}
>
📄
</Button>
<Button
variant={
playSelection === "scissors" ? "default" : "outline"
}
size="lg"
className={`h-20 transition-all duration-200 ${
playSelection === "scissors"
? "bg-gradient-to-br from-red-400 to-red-600 text-white shadow-lg scale-105"
: "hover:scale-105"
} text-3xl`}
onClick={() => handleSelection("scissors")}
aria-label="Select Scissors"
aria-selected={playSelection === "scissors"}
>
</Button>
</div>
{/* Submit Button */}
<Button
disabled={submitDisabled}
onClick={() => handleSubmit(playSelection)}
className="w-full bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white font-semibold py-3 text-lg"
size="lg"
>
{currentPlayerState?.submitted ? (
<>
<CheckCircle className="w-5 h-5 mr-2" />
Selection Made!
</>
) : (
<>
<Gamepad2 className="w-5 h-5 mr-2" />
Make Your Move!
</>
)}
</Button>
</>
)}
</CardContent>
</Card>
{/* Opponent Selection */}
<Card className="bg-white/80 backdrop-blur-sm border-0 shadow-lg">
<CardHeader className="text-center pb-4">
<CardTitle className="text-xl font-semibold text-purple-600">
Opponent's Selection
</CardTitle>
</CardHeader>
<CardContent className="text-center">
{opponentSelection && (
<div className="mb-6">
{playIcon(opponentSelection?.value, "lg")}
</div>
)}
{!opponentSelection && !gameComplete ? (
opponentHasSelected ? (
<div className="text-muted-foreground">
<Clock className="w-8 h-8 mx-auto mb-2 animate-pulse" />
<p>The opponent has made their move</p>
</div>
) : (
<div className="text-muted-foreground">
<Clock className="w-8 h-8 mx-auto mb-2 animate-pulse" />
<p>Waiting for opponent...</p>
</div>
)
) : null}
</CardContent>
</Card>
</div>
{/* Game Status */}
{!gameComplete && (
<Card className="mt-8 bg-white/60 backdrop-blur-sm border-0">
<CardContent className="p-4 text-center">
<div className="flex items-center justify-center gap-2 text-muted-foreground">
<Clock className="w-4 h-4" />
<span className="text-sm">
{currentPlayer?.playSelection && !opponentSelection
? "Waiting for opponent to make their move..."
: "Make your selection to start the game"}
</span>
</div>
</CardContent>
</Card>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,120 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,37 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner";
import { Jazz } from "../jazz";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Jazz Server Side Validation",
description: "An example of server side validation with Jazz",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Jazz>{children}</Jazz>
<Toaster />
</body>
</html>
);
}

View File

@@ -0,0 +1,97 @@
"use client";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { serverApi } from "@/serverApi";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function HomeComponent() {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const onNewGameClick = async () => {
setIsLoading(true);
const { waitingRoom } = await serverApi.createGame.send({});
router.push(`/waiting-room/${waitingRoom.id}`);
};
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900 flex flex-col items-center justify-center p-4 relative overflow-hidden">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
</div>
{/* Main content */}
<div className="relative z-10 text-center space-y-8 max-w-2xl mx-auto">
{/* Game title and emojis */}
<div className="space-y-4">
<div className="flex justify-center items-center space-x-4 text-6xl mb-6">
<span className="animate-bounce" style={{ animationDelay: "0ms" }}>
🪨
</span>
<span
className="animate-bounce"
style={{ animationDelay: "150ms" }}
>
📄
</span>
<span
className="animate-bounce"
style={{ animationDelay: "300ms" }}
>
</span>
</div>
<h1 className="text-5xl font-bold text-white mb-2">
Rock, Paper, Scissors
</h1>
<p className="text-xl text-gray-300 max-w-md mx-auto">
Challenge your friends in this classic multiplayer game powered by
Jazz
</p>
</div>
{/* Game card */}
<Card className="w-full max-w-md mx-auto bg-white/10 backdrop-blur-lg border-white/20 shadow-2xl">
<CardHeader className="text-center pb-4">
<CardTitle className="text-2xl font-bold text-white">
Ready to Play?
</CardTitle>
<p className="text-gray-300 text-sm mt-2">
Create a new game and invite your friends
</p>
</CardHeader>
<CardContent className="space-y-4">
<Button
onClick={onNewGameClick}
disabled={isLoading}
className="w-full h-12 text-lg font-semibold bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
>
{isLoading ? (
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<span>Creating game...</span>
</div>
) : (
<div className="flex items-center space-x-2">
<span>🎮</span>
<span>Start New Game</span>
</div>
)}
</Button>
</CardContent>
</Card>
{/* Footer */}
<div className="text-center text-gray-400 text-sm mt-12">
<p>Built with Jazz Framework</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,173 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { WaitingRoom } from "@/schema";
import { serverApi } from "@/serverApi";
import { Account, JazzRequestError, co } from "jazz-tools";
import { useCoState } from "jazz-tools/react-core";
import { ClipboardCopyIcon, Loader2Icon } from "lucide-react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
function useWindowLocation() {
const [location, setLocation] = useState<string>("");
useEffect(() => {
setLocation(window.location.href);
}, []);
return location;
}
async function askToJoinGame(
waitingRoom: co.loaded<typeof WaitingRoom, { creator: true }>,
) {
if (waitingRoom.creator.isMe) {
return null;
}
try {
await serverApi.joinGame.send({
waitingRoom,
});
} catch (error) {
if (error instanceof JazzRequestError) {
toast.error(error.message);
} else {
toast.error("An unexpected error occurred");
}
}
}
export default function RouteComponent() {
const params = useParams<{ id: string }>();
const searchParams = useSearchParams();
const waitingRoom = useCoState(WaitingRoom, params.id, {
resolve: {
creator: true,
game: true,
},
});
const [copied, setCopied] = useState(false);
const router = useRouter();
const location = useWindowLocation();
const joinLocation = location + "?join=true";
const isJoining = !waitingRoom
? searchParams.get("join") === "true"
: Account.getMe().id !== waitingRoom.creator.id;
useEffect(() => {
if (!waitingRoom) {
return;
}
askToJoinGame(waitingRoom);
}, [waitingRoom?.id]);
useEffect(() => {
if (!waitingRoom?.game?.id) {
return;
}
router.push(`/game/${waitingRoom.game.id}`);
}, [waitingRoom?.game?.id]);
const onCopyClick = () => {
navigator.clipboard.writeText(joinLocation);
setCopied(true);
toast.success("Link copied to clipboard!");
};
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900 flex flex-col items-center justify-center p-4 relative overflow-hidden">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
</div>
{/* Main content */}
<div className="relative z-10 text-center space-y-8 max-w-2xl mx-auto">
{/* Game title and emojis */}
<div className="space-y-4">
<div className="flex justify-center items-center space-x-4 text-6xl mb-6">
<span className="animate-bounce" style={{ animationDelay: "0ms" }}>
🪨
</span>
<span
className="animate-bounce"
style={{ animationDelay: "150ms" }}
>
📄
</span>
<span
className="animate-bounce"
style={{ animationDelay: "300ms" }}
>
</span>
</div>
<h1 className="text-5xl font-bold text-white mb-2">Waiting Room</h1>
{!isJoining && (
<p className="text-xl text-gray-300 max-w-md mx-auto">
Share this link with your friend to join the game
</p>
)}
</div>
{/* Waiting room card */}
<Card className="w-full max-w-md mx-auto bg-white/10 backdrop-blur-lg border-white/20 shadow-2xl">
<CardHeader className="text-center pb-4">
<CardTitle className="text-2xl font-bold text-white flex items-center justify-center">
<Loader2Icon className="animate-spin inline h-8 w-8 mr-3" />
{isJoining ? "Joining the game" : "Waiting for opponent"}
</CardTitle>
<CardDescription className="text-gray-300 text-sm mt-2">
The game will automatically start once{" "}
{isJoining ? "ready" : "they join"}
</CardDescription>
</CardHeader>
{!isJoining && (
<CardContent className="space-y-4">
<div className="flex">
<Input
className="w-full border-white/20 bg-white/5 text-white placeholder:text-gray-400 rounded-e-none focus:border-white/40 focus:ring-white/20"
readOnly
value={joinLocation}
/>
<Button
onClick={onCopyClick}
className="rounded-s-none w-25 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 border-0 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-105"
>
{copied ? (
"Copied!"
) : (
<>
<ClipboardCopyIcon className="w-5 h-5" />
Copy
</>
)}
</Button>
</div>
</CardContent>
)}
</Card>
{/* Footer */}
<div className="text-center text-gray-400 text-sm mt-12">
<p>Built with Jazz Framework</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,46 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,59 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,24 @@
"use client";
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,28 @@
"use client";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

View File

@@ -0,0 +1,25 @@
"use client";
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@@ -0,0 +1,15 @@
import { JazzReactProvider } from "jazz-tools/react";
import { apiKey } from "./apiKey";
export function Jazz({ children }: { children: React.ReactNode }) {
return (
<JazzReactProvider
enableSSR
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
>
{children}
</JazzReactProvider>
);
}

View File

@@ -0,0 +1,5 @@
import { startWorker } from "jazz-tools/worker";
export const jazzServerAccount = await startWorker({
syncServer: "wss://cloud.jazz.tools/?key=jazz-paper-scissors@garden.co ",
});

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,94 @@
import { Account, Group, co, z } from "jazz-tools";
export const PlayerState = co.map({
currentSelection: z.literal(["rock", "paper", "scissors"]).optional(),
submitted: z.boolean(),
resetRequested: z.boolean(),
});
export type PlayerState = co.loaded<typeof PlayerState>;
export const PlaySelection = co.map({
value: z.literal(["rock", "paper", "scissors"]),
group: Group,
});
export type PlaySelection = co.loaded<typeof PlaySelection>;
export const Player = co.map({
account: co.account(),
playSelection: PlaySelection.optional(),
});
export type Player = co.loaded<typeof Player>;
export const Game = co.map({
player1: Player,
player2: Player,
player1State: PlayerState,
player2State: PlayerState,
outcome: z.literal(["player1", "player2", "draw"]).optional(),
player1Score: z.number(),
player2Score: z.number(),
});
export type Game = co.loaded<typeof Game>;
export const WaitingRoom = co.map({
creator: co.account(),
game: co.optional(Game),
});
export type WaitingRoom = co.loaded<typeof WaitingRoom>;
export function createGameState(params: {
account1: Account;
account2: Account;
worker: Account;
}) {
const { account1, account2, worker } = params;
const gameGroup = Group.create({ owner: worker });
gameGroup.addMember(account1, "reader");
gameGroup.addMember(account2, "reader");
const player1 = Player.create(
{
account: account1,
},
gameGroup,
);
const player2 = Player.create(
{
account: account2,
},
gameGroup,
);
const player1StateGroup = Group.create(worker);
player1StateGroup.addMember(account1, "writer");
const player1State = PlayerState.create(
{
currentSelection: undefined,
submitted: false,
resetRequested: false,
},
player1StateGroup,
);
const player2StateGroup = Group.create(worker);
player2StateGroup.addMember(account2, "writer");
const player2State = PlayerState.create(
{
currentSelection: undefined,
submitted: false,
resetRequested: false,
},
player2StateGroup,
);
return {
player1,
player2,
player1State,
player2State,
outcome: undefined,
};
}

View File

@@ -0,0 +1,125 @@
import { experimental_defineRequest, z } from "jazz-tools";
import { Game, WaitingRoom } from "./schema";
const workerId = process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT!;
const createGameRequest = experimental_defineRequest({
url: "/api/create-game",
workerId,
request: {},
response: {
schema: { waitingRoom: WaitingRoom },
resolve: { waitingRoom: { creator: true } },
},
});
const joinGameRequest = experimental_defineRequest({
url: "/api/join-game",
workerId,
request: {
schema: {
waitingRoom: WaitingRoom,
},
resolve: {
waitingRoom: {
creator: true,
},
},
},
response: {
schema: {
waitingRoom: WaitingRoom,
},
resolve: {
waitingRoom: {
game: {
player1: {
account: true,
},
player2: {
account: true,
},
},
},
},
},
});
const newGameRequest = experimental_defineRequest({
url: "/api/new-game",
workerId,
request: {
schema: {
game: Game,
},
resolve: {
game: {
player1: {
account: true,
},
player2: {
account: true,
},
},
},
},
response: {
schema: {
game: Game,
},
resolve: {
game: {
player1State: {
$onError: null,
},
player2State: {
$onError: null,
},
player1: true,
player2: true,
},
},
},
});
const playRequest = experimental_defineRequest({
url: "/api/play",
workerId,
request: {
schema: {
game: Game,
selection: z.literal(["rock", "paper", "scissors"]),
},
resolve: {
game: {
player1: {
account: true,
playSelection: {
group: true,
},
},
player2: {
account: true,
playSelection: {
group: true,
},
},
},
},
},
response: {
schema: {
game: Game,
},
resolve: {
game: true,
},
},
});
export const serverApi = {
createGame: createGameRequest,
joinGame: joinGameRequest,
newGame: newGameRequest,
play: playRequest,
};

View File

@@ -0,0 +1,37 @@
import { expect, test } from "@playwright/test";
test("start a new game and play", async ({ page: marioPage, browser }) => {
await marioPage.goto("/");
await marioPage.getByRole("button", { name: /Start New Game/ }).click();
await expect(marioPage.getByText("Waiting for opponent")).toBeVisible();
const url = await marioPage.url();
const luigiContext = await browser.newContext();
const luigiPage = await luigiContext.newPage();
await luigiPage.goto(url);
await expect(marioPage.getByText("Waiting for selection")).toBeVisible();
await expect(luigiPage.getByText("Waiting for selection")).toBeVisible();
await expect(luigiPage.getByText("Waiting for opponent")).toBeVisible();
await marioPage.getByLabel("Select Rock").click();
await marioPage.getByRole("button", { name: /Make your move!/i }).click();
await expect(
luigiPage.getByText("The opponent has made their move"),
).toBeVisible();
await luigiPage.getByLabel("Select Paper").click();
await luigiPage.getByRole("button", { name: /Make your move!/i }).click();
await expect(luigiPage.getByText(/🎉 You Win! 🎉/)).toBeVisible();
await expect(marioPage.getByText(/😔 You Lose! 😔/)).toBeVisible();
await marioPage.getByRole("button", { name: /Start New Game/ }).click();
await expect(marioPage.getByText("Waiting for opponent")).toBeVisible();
await expect(luigiPage.getByText("Waiting for opponent")).toBeVisible();
});

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,3 +1,3 @@
{
"ignoreCommand": "echo true"
"ignoreCommand": "npx turbo-ignore"
}

View File

@@ -431,16 +431,16 @@ const reactExamples: Example[] = [
demoUrl: "https://music.demo.jazz.tools",
illustration: <MusicIllustration />,
},
// {
// name: "Jazz paper scissors",
// slug: "jazz-paper-scissors",
// description:
// "A game that shows how to communicate with other accounts through the experimental Inbox API.",
// tech: [tech.react],
// features: [features.serverWorker, features.inbox],
// illustration: <JazzPaperScissorsIllustration />,
// demoUrl: "https://jazz-paper-scissors.demo.jazz.tools",
// },
{
name: "Server-side HTTP requests",
slug: "server-worker-http",
description:
"A game that shows how to manage state in a trusted environment through the experimental HTTP API.",
tech: [tech.react],
features: [features.serverWorker],
illustration: <JazzPaperScissorsIllustration />,
demoUrl: "https://jazz-paper-scissors.demo.jazz.tools",
},
{
name: "Clerk",
slug: "clerk",

View File

@@ -244,7 +244,7 @@ export function CreateOrder() {
In a `BubbleTeaOrder`, the `name` field is required, so it would be a good idea to validate this before turning the draft into a real order.
Update the schema to include a `validate` helper.
Update the schema to include a `validateDraftOrder` helper.
<CodeGroup>
```ts twoslash
@@ -253,17 +253,17 @@ import { co, z } from "jazz-tools";
// schema.ts
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({ // [!code ++:11]
validate(draft: co.loaded<typeof Self>) {
const errors: string[] = [];
});
if (!draft.name) {
errors.push("Please enter a name.");
}
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) { // [!code ++:9]
const errors: string[] = [];
return { errors };
},
}));
if (!draft.name) {
errors.push("Please enter a name.");
}
return { errors };
};
```
</CodeGroup>
@@ -282,17 +282,17 @@ export const BubbleTeaOrder = co.map({
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded<typeof Self>) {
const errors: string[] = [];
});
if (!draft.name) {
errors.push("Please enter a name.");
}
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
const errors: string[] = [];
return { errors };
},
}));
if (!draft.name) {
errors.push("Please enter a name.");
}
return { errors };
};
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
@@ -340,7 +340,7 @@ export function CreateOrder() {
e.preventDefault();
if (!draft) return;
const validation = DraftBubbleTeaOrder.validate(draft); // [!code ++:5]
const validation = validateDraftOrder(draft); // [!code ++:5]
if (validation.errors.length > 0) {
console.log(validation.errors);
return;
@@ -455,17 +455,17 @@ export const BubbleTeaOrder = co.map({
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded<typeof Self>) {
const errors: string[] = [];
});
if (!draft.name) {
errors.push("Please enter a name.");
}
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
const errors: string[] = [];
return { errors };
},
}));
if (!draft.name) {
errors.push("Please enter a name.");
}
return { errors };
};
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
@@ -485,7 +485,7 @@ export const JazzAccount = co.account({
// @filename: CreateOrder.tsx
import * as React from "react";
import { useCoState, useAccount } from "jazz-tools/react";
import { BubbleTeaOrder, DraftBubbleTeaOrder, JazzAccount } from "schema";
import { BubbleTeaOrder, DraftBubbleTeaOrder, JazzAccount, validateDraftOrder } from "schema";
import { co } from "jazz-tools";
export function OrderForm({
@@ -527,7 +527,7 @@ export function CreateOrder() {
const draft = me.root.draft; // [!code ++:2]
if (!draft) return;
const validation = DraftBubbleTeaOrder.validate(draft);
const validation = validateDraftOrder(draft);
if (validation.errors.length > 0) {
console.log(validation.errors);
return;
@@ -579,21 +579,21 @@ import { co, z } from "jazz-tools";
// schema.ts
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded<typeof Self>) {
const errors: string[] = [];
});
if (!draft.name) {
errors.push("Plese enter a name.");
}
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
const errors: string[] = [];
return { errors };
},
if (!draft.name) {
errors.push("Please enter a name.");
}
hasChanges(draft?: co.loaded<typeof Self>) { // [!code ++:3]
return draft ? Object.keys(draft._edits).length : false;
},
}));
return { errors };
};
export function hasChanges(draft?: co.loaded<typeof DraftBubbleTeaOrder>) { // [!code ++:3]
return draft ? Object.keys(draft._edits).length : false;
};
```
</CodeGroup>
@@ -611,21 +611,21 @@ export const BubbleTeaOrder = co.map({
export const DraftBubbleTeaOrder = co.map({
name: z.optional(z.string()),
}).withHelpers((Self) => ({
validate(draft: co.loaded<typeof Self>) {
const errors: string[] = [];
});
if (!draft.name) {
errors.push("Plese enter a name.");
}
export function validateDraftOrder(draft: co.loaded<typeof DraftBubbleTeaOrder>) {
const errors: string[] = [];
return { errors };
},
if (!draft.name) {
errors.push("Please enter a name.");
}
hasChanges(draft?: co.loaded<typeof Self>) { // [!code ++:3]
return draft ? Object.keys(draft._edits).length : false;
},
}));
return { errors };
};
export function hasChanges(draft?: co.loaded<typeof DraftBubbleTeaOrder>) {
return draft ? Object.keys(draft._edits).length : false;
};
export const AccountRoot = co.map({
draft: DraftBubbleTeaOrder,
@@ -649,7 +649,7 @@ export function DraftIndicator() {
resolve: { root: { draft: true } },
});
if (DraftBubbleTeaOrder.hasChanges(me?.root.draft)) {
if (hasChanges(me?.root.draft)) {
return (
<p>You have a draft</p>
);

View File

@@ -47,11 +47,6 @@ export const docNavigationItems = [
href: "/docs/sync-and-storage",
done: 100,
},
{
name: "Node.JS / server workers",
href: "/docs/project-setup/server-side",
done: 80,
},
{
name: "Providers",
href: "/docs/project-setup/providers",
@@ -84,11 +79,41 @@ export const docNavigationItems = [
},
],
},
{
name: "Server-side",
items: [
{
name: "Setup",
href: "/docs/server-side/setup",
done: 100,
},
{
name: "Communicating with workers",
href: "/docs/server-side/communicating-with-workers",
done: 100,
},
{
name: "HTTP requests",
href: "/docs/server-side/http-requests",
done: 100,
},
{
name: "Inbox",
href: "/docs/server-side/inbox",
done: 100,
},
],
},
{
name: "Upgrade guides",
// collapse: true,
prefix: "/docs/upgrade",
items: [
{
name: "0.16.0 - Cleaner separation between Zod and CoValue schemas",
href: "/docs/upgrade/0-16-0",
done: 100,
},
{
name: "0.15.0 - Everything inside `jazz-tools`",
href: "/docs/upgrade/0-15-0",

View File

@@ -1,76 +0,0 @@
export const metadata = {
description: "Use Jazz server-side through Server Workers which act like Jazz accounts."
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Node.JS / server workers
The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do.
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
[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>
## Generating credentials
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.
To generate new credentials for a Server Worker, you can run:
<CodeGroup>
```sh
npx jazz-run account create --name "My Server Worker"
```
</CodeGroup>
The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done.
## Storing & providing credentials
Server Worker credentials are typically stored and provided as environmental variables.
**Take extra care with the Account Secret &mdash; handle it like any other secret environment variable such as a DB password.**
## Starting a server worker
You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly to setting up a client-side Jazz context, it:
- takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in it's private account root)
- takes a URL for a sync & storage server
`startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere.
<CodeGroup>
```ts twoslash
import { co } from "jazz-tools";
const MyWorkerAccount = co.account();
type MyWorkerAccount = co.loaded<typeof MyWorkerAccount>;
// ---cut---
import { startWorker } from 'jazz-tools/worker';
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
});
```
</CodeGroup>
`worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to:
- load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})`
- create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })`
## Using CoValues instead of requests
Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about.
What's less clear is how you can trigger this work to happen.
- One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients
- The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients.
- A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`).
- This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible.

View File

@@ -53,9 +53,7 @@ import { co, z } from "jazz-tools";
// ...somewhere in jazz-tools itself...
const Account = co.account({
root: co.map({}),
profile: co.profile({
name: z.string(),
}),
profile: co.profile(),
});
```
</CodeGroup>
@@ -91,7 +89,7 @@ export const MyAppRoot = co.map({
export const MyAppProfile = co.profile({ // [!code ++:4]
name: z.string(), // compatible with default Profile schema
avatar: z.optional(co.image()),
avatar: co.optional(co.image()),
});
export const MyAppAccount = co.account({
@@ -243,7 +241,7 @@ const MyAppProfile = co.profile({
// ---cut---
const MyAppRoot = co.map({
myChats: co.list(Chat),
myBookmarks: z.optional(co.list(Bookmark)), // [!code ++:1]
myBookmarks: co.optional(co.list(Bookmark)), // [!code ++:1]
});

View File

@@ -353,16 +353,16 @@ const Person = co.map({
```
</CodeGroup>
You can use the same technique for mutually recursive references, but you'll need to help TypeScript along:
You can use the same technique for mutually recursive references:
<CodeGroup>
```ts twoslash
// ---cut---
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const Person = co.map({
name: z.string(),
get friends(): CoListSchema<typeof Person> {
get friends() {
return ListOfPeople;
}
});
@@ -372,22 +372,6 @@ const ListOfPeople = co.list(Person);
</CodeGroup>
Note: similarly, if you use modifiers like `co.optional()` you'll need to help TypeScript along:
<CodeGroup>
```ts twoslash
import { co, z } from "jazz-tools";
// ---cut---
const Person = co.map({
name: z.string(),
get bestFriend(): z.ZodOptional<typeof Person> {
return co.optional(Person);
}
});
```
</CodeGroup>
### Helper methods
If you find yourself repeating the same logic to access computed CoValues properties,

View File

@@ -0,0 +1,45 @@
export const metadata = {
description: "How to send data to Server Workers, set permissions and subscriptions."
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Communicating with Server Workers
Server Workers in Jazz can receive data from clients through two different APIs, each with their own characteristics and use cases.
This guide covers the key properties of each approach to help you choose the right one for your application.
## Overview
Jazz provides two ways to communicate with Server Workers:
1. **HTTP Requests** - The easiest to work with and deploy, uses standard Request/Response objects
2. **Inbox** - Fully built using the Jazz data model with offline support
## HTTP Requests (Recommended)
HTTP requests are the most straightforward way to communicate with Server Workers. They work well with any framework or runtime that supports standard Request and Response objects, can be scaled horizontally, and put clients and workers in direct communication.
### When to use HTTP Requests
Use HTTP requests when you need immediate responses, are deploying to serverless environments, need horizontal scaling, or are working with standard web frameworks.
It's also a good solution when using full-stack frameworks like Next.js, where you can use the API routes to handle the server-side logic.
[Learn more about HTTP Requests →](/docs/server-side/http-requests)
## Inbox
The Inbox API is fully built using the Jazz data model and provides offline support. Requests and responses are synced as soon as the device becomes online, but require the Worker to always be online to work properly.
### When to use Inbox
Use Inbox when you need offline support, want to leverage the Jazz data model, can ensure the worker stays online, need persistent message storage, or want to review message history.
It works great when you don't want to expose your server with a public address, because it uses Jazz's sync to make the communication happen.
Since Jazz handles all the network communication, the entire class of network errors that usually come with traditional HTTP requests are not a problem when
using the Inbox API.
[Learn more about Inbox →](/docs/server-side/inbox)

View File

@@ -0,0 +1,264 @@
export const metadata = {
description: "How to use HTTP requests to communicate with Server Workers using experimental_defineRequest."
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# HTTP Requests with Server Workers
HTTP requests are the easiest way to communicate with Server Workers in Jazz. They work well with any framework or runtime that supports standard Request and Response objects, can be scaled horizontally, and put clients and workers in direct communication.
## Setting up HTTP requests
### Defining request schemas
Use `experimental_defineRequest` to define your API schema:
<CodeGroup>
```ts
import { experimental_defineRequest, z } from "jazz-tools";
import { Event, Ticket } from "./schema";
const workerId = process.env.NEXT_PUBLIC_JAZZ_WORKER_ACCOUNT!;
export const bookEventTicket = experimental_defineRequest({
url: "/api/book-event-ticket",
// The id of the worker Account or Group
workerId,
// The schema definition of the data we send to the server
request: {
schema: {
event: Event,
},
// The data that will be considered as "loaded" in the server input
resolve: {
event: { reservations: true },
},
},
// The schema definition of the data we expect to receive from the server
response: {
schema: { ticket: Ticket },
// The data that will be considered as "loaded" in the client response
// It defines the content that the server directly sends to the client, without involving the sync server
resolve: { ticket: true },
},
});
```
</CodeGroup>
### Setting up the Server Worker
We need to start a Server Worker instance that will be able to sync data with the sync server, and handle the requests.
<CodeGroup>
```ts
import { startWorker } from "jazz-tools/worker";
export const jazzServer = await startWorker({
syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
});
```
</CodeGroup>
## Handling requests on the server
### Creating API routes
Create API routes to handle the defined requests. Here's an example using Next.js API routes:
<CodeGroup>
```ts
import { jazzServer } from "@/jazzServer";
import { bookEventTicket, Ticket, Event } from "@/schema";
import { JazzRequestError } from "jazz-tools";
export async function POST(request: Request) {
return bookEventTicket.handle(
request,
jazzServer.worker,
async ({ event }, madeBy) => {
const ticketGroup = Group.create(jazzServer.worker);
const ticket = Ticket.create({
account: madeBy,
event,
});
// Give access to the ticket to the client
ticketGroup.addMember(madeBy, "reader");
event.reservations.push(ticket);
return {
ticket,
};
},
);
}
```
</CodeGroup>
## Making requests from the client
### Using the defined API
Make requests from the client using the defined API:
<CodeGroup>
```ts
import { bookEventTicket, Ticket, Event } from "@/schema";
import { isJazzRequestError } from "jazz-tools";
export async function sendEventBookingRequest(event: Event) {
const { ticket } = await bookEventTicket.send({ event });
return ticket;
}
```
</CodeGroup>
## Error handling
### Server-side error handling
Use `JazzRequestError` to return proper HTTP error responses:
<CodeGroup>
```ts
import { jazzServer } from "@/jazzServer";
import { bookEventTicket, Ticket, Event } from "@/schema";
import { JazzRequestError } from "jazz-tools";
export async function POST(request: Request) {
return bookEventTicket.handle(
request,
jazzServer.worker,
async ({ event }, madeBy) => {
// Check if the event is full
if (event.reservations.length >= event.capacity) {
// The JazzRequestError is propagated to the client, use it for any validation errors
throw new JazzRequestError("Event is full", 400);
}
const ticketGroup = Group.create(jazzServer.worker);
const ticket = Ticket.create({
account: madeBy,
event,
});
// Give access to the ticket to the client
ticketGroup.addMember(madeBy, "reader");
event.reservations.push(ticket);
return {
ticket,
};
},
);
}
```
</CodeGroup>
<Alert variant="info" className="mt-4">
**Note**: To ensure that the limit is correctly enforced, the handler should be deployed in a single worker instance (e.g. a single Cloudflare DurableObject).
Details on how to deploy a single instance Worker are available in the [Deployments & Transactionality](#deployments--transactionality) section.
</Alert>
### Client-side error handling
Handle errors on the client side:
<CodeGroup>
```ts
import { bookEventTicket, Ticket, Event } from "@/schema";
import { isJazzRequestError } from "jazz-tools";
export async function sendEventBookingRequest(event: Event) {
try {
const { ticket } = await bookEventTicket.send({ event });
return ticket;
} catch (error) {
// This works as a type guard, so you can easily get the error message and details
if (isJazzRequestError(error)) {
alert(error.message);
return;
}
}
}
```
</CodeGroup>
<Alert variant="info" className="mt-4">
**Note**: The `experimental_defineRequest` API is still experimental and may change in future versions. For production applications, consider the stability implications.
</Alert>
## Security safeguards provided by Jazz
Jazz HTTP requests include several built-in security measures to protect against common attacks:
### Cryptographic Authentication
- **Digital Signatures**: Each request is cryptographically signed using the sender's private key
- **Signature Verification**: The server verifies the signature using the sender's public key to ensure message authenticity and to identify the sender account
- **Tamper Protection**: Any modification to the request payload will invalidate the signature
### Replay Attack Prevention
- **Unique Message IDs**: Each request has a unique identifier (`co_z${string}`)
- **Duplicate Detection**: incoming messages ids are tracked to prevent replay attacks
- **Message Expiration**: Requests expire after 60 seconds to provide additional protection
These safeguards ensure that HTTP requests in Jazz are secure, authenticated, and protected against common attack vectors while maintaining the simplicity of standard HTTP communication.
## Deployments & Transactionality
### Single Instance Requirements
Some operations need to happen one at a time and in the same place, otherwise the data can get out of sync.
For example, if you are checking capacity for an event and creating tickets, you must ensure only one server is doing it.
If multiple servers check at the same time, they might all think there is space and allow too many tickets.
Jazz uses eventual consistency (data takes a moment to sync between regions), so this problem is worse if you run multiple server copies in different locations.
Until Jazz supports transactions across regions, the solution is to deploy a single server instance for these sensitive operations.
Examples of when you must deploy on a single instance are:
1. Distribute a limited number of tickets
* Limiting ticket sales so that only 100 tickets are sold for an event.
* The check (“is there space left?”) and ticket creation must happen together, or you risk overselling.
2. Inventory stock deduction
* Managing a product stock count (e.g., 5 items left in store).
* Multiple instances could let multiple buyers purchase the last item at the same time.
3. Sequential ID or token generation
* Generating unique incremental order numbers (e.g., #1001, #1002).
* Multiple instances could produce duplicates if not coordinated.
Single servers are necessary to enforce invariants or provide a consistent view of the data.
As a rule of thumb, when the output of the request depends on the state of the database, you should probably deploy on a single instance.
### Multi-Region Deployment
If your code doesnt need strict rules to keep data in sync (no counters, no limits, no “checkthenupdate” logic), you can run your workers in many regions at the same time.
This way:
* Users connect to the closest server (faster).
* If one region goes down, others keep running (more reliable).
Examples of when it's acceptable to deploy across multiple regions are:
1. Sending confirmation emails
* After an action is complete, sending an email to the user does not depend on current database state.
2. Pushing notifications
* Broadcasting “event booked” notifications to multiple users can be done from any region.
3. Logging or analytics events
* Recording “user clicked this button” or “page viewed” events, since these are additive and dont require strict ordering.
4. Calling external APIs (e.g., LLMs, payment confirmations)
* If the response does not modify shared counters or limits, it can be done from any region.
5. Pre-computing cached data or summaries
* Generating read-only previews or cached summaries where stale data is acceptable and does not affect core logic.
Generally speaking, if the output of the request does not depend on the state of the database, you can deploy across multiple regions.

View File

@@ -0,0 +1,166 @@
export const metadata = {
description: "How to use the Inbox API to communicate with Server Workers using experimental_useInboxSender and inbox.subscribe."
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Inbox API with Server Workers
The Inbox API provides a message-based communication system for Server Workers in Jazz.
It works on top of the Jazz APIs and uses sync to transfer messages between the client and the server.
## Setting up the Inbox API
### Define the inbox message schema
Define the inbox message schema in your schema file:
<CodeGroup>
```ts
export const BookTicketMessage = co.map({
type: co.literal("bookTicket"),
event: Event,
})
```
</CodeGroup>
Any kind of CoMap is valid as an inbox message.
### Setting up the Server Worker
Run a server worker and subscribe to the `inbox`:
<CodeGroup>
```ts
import { startWorker } from "jazz-tools/worker";
import { BookTicketMessage } from "@/schema";
const {
worker,
experimental: { inbox },
} = await startWorker({
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
syncServer: "wss://cloud.jazz.tools/?key=your-api-key",
});
inbox.subscribe(
BookTicketMessage,
async (message, senderID) => {
const madeBy = await co.account().load(senderID, { loadAs: worker });
const { event } = await message.ensureLoaded({
resolve: {
event: {
reservations: true,
},
},
});
const ticketGroup = Group.create(jazzServer.worker);
const ticket = Ticket.create({
account: madeBy,
event,
});
// Give access to the ticket to the client
ticketGroup.addMember(madeBy, "reader");
event.reservations.push(ticket);
return ticket;
},
);
```
</CodeGroup>
### Handling multiple message types
`inbox.subscribe` should be called once per worker instance.
If you need to handle multiple message types, you can use the `co.discriminatedUnion` function to create a union of the message types.
<CodeGroup>
```ts
const CancelReservationMessage = co.map({
type: co.literal("cancelReservation"),
event: Event,
ticket: Ticket,
});
export const InboxMessage = co.discriminatedUnion("type", [
BookTicketMessage,
CancelReservationMessage
]);
```
</CodeGroup>
And check the message type in the handler:
<CodeGroup>
```ts
import { InboxMessage } from "@/schema";
inbox.subscribe(
InboxMessage,
async (message, senderID) => {
switch (message.type) {
case "bookTicket":
return await handleBookTicket(message, senderID);
case "cancelReservation":
return await handleCancelReservation(message, senderID);
}
},
);
```
</CodeGroup>
## Sending messages from the client
### Using the Inbox Sender hook
Use `experimental_useInboxSender` to send messages from React components:
<CodeGroup>
```ts
import { experimental_useInboxSender } from "jazz-tools/react";
import { BookTicketMessage, Event } from "@/schema";
function EventComponent({ event }: { event: Event }) {
const sendInboxMessage = experimental_useInboxSender(WORKER_ID);
const [isLoading, setIsLoading] = useState(false);
const onBookTicketClick = async () => {
setIsLoading(true);
const ticketId = await sendInboxMessage(
BookTicketMessage.create({
type: "bookTicket",
event: event,
}),
);
alert(`Ticket booked: ${ticketId}`);
};
return (
<Button onClick={onBookTicketClick} loading={isLoading}>
Book Ticket
</Button>
);
}
```
</CodeGroup>
The `sendInboxMessage` API returns a Promise that waits for the message to be handled by a Worker.
A message is considered to be handled when the Promise returned by `inbox.subscribe` resolves.
The value returned will be the id of the CoValue returned in the `inbox.subscribe` resolved promise.
## Deployment considerations
Multi-region deployments are not supported when using the Inbox API.
If you need to split the workload across multiple regions, you can use the [HTTP API](./http-requests.mdx) instead.

View File

@@ -0,0 +1,78 @@
export const metadata = {
description: "Use Jazz server-side through Server Workers which act like Jazz accounts."
};
import { CodeGroup } from "@/components/forMdx";
import { Alert } from "@garden-co/design-system/src/components/atoms/Alert";
# Running Jazz on the server
Jazz is a distributed database that can be used on both clients or servers without any distinction.
You can use servers to:
- perform operations that can't be done on the client (e.g. sending emails, making HTTP requests, etc.)
- validate actions that require a central authority (e.g. a payment gateway, booking a hotel, etc.)
We call the code that runs on the server a "Server Worker".
The main difference to keep in mind when working with Jazz compared to traditional systems is that server code doesn't have any
special or privileged access to the user data. You need to be explicit about what you want to share with the server.
This means that your server workers will have their own accounts, and they need to be explicitly given access to the CoValues they need to work on.
## Generating credentials
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.
To generate new credentials for a Server Worker, you can run:
<CodeGroup>
```sh
npx jazz-run account create --name "My Server Worker"
```
</CodeGroup>
The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done.
<Alert variant="info" className="mt-4">
**Note**: By default the account will be stored in Jazz Cloud. You can use the `--peer` flag to store the account on a different sync server.
</Alert>
## Running a server worker
You can use `startWorker` to run a Server Worker. Similarly to setting up a client-side Jazz context, it:
- takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in its private account root)
- takes a URL for a sync & storage server
The migration defined in the `AccountSchema` will be executed every time the worker starts, the same way as it would be for a client-side Jazz context.
<CodeGroup>
```ts twoslash
import { co } from "jazz-tools";
const MyWorkerAccount = co.account();
type MyWorkerAccount = co.loaded<typeof MyWorkerAccount>;
// ---cut---
import { startWorker } from 'jazz-tools/worker';
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
accountID: process.env.JAZZ_WORKER_ACCOUNT,
accountSecret: process.env.JAZZ_WORKER_SECRET,
});
```
</CodeGroup>
`worker` is an instance of the `Account` schema provided, and acts like `me` (as returned by `useAccount` on the client).
It will implicitly become the current account, and you can avoid mentioning it in most cases.
For this reason we also recommend running a single worker instance per server, because it makes your code much more predictable.
## Storing & providing credentials
Server Worker credentials are typically stored and provided as environment variables.
**Take extra care with the Account Secret &mdash; handle it like any other secret environment variable such as a DB password.**

View File

@@ -0,0 +1,158 @@
import { CodeGroup } from '@/components/forMdx'
# Jazz 0.16.0 - Cleaner separation between Zod and CoValue schemas
This release introduces a cleaner separation between Zod and CoValue schemas, improves type inference with circular references, and simplifies how you access internal schemas.
While most applications won't require extensive refactors, some breaking changes will require action.
## Motivation
Before 0.16.0, CoValue schemas were a thin wrapper around Zod schemas. This made it easy to use Zod methods on CoValue schemas,
but it also prevented the type checker from detecting issues when combining Zod and CoValue schemas.
For example, the following code would previously compile without errors, but would have severe limitations:
<CodeGroup>
```tsx
import { co, z } from "jazz-tools";
const Dog = co.map({
breed: z.string(),
});
const Person = co.map({
pets: z.array(Dog),
});
// You can create a CoMap with a z.array field that contains another CoMap
const map = Person.create({
pets: [Dog.create({ breed: "Labrador" })],
});
// But then you cannot eagerly load the nested CoMap, because
// there's a plain JS object in between. So this would fail:
Person.load(map.id, { resolve: { pets: { $each: true } } });
```
</CodeGroup>
Schema composition rules are now stricter: Zod schemas can only be composed with other Zod schemas.
CoValue schemas can be composed with either Zod or other CoValue schemas. These rules are enforced at the type level, to make it easier
to spot errors in schema definitions and avoid possible footguns when mixing Zod and CoValue schemas.
Having a stricter separation between Zod and CoValue schemas also allowed us to improve type inference with circular references.
Previously, the type checker would not be able to infer types for even simple circular references, but now it can!
<CodeGroup>
```tsx
import { co, z } from "jazz-tools";
const Person = co.map({
name: z.string(),
get friends(): CoListSchema<typeof Person> { // [!code --]
get friends() { // [!code ++]
return co.list(Person);
},
});
```
</CodeGroup>
There are some scenarios where recursive type inference can still fail due to TypeScript limitations, but these should be rare.
## Breaking changes
### The Account root id is now discoverable
In prior Jazz releases, the Account root id was stored encrypted and accessible only by the account owner.
This made it impossible to load the account root this way:
<CodeGroup>
```tsx
const bob = MyAppAccount.load(bobId, { resolve: { root: true }, loadAs: me });
```
</CodeGroup>
So we changed Account root id to be discoverable by everyone.
**This doesn't affect the visibility of the account root**, which still follows the permissions defined in its group.
For existing accounts, the change is applied the next time the user loads their account.
No action is required on your side, but we preferred to mark this as a breaking change because it
minimally affects access to the account root. (e.g., if in your app the root is public, now users can access other users' root by knowing their account ID)
### `z.optional()` and `z.discriminatedUnion()` no longer work with CoValue schemas
You'll now need to use the `co.optional()` and `co.discriminatedUnion()` equivalents.
This change may require you to update any explicitly typed cyclic references.
<CodeGroup>
```tsx
import { co, z } from "jazz-tools";
const Person = co.map({
name: z.string(),
get bestFriend(): z.ZodOptional<typeof Person> { // [!code --]
return z.optional(Person); // [!code --]
get bestFriend(): co.Optional<typeof Person> { // [!code ++]
return co.optional(Person); // [!code ++]
}
});
```
</CodeGroup>
### CoValue schema types are now under the `co.` namespace
All CoValue schema types are now accessed via the `co.` namespace. If you're using explicit types (especially in recursive schemas), you'll need to update them accordingly.
<CodeGroup>
```tsx
import { co, z } from "jazz-tools";
const Person = co.map({
name: z.string(),
get friends(): CoListSchema<typeof Person> { // [!code --]
get friends(): co.List<typeof Person> { // [!code ++]
return co.list(Person);
}
});
```
</CodeGroup>
### Unsupported Zod methods have been removed from CoMap schemas
CoMap schemas no longer incorrectly inherit Zod methods like `.extend()` and `.partial()`. These methods previously appeared to work but could behave unpredictably. They have now been disabled.
We're keeping `.optional()` and plan to introduce more Zod-like methods in future releases.
### Internal schema access is now simpler
You no longer need to use Zod's `.def` to access schema internals. Instead, you can directly use methods like `CoMapSchema.shape`, `CoListSchema.element`, and `CoOptionalSchema.innerType`.
<CodeGroup>
```tsx
import { co, z } from "jazz-tools";
const Message = co.map({
content: co.richText(),
});
const Thread = co.map({
messages: co.list(Message),
});
const thread = Thread.create({
messages: Thread.def.shape.messages.create([ // [!code --]
messages: Thread.shape.messages.create([ // [!code ++]
Message.create({
content: co.richText().create("Hi!"),
}),
Message.create({
content: co.richText().create("What's up?"),
}),
]),
});
```
</CodeGroup>
### Removed the deprecated `withHelpers` method from CoValue schemas
The deprecated `withHelpers()` method has been removed from CoValue schemas. You can define helper functions manually to encapsulate CoValue-related logic.
[Learn how to define helper methods](https://jazz.tools/docs/vanilla/schemas/covalues#helper-methods).

View File

@@ -343,14 +343,14 @@ CoLists can be used to create one-to-many relationships:
<CodeGroup>
```ts twoslash
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "complete"]),
get project(): z.ZodOptional<typeof Project> {
return z.optional(Project);
get project(): co.Optional<typeof Project> {
return co.optional(Project);
}
});
@@ -359,7 +359,7 @@ const ListOfTasks = co.list(Task);
const Project = co.map({
name: z.string(),
get tasks(): CoListSchema<typeof Task> {
get tasks(): co.List<typeof Task> {
return ListOfTasks;
}
});

View File

@@ -213,14 +213,14 @@ const Member = co.map({
name: z.string(),
});
// ---cut---
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const Project = co.map({
name: z.string(),
startDate: z.date(),
status: z.literal(["planning", "active", "completed"]),
coordinator: co.optional(Member),
get subProjects(): z.ZodOptional<CoListSchema<typeof Project>> {
get subProjects(): co.Optional<co.List<typeof Project>> {
return co.optional(co.list(Project));
}
});

View File

@@ -50,51 +50,3 @@ export type User = co.loaded<typeof User>;
This direct linking approach offers a single source of truth. When you update a referenced CoValue, all other CoValues that point to it are automatically updated, ensuring data consistency across your application.
By connecting CoValues through these direct references, you can build robust and collaborative applications where data is consistent, efficient to manage, and relationships are clearly defined. The ability to link different CoValue types to the same underlying data is fundamental to building complex applications with Jazz.
## Recursive references with DiscriminatedUnion
In advanced schemas, you may want a CoValue that recursively references itself. For example, a `ReferenceItem` that contains a list of other items like `NoteItem` or `AttachmentItem`. This is common in tree-like structures such as threaded comments or nested project outlines.
You can model this with a Zod `z.discriminatedUnion`, but TypeScripts type inference doesn't handle recursive unions well without a workaround.
Heres how to structure your schema to avoid circular reference errors.
### Use this pattern for recursive discriminated unions
<CodeGroup>
```ts twoslash
import { CoListSchema, co, z } from "jazz-tools";
// Recursive item modeling pattern using discriminated unions
// First, define the non-recursive types
export const NoteItem = co.map({
type: z.literal("note"),
internal: z.boolean(),
content: co.plainText(),
});
export const AttachmentItem = co.map({
type: z.literal("attachment"),
internal: z.boolean(),
content: co.fileStream(),
});
export const ReferenceItem = co.map({
type: z.literal("reference"),
internal: z.boolean(),
content: z.string(),
// Workaround: declare the field type using CoListSchema and ZodDiscriminatedUnion so TS can safely recurse
get children(): CoListSchema<z.ZodDiscriminatedUnion<[typeof NoteItem, typeof AttachmentItem, typeof ReferenceItem]>> {
return ProjectContextItemList;
},
});
// Create the recursive union
export const ProjectContextItem = z.discriminatedUnion("type", [NoteItem, AttachmentItem, ReferenceItem]);
// Final list of recursive types
export const ProjectContextItemList = co.list(ProjectContextItem);
```
</CodeGroup>
Even though this seems like a shortcut, TypeScript and Zod can't resolve the circular reference this way. Always define the discriminated union before introducing recursive links.

View File

@@ -24,7 +24,7 @@ import { Group, co, z } from "jazz-tools";
const MyProfile = co.profile({
name: z.string(),
image: z.optional(co.image()),
image: co.optional(co.image()),
});
const MyAccount = co.account({

View File

@@ -25,7 +25,7 @@ import { Group, co, z } from "jazz-tools";
const MyProfile = co.profile({
name: z.string(),
image: z.optional(co.image()),
image: co.optional(co.image()),
});
const MyAccount = co.account({

View File

@@ -249,7 +249,7 @@ Resolve queries let you declare exactly which references to load and how deep to
<CodeGroup>
```ts twoslash
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const projectId = "co_123";
// ---cut-before---
@@ -259,8 +259,8 @@ const TeamMember = co.map({
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema<typeof Task> { return co.list(Task) },
assignee: co.optional(TeamMember),
get subtasks(): co.List<typeof Task> { return co.list(Task) },
});
const Project = co.map({
@@ -349,7 +349,7 @@ When a user tries to load a reference they don't have access to:
<CodeGroup>
```ts twoslash
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const TeamMember = co.map({
name: z.string(),
@@ -357,8 +357,8 @@ const TeamMember = co.map({
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema<typeof Task> { return co.list(Task) },
assignee: co.optional(TeamMember),
get subtasks(): co.List<typeof Task> { return co.list(Task) },
});
const Project = co.map({
@@ -388,7 +388,7 @@ When a list contains references to items the user can't access:
<CodeGroup>
```ts twoslash
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const TeamMember = co.map({
name: z.string(),
@@ -396,8 +396,8 @@ const TeamMember = co.map({
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema<typeof Task> { return co.list(Task) },
assignee: co.optional(TeamMember),
get subtasks(): co.List<typeof Task> { return co.list(Task) },
});
const Project = co.map({
@@ -424,7 +424,7 @@ When trying to load an object with an inaccessible reference without directly re
<CodeGroup>
```ts twoslash
import { co, z, CoListSchema } from "jazz-tools";
import { co, z } from "jazz-tools";
const TeamMember = co.map({
name: z.string(),
@@ -432,8 +432,8 @@ const TeamMember = co.map({
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema<typeof Task> { return co.list(Task) },
assignee: co.optional(TeamMember),
get subtasks(): co.List<typeof Task> { return co.list(Task) },
});
const Project = co.map({
@@ -468,7 +468,7 @@ This way the inaccessible items are replaced with `null` in the returned list.
<CodeGroup>
```ts twoslash
import { co, z, CoListSchema, Group } from "jazz-tools";
import { co, z, Group } from "jazz-tools";
import { createJazzTestAccount } from "jazz-tools/testing";
const me = await createJazzTestAccount();
@@ -653,7 +653,7 @@ The `co.loaded` type is especially useful when passing data between components,
<ContentByFramework framework="react">
<CodeGroup>
```tsx twoslash
import { CoListSchema, co, z } from "jazz-tools";
import { co, z } from "jazz-tools";
import React from "react";
const TeamMember = co.map({
@@ -662,8 +662,8 @@ const TeamMember = co.map({
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema<typeof Task> {
assignee: co.optional(TeamMember),
get subtasks(): co.List<typeof Task> {
return co.list(Task);
},
});
@@ -727,7 +727,7 @@ function processProject(project: FullyLoadedProject) {
<ContentByFramework framework="vanilla">
<CodeGroup>
```ts twoslash
import { CoListSchema, co, z } from "jazz-tools";
import { co, z } from "jazz-tools";
const TeamMember = co.map({
name: z.string(),
@@ -735,8 +735,8 @@ const TeamMember = co.map({
const Task = co.map({
title: z.string(),
assignee: z.optional(TeamMember),
get subtasks(): CoListSchema<typeof Task> {
assignee: co.optional(TeamMember),
get subtasks(): co.List<typeof Task> {
return co.list(Task);
},
});
@@ -799,7 +799,7 @@ Sometimes you need to make sure data is loaded before proceeding with an operati
<CodeGroup>
```ts twoslash
import { CoListSchema, co, z } from "jazz-tools";
import { co, z } from "jazz-tools";
const TeamMember = co.map({
name: z.string(),
@@ -809,7 +809,7 @@ const Task = co.map({
title: z.string(),
status: z.literal(["todo", "in-progress", "completed"]),
assignee: z.string().optional(),
get subtasks(): CoListSchema<typeof Task> {
get subtasks(): co.List<typeof Task> {
return co.list(Task);
},
});

View File

@@ -1,5 +1,24 @@
# cojson-storage-indexeddb
## 0.16.0
### Patch Changes
- Updated dependencies [c09dcdf]
- cojson@0.16.0
## 0.15.16
### Patch Changes
- cojson@0.15.16
## 0.15.15
### Patch Changes
- cojson@0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,24 @@
# cojson-storage-sqlite
## 0.16.0
### Patch Changes
- Updated dependencies [c09dcdf]
- cojson@0.16.0
## 0.15.16
### Patch Changes
- cojson@0.15.16
## 0.15.15
### Patch Changes
- cojson@0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,24 @@
# cojson-transport-nodejs-ws
## 0.16.0
### Patch Changes
- Updated dependencies [c09dcdf]
- cojson@0.16.0
## 0.15.16
### Patch Changes
- cojson@0.15.16
## 0.15.15
### Patch Changes
- cojson@0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,17 @@
# cojson
## 0.16.0
### Minor Changes
- c09dcdf: Change the root attribute to be public on Account. The root content will still follow the visiblity rules specified in their group.
Existing accounts will be gradually migrated as they are loaded.
## 0.15.16
## 0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -50,6 +50,7 @@ export type DecryptedTransaction = {
txID: TransactionID;
changes: JsonValue[];
madeAt: number;
trusting?: boolean;
};
const readKeyCache = new WeakMap<CoValueCore, { [id: KeyID]: KeySecret }>();
@@ -657,6 +658,7 @@ export class CoValueCore {
txID,
madeAt: tx.madeAt,
changes: parseJSON(tx.changes),
trusting: true,
});
} catch (e) {
logger.error("Failed to parse trusting transaction on " + this.id, {

View File

@@ -16,6 +16,7 @@ type MapOp<K extends string, V extends JsonValue | undefined> = {
madeAt: number;
changeIdx: number;
change: MapOpPayload<K, V>;
trusting?: boolean;
};
// TODO: add after TransactionID[] for conflicts/ordering
@@ -112,7 +113,7 @@ export class RawCoMapView<
NonNullable<(typeof ops)[keyof typeof ops]>
>();
for (const { txID, changes, madeAt } of newValidTransactions) {
for (const { txID, changes, madeAt, trusting } of newValidTransactions) {
if (madeAt > this.latestTxMadeAt) {
this.latestTxMadeAt = madeAt;
}
@@ -127,6 +128,7 @@ export class RawCoMapView<
madeAt,
changeIdx,
change,
trusting,
};
const entries = ops[change.key];

View File

@@ -63,7 +63,7 @@ import { DisconnectedError, SyncManager, emptyKnownState } from "./sync.js";
type Value = JsonValue | AnyRawCoValue;
export { PriorityBasedMessageQueue } from "./PriorityBasedMessageQueue.js";
export { PriorityBasedMessageQueue } from "./queue/PriorityBasedMessageQueue.js";
import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
import {
CO_VALUE_LOADING_CONFIG,
@@ -73,6 +73,7 @@ import {
} from "./config.js";
import { LogLevel, logger } from "./logger.js";
import { CO_VALUE_PRIORITY, getPriorityFromHeader } from "./priority.js";
import { getDependedOnCoValues } from "./storage/syncUtils.js";
/** @hidden */
export const cojsonInternals = {
@@ -86,6 +87,7 @@ export const cojsonInternals = {
bytesToBase64url,
parseJSON,
stableStringify,
getDependedOnCoValues,
getDependedOnCoValuesFromRawData,
accountOrAgentIDfromSessionID,
isAccountID,

View File

@@ -313,6 +313,15 @@ export class LocalNode {
throw new Error("Account has no profile");
}
const rootID = account.get("root");
if (rootID) {
const rawEntry = account.getRaw("root");
if (!rawEntry?.trusting) {
account.set("root", rootID, "trusting");
}
}
// Preload the profile
await node.load(profileID);
@@ -563,15 +572,14 @@ export class LocalNode {
: "reader",
);
group.core.internalShamefullyCloneVerifiedStateFrom(
groupAsInvite.core.verified,
{ forceOverwrite: true },
);
const contentPieces =
groupAsInvite.core.verified.newContentSince(group.core.knownState()) ??
[];
group.processNewTransactions();
group.core.notifyUpdate("immediate");
this.syncManager.requestCoValueSync(group.core);
// Import the new transactions to the current localNode
for (const contentPiece of contentPieces) {
this.syncManager.handleNewContent(contentPiece, "import");
}
}
/** @internal */

View File

@@ -2,7 +2,7 @@ import { CoID } from "./coValue.js";
import { CoValueCore } from "./coValueCore/coValueCore.js";
import { Transaction } from "./coValueCore/verifiedState.js";
import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
import { MapOpPayload } from "./coValues/coMap.js";
import { MapOpPayload, RawCoMap } from "./coValues/coMap.js";
import {
EVERYONE,
Everyone,
@@ -270,6 +270,7 @@ function determineValidTransactionsForGroup(
| MapOpPayload<RawAccountID | AgentID | Everyone, Role>
| MapOpPayload<"readKey", JsonValue>
| MapOpPayload<"profile", CoID<RawProfile>>
| MapOpPayload<"root", CoID<RawCoMap>>
| MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
| MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
@@ -297,6 +298,14 @@ function determineValidTransactionsForGroup(
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (change.key === "root") {
if (memberState[transactor] !== "admin") {
logPermissionError("Only admins can set root");
continue;
}
validTransactions.push({ txID: { sessionID, txIndex }, tx });
continue;
} else if (

View File

@@ -1,9 +1,9 @@
import { Counter, ValueType, metrics } from "@opentelemetry/api";
import type { PeerState } from "./PeerState.js";
import { LinkedList } from "./PriorityBasedMessageQueue.js";
import { SYNC_SCHEDULER_CONFIG } from "./config.js";
import { logger } from "./logger.js";
import type { SyncMessage } from "./sync.js";
import type { PeerState } from "../PeerState.js";
import { SYNC_SCHEDULER_CONFIG } from "../config.js";
import { logger } from "../logger.js";
import type { SyncMessage } from "../sync.js";
import { LinkedList } from "./LinkedList.js";
/**
* A queue that schedules messages across different peers using a round-robin approach.

View File

@@ -1,6 +1,5 @@
import { Counter, ValueType, metrics } from "@opentelemetry/api";
import { CO_VALUE_PRIORITY, type CoValuePriority } from "./priority.js";
import type { SyncMessage } from "./sync.js";
import type { SyncMessage } from "../sync.js";
/**
* Since we have a fixed range of priority values (0-7) we can create a fixed array of queues.
@@ -10,18 +9,16 @@ type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
}
? A
: Tuple<T, N, [...A, T]>;
type QueueTuple = Tuple<LinkedList<SyncMessage>, 3>;
export type QueueTuple = Tuple<LinkedList<SyncMessage>, 3>;
type LinkedListNode<T> = {
value: T;
next: LinkedListNode<T> | undefined;
};
/**
* Using a linked list to make the shift operation O(1) instead of O(n)
* as our queues can grow very large when the system is under pressure.
*/
export class LinkedList<T> {
constructor(private meter?: QueueMeter) {}
@@ -70,7 +67,6 @@ export class LinkedList<T> {
return this.head === undefined;
}
}
class QueueMeter {
private pullCounter: Counter;
private pushCounter: Counter;
@@ -111,52 +107,9 @@ class QueueMeter {
this.pushCounter.add(1, this.attrs);
}
}
function meteredList<T>(
export function meteredList<T>(
type: "incoming" | "outgoing",
attrs?: Record<string, string | number>,
) {
return new LinkedList<T>(new QueueMeter("jazz.messagequeue." + type, attrs));
}
const PRIORITY_TO_QUEUE_INDEX = {
[CO_VALUE_PRIORITY.HIGH]: 0,
[CO_VALUE_PRIORITY.MEDIUM]: 1,
[CO_VALUE_PRIORITY.LOW]: 2,
} as const;
export class PriorityBasedMessageQueue {
private queues: QueueTuple;
constructor(
private defaultPriority: CoValuePriority,
type: "incoming" | "outgoing",
/**
* Optional attributes to be added to the generated metrics.
* By default the metrics will have the priority as an attribute.
*/
attrs?: Record<string, string | number>,
) {
this.queues = [
meteredList(type, { priority: CO_VALUE_PRIORITY.HIGH, ...attrs }),
meteredList(type, { priority: CO_VALUE_PRIORITY.MEDIUM, ...attrs }),
meteredList(type, { priority: CO_VALUE_PRIORITY.LOW, ...attrs }),
];
}
private getQueue(priority: CoValuePriority) {
return this.queues[PRIORITY_TO_QUEUE_INDEX[priority]];
}
public push(msg: SyncMessage) {
const priority = "priority" in msg ? msg.priority : this.defaultPriority;
this.getQueue(priority).push(msg);
}
public pull() {
const priority = this.queues.findIndex((queue) => queue.length > 0);
return this.queues[priority]?.shift();
}
}

View File

@@ -0,0 +1,45 @@
import { CO_VALUE_PRIORITY, type CoValuePriority } from "../priority.js";
import type { SyncMessage } from "../sync.js";
import { QueueTuple, meteredList } from "./LinkedList.js";
const PRIORITY_TO_QUEUE_INDEX = {
[CO_VALUE_PRIORITY.HIGH]: 0,
[CO_VALUE_PRIORITY.MEDIUM]: 1,
[CO_VALUE_PRIORITY.LOW]: 2,
} as const;
export class PriorityBasedMessageQueue {
private queues: QueueTuple;
constructor(
private defaultPriority: CoValuePriority,
type: "incoming" | "outgoing",
/**
* Optional attributes to be added to the generated metrics.
* By default the metrics will have the priority as an attribute.
*/
attrs?: Record<string, string | number>,
) {
this.queues = [
meteredList(type, { priority: CO_VALUE_PRIORITY.HIGH, ...attrs }),
meteredList(type, { priority: CO_VALUE_PRIORITY.MEDIUM, ...attrs }),
meteredList(type, { priority: CO_VALUE_PRIORITY.LOW, ...attrs }),
];
}
private getQueue(priority: CoValuePriority) {
return this.queues[PRIORITY_TO_QUEUE_INDEX[priority]];
}
public push(msg: SyncMessage) {
const priority = "priority" in msg ? msg.priority : this.defaultPriority;
this.getQueue(priority).push(msg);
}
public pull() {
const priority = this.queues.findIndex((queue) => queue.length > 0);
return this.queues[priority]?.shift();
}
}

View File

@@ -1,6 +1,6 @@
import { LinkedList } from "../PriorityBasedMessageQueue.js";
import { logger } from "../logger.js";
import { CoValueKnownState, NewContentMessage } from "../sync.js";
import { LinkedList } from "./LinkedList.js";
type StoreQueueEntry = {
data: NewContentMessage[];

View File

@@ -1,4 +1,3 @@
import { LinkedList } from "../PriorityBasedMessageQueue.js";
import {
type CoValueCore,
MAX_RECOMMENDED_TX_SIZE,
@@ -7,12 +6,12 @@ import {
type StorageAPI,
} from "../exports.js";
import { getPriorityFromHeader } from "../priority.js";
import { StoreQueue } from "../queue/StoreQueue.js";
import {
CoValueKnownState,
NewContentMessage,
emptyKnownState,
} from "../sync.js";
import { StoreQueue } from "./StoreQueue.js";
import { StorageKnownState } from "./knownState.js";
import { collectNewTxs, getDependedOnCoValues } from "./syncUtils.js";
import type {

View File

@@ -1,5 +1,4 @@
import { Histogram, ValueType, metrics } from "@opentelemetry/api";
import { IncomingMessagesQueue } from "./IncomingMessagesQueue.js";
import { PeerState } from "./PeerState.js";
import { SyncStateManager } from "./SyncStateManager.js";
import { CoValueCore } from "./coValueCore/coValueCore.js";
@@ -10,6 +9,7 @@ import { RawCoID, SessionID } from "./ids.js";
import { LocalNode } from "./localNode.js";
import { logger } from "./logger.js";
import { CoValuePriority } from "./priority.js";
import { IncomingMessagesQueue } from "./queue/IncomingMessagesQueue.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
@@ -442,9 +442,18 @@ export class SyncManager {
}
}
handleNewContent(msg: NewContentMessage, from: PeerState | "storage") {
handleNewContent(
msg: NewContentMessage,
from: PeerState | "storage" | "import",
) {
const coValue = this.local.getCoValue(msg.id);
const peer = from === "storage" ? undefined : from;
const peer = from === "storage" || from === "import" ? undefined : from;
const sourceRole =
from === "storage"
? "storage"
: from === "import"
? "import"
: peer?.role;
if (!coValue.hasVerifiedContent()) {
if (!msg.header) {
@@ -619,7 +628,9 @@ export class SyncManager {
continue;
}
this.recordTransactionsSize(newTransactions, peer?.role ?? "storage");
if (sourceRole && sourceRole !== "import") {
this.recordTransactionsSize(newTransactions, sourceRole);
}
peer?.updateSessionCounter(
msg.id,
@@ -710,12 +721,31 @@ export class SyncManager {
return this.sendNewContentIncludingDependencies(msg.id, peer);
}
dirtyCoValuesTrackingSets: Set<Set<RawCoID>> = new Set();
trackDirtyCoValues() {
const trackingSet = new Set<RawCoID>();
this.dirtyCoValuesTrackingSets.add(trackingSet);
return {
done: () => {
this.dirtyCoValuesTrackingSets.delete(trackingSet);
return trackingSet;
},
};
}
requestedSyncs = new Set<RawCoID>();
requestCoValueSync(coValue: CoValueCore) {
if (this.requestedSyncs.has(coValue.id)) {
return;
}
for (const trackingSet of this.dirtyCoValuesTrackingSets) {
trackingSet.add(coValue.id);
}
queueMicrotask(() => {
if (this.requestedSyncs.has(coValue.id)) {
this.syncCoValue(coValue);

View File

@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { IncomingMessagesQueue } from "../IncomingMessagesQueue.js";
import { PeerState } from "../PeerState.js";
import { IncomingMessagesQueue } from "../queue/IncomingMessagesQueue.js";
import { ConnectedPeerChannel } from "../streamUtils.js";
import { Peer, SyncMessage } from "../sync.js";
import {

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";
import { LinkedList } from "../PriorityBasedMessageQueue";
import { LinkedList } from "../queue/LinkedList.js";
describe("LinkedList", () => {
let list: LinkedList<number>;

View File

@@ -1,6 +1,6 @@
import { afterEach, describe, expect, test } from "vitest";
import { PriorityBasedMessageQueue } from "../PriorityBasedMessageQueue.js";
import { CO_VALUE_PRIORITY } from "../priority.js";
import { PriorityBasedMessageQueue } from "../queue/PriorityBasedMessageQueue.js";
import type { SyncMessage } from "../sync.js";
import {
createTestMetricReader,

View File

@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { StoreQueue } from "../storage/StoreQueue.js";
import { StoreQueue } from "../queue/StoreQueue.js";
import type { CoValueKnownState, NewContentMessage } from "../sync.js";
function createMockNewContentMessage(id: string): NewContentMessage[] {

View File

@@ -74,6 +74,69 @@ test("Can create account with one node, and then load it on another", async () =
expect(map2.get("foo")).toEqual("bar");
});
test("Should migrate the root from private to trusting", async () => {
const { node, accountID, accountSecret } =
await LocalNode.withNewlyCreatedAccount({
creationProps: { name: "Hermes Puggington" },
crypto: Crypto,
});
const group = await node.createGroup();
expect(group).not.toBeNull();
const map = group.createMap();
map.set("foo", "bar", "private");
expect(map.get("foo")).toEqual("bar");
const peers1 = connectedPeers("node1", "node2", {
peer1role: "server",
peer2role: "client",
});
const account = await node.load(accountID);
if (account === "unavailable") throw new Error("Account unavailable");
account.set("root", map.id, "private");
node.syncManager.addPeer(peers1[1]);
const node2 = await LocalNode.withLoadedAccount({
accountID,
accountSecret,
sessionID: Crypto.newRandomSessionID(accountID),
peersToLoadFrom: [peers1[0]],
crypto: Crypto,
});
const account2 = await node2.load(accountID);
if (account2 === "unavailable") throw new Error("Account unavailable");
expect(account2.getRaw("root")?.trusting).toEqual(true);
node2.gracefulShutdown(); // Stop getting updates from node1
const peers2 = connectedPeers("node2", "node3", {
peer1role: "server",
peer2role: "client",
});
node.syncManager.addPeer(peers2[1]);
const node3 = await LocalNode.withLoadedAccount({
accountID,
accountSecret,
sessionID: Crypto.newRandomSessionID(accountID),
peersToLoadFrom: [peers2[0]],
crypto: Crypto,
});
const account3 = await node3.load(accountID);
if (account3 === "unavailable") throw new Error("Account unavailable");
expect(account3.getRaw("root")?.trusting).toEqual(true);
expect(account3.ops).toEqual(account2.ops); // No new transactions were made
});
test("throws an error if the user tried to create an invite from an account", async () => {
const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
creationProps: { name: "Hermes Puggington" },

View File

@@ -1,5 +1,34 @@
# jazz-auth-betterauth
## 0.16.0
### Patch Changes
- Updated dependencies [c09dcdf]
- Updated dependencies [2bbb07b]
- jazz-tools@0.16.0
- cojson@0.16.0
- jazz-betterauth-client-plugin@0.16.0
## 0.15.16
### Patch Changes
- Updated dependencies [9633d01]
- Updated dependencies [4beafb7]
- jazz-tools@0.15.16
- jazz-betterauth-client-plugin@0.15.16
- cojson@0.15.16
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- jazz-betterauth-client-plugin@0.15.15
- cojson@0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,24 @@
# jazz-betterauth-client-plugin
## 0.16.0
### Patch Changes
- Updated dependencies [2bbb07b]
- jazz-betterauth-server-plugin@0.16.0
## 0.15.16
### Patch Changes
- jazz-betterauth-server-plugin@0.15.16
## 0.15.15
### Patch Changes
- jazz-betterauth-server-plugin@0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,40 @@
# jazz-betterauth-server-plugin
## 0.16.0
### Patch Changes
- 2bbb07b: Introduce a cleaner separation between Zod and CoValue schemas:
- Zod schemas and CoValue schemas are fully separated. Zod schemas can only be composed with other Zod schemas. CoValue schemas can be composed with either Zod or other CoValue schemas.
- `z.optional()` and `z.discriminatedUnion()` no longer work with CoValue schemas. Use `co.optional()` and `co.discriminatedUnion()` instead.
- Internal schema access is now simpler. You no longer need to use Zods `.def` to access internals. Use properties like `CoMapSchema.shape`, `CoListSchema.element`, and `CoOptionalSchema.innerType` directly.
- CoValue schema types are now namespaced under `co.`. Non-namespaced exports have been removed
- CoMap schemas no longer incorrectly inherit from Zod. Previously, methods like `.extend()` and `.partial()` appeared available but could cause unexpected behavior. These methods are now disabled. In their place, `.optional()` has been added, and more Zod-like methods will be introduced in future releases.
- Upgraded Zod from `3.25.28` to `3.25.76`.
- Removed deprecated `withHelpers` method from CoValue schemas
- Removed deprecated `createCoValueObservable` function
- Updated dependencies [c09dcdf]
- Updated dependencies [2bbb07b]
- jazz-tools@0.16.0
- cojson@0.16.0
## 0.15.16
### Patch Changes
- Updated dependencies [9633d01]
- Updated dependencies [4beafb7]
- jazz-tools@0.15.16
- cojson@0.15.16
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- cojson@0.15.15
## 0.15.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-server-plugin",
"version": "0.15.14",
"version": "0.16.0",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
@@ -10,7 +10,7 @@
"jazz-tools": "workspace:*",
"better-auth": "^1.2.4",
"better-sqlite3": "^11.9.1",
"zod": "3.25.28"
"zod": "3.25.76"
},
"scripts": {
"format-and-lint": "biome check .",
@@ -24,4 +24,4 @@
"typescript": "~5.6.2",
"@types/better-sqlite3": "^7.6.12"
}
}
}

View File

@@ -1,5 +1,37 @@
# jazz-react-auth-betterauth
## 0.16.0
### Patch Changes
- Updated dependencies [c09dcdf]
- Updated dependencies [2bbb07b]
- jazz-tools@0.16.0
- cojson@0.16.0
- jazz-auth-betterauth@0.16.0
- jazz-betterauth-client-plugin@0.16.0
## 0.15.16
### Patch Changes
- Updated dependencies [9633d01]
- Updated dependencies [4beafb7]
- jazz-tools@0.15.16
- jazz-auth-betterauth@0.15.16
- jazz-betterauth-client-plugin@0.15.16
- cojson@0.15.16
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- jazz-auth-betterauth@0.15.15
- jazz-betterauth-client-plugin@0.15.15
- cojson@0.15.15
## 0.15.14
### Patch Changes

View File

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

View File

@@ -1,5 +1,46 @@
# jazz-run
## 0.16.0
### Patch Changes
- 2bbb07b: Introduce a cleaner separation between Zod and CoValue schemas:
- Zod schemas and CoValue schemas are fully separated. Zod schemas can only be composed with other Zod schemas. CoValue schemas can be composed with either Zod or other CoValue schemas.
- `z.optional()` and `z.discriminatedUnion()` no longer work with CoValue schemas. Use `co.optional()` and `co.discriminatedUnion()` instead.
- Internal schema access is now simpler. You no longer need to use Zods `.def` to access internals. Use properties like `CoMapSchema.shape`, `CoListSchema.element`, and `CoOptionalSchema.innerType` directly.
- CoValue schema types are now namespaced under `co.`. Non-namespaced exports have been removed
- CoMap schemas no longer incorrectly inherit from Zod. Previously, methods like `.extend()` and `.partial()` appeared available but could cause unexpected behavior. These methods are now disabled. In their place, `.optional()` has been added, and more Zod-like methods will be introduced in future releases.
- Upgraded Zod from `3.25.28` to `3.25.76`.
- Removed deprecated `withHelpers` method from CoValue schemas
- Removed deprecated `createCoValueObservable` function
- Updated dependencies [c09dcdf]
- Updated dependencies [2bbb07b]
- jazz-tools@0.16.0
- cojson@0.16.0
- cojson-storage-sqlite@0.16.0
- cojson-transport-ws@0.16.0
## 0.15.16
### Patch Changes
- Updated dependencies [9633d01]
- Updated dependencies [4beafb7]
- jazz-tools@0.15.16
- cojson@0.15.16
- cojson-storage-sqlite@0.15.16
- cojson-transport-ws@0.15.16
## 0.15.15
### Patch Changes
- Updated dependencies [3fe53a3]
- jazz-tools@0.15.15
- cojson@0.15.15
- cojson-storage-sqlite@0.15.15
- cojson-transport-ws@0.15.15
## 0.15.14
### Patch Changes

View File

@@ -3,7 +3,7 @@
"bin": "./dist/index.js",
"type": "module",
"license": "MIT",
"version": "0.15.14",
"version": "0.16.0",
"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.14",
"cojson-storage-sqlite": "workspace:0.15.14",
"cojson-transport-ws": "workspace:0.15.14",
"cojson": "workspace:0.16.0",
"cojson-storage-sqlite": "workspace:0.16.0",
"cojson-transport-ws": "workspace:0.16.0",
"effect": "^3.6.5",
"jazz-tools": "workspace:0.15.14",
"jazz-tools": "workspace:0.16.0",
"ws": "^8.14.2"
},
"devDependencies": {

View File

@@ -5,15 +5,12 @@ import { join } from "node:path";
import {
Account,
AccountClass,
AccountSchema,
AnyAccountSchema,
CoMap,
CoValueFromRaw,
Group,
InboxSender,
Loaded,
co,
coField,
z,
} from "jazz-tools";
import { startWorker } from "jazz-tools/worker";

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