Compare commits

..

112 Commits

Author SHA1 Message Date
Guido D'Orsi
4d8bb9cdb8 Merge pull request #1495 from garden-co/changeset-release/main
Version Packages
2025-02-25 12:37:27 +01:00
github-actions[bot]
1971448f5d Version Packages 2025-02-25 11:31:50 +00:00
Guido D'Orsi
0e861e7df8 Merge pull request #1501 from garden-co/revert-1487-jazz-730-passkeyauth-update-the-profile-name-only-if-the-value-on
Revert "fix(PasskeyAuth): set name iff username given"
2025-02-25 12:30:04 +01:00
Guido D'Orsi
6e3f1efcd0 Revert "fix(PasskeyAuth): set name iff username given (#1487)"
This reverts commit d469d68771.
2025-02-25 12:29:28 +01:00
Guido D'Orsi
eb87d10783 Merge pull request #1498 from garden-co/remove-quick-crypto
fix: don't export RNQuickCrypto to avoid install errors
2025-02-25 12:14:22 +01:00
Guido D'Orsi
5a54e4aa50 fix: don't export RNQuickCrypto to avoid install errors 2025-02-25 11:51:57 +01:00
Guido D'Orsi
16b0a22ded chore: remove RNQuickCrypto from the chat example 2025-02-25 10:42:16 +01:00
Emil Sayahi
d469d68771 fix(PasskeyAuth): set name iff username given (#1487)
* fix(PasskeyAuth): set name iff username given

If an empty username is passed to `signUp`, the `profile.name` of the user is not updated.

See: #1433

* chore: changeset

* docs(changeset): mention PasskeyAuth

Co-authored-by: Guido D'Orsi <gu.dorsi@gmail.com>

---------

Co-authored-by: Guido D'Orsi <gu.dorsi@gmail.com>
2025-02-24 16:23:09 -05:00
pax
c068d7a369 Merge pull request #1494 from garden-co/update-cursor-docs
fix: cursor docs structure
2025-02-24 22:31:15 +02:00
pax-k
25088ed5db fix: cursor docs structure 2025-02-24 22:29:04 +02:00
pax
caa6c147c8 Merge pull request #1490 from garden-co/changeset-release/main
Version Packages
2025-02-24 18:40:15 +02:00
github-actions[bot]
4da51e8f9c Version Packages 2025-02-24 16:37:25 +00:00
pax
a2076b179b Merge pull request #1488 from garden-co/cursor-docs
Cursor docs
2025-02-24 18:35:06 +02:00
pax-k
3fa276c18d chore: changeset 2025-02-24 18:18:33 +02:00
pax-k
1928519d39 feat: added cursor docs 2025-02-24 18:16:39 +02:00
Anselm Eickhoff
f4fa80b782 Merge pull request #1469 from garden-co/changeset-release/main 2025-02-23 10:13:07 +00:00
github-actions[bot]
782df5d4b8 Version Packages 2025-02-23 10:12:42 +00:00
Anselm Eickhoff
9db20ad630 Merge pull request #1468 from Schniz/schniz/fix-rn-kv-persistence 2025-02-23 10:11:09 +00:00
Gal Schlezinger
3405d8f275 Add changeset 2025-02-23 12:09:48 +02:00
Gal Schlezinger
0a64dca0cd [jazz-react-native] override Context#getKvStore to actually use the declared kvStore in the provider 2025-02-23 11:42:46 +02:00
Guido D'Orsi
4d0b9b1bf1 Merge pull request #1466 from garden-co/changeset-release/main
Version Packages
2025-02-22 17:59:46 +01:00
github-actions[bot]
403d61c8e8 Version Packages 2025-02-22 16:58:53 +00:00
Guido D'Orsi
0685c1cd5f Merge pull request #1465 from garden-co/passkey-attachment
fix: remove the attachment setting to restore the faceid login
2025-02-22 17:57:16 +01:00
Guido D'Orsi
834203f270 fix: remove the attachment setting to restore the faceid login 2025-02-22 17:54:02 +01:00
Guido D'Orsi
f01bc19257 test: remove .only 2025-02-21 16:07:33 +01:00
Guido D'Orsi
6405a77aa2 chore: fix tsconfig module value on the root 2025-02-21 11:30:43 +01:00
Guido D'Orsi
ace6486075 test: add deepLoading tests on CoMap.Record 2025-02-21 11:29:51 +01:00
Guido D'Orsi
23c3e5a125 Merge pull request #1456 from garden-co/changeset-release/main
Version Packages
2025-02-21 11:08:01 +01:00
github-actions[bot]
e3d75e5c97 Version Packages 2025-02-21 10:05:16 +00:00
Guido D'Orsi
7838075bd6 Merge pull request #1446 from garden-co/yubikey-asskey-support
feat: support external authenticators on passkey authentication
2025-02-21 11:02:04 +01:00
Guido D'Orsi
dd792bf0ca Merge pull request #1450 from garden-co/fix-missing-or-undefined-values
fix: improve subscribe behavoir on missing required and undeclared fields
2025-02-21 11:01:46 +01:00
Guido D'Orsi
233aae1deb fix: throw when a requested field on CoMap.Record is missing 2025-02-21 10:41:53 +01:00
Guido D'Orsi
153dc996a5 fix: catch errors on CoValueCore subscribers to avoid effects on the sync 2025-02-21 10:35:07 +01:00
Anselm Eickhoff
2b548c1758 Merge pull request #1442 from garden-co/jazz-734-add-docs-about-inspector
Add jazz inspector docs
2025-02-21 09:10:14 +00:00
Anselm Eickhoff
167b588553 Merge pull request #1455 from garden-co/jazz-738-table-of-contents-not-showing-in-upgrade-guides
Fix missing table of contents on upgrade guides
2025-02-21 09:09:18 +00:00
Anselm Eickhoff
424930d06d Merge pull request #1454 from garden-co/jazz-737-font-mono-doesnt-look-right-in-safari
Fix monospace font in safari
2025-02-21 09:08:07 +00:00
Giordano Ricci
d624a676d8 Merge pull request #1449 from garden-co/gio/docs/anchors-headings
add anchor to headings in mdx components
2025-02-21 10:05:05 +01:00
Trisha Lim
06f2af465d Fix missing table of contents on upgrade guides 2025-02-21 13:33:54 +07:00
Trisha Lim
c7332f84b9 Fix monospace font in safari 2025-02-21 11:39:22 +07:00
Trisha Lim
68fdbfbe94 Clean up 2025-02-21 11:22:32 +07:00
Trisha Lim
e98d0e0c7f Match # size with heading text 2025-02-21 11:19:45 +07:00
Trisha Lim
7efe89df31 Spacing and sizing adjustments 2025-02-21 10:56:51 +07:00
Guido D'Orsi
2fb6428ea1 fix: improve subscribe behavoir on missing required and undeclared fields 2025-02-20 20:05:36 +01:00
Giordano Ricci
5a8f5d8bc2 add anchor to headings in mdx components 2025-02-20 17:46:53 +00:00
Guido D'Orsi
2cc9daab37 test: add unsubscribe test 2025-02-20 18:02:03 +01:00
Guido D'Orsi
e0276f42ee Merge pull request #1448 from garden-co/revert-1445-gio/docs/add-profile-migration-example
Revert "docs: enrich migration example with profile"
2025-02-20 17:17:49 +01:00
Guido D'Orsi
363be52022 Revert "docs: enrich migration example with profile" 2025-02-20 17:17:35 +01:00
Guido D'Orsi
1e87fc7772 feat: support external authenticators on passkey authentication 2025-02-20 17:01:03 +01:00
Giordano Ricci
03ec5d3ec8 Merge pull request #1445 from garden-co/gio/docs/add-profile-migration-example
docs: enrich migration example with profile
2025-02-20 16:43:27 +01:00
Giordano Ricci
5f272ff6ba docs: enrich migration example with profile 2025-02-20 15:15:25 +00:00
Guido D'Orsi
12392424dd feat(llms-txt): add the music player example code 2025-02-20 21:14:57 +07:00
Guido D'Orsi
58eb3c0a98 test: fix the cloudflare test code 2025-02-20 14:55:05 +01:00
Guido D'Orsi
adf965d53d test: increase the timeout to 10s on the cloudflare integration test 2025-02-20 14:48:10 +01:00
Guido D'Orsi
3baa951bb9 Merge pull request #1424 from garden-co/changeset-release/main
Version Packages
2025-02-20 14:46:21 +01:00
github-actions[bot]
b726e31669 Version Packages 2025-02-20 13:44:04 +00:00
Guido D'Orsi
63bb31e0ad Merge pull request #1431 from garden-co/auth-state-sync
fix: isAuthenticated out-of-sync with the account state
2025-02-20 14:42:07 +01:00
Guido D'Orsi
d059460abd Merge pull request #1414 from garden-co/fix/examples-auth
Remove log out button in form and image upload example
2025-02-20 14:32:12 +01:00
Guido D'Orsi
ba81951331 test: fix the mobile e2e tests 2025-02-20 14:22:21 +01:00
Guido D'Orsi
bd132bb9ad Merge pull request #1437 from garden-co/feat/enable-cmd-j-default
enable Cmd+J by default to launch inspector on dev
2025-02-20 14:10:31 +01:00
Benjamin S. Leveritt
1ee435ad95 Merge pull request #1439 from garden-co/feat/big-llms-txt
Make llms.txt more complete
2025-02-20 12:51:42 +00:00
Trisha Lim
07273f7ab8 Add jazz inspector docs 2025-02-20 19:17:06 +07:00
Guido D'Orsi
646ad330ae Merge pull request #1430 from garden-co/tests/cloudflare
test: add a cloudflare workers integration test
2025-02-20 13:06:58 +01:00
Guido D'Orsi
af8e6e3f82 chore: inline callbacks
Co-authored-by: Giordano Ricci <me@giordanoricci.com>
2025-02-20 13:05:05 +01:00
Guido D'Orsi
f247525dfe test: fix cloudflare tests on CI 2025-02-20 13:01:42 +01:00
Guido D'Orsi
012022db2b chore: changeset 2025-02-20 12:46:58 +01:00
Guido D'Orsi
da92891498 test: add cloudflare integration tests 2025-02-20 12:46:24 +01:00
Guido D'Orsi
b0c55720f8 feat: crypto option to provide PureJSCrypto on the runtimes where wasm is not supported 2025-02-20 12:45:59 +01:00
Trisha Lim
a3d825fc6f make llms.txt more complete 2025-02-20 18:34:01 +07:00
Guido D'Orsi
29ca2e6f65 update lockfile again 2025-02-20 12:07:29 +01:00
Guido D'Orsi
07a683c13d chore: remove cloudflare tests for compat issues with Vitest 3 2025-02-20 12:07:28 +01:00
Guido D'Orsi
e53f02d6d7 chore: update lockfile and format vitest config 2025-02-20 12:07:28 +01:00
Guido D'Orsi
4915bfa26d test: add a cloudflare workers integration test 2025-02-20 12:07:27 +01:00
Guido D'Orsi
af45aac5f2 feat: improve error logging on sync errors 2025-02-20 12:07:27 +01:00
Guido D'Orsi
1136d9b744 chore: changeset 2025-02-20 12:01:35 +01:00
Guido D'Orsi
92e78dc262 chore: clean up debug code 2025-02-20 11:54:57 +01:00
Trisha Lim
bf76d798c4 add changeset 2025-02-20 17:54:21 +07:00
Benjamin S. Leveritt
627f8c4c28 Merge pull request #1413 from garden-co/jazz-720-fix-jazz-crypto-rs-imports-for-nextjs
Bumps crypto version
2025-02-20 10:52:41 +00:00
Trisha Lim
a1e0410863 enable Cmd+J to launch inspector on dev 2025-02-20 17:52:41 +07:00
Guido D'Orsi
03897a2689 fix: clean up AuthSecretStorage notification management and fix the tests 2025-02-20 11:45:25 +01:00
Guido D'Orsi
823f546028 test: cover the logOut and authenticate updates with tests 2025-02-20 11:45:24 +01:00
Guido D'Orsi
9bc54d1939 chore: clean up the useCoState code 2025-02-20 11:45:24 +01:00
Guido D'Orsi
30780c05f0 fix: initial fix for logIn/logOut state sync 2025-02-20 11:45:24 +01:00
Guido D'Orsi
118b6294ac Merge pull request #1435 from garden-co/jazz-725-react-starter-profile-name-is-lost-on-signup
Fix wrong name field on form in starter app
2025-02-20 11:45:08 +01:00
Trisha Lim
6dc9b9d2ec Improve llms.txt documentation (#1436)
* Make download link to llms-full.txt more prominent

* add separate section for llms.txt convention
2025-02-20 17:41:38 +07:00
Benjamin S. Leveritt
ccbcee5102 Bumps crypto version 2025-02-20 10:41:31 +00:00
Giordano Ricci
0ae2067c3c Merge pull request #1403 from garden-co/gio/auth-fixes
fix: fixes clerk auth flow
2025-02-20 11:33:17 +01:00
Trisha Lim
2137938ead Fix wrong name field on form in starter app 2025-02-20 16:31:08 +07:00
Benjamin S. Leveritt
d14bb57ff5 Tweaks LLMs docs copy (#1423) 2025-02-20 11:17:19 +07:00
Giordano Ricci
3f42a4ddf9 add test 2025-02-19 15:25:02 +00:00
Giordano Ricci
0eed228170 add changeset 2025-02-19 14:32:48 +00:00
Giordano Ricci
a519537701 remove unused import 2025-02-19 13:52:14 +00:00
Giordano Ricci
43c79cac2a add simple test 2025-02-19 13:46:37 +00:00
Giordano Ricci
44dbaa00d4 revert wrong change 2025-02-19 12:12:01 +00:00
Giordano Ricci
a0df32e81a add types test 2025-02-19 12:08:42 +00:00
Giordano Ricci
236d8226d8 remove prevContext promise 2025-02-19 10:39:32 +00:00
Giordano Ricci
1220fa5d97 remove reset 2025-02-19 10:37:33 +00:00
Benjamin S. Leveritt
f3a5f83f25 Initializes the wasm module on creation 2025-02-19 10:36:29 +00:00
Benjamin S. Leveritt
a1bd6fc79b Bumps crypto version 2025-02-19 10:36:29 +00:00
Benjamin S. Leveritt
0f83320222 Chore: changeset 2025-02-19 10:36:29 +00:00
Benjamin S. Leveritt
a3c4067de3 Bumps crypto version 2025-02-19 10:36:29 +00:00
Benjamin S. Leveritt
3042627748 Merge pull request #1419 from garden-co/docs/llms
Add LLM page to docs
2025-02-19 10:18:44 +00:00
Guido D'Orsi
8cea1e96cf Merge pull request #1418 from garden-co/fix/idb-transactions
fix: improve the rollback on failure when handling new content in storage
2025-02-19 11:12:25 +01:00
Giordano Ricci
5cc58c8e02 whoopsie 2025-02-18 22:40:01 +00:00
Giordano Ricci
9df644c578 fix 2025-02-18 22:35:35 +00:00
Guido D'Orsi
1e625f3c12 chore: changeset 2025-02-18 18:14:39 +01:00
Guido D'Orsi
8b3686c7ce feat: use the uniqueSessions index to get the single coValue session 2025-02-18 18:12:47 +01:00
Guido D'Orsi
bce04ee06d chore: restore the transactions autobatching 2025-02-18 18:05:53 +01:00
Guido D'Orsi
f2e9115f4c fix: improve transactions management on IDB 2025-02-18 17:51:32 +01:00
Giordano Ricci
6854f9930c wip: fix clerk auth flow 2025-02-18 16:47:49 +00:00
Guido D'Orsi
ee0897d9a8 fix: improve the rollback on failure when handling new content in storage 2025-02-18 14:42:29 +01:00
Trisha Lim
243ab074eb Remove non-functional nav from image upload example 2025-02-18 20:21:55 +07:00
Trisha Lim
938f9256db Remove non-functional nav from form example 2025-02-18 15:31:53 +07:00
200 changed files with 7369 additions and 1563 deletions

View File

@@ -1,5 +1,48 @@
# chat-rn-clerk
## 1.0.76
### Patch Changes
- Updated dependencies [5a54e4a]
- jazz-react-native@0.10.11
- jazz-react-native-auth-clerk@0.10.11
## 1.0.75
### Patch Changes
- Updated dependencies [3405d8f]
- jazz-react-native@0.10.10
- jazz-react-native-auth-clerk@0.10.10
## 1.0.74
### Patch Changes
- jazz-react-native-auth-clerk@0.10.9
## 1.0.73
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react-native@0.10.8
- jazz-react-native-auth-clerk@0.10.8
- jazz-react-native-media-images@0.10.8
## 1.0.72
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react-native@0.10.7
- jazz-tools@0.10.7
- jazz-react-native-auth-clerk@0.10.7
- jazz-react-native-media-images@0.10.7
## 1.0.71
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "chat-rn-clerk",
"main": "index.js",
"version": "1.0.71",
"version": "1.0.76",
"scripts": {
"build": "expo export -p ios",
"start": "expo start",

View File

@@ -1,5 +1,36 @@
# chat-rn
## 1.0.72
### Patch Changes
- Updated dependencies [5a54e4a]
- jazz-react-native@0.10.11
## 1.0.71
### Patch Changes
- Updated dependencies [3405d8f]
- jazz-react-native@0.10.10
## 1.0.70
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react-native@0.10.8
## 1.0.69
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react-native@0.10.7
- jazz-tools@0.10.7
## 1.0.68
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.68",
"version": "1.0.72",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",
@@ -35,8 +35,6 @@
"react": "^18.3.1",
"react-native": "~0.76.3",
"react-native-get-random-values": "^1.11.0",
"react-native-nitro-modules": "0.21.0",
"react-native-quick-crypto": "1.0.0-beta.12",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.1.0",
"react-native-url-polyfill": "^2.0.0",

View File

@@ -9,7 +9,7 @@ import * as Linking from "expo-linking";
import React, { StrictMode, useEffect, useState } from "react";
import HandleInviteScreen from "./invite";
import { JazzProvider, RNQuickCrypto } from "jazz-react-native";
import { JazzProvider } from "jazz-react-native";
import { apiKey } from "./apiKey";
import ChatScreen from "./chat";
@@ -50,7 +50,6 @@ function App() {
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
CryptoProvider={RNQuickCrypto}
>
<NavigationContainer linking={linking} ref={navigationRef}>
<Stack.Navigator initialRouteName={initialRoute}>

View File

@@ -44,4 +44,5 @@ appId: com.jazz.chatrn
# logout
- tapOn: "Logout"
- assertVisible: "Anonymous user"
- assertVisible: "boorad"
- assertVisible: "bro, low key, it do be like that tho"

View File

@@ -1,5 +1,34 @@
# chat-vue
## 0.0.58
### Patch Changes
- Updated dependencies [834203f]
- jazz-browser@0.10.9
- jazz-vue@0.10.9
## 0.0.57
### Patch Changes
- Updated dependencies [1e87fc7]
- Updated dependencies [2fb6428]
- jazz-browser@0.10.8
- jazz-tools@0.10.8
- jazz-vue@0.10.8
## 0.0.56
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [bf76d79]
- Updated dependencies [0eed228]
- jazz-browser@0.10.7
- jazz-tools@0.10.7
- jazz-vue@0.10.7
## 0.0.55
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-vue",
"version": "0.0.55",
"version": "0.0.58",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,31 @@
# jazz-example-chat
## 0.0.154
### Patch Changes
- jazz-browser-media-images@0.10.9
- jazz-react@0.10.9
## 0.0.153
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
- jazz-browser-media-images@0.10.8
## 0.0.152
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
- jazz-browser-media-images@0.10.7
## 0.0.151
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-chat",
"private": true,
"version": "0.0.151",
"version": "0.0.154",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,31 @@
# minimal-auth-clerk
## 0.0.53
### Patch Changes
- jazz-react@0.10.9
- jazz-react-auth-clerk@0.10.9
## 0.0.52
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
- jazz-react-auth-clerk@0.10.8
## 0.0.51
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react-auth-clerk@0.10.7
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.50
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.50",
"version": "0.0.53",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,7 +13,7 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-react": "workspace:*",
"jazz-react-auth-clerk": "workspace:0.10.6",
"jazz-react-auth-clerk": "workspace:0.10.9",
"jazz-tools": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"

View File

@@ -1,5 +1,28 @@
# file-share-svelte
## 0.0.38
### Patch Changes
- jazz-svelte@0.10.9
## 0.0.37
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-svelte@0.10.8
## 0.0.36
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-svelte@0.10.7
- jazz-tools@0.10.7
## 0.0.35
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "file-share-svelte",
"version": "0.0.35",
"version": "0.0.38",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,31 @@
# form
## 0.0.49
### Patch Changes
- jazz-browser-media-images@0.10.9
- jazz-react@0.10.9
## 0.0.48
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
- jazz-browser-media-images@0.10.8
## 0.0.47
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
- jazz-browser-media-images@0.10.7
## 0.0.46
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "form",
"private": true,
"version": "0.0.46",
"version": "0.0.49",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,4 @@
import { useIframeHashRouter } from "hash-slash";
import { useAccount } from "jazz-react";
import { ID } from "jazz-tools";
import { CreateOrder } from "./CreateOrder.tsx";
import { EditOrder } from "./EditOrder.tsx";
@@ -7,25 +6,10 @@ import { Orders } from "./Orders.tsx";
import { BubbleTeaOrder } from "./schema.ts";
function App() {
const { me, logOut } = useAccount();
const router = useIframeHashRouter();
return (
<>
<header>
<nav className="container py-2 border-b flex items-center justify-between">
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button
className="bg-stone-100 py-1.5 px-3 text-sm rounded-md dark:bg-stone-900 dark:text-white"
onClick={() => logOut()}
>
Log out
</button>
</nav>
</header>
<main className="container py-8 space-y-8">
{router.route({
"/": () => <Orders />,

View File

@@ -1,5 +1,31 @@
# image-upload
## 0.0.51
### Patch Changes
- jazz-browser-media-images@0.10.9
- jazz-react@0.10.9
## 0.0.50
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
- jazz-browser-media-images@0.10.8
## 0.0.49
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
- jazz-browser-media-images@0.10.7
## 0.0.48
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "image-upload",
"private": true,
"version": "0.0.48",
"version": "0.0.51",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,19 +1,8 @@
import { useAccount } from "jazz-react";
import ImageUpload from "./ImageUpload.tsx";
function App() {
const { me, logOut } = useAccount();
return (
<>
<header>
<nav className="container">
<span>
You're logged in as <strong>{me?.profile?.name}</strong>
</span>
<button onClick={() => logOut()}>Log out</button>
</nav>
</header>
<main className="container">
<ImageUpload />
</main>

View File

@@ -46,7 +46,12 @@ export default function ImageUpload() {
) : (
<div>
<label>Upload image</label>
<input ref={inputRef} type="file" onChange={onImageChange} />
<input
ref={inputRef}
type="file"
accept="image/png, image/jpeg, image/gif"
onChange={onImageChange}
/>
</div>
)}
</div>

View File

@@ -72,8 +72,7 @@ nav {
.container {
margin-right: auto;
margin-left: auto;
padding-right: 0.75rem;
padding-left: 0.75rem;
padding: 2rem 0.75rem;
max-width: 800px;
}

View File

@@ -1,5 +1,22 @@
# jazz-example-inspector
## 0.0.109
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
- cojson-transport-ws@0.10.8
## 0.0.108
### Patch Changes
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- cojson@0.10.7
- cojson-transport-ws@0.10.7
## 0.0.107
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.107",
"version": "0.0.109",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cojson": "workspace:0.10.6",
"cojson-transport-ws": "workspace:0.10.6",
"cojson": "workspace:0.10.8",
"cojson-transport-ws": "workspace:0.10.8",
"hash-slash": "workspace:0.2.2",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",

View File

@@ -1,5 +1,30 @@
# jazz-example-musicplayer
## 0.0.75
### Patch Changes
- jazz-react@0.10.9
## 0.0.74
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-inspector@0.10.8
- jazz-react@0.10.8
## 0.0.73
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
- jazz-inspector@0.10.7
## 0.0.72
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.72",
"version": "0.0.75",
"type": "module",
"scripts": {
"dev": "vite",
@@ -22,8 +22,8 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:0.10.6",
"jazz-tools": "workspace:0.10.6",
"jazz-react": "workspace:0.10.9",
"jazz-tools": "workspace:0.10.8",
"lucide-react": "^0.274.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",

View File

@@ -1,5 +1,28 @@
# organization
## 0.0.47
### Patch Changes
- jazz-react@0.10.9
## 0.0.46
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
## 0.0.45
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.44
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "organization",
"private": true,
"version": "0.0.44",
"version": "0.0.47",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,24 @@
# passkey-svelte
## 0.0.42
### Patch Changes
- jazz-svelte@0.10.9
## 0.0.41
### Patch Changes
- jazz-svelte@0.10.8
## 0.0.40
### Patch Changes
- Updated dependencies [1136d9b]
- jazz-svelte@0.10.7
## 0.0.39
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "passkey-svelte",
"version": "0.0.39",
"version": "0.0.42",
"type": "module",
"private": true,
"scripts": {

View File

@@ -1,5 +1,28 @@
# minimal-auth-passkey
## 0.0.52
### Patch Changes
- jazz-react@0.10.9
## 0.0.51
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
## 0.0.50
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.49
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passkey",
"private": true,
"version": "0.0.49",
"version": "0.0.52",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,28 @@
# passphrase
## 0.0.49
### Patch Changes
- jazz-react@0.10.9
## 0.0.48
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
## 0.0.47
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.46
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "passphrase",
"private": true,
"version": "0.0.46",
"version": "0.0.49",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,28 @@
# jazz-password-manager
## 0.0.73
### Patch Changes
- jazz-react@0.10.9
## 0.0.72
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
## 0.0.71
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.70
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.70",
"version": "0.0.73",
"type": "module",
"scripts": {
"dev": "vite",
@@ -12,8 +12,8 @@
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
},
"dependencies": {
"jazz-react": "workspace:0.10.6",
"jazz-tools": "workspace:0.10.6",
"jazz-react": "workspace:0.10.9",
"jazz-tools": "workspace:0.10.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.41.5",

View File

@@ -1,5 +1,31 @@
# jazz-example-pets
## 0.0.171
### Patch Changes
- jazz-browser-media-images@0.10.9
- jazz-react@0.10.9
## 0.0.170
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
- jazz-browser-media-images@0.10.8
## 0.0.169
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
- jazz-browser-media-images@0.10.7
## 0.0.168
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.168",
"version": "0.0.171",
"type": "module",
"scripts": {
"dev": "vite",
@@ -19,9 +19,9 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-browser-media-images": "workspace:0.10.6",
"jazz-react": "workspace:0.10.6",
"jazz-tools": "workspace:0.10.6",
"jazz-browser-media-images": "workspace:0.10.9",
"jazz-react": "workspace:0.10.9",
"jazz-tools": "workspace:0.10.8",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",
@@ -41,7 +41,7 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.20",
"is-ci": "^3.0.1",
"jazz-run": "workspace:0.10.6",
"jazz-run": "workspace:0.10.8",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",

View File

@@ -1,5 +1,31 @@
# reactions
## 0.0.51
### Patch Changes
- jazz-browser-media-images@0.10.9
- jazz-react@0.10.9
## 0.0.50
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
- jazz-browser-media-images@0.10.8
## 0.0.49
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
- jazz-browser-media-images@0.10.7
## 0.0.48
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "reactions",
"private": true,
"version": "0.0.48",
"version": "0.0.51",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,34 @@
# todo-vue
## 0.0.56
### Patch Changes
- Updated dependencies [834203f]
- jazz-browser@0.10.9
- jazz-vue@0.10.9
## 0.0.55
### Patch Changes
- Updated dependencies [1e87fc7]
- Updated dependencies [2fb6428]
- jazz-browser@0.10.8
- jazz-tools@0.10.8
- jazz-vue@0.10.8
## 0.0.54
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [bf76d79]
- Updated dependencies [0eed228]
- jazz-browser@0.10.7
- jazz-tools@0.10.7
- jazz-vue@0.10.7
## 0.0.53
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "todo-vue",
"version": "0.0.53",
"version": "0.0.56",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,5 +1,28 @@
# jazz-example-todo
## 0.0.170
### Patch Changes
- jazz-react@0.10.9
## 0.0.169
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
## 0.0.168
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.167
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.167",
"version": "0.0.170",
"type": "module",
"scripts": {
"dev": "vite",
@@ -16,8 +16,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.10.6",
"jazz-tools": "workspace:0.10.6",
"jazz-react": "workspace:0.10.9",
"jazz-tools": "workspace:0.10.8",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",

View File

@@ -1,5 +1,28 @@
# version-history
## 0.0.48
### Patch Changes
- jazz-react@0.10.9
## 0.0.47
### Patch Changes
- Updated dependencies [2fb6428]
- jazz-tools@0.10.8
- jazz-react@0.10.8
## 0.0.46
### Patch Changes
- Updated dependencies [1136d9b]
- Updated dependencies [0eed228]
- jazz-react@0.10.7
- jazz-tools@0.10.7
## 0.0.45
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "version-history",
"private": true,
"version": "0.0.45",
"version": "0.0.48",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -14,6 +14,7 @@ import {
FolderArchiveIcon,
GaugeIcon,
GlobeIcon,
HashIcon,
ImageIcon,
LinkIcon,
LockKeyholeIcon,
@@ -53,6 +54,7 @@ const icons = {
encryption: LockKeyholeIcon,
faceId: ScanFace,
file: FileTextIcon,
hash: HashIcon,
help: MessageCircleQuestionIcon,
image: ImageIcon,
instant: GaugeIcon,

View File

@@ -136,6 +136,7 @@ const config = {
padding: "0.15rem 0.25rem",
borderRadius: "2px",
whiteSpace: "nowrap",
fontWeight: 400,
},
p: {
marginBottom: theme("spacing.3"),

View File

@@ -1,27 +1,21 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
import { ContentByFramework, FileDownloadLink, CodeGroup } from '@/components/forMdx'
# Using AI to build Jazz apps
AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at.
Jazz is a rapidly evolving framework, and the docs are a work in progress, and sometimes AI might get things a little wrong.
However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong.
To help the LLMs, we provide a number of [llms.txt](https://llmstxt.org/) files that are optimised for use with AI tools.
To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor.
## Getting started with AI tools
[llms.txt](https://llmstxt.org/) is a proposal to standardise the way that documentation is provided to AI tools at inference time that helps them understand the context of the code you're writing.
Many AI tools are starting to support this, and we've created a [llms-full.txt](https://jazz.tools/llms-full.txt) file that is optimised for use with AI tools, like Cursor.
<FileDownloadLink href="/llms-full.txt">llms-full.txt</FileDownloadLink>
## Setting up AI tools
Every AI tool is different, but generally you'll need to either provide the contents of the [llms-full.txt](https://jazz.tools/llms-full.txt) file in your prompt, or upload the file to the tool.
Every tool is different, but generally, you'll need to either paste the contents of the [llms-full.txt](https://jazz.tools/llms-full.txt) file directly in your prompt, or attach the file to the tool.
### ChatGPT and v0
<a href="/llms-full.txt" download>Download llms-full.txt</a>
Upload the txt file in your prompt.
![ChatGPT prompt with llms-full.txt attached](/chatgpt-with-llms-full-txt.jpg)
@@ -38,6 +32,10 @@ https://jazz.tools/llms-full.txt
```
</CodeGroup>
## llms.txt convention
We follow the llms.txt [proposed standard](https://llmstxt.org/) for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing.
## Limitations and considerations
AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good).

View File

@@ -0,0 +1,39 @@
import { CodeGroup } from '@/components/forMdx'
# Jazz Inspector
[Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues.
For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app.
[https://inspector.jazz.tools](https://inspector.jazz.tools)
## Exporting current account to Inspector from your app
In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`.
## Embedding the Inspector widget into your app
Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window.
Install the package.
<CodeGroup>
```sh
npm install jazz-inspector
```
</CodeGroup>
Render the component within your `JazzProvider`.
<CodeGroup>
```sh
import { JazzInspector } from "jazz-inspector";
<JazzProvider> // old
<JazzInspector />
</JazzProvider> // old
```
</CodeGroup>
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2_main.tsx) for a full example.

View File

@@ -4,31 +4,24 @@ export const metadata = { title: "Jazz 0.10.0 is out!" };
# Jazz 0.10.0 is out!
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
11 February 2025
</h2>
For Jazz 0.10.0 we have been focusing on enhancing authentication to make it optional, more flexible and easier to use.
<div>
For Jazz 0.10.0 we have been focusing on enhancing authentication to make it optional, more flexible and easier to use.
The default is now anonymous auth, which means that you can build the functionality of your app first and figure out auth later. For users this means that they can start using your app right away on one device -- and once you integrate an auth method, users can sign up and their anonymous accounts are transparently upgraded to authenticated accounts that work across devices.
The default is now anonymous auth, which means that you can build the functionality of your app first and figure out auth later. For users this means that they can start using your app right away on one device -- and once you integrate an auth method, users can sign up and their anonymous accounts are transparently upgraded to authenticated accounts that work across devices.
There are also some other minor improvements that will make your Jazz experience even better!
There are also some other minor improvements that will make your Jazz experience even better!
<h3>What's new?</h3>
Here is what's changed in this release:
- [New authentication flow](#new-authentication-flow): Now with anonymous auth, redesigned to make Jazz easier to start with and be more flexible.
- [Local-only mode](#local-only-mode): Users can now explore your app in local-only mode before signing up.
- [Improvements on the loading APIs](#improved-loading-api); `ensureLoaded` now always returns a value and `useCoState` now returns `null` if the value is not found.
- [Jazz Workers on native WebSockets](#native-websocket-for-jazz-workers): Improves compatibility with a wider set of Javascript runtimes.
- [Group inheritance with role mapping](#group-inheritance): Groups can now inherit members from other groups with a fixed role.
- Support for Node 14 dropped on cojson.
- Bugfix: `Group.removeMember` now returns a promise.
- Now `cojson` and `jazz-tools` don't export directly the crypto providers anymore. Replace the import with `cojson/crypto/WasmCrypto` or `cojson/crypto/PureJSCrypto` depending on your use case.
</div>
## What's new?
Here is what's changed in this release:
- [New authentication flow](#new-authentication-flow): Now with anonymous auth, redesigned to make Jazz easier to start with and be more flexible.
- [Local-only mode](#local-only-mode): Users can now explore your app in local-only mode before signing up.
- [Improvements on the loading APIs](#improved-loading-api); `ensureLoaded` now always returns a value and `useCoState` now returns `null` if the value is not found.
- [Jazz Workers on native WebSockets](#native-websocket-for-jazz-workers): Improves compatibility with a wider set of Javascript runtimes.
- [Group inheritance with role mapping](#group-inheritance): Groups can now inherit members from other groups with a fixed role.
- Support for Node 14 dropped on cojson.
- Bugfix: `Group.removeMember` now returns a promise.
- Now `cojson` and `jazz-tools` don't export directly the crypto providers anymore. Replace the import with `cojson/crypto/WasmCrypto` or `cojson/crypto/PureJSCrypto` depending on your use case.
<h3 id="new-authentication-flow">New authentication flow</h3>
<div>
## New authentication flow
Up until now authentication has been the first part to figure out when building a Jazz app, and this was a stumbling block for many.
Now it is no longer required and setting up a Jazz app is as easy as writing this:
@@ -191,8 +184,8 @@ export function AuthButton() {
if (isAuthenticated) {
return (
<Button
variant="outline"
<Button
variant="outline"
onClick={logOut}
>
Sign out
@@ -224,8 +217,8 @@ export function AuthButton() {
if (isAuthenticated) {
return (
<Button
variant="outline"
<Button
variant="outline"
onPress={logOut}
>
Sign out
@@ -267,16 +260,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
```
</CodeGroup>
</ContentByFramework>
For the changes related to the specific auth providers see the updated [authentication docs](/docs/authentication/overview).
</div>
<h3 id="local-only-mode">Local-only mode</h3>
<div>
For the changes related to the specific auth providers see the updated [authentication docs](/docs/authentication/overview).
## Local-only mode
If you are ok with data not being persisted on the sync server for anonymous users, you can now set your app to local-only depending on the user's authentication state.
With `sync.when` set to `"signedUp"` the app will work in local-only mode when the user is anonymous and unlock the multiplayer/multi-device features and cloud persistence when they sign up:
<CodeGroup>
```ts
```tsx
<JazzProvider
sync={{
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
@@ -290,10 +283,9 @@ With `sync.when` set to `"signedUp"` the app will work in local-only mode when t
</CodeGroup>
You can control when Jazz will sync by switching the `when` config to `"always"` or `"never"`.
</div>
<h3 id="improved-loading-api">Improvements on the loading APIs</h3>
<div>
## Improvements on the loading APIs
Before 0.10.0 `ensureLoaded` was returning a nullable value forcing the Typescript code to always include null checks:
<CodeGroup>
```ts
@@ -336,17 +328,17 @@ if (value === null) {
return <div>Track not found</div>;
}
```
</CodeGroup>
</div>
</CodeGroup>
<h3 id="native-websocket-for-jazz-workers">Jazz Workers on native WebSockets</h3>
<div>
We have removed the dependency on `ws` and switched to the native WebSocket API for Jazz Workers.
## Jazz Workers on native WebSockets
This improves the compatibility with a wider set of Javascript runtimes adding drop-in support for Deno, Bun, Browsers and Cloudflare Durable Objects.
We have removed the dependency on `ws` and switched to the native WebSocket API for Jazz Workers.
If you are using a Node.js version lower than 22 you will need to install the `ws` package and provide the WebSocket constructor:
<CodeGroup>
This improves the compatibility with a wider set of Javascript runtimes adding drop-in support for Deno, Bun, Browsers and Cloudflare Durable Objects.
If you are using a Node.js version lower than 22 you will need to install the `ws` package and provide the WebSocket constructor:
<CodeGroup>
```ts
import { WebSocket } from "ws";
import { startWorker } from "jazz-nodejs";
@@ -355,24 +347,21 @@ const { worker } = await startWorker({
WebSocket,
});
```
</CodeGroup>
</div>
</CodeGroup>
<h3 id="group-inheritance">Group inheritance with role mapping</h3>
<div>
You can override the inherited role by passing a second argument to `extend`.
## Group inheritance with role mapping
You can override the inherited role by passing a second argument to `extend`.
This can be used to give users limited access to a child group:
<CodeGroup>
```ts
const organization = Group.create();
const billing = Group.create();
This can be used to give users limited access to a child group:
<CodeGroup>
```ts
const organization = Group.create();
const billing = Group.create();
billing.extend(organization, "reader");
```
</CodeGroup>
billing.extend(organization, "reader");
```
</CodeGroup>
This way the members of the organization can only read the billing data, even if they are admins in the organization group.
This way the members of the organization can only read the billing data, even if they are admins in the organization group.
More about the group inheritance can be found in the [dedicated docs page](/docs/groups/inheritance).
</div>
More about the group inheritance can be found in the [dedicated docs page](/docs/groups/inheritance).

View File

@@ -1,545 +0,0 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
# Upgrade to Jazz 0.9.0
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
08 January 2025
</h2>
<ContentByFramework framework="react">
<div>
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzReactApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
</div>
<h3>New provider setup</h3>
<div>
The `JazzProvider` is now imported from `jazz-react` instead of `createJazzReactApp`.
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
import { MyAppAccount } from "./schema";
// Remove these lines // *bin*
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
return (
<>
{/* Replace Jazz.Provider with provider from jazz-react */}
<JazzProvider
auth={passkeyAuth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
>
{children} // old
</JazzProvider>
<PasskeyAuthBasicUI state={passKeyState} /> // old
</> // old
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" { // *add*
interface Register { // *add*
Account: MyAppAccount; // *add*
} // *add*
} // *add*
```
</CodeGroup>
<h3>Top level imports for hooks</h3>
<div>
All Jazz hooks are now available as top-level imports from the `jazz-react` package.
This change improves IDE intellisense support and simplifies imports:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
// Replace local imports with "jazz-react" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-react"; // *add*
export function Hello() {
const { me } = useAccount();
return (
<>
Hello {me.profile?.name}
</>
);
}
```
</CodeGroup>
<h3>New testing utilities</h3>
<div>
Removing `createJazzReactApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-react/testing";
import { renderHook } from "@testing-library/react"; // old
import { usePlaylist } from "./usePlaylist"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Use JazzTestProvider in your tests
const { result } = renderHook(() => usePlaylist(playlist.id), {
wrapper: ({ children }) => (
<JazzTestProvider account={account}>
{children}
</JazzTestProvider>
),
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result.current?.name).toBe("My playlist");
});
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="react-native">
<div>
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzRNApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
</div>
<h3>New provider setup</h3>
<div>
The `JazzProvider` is now imported from `jazz-react-native` instead of `createJazzRNApp`.
While `createJazzRNApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzRNApp` step and to provide the types through namespace declarations:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
import { MyAppAccount } from "./schema";
// Remove these lines // *bin*
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [auth, state] = useDemoAuth(); // old
return (
<>
{/* Replace Jazz.Provider with provider from jazz-react */}
<JazzProvider
auth={auth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */}
>
{children} // old
</JazzProvider>
<DemoAuthBasicUI appName="My App" state={state} /> // old
</> // old
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react-native" {
interface Register {
Account: MyAppAccount;
}
}
```
</CodeGroup>
<h3>Top level imports for hooks</h3>
<div>
All Jazz hooks are now available as top-level imports from the `jazz-react-native` package.
This change improves IDE intellisense support and simplifies imports:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
// Replace local imports with "jazz-react-native" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-react-native"; // *add*
export function Hello() {
const { me } = useAccount();
return (
<>
Hello {me.profile?.name}
</>
);
}
```
</CodeGroup>
<h3>New testing utilities</h3>
<div>
Removing `createJazzRNApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-react-native/testing";
import { renderHook } from "@testing-library/react-native"; // old
import { usePlaylist } from "./usePlaylist"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Use JazzTestProvider in your tests
const { result } = renderHook(() => usePlaylist(playlist.id), {
wrapper: ({ children }) => (
<JazzTestProvider account={account}>
{children}
</JazzTestProvider>
),
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result.current?.name).toBe("My playlist");
});
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="svelte">
<div>
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
</div>
<h3>New provider setup</h3>
<div>
The `JazzProvider` is now imported from `jazz-svelte` instead of `createJazzApp`.
While `createJazzApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzApp` step and to provide the types through namespace declarations:
</div>
<CodeGroup>
{/* prettier-ignore */}
```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts" module>
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module 'jazz-svelte' {
interface Register {
Account: MyAccount;
}
}
</script>
<script lang="ts">
import { Provider } from '$lib/jazz'; // *bin*
import { JazzProvider } from 'jazz-svelte';
// Example configuration for authentication and peer connection
let auth = null; // Replace with your auth implementation
let peer = "wss://your-peer-endpoint";
// The custom Account schema is passed now as a prop
let AccountSchema = MyAccount;
</script>
<JazzProvider {auth} {peer} {AccountSchema}>
<App />
</JazzProvider>
```
</CodeGroup>
<h3>Top level imports for hooks</h3>
<div>
All Jazz hooks are now available as top-level imports from the `jazz-svelte` package.
This change improves IDE intellisense support and simplifies imports:
</div>
<CodeGroup>
{/* prettier-ignore */}
```svelte
<script lang="ts">
import { useAccount } from '$lib/jazz'; // *bin*
import { useAccount } from 'jazz-svelte'; // *add*
const { me } = useAccount();
</script>
<div>
Hello {me.profile?.name}
</div>
```
</CodeGroup>
<h3>New testing utilities</h3>
<div>
Removing `createJazzApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
</div>
<CodeGroup>
{/* prettier-ignore */}
```ts
import { useCoState } from "jazz-svelte";
import { createJazzTestAccount, JazzTestProvider } from "jazz-svelte/testing";
import { render } from "@testing-library/svelte"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Use createJazzTestContext in your tests
render(PlaylistComponent, {
context: createJazzTestContext({ account: options.account }),
props: {
id: playlist.id,
},
});
expect(await screen.findByRole("heading", { name: "My playlist" })).toBeInTheDocument();
});
```
</CodeGroup>
</ContentByFramework>
<ContentByFramework framework="vue">
<div>
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzVueApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
</div>
<h3>New provider setup</h3>
<div>
The `JazzProvider` is now imported from `jazz-vue` instead of `createJazzVueApp`.
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
</div>
<CodeGroup>
{/* prettier-ignore */}
```typescript
import "./assets/main.css"; // old
import { DemoAuthBasicUI, useDemoAuth, JazzProvider } from "jazz-vue";
import { createApp, defineComponent, h } from "vue"; // old
import App from "./App.vue"; // old
import router from "./router"; // old
import { ToDoAccount } from "./schema"; // old
// Remove these lines // *bin*
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
const { JazzProvider } = Jazz; // *bin*
const RootComponent = defineComponent({ // old
name: "RootComponent", // old
setup() { // old
const { authMethod, state } = useDemoAuth(); // old
return () => [ // old
h( // old
JazzProvider, // old
{ // old
AccountSchema: ToDoAccount, // The custom Account schema is passed here now
auth: authMethod.value, // old
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co", // old
}, // old
{ // old
default: () => h(App), // old
}, // old
), // old
state.state !== "signedIn" && // old
h(DemoAuthBasicUI, { // old
appName: "Jazz Vue Todo", // old
state, // old
}), // old
]; // old
}, // old
}); // old
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const app = createApp(RootComponent); // old
app.use(router); // old
app.mount("#app"); // old
```
</CodeGroup>
<h3>Top level imports for hooks</h3>
<div>
All Jazz hooks are now available as top-level imports from the `jazz-vue` package.
This change improves IDE intellisense support and simplifies imports:
</div>
<CodeGroup>
{/* prettier-ignore */}
```typescript
<template>
Hello {{ me.profile?.name }}
</template>
<script setup lang="ts">
// Replace local imports with "jazz-vue" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-vue"; // *add*
const { me, logOut } = useAccount();
</script>
```
</CodeGroup>
<h3>New testing utilities</h3>
<div>
Removing `createJazzTestApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-vue/testing";
import { createApp, defineComponent, h } from "vue";
import { usePlaylist } from "./usePlaylist";
import { Playlist, MusicAccount } from "./schema"; // old
// This can be reused on other tests!
export const renderComposableWithJazz = <C extends (...args: any[]) => any>(
composable: C,
{ account }: { account: Account | { guest: AnonymousJazzAgent } },
) => {
let result;
const wrapper = defineComponent({
setup() {
result = composable();
// suppress missing template warning
return () => {};
},
});
// ✅ Use JazzTestProvider in your tests
const app = createApp({
setup() {
return () =>
h(
JazzTestProvider,
{
account,
},
{
default: () => h(wrapper),
},
);
},
});
app.mount(document.createElement("div"));
return [result, app] as [ReturnType<C>, ReturnType<typeof createApp>];
};
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Set up test data
const { result } = renderComposableWithJazz(() => usePlaylist(playlist.id), {
account,
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result?.name).toBe("My playlist");
});
```
</CodeGroup>
</ContentByFramework>

View File

@@ -0,0 +1,123 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
# Upgrade to Jazz 0.9.0
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzRNApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
## New provider setup
The `JazzProvider` is now imported from `jazz-react-native` instead of `createJazzRNApp`.
While `createJazzRNApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzRNApp` step and to provide the types through namespace declarations:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
import { MyAppAccount } from "./schema";
// Remove these lines // *bin*
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [auth, state] = useDemoAuth(); // old
return (
<>
{/* Replace Jazz.Provider with provider from jazz-react */}
<JazzProvider
auth={auth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */}
>
{children} // old
</JazzProvider>
<DemoAuthBasicUI appName="My App" state={state} /> // old
</> // old
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react-native" {
interface Register {
Account: MyAppAccount;
}
}
```
</CodeGroup>
## Top level imports for hooks
All Jazz hooks are now available as top-level imports from the `jazz-react-native` package.
This change improves IDE intellisense support and simplifies imports:
<CodeGroup>
{/* prettier-ignore */}
```tsx
// Replace local imports with "jazz-react-native" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-react-native"; // *add*
export function Hello() {
const { me } = useAccount();
return (
<>
Hello {me.profile?.name}
</>
);
}
```
</CodeGroup>
## New testing utilities
<div>
Removing `createJazzRNApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
</div>
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-react-native/testing";
import { renderHook } from "@testing-library/react-native"; // old
import { usePlaylist } from "./usePlaylist"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Use JazzTestProvider in your tests
const { result } = renderHook(() => usePlaylist(playlist.id), {
wrapper: ({ children }) => (
<JazzTestProvider account={account}>
{children}
</JazzTestProvider>
),
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result.current?.name).toBe("My playlist");
});
```
</CodeGroup>

View File

@@ -0,0 +1,120 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
# Upgrade to Jazz 0.9.0
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzReactApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
## New provider setup
The `JazzProvider` is now imported from `jazz-react` instead of `createJazzReactApp`.
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
import { MyAppAccount } from "./schema";
// Remove these lines // *bin*
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
return (
<>
{/* Replace Jazz.Provider with provider from jazz-react */}
<JazzProvider
auth={passkeyAuth} // old
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
>
{children} // old
</JazzProvider>
<PasskeyAuthBasicUI state={passKeyState} /> // old
</> // old
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" { // *add*
interface Register { // *add*
Account: MyAppAccount; // *add*
} // *add*
} // *add*
```
</CodeGroup>
## Top level imports for hooks
All Jazz hooks are now available as top-level imports from the `jazz-react` package.
This change improves IDE intellisense support and simplifies imports:
<CodeGroup>
{/* prettier-ignore */}
```tsx
// Replace local imports with "jazz-react" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-react"; // *add*
export function Hello() {
const { me } = useAccount();
return (
<>
Hello {me.profile?.name}
</>
);
}
```
</CodeGroup>
## New testing utilities
Removing `createJazzReactApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-react/testing";
import { renderHook } from "@testing-library/react"; // old
import { usePlaylist } from "./usePlaylist"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Use JazzTestProvider in your tests
const { result } = renderHook(() => usePlaylist(playlist.id), {
wrapper: ({ children }) => (
<JazzTestProvider account={account}>
{children}
</JazzTestProvider>
),
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result.current?.name).toBe("My playlist");
});
```
</CodeGroup>

View File

@@ -0,0 +1,113 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
# Upgrade to Jazz 0.9.0
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
## New provider setup
The `JazzProvider` is now imported from `jazz-svelte` instead of `createJazzApp`.
While `createJazzApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzApp` step and to provide the types through namespace declarations:
<CodeGroup>
{/* prettier-ignore */}
```svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts" module>
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module 'jazz-svelte' {
interface Register {
Account: MyAccount;
}
}
</script>
<script lang="ts">
import { Provider } from '$lib/jazz'; // *bin*
import { JazzProvider } from 'jazz-svelte';
// Example configuration for authentication and peer connection
let auth = null; // Replace with your auth implementation
let peer = "wss://your-peer-endpoint";
// The custom Account schema is passed now as a prop
let AccountSchema = MyAccount;
</script>
<JazzProvider {auth} {peer} {AccountSchema}>
<App />
</JazzProvider>
```
</CodeGroup>
## Top level imports for hooks
<div>
All Jazz hooks are now available as top-level imports from the `jazz-svelte` package.
This change improves IDE intellisense support and simplifies imports:
</div>
<CodeGroup>
{/* prettier-ignore */}
```svelte
<script lang="ts">
import { useAccount } from '$lib/jazz'; // *bin*
import { useAccount } from 'jazz-svelte'; // *add*
const { me } = useAccount();
</script>
<div>
Hello {me.profile?.name}
</div>
```
</CodeGroup>
## New testing utilities
Removing `createJazzApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
<CodeGroup>
{/* prettier-ignore */}
```ts
import { useCoState } from "jazz-svelte";
import { createJazzTestAccount, JazzTestProvider } from "jazz-svelte/testing";
import { render } from "@testing-library/svelte"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Use createJazzTestContext in your tests
render(PlaylistComponent, {
context: createJazzTestContext({ account: options.account }),
props: {
id: playlist.id,
},
});
expect(await screen.findByRole("heading", { name: "My playlist" })).toBeInTheDocument();
});
```
</CodeGroup>

View File

@@ -0,0 +1,165 @@
import { ContentByFramework, CodeGroup } from '@/components/forMdx'
export const metadata = { title: "Upgrade to Jazz 0.9.0" };
# Upgrade to Jazz 0.9.0
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzVueApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. 🌬️
## New provider setup
The `JazzProvider` is now imported from `jazz-vue` instead of `createJazzVueApp`.
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
<CodeGroup>
{/* prettier-ignore */}
```typescript
import "./assets/main.css"; // old
import { DemoAuthBasicUI, useDemoAuth, JazzProvider } from "jazz-vue";
import { createApp, defineComponent, h } from "vue"; // old
import App from "./App.vue"; // old
import router from "./router"; // old
import { ToDoAccount } from "./schema"; // old
// Remove these lines // *bin*
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
const { JazzProvider } = Jazz; // *bin*
const RootComponent = defineComponent({ // old
name: "RootComponent", // old
setup() { // old
const { authMethod, state } = useDemoAuth(); // old
return () => [ // old
h( // old
JazzProvider, // old
{ // old
AccountSchema: ToDoAccount, // The custom Account schema is passed here now
auth: authMethod.value, // old
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co", // old
}, // old
{ // old
default: () => h(App), // old
}, // old
), // old
state.state !== "signedIn" && // old
h(DemoAuthBasicUI, { // old
appName: "Jazz Vue Todo", // old
state, // old
}), // old
]; // old
}, // old
}); // old
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const app = createApp(RootComponent); // old
app.use(router); // old
app.mount("#app"); // old
```
</CodeGroup>
## Top level imports for hooks
All Jazz hooks are now available as top-level imports from the `jazz-vue` package.
This change improves IDE intellisense support and simplifies imports:
<CodeGroup>
{/* prettier-ignore */}
```typescript
<template>
Hello {{ me.profile?.name }}
</template>
<script setup lang="ts">
// Replace local imports with "jazz-vue" imports
import { useAccount } from "./main"; // *bin*
import { useAccount } from "jazz-vue"; // *add*
const { me, logOut } = useAccount();
</script>
```
</CodeGroup>
## New testing utilities
Removing `createJazzTestApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
<CodeGroup>
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-vue/testing";
import { createApp, defineComponent, h } from "vue";
import { usePlaylist } from "./usePlaylist";
import { Playlist, MusicAccount } from "./schema"; // old
// This can be reused on other tests!
export const renderComposableWithJazz = <C extends (...args: any[]) => any>(
composable: C,
{ account }: { account: Account | { guest: AnonymousJazzAgent } },
) => {
let result;
const wrapper = defineComponent({
setup() {
result = composable();
// suppress missing template warning
return () => {};
},
});
// ✅ Use JazzTestProvider in your tests
const app = createApp({
setup() {
return () =>
h(
JazzTestProvider,
{
account,
},
{
default: () => h(wrapper),
},
);
},
});
app.mount(document.createElement("div"));
return [result, app] as [ReturnType<C>, ReturnType<typeof createApp>];
};
test("should load the playlist", async () => {
// ✅ Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// ✅ Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// ✅ Set up test data
const { result } = renderComposableWithJazz(() => usePlaylist(playlist.id), {
account,
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result?.name).toBe("My playlist");
});
```
</CodeGroup>

View File

@@ -4,15 +4,9 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
# Jazz 0.9.8 - Without me!
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
14 January 2025
</h2>
We have simplified the API to make the "me" value always optional!
<div>
We have simplified the API to make the "me" value always optional!
This removes the need of using `useAccount` like the 90% of the time!
</div>
This removes the need of using `useAccount` like the 90% of the time!
<CodeGroup>
{/* prettier-ignore */}
@@ -20,6 +14,7 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
import { useState } from "react";
import { Issue } from "./schema";
import { IssueComponent } from "./components/Issue.tsx";
function App() {
const [issue, setIssue] = useState<Issue>();
@@ -43,9 +38,7 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
```
</CodeGroup>
<div>
This also applies to the load API:
</div>
This also applies to the load API:
<CodeGroup>
{/* prettier-ignore */}
@@ -54,9 +47,7 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
```
</CodeGroup>
<div>
And `Group.create`:
</div>
And `Group.create`:
<CodeGroup>
{/* prettier-ignore */}
@@ -67,8 +58,6 @@ export const metadata = { title: "Upgrade to Jazz 0.9.0" };
```
</CodeGroup>
<div>
Everything is backward compatible, so no upgrade steps are required.
With this Jazz API becomes way more lean and more is coming!
</div>

View File

@@ -4,46 +4,34 @@ export const metadata = { title: "Enable local persistence" };
# Enable local persistence
<h2 className="not-prose text-sm text-stone-600 dark:text-stone-400 mb-5 pb-2 border-b">
10 January 2025
</h2>
Version 0.9.2 introduces local persistence for React Native apps using SQLite.
<ContentByFramework framework="react-native">
<div>
Version 0.9.2 introduces local persistence for React Native apps using SQLite.
If you are upgrading from a version before 0.9.2, you need to enable local persistence by following the steps below.
If you are upgrading from a version before 0.9.2, you need to enable local persistence by following the steps below.
Local persistence will become the default in 0.10.0.
Local persistence will become the default in 0.10.0.
</div>
## Add the required dependencies
<h3>Add the required dependencies</h3>
As SQLite package we now use `@op-engineering/op-sqlite`.
<div>
As SQLite package we now use `@op-engineering/op-sqlite`.
To get local persistence working, you need to add `@op-engineering/op-sqlite` as a direct dependency to your project and run `npx pod-install`.
To get local persistence working, you need to add `@op-engineering/op-sqlite` as a direct dependency to your project and run `npx pod-install`.
</div>
## Update your JazzProvider to enable local persistence
<h3>Update your JazzProvider to enable local persistence</h3>
Local persistence is now disabled by default.
<div>
Local persistence is now disabled by default.
To enable it, you need to pass the `storage` option to the `JazzProvider` component:
To enable it, you need to pass the `storage` option to the `JazzProvider` component:
</div>
<CodeGroup>
```tsx
<JazzProvider
auth={auto}
storage="sqlite"
peer="wss://cloud.jazz.tools/?key=you@example.com"
AccountSchema={MyAppAccount}
>
<App />
</JazzProvider>
```
</CodeGroup>
</ContentByFramework>
<CodeGroup>
```tsx
<JazzProvider
auth={auto}
storage="sqlite"
peer="wss://cloud.jazz.tools/?key=you@example.com"
AccountSchema={MyAppAccount}
>
<App />
</JazzProvider>
```
</CodeGroup>

View File

@@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import { AnchorHTMLAttributes, DetailedHTMLProps } from "react";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
export function FileDownloadLink(
props: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>,
) {
if (!props.href) {
return props.children;
}
const { children, href } = props;
return (
<div className="inline-flex items-center font-medium text-stone-900 rounded-md border p-3 shadow-sm dark:text-white dark:bg-stone-925 flex py-2 rounded-lg ">
<Icon name="file" size="sm" className="mr-2" />
{children}
<a href={href} download className="ml-12">
Download
</a>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import { type ReactNode, useRef } from "react";
interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
children?: ReactNode;
}
export const Heading = ({ children, tag: Tag, ...props }: HeadingProps) => {
const linkRef = useRef<HTMLAnchorElement>(null);
return (
<Tag {...props} className="group">
<a
href={`#${props.id}`}
className="no-underline float-left absolute -ml-[1.25em] hidden sm:block opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Navigate to header"
ref={linkRef}
>
<span aria-hidden="true">#</span>
</a>
<span className="cursor-pointer" onClick={() => linkRef.current?.click()}>
{children}
</span>
</Tag>
);
};

View File

@@ -9,6 +9,8 @@ import {
} from "@/components/docs/ContentByFramework";
import { JazzLogo as JazzLogoClient } from "gcmp-design-system/src/app/components/atoms/logos/JazzLogo";
import { CodeGroup as CodeGroupClient } from "gcmp-design-system/src/app/components/molecules/CodeGroup";
import { AnchorHTMLAttributes, DetailedHTMLProps } from "react";
import { FileDownloadLink as FileDownloadLinkClient } from "./FileDownloadLink";
import { ComingSoon as ComingSoonClient } from "./docs/ComingSoon";
import { IssueTrackerPreview as IssueTrackerPreviewClient } from "./docs/IssueTrackerPreview";
@@ -35,3 +37,12 @@ export function IssueTrackerPreview() {
export function JazzLogo(props: { className?: string }) {
return <JazzLogoClient {...props} />;
}
export function FileDownloadLink(
props: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>,
) {
return <FileDownloadLinkClient {...props} />;
}

View File

@@ -1,12 +1,10 @@
import { promises as fs } from "fs";
import path from "path";
import { Deserializer, ReflectionKind } from "typedoc";
import { DOC_SECTIONS, PACKAGES } from "./utils/config.mjs";
import {
getPackageDescription,
loadTypedocFiles,
writeDocsFile,
} from "./utils/index.mjs";
import { join } from "path";
import { readFile, readdir } from "fs/promises";
import { Deserializer } from "typedoc";
import { DOC_SECTIONS } from "./utils/config.mjs";
import { loadTypedocFiles, writeDocsFile } from "./utils/index.mjs";
function formatType(type) {
if (!type) return "unknown";
@@ -500,7 +498,90 @@ async function generateDetailedDocs(docs) {
"- [Examples](https://jazz.tools/examples): Code examples and tutorials\n",
);
await writeDocsFile("llms-full.txt", output.join("\n"));
const outputWithExamples = [...output];
await readMusicExample(outputWithExamples);
await writeDocsFile("llms.txt", output.join("\n"));
await writeDocsFile("llms-full.txt", outputWithExamples.join("\n"));
}
/**
* @typedef {Object} FileContent
* @property {string} filepath - The relative path to the file
* @property {string} content - The content of the file
*/
/**
* Recursively loads all files from a directory and its subdirectories
* @param {string} directoryPath - The path to the directory to load
* @param {Object} options - Optional configuration
* @param {string[]} options.exclude - File patterns to exclude (e.g., ['*.md', '*.git'])
* @param {string} options.encoding - File encoding (default: 'utf-8')
* @returns {Promise<FileContent[]>} Array of filepath/content pairs
*/
async function loadDirectoryContent(directoryPath, options = {}) {
const { exclude = [], encoding = "utf-8" } = options;
async function processDirectory(currentPath) {
const results = [];
try {
const entries = await readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentPath, entry.name);
if (entry.isDirectory()) {
// Recursively process subdirectories
const subDirResults = await processDirectory(fullPath);
results.push(...subDirResults);
} else if (entry.isFile()) {
// Check if file should be excluded
const shouldExclude = exclude.some((pattern) => {
if (pattern.startsWith("*.")) {
const extension = pattern.slice(1);
return entry.name.endsWith(extension);
}
return entry.name === pattern;
});
if (!shouldExclude) {
try {
const content = await readFile(fullPath, { encoding });
results.push({
filepath: fullPath,
content: content,
});
} catch (error) {
console.error(`Error reading file ${fullPath}:`, error);
}
}
}
}
} catch (error) {
console.error(`Error reading directory ${currentPath}:`, error);
}
return results;
}
try {
return await processDirectory(directoryPath);
} catch (error) {
throw new Error(`Failed to load directory content: ${error.message}`);
}
}
async function readMusicExample(output) {
const files = await loadDirectoryContent(
path.join(process.cwd(), "../../examples/music-player/src"),
);
output.push("## Music Example\n\n");
for (const file of files) {
output.push(`### ${file.filepath}\n`);
output.push(`\`\`\`ts\n${file.content}\n\`\`\`\n`);
}
}
// Main execution

View File

@@ -1,80 +0,0 @@
import { Deserializer } from "typedoc";
import { DOC_SECTIONS, PACKAGES } from "./utils/config.mjs";
import {
cleanDescription,
loadTypedocFiles,
writeDocsFile,
} from "./utils/index.mjs";
async function generateConciseDocs(docs) {
const output = [];
const deserializer = new Deserializer();
// Project title
output.push("# Jazz\n");
// Documentation sections
output.push("## Documentation\n");
DOC_SECTIONS.forEach((section) => {
output.push(`### ${section.title}\n`);
section.pages.forEach((page) => {
output.push(`- [${page.title}](https://jazz.tools${page.url})\n`);
});
output.push("\n");
});
// API Reference by package
for (const [packageName, packageDocs] of Object.entries(docs)) {
const project = deserializer.reviveProject(packageDocs, packageName);
// Add package heading
output.push(`## ${packageName}\n`);
// Process each category and its exports with direct links
if (project.categories) {
const seen = new Set(); // Track seen names to avoid duplicates
project.categories.forEach((category) => {
category.children.forEach((child) => {
if (seen.has(child.name)) return;
seen.add(child.name);
// Get and clean up description
let description = child.comment?.summary
? cleanDescription(child.comment.summary)
: "";
// Truncate description if it's too long
if (description && description.length > 150) {
description = description.substring(0, 147) + "...";
}
// Create the line without wrapping
output.push(
`- [${child.name}](https://jazz.tools/api-reference/${packageName}#${child.name})${description ? `: ${description}` : ""}\n`,
);
});
});
output.push("\n");
}
}
// Optional section for additional resources
output.push("## Optional\n");
output.push(
"- [Documentation](https://jazz.tools/docs): Detailed documentation about Jazz\n",
);
output.push(
"- [Examples](https://jazz.tools/examples): Code examples and tutorials\n",
);
await writeDocsFile("llms.txt", output.join(""));
}
// Main execution
async function main() {
console.log("Generating concise LLM docs...");
const docs = await loadTypedocFiles();
await generateConciseDocs(docs);
}
main().catch(console.error);

View File

@@ -29,6 +29,11 @@ export const docNavigationItems = [
href: "/docs/ai-tools",
done: 100,
},
{
name: "Inspector",
href: "/docs/inspector",
done: 100,
},
],
},
{

View File

@@ -1,16 +1,22 @@
import { DocsLink } from "@/components/docs/DocsLink";
import type { MDXComponents } from "mdx/types";
import { Heading } from "./components/docs/DocHeading";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
a: (props) => <DocsLink {...props} />,
h2: (props) => <Heading tag="h2" {...props} />,
h3: (props) => <Heading tag="h3" {...props} />,
h4: (props) => <Heading tag="h4" {...props} />,
h5: (props) => <Heading tag="h5" {...props} />,
h6: (props) => <Heading tag="h6" {...props} />,
...components,
CodeWithInterpolation: ({
highlightedCode,
}: { highlightedCode: string }) => {
return <div dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
},
};
} satisfies MDXComponents;
}
export function InterpolateInCode(replace: { [key: string]: string }) {

View File

@@ -5,15 +5,13 @@
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=8192 next dev",
"build:generate-docs": "pnpm run generate:docs && pnpm run generate:llm-docs:all",
"build:generate-docs": "pnpm run generate:docs && pnpm run generate:llm-docs",
"build": "next build",
"start": "next start",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"generate:docs": "node generate-docs/typedocs.mjs --build",
"generate:llm-docs:all": "pnpm run generate:llm-docs:concise && pnpm run generate:llm-docs:full",
"generate:llm-docs:concise": "node generate-docs/llms.mjs",
"generate:llm-docs:full": "node generate-docs/llms-full.mjs",
"generate:llm-docs": "node generate-docs/llms-full.mjs",
"test": "pnpm run test:llm-docs",
"test:llm-docs": "node generate-docs/llms-full.test.mjs"
},

View File

@@ -23,7 +23,6 @@
"lefthook": "^1.8.2",
"pkg-pr-new": "^0.0.39",
"playwright": "^1.50.1",
"ts-node": "^10.9.1",
"turbo": "^2.3.1",
"typedoc": "^0.25.13",
"vitest": "3.0.5"

View File

@@ -1,5 +1,24 @@
# cojson-storage-indexeddb
## 0.10.8
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
- cojson-storage@0.10.8
## 0.10.7
### Patch Changes
- 1e625f3: Improve rollback on error when failing to add new content
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- Updated dependencies [1e625f3]
- cojson@0.10.7
- cojson-storage@0.10.7
## 0.10.6
### Patch Changes

View File

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

View File

@@ -0,0 +1,111 @@
export type StoreName =
| "coValues"
| "sessions"
| "transactions"
| "signatureAfter";
// A access unit for the IndexedDB Jazz database
// It's a wrapper around the IDBTransaction object that helps on batching multiple operations
// in a single transaction.
export class CoJsonIDBTransaction {
db: IDBDatabase;
tx: IDBTransaction;
pendingRequests: ((txEntry: this) => void)[] = [];
rejectHandlers: (() => void)[] = [];
id = Math.random();
running = false;
failed = false;
done = false;
constructor(db: IDBDatabase) {
this.db = db;
this.tx = this.db.transaction(
["coValues", "sessions", "transactions", "signatureAfter"],
"readwrite",
);
this.tx.oncomplete = () => {
this.done = true;
};
this.tx.onabort = () => {
this.done = true;
};
}
startedAt = performance.now();
isReusable() {
const delta = performance.now() - this.startedAt;
return !this.done && delta <= 20;
}
getObjectStore(name: StoreName) {
return this.tx.objectStore(name);
}
private pushRequest<T>(
handler: (txEntry: this, next: () => void) => Promise<T>,
) {
const next = () => {
const next = this.pendingRequests.shift();
if (next) {
next(this);
} else {
this.running = false;
this.done = true;
}
};
if (this.running) {
return new Promise<T>((resolve, reject) => {
this.rejectHandlers.push(reject);
this.pendingRequests.push(async () => {
try {
const result = await handler(this, next);
resolve(result);
} catch (error) {
reject(error);
}
});
});
}
this.running = true;
return handler(this, next);
}
handleRequest<T>(handler: (txEntry: this) => IDBRequest<T>) {
return this.pushRequest<T>((txEntry, next) => {
return new Promise<T>((resolve, reject) => {
const request = handler(txEntry);
request.onerror = () => {
this.failed = true;
this.tx.abort();
console.error(request.error);
reject(request.error);
// Don't leave any pending promise
for (const handler of this.rejectHandlers) {
handler();
}
};
request.onsuccess = () => {
resolve(request.result as T);
next();
};
});
});
}
commit() {
if (!this.done) {
this.tx.commit();
}
}
}

View File

@@ -1,4 +1,4 @@
import type { CojsonInternalTypes, RawCoID } from "cojson";
import type { CojsonInternalTypes, RawCoID, SessionID } from "cojson";
import type {
CoValueRow,
DBClientInterface,
@@ -8,119 +8,60 @@ import type {
StoredSessionRow,
TransactionRow,
} from "cojson-storage";
import { SyncPromise } from "./syncPromises.js";
import { CoJsonIDBTransaction } from "./CoJsonIDBTransaction.js";
export class IDBClient implements DBClientInterface {
private db;
currentTx:
| {
id: number;
tx: IDBTransaction;
stores: {
coValues: IDBObjectStore;
sessions: IDBObjectStore;
transactions: IDBObjectStore;
signatureAfter: IDBObjectStore;
};
startedAt: number;
pendingRequests: ((txEntry: {
stores: {
coValues: IDBObjectStore;
sessions: IDBObjectStore;
transactions: IDBObjectStore;
signatureAfter: IDBObjectStore;
};
}) => void)[];
}
| undefined;
currentTxID = 0;
activeTransaction: CoJsonIDBTransaction | undefined;
autoBatchingTransaction: CoJsonIDBTransaction | undefined;
constructor(db: IDBDatabase) {
this.db = db;
}
makeRequest<T>(
handler: (stores: {
coValues: IDBObjectStore;
sessions: IDBObjectStore;
transactions: IDBObjectStore;
signatureAfter: IDBObjectStore;
}) => IDBRequest,
): SyncPromise<T> {
return new SyncPromise((resolve, reject) => {
let txEntry = this.currentTx;
handler: (txEntry: CoJsonIDBTransaction) => IDBRequest<T>,
): Promise<T> {
if (this.activeTransaction) {
return this.activeTransaction.handleRequest<T>(handler);
}
const requestEntry = ({
stores,
}: {
stores: {
coValues: IDBObjectStore;
sessions: IDBObjectStore;
transactions: IDBObjectStore;
signatureAfter: IDBObjectStore;
};
}) => {
const request = handler(stores);
request.onerror = () => {
console.error("Error in request", request.error);
this.currentTx = undefined;
reject(request.error);
};
request.onsuccess = () => {
const value = request.result as T;
resolve(value);
if (this.autoBatchingTransaction?.isReusable()) {
return this.autoBatchingTransaction.handleRequest<T>(handler);
}
const next = txEntry?.pendingRequests.shift();
const tx = new CoJsonIDBTransaction(this.db);
if (next) {
next({ stores });
} else {
if (this.currentTx === txEntry) {
this.currentTx = undefined;
}
}
};
};
this.autoBatchingTransaction = tx;
// Transaction batching
if (!txEntry || performance.now() - txEntry.startedAt > 20) {
const tx = this.db.transaction(
["coValues", "sessions", "transactions", "signatureAfter"],
"readwrite",
);
txEntry = {
id: this.currentTxID++,
tx,
stores: {
coValues: tx.objectStore("coValues"),
sessions: tx.objectStore("sessions"),
transactions: tx.objectStore("transactions"),
signatureAfter: tx.objectStore("signatureAfter"),
},
startedAt: performance.now(),
pendingRequests: [],
};
this.currentTx = txEntry;
requestEntry(txEntry);
} else {
txEntry.pendingRequests.push(requestEntry);
}
});
return tx.handleRequest<T>(handler);
}
async getCoValue(coValueId: RawCoID): Promise<StoredCoValueRow | undefined> {
return this.makeRequest<StoredCoValueRow | undefined>(({ coValues }) =>
coValues.index("coValuesById").get(coValueId),
return this.makeRequest<StoredCoValueRow | undefined>((tx) =>
tx.getObjectStore("coValues").index("coValuesById").get(coValueId),
);
}
async getCoValueSessions(coValueRowId: number): Promise<StoredSessionRow[]> {
return this.makeRequest<StoredSessionRow[]>(({ sessions }) =>
sessions.index("sessionsByCoValue").getAll(coValueRowId),
return this.makeRequest<StoredSessionRow[]>((tx) =>
tx
.getObjectStore("sessions")
.index("sessionsByCoValue")
.getAll(coValueRowId),
);
}
async getSingleCoValueSession(
coValueRowId: number,
sessionID: SessionID,
): Promise<StoredSessionRow | undefined> {
return this.makeRequest<StoredSessionRow>((tx) =>
tx
.getObjectStore("sessions")
.index("uniqueSessions")
.get([coValueRowId, sessionID]),
);
}
@@ -128,13 +69,15 @@ export class IDBClient implements DBClientInterface {
sessionRowId: number,
firstNewTxIdx: number,
): Promise<TransactionRow[]> {
return this.makeRequest<TransactionRow[]>(({ transactions }) =>
transactions.getAll(
IDBKeyRange.bound(
[sessionRowId, firstNewTxIdx],
[sessionRowId, Number.POSITIVE_INFINITY],
return this.makeRequest<TransactionRow[]>((tx) =>
tx
.getObjectStore("transactions")
.getAll(
IDBKeyRange.bound(
[sessionRowId, firstNewTxIdx],
[sessionRowId, Number.POSITIVE_INFINITY],
),
),
),
);
}
@@ -142,9 +85,10 @@ export class IDBClient implements DBClientInterface {
sessionRowId: number,
firstNewTxIdx: number,
): Promise<SignatureAfterRow[]> {
return this.makeRequest<SignatureAfterRow[]>(
({ signatureAfter }: { signatureAfter: IDBObjectStore }) =>
signatureAfter.getAll(
return this.makeRequest<SignatureAfterRow[]>((tx) =>
tx
.getObjectStore("signatureAfter")
.getAll(
IDBKeyRange.bound(
[sessionRowId, firstNewTxIdx],
[sessionRowId, Number.POSITIVE_INFINITY],
@@ -160,8 +104,8 @@ export class IDBClient implements DBClientInterface {
throw new Error(`Header is required, coId: ${msg.id}`);
}
return (await this.makeRequest<IDBValidKey>(({ coValues }) =>
coValues.put({
return (await this.makeRequest<IDBValidKey>((tx) =>
tx.getObjectStore("coValues").put({
id: msg.id,
// biome-ignore lint/style/noNonNullAssertion: TODO(JAZZ-561): Review
header: msg.header!,
@@ -176,25 +120,26 @@ export class IDBClient implements DBClientInterface {
sessionUpdate: SessionRow;
sessionRow?: StoredSessionRow;
}): Promise<number> {
return this.makeRequest<number>(({ sessions }) =>
sessions.put(
sessionRow?.rowID
? {
rowID: sessionRow.rowID,
...sessionUpdate,
}
: sessionUpdate,
),
return this.makeRequest<number>(
(tx) =>
tx.getObjectStore("sessions").put(
sessionRow?.rowID
? {
rowID: sessionRow.rowID,
...sessionUpdate,
}
: sessionUpdate,
) as IDBRequest<number>,
);
}
addTransaction(
async addTransaction(
sessionRowID: number,
idx: number,
newTransaction: CojsonInternalTypes.Transaction,
) {
return this.makeRequest(({ transactions }) =>
transactions.add({
await this.makeRequest((tx) =>
tx.getObjectStore("transactions").add({
ses: sessionRowID,
idx,
tx: newTransaction,
@@ -211,8 +156,8 @@ export class IDBClient implements DBClientInterface {
idx: number;
signature: CojsonInternalTypes.Signature;
}) {
return this.makeRequest(({ signatureAfter }) =>
signatureAfter.put({
return this.makeRequest((tx) =>
tx.getObjectStore("signatureAfter").put({
ses: sessionRowID,
idx,
signature,
@@ -220,7 +165,24 @@ export class IDBClient implements DBClientInterface {
);
}
async unitOfWork(operationsCallback: () => unknown[]) {
return Promise.all(operationsCallback());
closeTransaction(tx: CoJsonIDBTransaction) {
tx.commit();
if (tx === this.activeTransaction) {
this.activeTransaction = undefined;
}
}
async transaction(operationsCallback: () => unknown) {
const tx = new CoJsonIDBTransaction(this.db);
this.activeTransaction = tx;
try {
await operationsCallback();
tx.commit(); // Tells the browser to not wait for another possible request and commit the transaction immediately
} finally {
this.activeTransaction = undefined;
}
}
}

View File

@@ -33,18 +33,7 @@ export class IDBNode {
}
await this.syncManager.handleSyncMessage(msg);
} catch (e) {
console.error(
new Error(
`Error reading from localNode, handling msg\n\n${JSON.stringify(
msg,
(k, v) =>
k === "changes" || k === "encryptedChanges"
? `${v.slice(0, 20)}...`
: v,
)}`,
{ cause: e },
),
);
console.error(e);
}
}
};

View File

@@ -1,224 +0,0 @@
const isFunction = (func: any) => typeof func === "function";
const isObject = (supposedObject: any) =>
typeof supposedObject === "object" &&
supposedObject !== null &&
!Array.isArray(supposedObject);
const isThenable = (obj: any) => isObject(obj) && isFunction(obj.then);
const identity = (co: any) => co;
export { identity, isFunction, isObject, isThenable };
enum States {
PENDING = "PENDING",
RESOLVED = "RESOLVED",
REJECTED = "REJECTED",
}
interface Handler<T, U> {
onSuccess: HandlerOnSuccess<T, U>;
onFail: HandlerOnFail<U>;
}
type HandlerOnSuccess<T, U = any> = (value: T) => U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) => U | Thenable<U>;
type Finally<U> = () => U | Thenable<U>;
interface Thenable<T> {
then<U>(
onSuccess?: HandlerOnSuccess<T, U>,
onFail?: HandlerOnFail<U>,
): Thenable<U>;
then<U>(
onSuccess?: HandlerOnSuccess<T, U>,
onFail?: (reason: any) => void,
): Thenable<U>;
}
type Resolve<R> = (value?: R | Thenable<R>) => void;
type Reject = (value?: any) => void;
export class SyncPromise<T> {
private state: States = States.PENDING;
private handlers: Handler<T, any>[] = [];
private value: T | any;
public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) {
try {
callback(this.resolve as Resolve<T>, this.reject);
} catch (e) {
this.reject(e);
}
}
private resolve = (value: T) => {
return this.setResult(value, States.RESOLVED);
};
private reject = (reason: any) => {
return this.setResult(reason, States.REJECTED);
};
private setResult = (value: T | any, state: States) => {
const set = () => {
if (this.state !== States.PENDING) {
return null;
}
if (isThenable(value)) {
return (value as Thenable<T>).then(this.resolve, this.reject);
}
this.value = value;
this.state = state;
return this.executeHandlers();
};
void set();
};
private executeHandlers = () => {
if (this.state === States.PENDING) {
return null;
}
for (const handler of this.handlers) {
if (this.state === States.REJECTED) {
handler.onFail(this.value);
} else {
handler.onSuccess(this.value);
}
}
this.handlers = [];
};
private attachHandler = (handler: Handler<T, any>) => {
this.handlers = [...this.handlers, handler];
this.executeHandlers();
};
// biome-ignore lint/suspicious/noThenProperty: TODO(JAZZ-561): Review
public then<U>(onSuccess: HandlerOnSuccess<T, U>, onFail?: HandlerOnFail<U>) {
return new SyncPromise<U>((resolve, reject) => {
return this.attachHandler({
onSuccess: (result) => {
try {
return resolve(onSuccess(result));
} catch (e) {
return reject(e);
}
},
onFail: (reason) => {
if (!onFail) {
return reject(reason);
}
try {
return resolve(onFail(reason));
} catch (e) {
return reject(e);
}
},
});
});
}
public catch<U>(onFail: HandlerOnFail<U>) {
return this.then<U>(identity, onFail);
}
// methods
public toString() {
return "[object SyncPromise]";
}
public finally<U>(cb: Finally<U>) {
return new SyncPromise<U>((resolve, reject) => {
let co: U | any;
let isRejected: boolean;
return this.then(
(value) => {
isRejected = false;
co = value;
return cb();
},
(reason) => {
isRejected = true;
co = reason;
return cb();
},
).then(() => {
if (isRejected) {
return reject(co);
}
return resolve(co);
});
});
}
public spread<U>(handler: (...args: any[]) => U) {
return this.then<U>((collection) => {
if (Array.isArray(collection)) {
return handler(...collection);
}
return handler(collection);
});
}
// static
public static resolve<U = any>(value?: U | Thenable<U>) {
return new SyncPromise<U>((resolve) => {
return resolve(value);
});
}
public static reject<U>(reason?: any) {
return new SyncPromise<U>((_resolve, reject) => {
return reject(reason);
});
}
public static all<U = any>(collection: (U | Thenable<U>)[]) {
return new SyncPromise<U[]>((resolve, reject) => {
if (!Array.isArray(collection)) {
return reject(new TypeError("An array must be provided."));
}
if (collection.length === 0) {
return resolve([]);
}
let counter = collection.length;
const resolvedCollection: U[] = [];
const tryResolve = (value: U, index: number) => {
counter -= 1;
resolvedCollection[index] = value;
if (counter !== 0) {
return null;
}
return resolve(resolvedCollection);
};
return collection.forEach((item, index) => {
return SyncPromise.resolve(item)
.then((value) => {
return tryResolve(value, index);
})
.catch(reject);
});
});
}
}

View File

@@ -0,0 +1,170 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { CoJsonIDBTransaction } from "../CoJsonIDBTransaction";
const TEST_DB_NAME = "test-cojson-idb-transaction";
describe("CoJsonIDBTransaction", () => {
let db: IDBDatabase;
beforeEach(async () => {
// Create test database
await new Promise<void>((resolve, reject) => {
const request = indexedDB.open(TEST_DB_NAME, 1);
request.onerror = () => reject(request.error);
request.onupgradeneeded = (event) => {
const db = request.result;
// Create test stores
db.createObjectStore("coValues", { keyPath: "id" });
const sessions = db.createObjectStore("sessions", { keyPath: "id" });
sessions.createIndex("uniqueSessions", ["coValue", "sessionID"], {
unique: true,
});
db.createObjectStore("transactions", { keyPath: "id" });
db.createObjectStore("signatureAfter", { keyPath: "id" });
};
request.onsuccess = () => {
db = request.result;
resolve();
};
});
});
afterEach(async () => {
// Close and delete test database
db.close();
await new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase(TEST_DB_NAME);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
});
test("handles successful write and read operations", async () => {
const tx = new CoJsonIDBTransaction(db);
// Write test
await tx.handleRequest((tx) =>
tx.getObjectStore("coValues").put({
id: "test1",
value: "hello",
}),
);
// Read test
const readTx = new CoJsonIDBTransaction(db);
const result = await readTx.handleRequest((tx) =>
tx.getObjectStore("coValues").get("test1"),
);
expect(result).toEqual({
id: "test1",
value: "hello",
});
});
test("handles multiple operations in single transaction", async () => {
const tx = new CoJsonIDBTransaction(db);
// Multiple writes
await Promise.all([
tx.handleRequest((tx) =>
tx.getObjectStore("coValues").put({
id: "test1",
value: "hello",
}),
),
tx.handleRequest((tx) =>
tx.getObjectStore("coValues").put({
id: "test2",
value: "world",
}),
),
]);
// Read results
const readTx = new CoJsonIDBTransaction(db);
const [result1, result2] = await Promise.all([
readTx.handleRequest((tx) => tx.getObjectStore("coValues").get("test1")),
readTx.handleRequest((tx) => tx.getObjectStore("coValues").get("test2")),
]);
expect(result1).toEqual({
id: "test1",
value: "hello",
});
expect(result2).toEqual({
id: "test2",
value: "world",
});
});
test("handles transaction across multiple stores", async () => {
const tx = new CoJsonIDBTransaction(db);
await Promise.all([
tx.handleRequest((tx) =>
tx.getObjectStore("coValues").put({
id: "value1",
data: "value data",
}),
),
tx.handleRequest((tx) =>
tx.getObjectStore("sessions").put({
id: "session1",
data: "session data",
}),
),
]);
const readTx = new CoJsonIDBTransaction(db);
const [valueResult, sessionResult] = await Promise.all([
readTx.handleRequest((tx) => tx.getObjectStore("coValues").get("value1")),
readTx.handleRequest((tx) =>
tx.getObjectStore("sessions").get("session1"),
),
]);
expect(valueResult).toEqual({
id: "value1",
data: "value data",
});
expect(sessionResult).toEqual({
id: "session1",
data: "session data",
});
});
test("handles failed transactions", async () => {
const tx = new CoJsonIDBTransaction(db);
await expect(
tx.handleRequest((tx) =>
tx.getObjectStore("sessions").put({
id: 1,
coValue: "value1",
sessionID: "session1",
data: "session data",
}),
),
).resolves.toBe(1);
expect(tx.failed).toBe(false);
const badTx = new CoJsonIDBTransaction(db);
await expect(
badTx.handleRequest((tx) =>
tx.getObjectStore("sessions").put({
id: 2,
coValue: "value1",
sessionID: "session1",
data: "session data",
}),
),
).rejects.toThrow();
expect(badTx.failed).toBe(true);
});
});

View File

@@ -1,5 +1,24 @@
# cojson-storage-sqlite
## 0.8.66
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
- cojson-storage@0.10.8
## 0.8.65
### Patch Changes
- 1e625f3: Improve rollback on error when failing to add new content
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- Updated dependencies [1e625f3]
- cojson@0.10.7
- cojson-storage@0.10.7
## 0.8.64
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-storage-rn-sqlite",
"type": "module",
"version": "0.8.64",
"version": "0.8.66",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",

View File

@@ -1,5 +1,10 @@
import { type DB as DatabaseT } from "@op-engineering/op-sqlite";
import { CojsonInternalTypes, type OutgoingSyncQueue, RawCoID } from "cojson";
import {
CojsonInternalTypes,
type OutgoingSyncQueue,
RawCoID,
SessionID,
} from "cojson";
import type {
DBClientInterface,
SessionRow,
@@ -48,6 +53,17 @@ export class SQLiteClient implements DBClientInterface {
return rows as StoredSessionRow[];
}
async getSingleCoValueSession(
coValueRowId: number,
sessionID: SessionID,
): Promise<StoredSessionRow | undefined> {
const { rows } = await this.db.execute(
"SELECT * FROM sessions WHERE coValue = ? AND sessionID = ?",
[coValueRowId, sessionID],
);
return rows[0] as StoredSessionRow | undefined;
}
async getNewTransactionInSession(
sessionRowId: number,
firstNewTxIdx: number,
@@ -142,12 +158,10 @@ export class SQLiteClient implements DBClientInterface {
);
}
async unitOfWork(
operationsCallback: () => Promise<unknown>[],
): Promise<void> {
async transaction(operationsCallback: () => unknown) {
try {
await this.db.transaction(async () => {
await Promise.all(operationsCallback());
await operationsCallback();
});
} catch (e) {
console.error("Transaction failed:", e);

View File

@@ -42,18 +42,6 @@ export class SQLiteReactNative {
await new Promise((resolve) => setTimeout(resolve, 0));
}
} catch (e) {
console.error(
new Error(
`Error reading from localNode, handling msg\n\n${JSON.stringify(
msg,
(k, v) =>
k === "changes" || k === "encryptedChanges"
? `${v.slice(0, 20)}...`
: v,
)}`,
{ cause: e },
),
);
console.error(e);
}
}

View File

@@ -1,5 +1,24 @@
# cojson-storage-sqlite
## 0.10.8
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
- cojson-storage@0.10.8
## 0.10.7
### Patch Changes
- 1e625f3: Improve rollback on error when failing to add new content
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- Updated dependencies [1e625f3]
- cojson@0.10.7
- cojson-storage@0.10.7
## 0.10.6
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.10.6",
"version": "0.10.8",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.7.0",
"cojson": "workspace:0.10.6",
"cojson": "workspace:0.10.8",
"cojson-storage": "workspace:*"
},
"devDependencies": {

View File

@@ -71,6 +71,17 @@ export class SQLiteClient implements DBClientInterface {
.all(coValueRowId) as StoredSessionRow[];
}
getSingleCoValueSession(
coValueRowId: number,
sessionID: SessionID,
): StoredSessionRow | undefined {
return this.db
.prepare<[number, string]>(
`SELECT * FROM sessions WHERE coValue = ? AND sessionID = ?`,
)
.get(coValueRowId, sessionID) as StoredSessionRow | undefined;
}
getNewTransactionInSession(
sessionRowId: number,
firstNewTxIdx: number,
@@ -159,7 +170,7 @@ export class SQLiteClient implements DBClientInterface {
.run(sessionRowID, idx, signature);
}
unitOfWork(operationsCallback: () => any[]) {
transaction(operationsCallback: () => unknown) {
this.db.transaction(operationsCallback)();
}
}

View File

@@ -1,5 +1,21 @@
# cojson-storage
## 0.10.8
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
## 0.10.7
### Patch Changes
- 1e625f3: Improve rollback on error when failing to add new content
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- cojson@0.10.7
## 0.10.6
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage",
"version": "0.10.6",
"version": "0.10.8",
"main": "dist/index.js",
"type": "module",
"types": "src/index.ts",

View File

@@ -200,15 +200,6 @@ export class SyncManager {
? coValueRow.rowID
: await this.dbClient.addCoValue(msg);
const allOurSessionsEntries =
await this.dbClient.getCoValueSessions(storedCoValueRowID);
const allOurSessions: {
[sessionID: SessionID]: StoredSessionRow;
} = Object.fromEntries(
allOurSessionsEntries.map((row) => [row.sessionID, row]),
);
const ourKnown: CojsonInternalTypes.CoValueKnownState = {
id: msg.id,
header: true,
@@ -217,9 +208,13 @@ export class SyncManager {
let invalidAssumptions = false;
await this.dbClient.unitOfWork(() =>
(Object.keys(msg.new) as SessionID[]).map((sessionID) => {
const sessionRow = allOurSessions[sessionID];
for (const sessionID of Object.keys(msg.new) as SessionID[]) {
await this.dbClient.transaction(async () => {
const sessionRow = await this.dbClient.getSingleCoValueSession(
storedCoValueRowID,
sessionID,
);
if (sessionRow) {
ourKnown.sessions[sessionRow.sessionID] = sessionRow.lastIdx;
}
@@ -229,8 +224,8 @@ export class SyncManager {
} else {
return this.putNewTxs(msg, sessionID, sessionRow, storedCoValueRowID);
}
}),
);
});
}
if (invalidAssumptions) {
this.sendStateMessage({

View File

@@ -45,9 +45,11 @@ describe("DB sync manager", () => {
const DBClient = vi.fn();
DBClient.prototype.getCoValue = vi.fn();
DBClient.prototype.getCoValueSessions = vi.fn();
DBClient.prototype.getSingleCoValueSession = vi.fn();
DBClient.prototype.getNewTransactionInSession = vi.fn();
DBClient.prototype.addSessionUpdate = vi.fn();
DBClient.prototype.addTransaction = vi.fn();
DBClient.prototype.unitOfWork = vi.fn((callback) => Promise.all(callback()));
DBClient.prototype.transaction = vi.fn((callback) => callback());
beforeEach(async () => {
const idbClient = new DBClient() as unknown as Mocked<DBClientInterface>;

View File

@@ -41,6 +41,11 @@ export interface DBClientInterface {
coValueRowId: number,
): Promise<StoredSessionRow[]> | StoredSessionRow[];
getSingleCoValueSession(
coValueRowId: number,
sessionID: SessionID,
): Promise<StoredSessionRow | undefined> | StoredSessionRow | undefined;
getNewTransactionInSession(
sessionRowId: number,
firstNewTxIdx: number,
@@ -79,5 +84,5 @@ export interface DBClientInterface {
signature: Signature;
}): Promise<number> | void | unknown;
unitOfWork(operationsCallback: () => unknown[]): Promise<unknown> | void;
transaction(callback: () => unknown): Promise<unknown> | void;
}

View File

@@ -1,5 +1,20 @@
# cojson-transport-nodejs-ws
## 0.10.8
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
## 0.10.7
### Patch Changes
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- cojson@0.10.7
## 0.10.6
### Patch Changes

View File

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

View File

@@ -1,5 +1,18 @@
# cojson
## 0.10.8
### Patch Changes
- 153dc99: Catch errors on CoValueCore subscribers to avoid effects on the sync
## 0.10.7
### Patch Changes
- 0f83320: Use jazz-crypto-rs isomorphic bundle
- 012022d: Improve error logging on sync errors
## 0.10.6
### Patch Changes

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.10.6",
"version": "0.10.8",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^1.29.0",
"typescript": "~5.6.2",
@@ -37,7 +37,7 @@
"@noble/hashes": "^1.4.0",
"@opentelemetry/api": "^1.0.0",
"@scure/base": "1.2.1",
"jazz-crypto-rs": "0.0.3",
"jazz-crypto-rs": "0.0.6",
"neverthrow": "^7.0.1",
"queueueue": "^4.1.2"
},

View File

@@ -35,6 +35,7 @@ import { CoValueKnownState, NewContentMessage } from "./sync.js";
import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
import { expectGroup } from "./typeUtils/expectGroup.js";
import { isAccountID } from "./typeUtils/isAccountID.js";
import { parseError } from "./utils.js";
/**
In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
@@ -326,7 +327,14 @@ export class CoValueCore {
if (notifyMode === "immediate") {
const content = this.getCurrentContent();
for (const listener of this.listeners) {
listener(content);
try {
listener(content);
} catch (e) {
logger.error(
"Error in listener for coValue " + this.id,
parseError(e),
);
}
}
} else {
if (!this.nextDeferredNotify) {
@@ -336,7 +344,14 @@ export class CoValueCore {
this.deferredUpdates = 0;
const content = this.getCurrentContent();
for (const listener of this.listeners) {
listener(content);
try {
listener(content);
} catch (e) {
logger.error(
"Error in listener for coValue " + this.id,
parseError(e),
);
}
}
resolve();
}, 0);

View File

@@ -7,6 +7,7 @@ import {
encrypt,
get_sealer_id,
get_signer_id,
initialize,
new_ed25519_signing_key,
new_x25519_private_key,
seal,
@@ -49,6 +50,7 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
}
static async create(): Promise<WasmCrypto> {
await initialize();
return new WasmCrypto();
}

View File

@@ -341,20 +341,8 @@ export class SyncManager {
});
return;
}
try {
await this.handleSyncMessage(msg, peerState);
} catch (e) {
throw new Error(
`Error reading from peer ${
peer.id
}, handling msg\n\n${JSON.stringify(msg, (k, v) =>
k === "changes" || k === "encryptedChanges"
? v.slice(0, 20) + "..."
: v,
)}`,
{ cause: e },
);
}
await this.handleSyncMessage(msg, peerState);
}
};

View File

@@ -7,6 +7,8 @@ import { LocalNode } from "../localNode.js";
import { Role } from "../permissions.js";
import {
createTestNode,
createTwoConnectedNodes,
loadCoValueOrFail,
randomAnonymousAccountAndSessionID,
} from "./testUtils.js";
@@ -221,3 +223,38 @@ test("creating a coValue with a group should't trigger automatically a content c
getCurrentContentSpy.mockRestore();
});
test("listeners are notified even if the previous listener threw an error", async () => {
const { node1, node2 } = await createTwoConnectedNodes("server", "server");
const group = node1.node.createGroup();
group.addMember("everyone", "writer");
const coMap = group.createMap();
const spy1 = vi.fn();
const spy2 = vi.fn();
coMap.subscribe(spy1);
coMap.subscribe(spy2);
spy1.mockImplementation(() => {
throw new Error("test");
});
const errorLog = vi.spyOn(console, "error").mockImplementation(() => {});
coMap.set("hello", "world");
expect(spy1).toHaveBeenCalledTimes(2);
expect(spy2).toHaveBeenCalledTimes(2);
expect(errorLog).toHaveBeenCalledTimes(1);
await coMap.core.waitForSync();
const mapOnNode2 = await loadCoValueOrFail(node2.node, coMap.id);
expect(mapOnNode2.get("hello")).toBe("world");
errorLog.mockRestore();
});

View File

@@ -4,6 +4,7 @@ import { ControlledAgent } from "../coValues/account.js";
import { WasmCrypto } from "../crypto/WasmCrypto.js";
import { expectGroup } from "../typeUtils/expectGroup.js";
import {
connectTwoPeers,
createTwoConnectedNodes,
groupWithTwoAdmins,
groupWithTwoAdminsHighLevel,
@@ -2029,9 +2030,13 @@ test("Can give write permissions to 'everyone' (high-level)", async () => {
.getCurrentContent(),
);
connectTwoPeers(group.core.node, childContent2.core.node, "server", "server");
// Ensure that the group is available to newAccount
await group.core.waitForSync();
expect(childContent2.get("foo")).toEqual("bar");
console.log("Before anon set");
childContent2.set("foo", "bar2", "private");
expect(childContent2.get("foo")).toEqual("bar2");
});

View File

@@ -0,0 +1,9 @@
export function parseError(e: unknown): {
message: string | null;
stack: string | null;
} {
return {
message: e instanceof Error ? e.message : null,
stack: e instanceof Error ? (e.stack ?? null) : null,
};
}

View File

@@ -0,0 +1,946 @@
---
## **CoMap Overview**
**CoMap** is a collaborative object mapping system from `jazz-tools`, mapping string keys to values.
### **1. Basic Definition**
```typescript
import { CoMap, co } from "jazz-tools";
class Person extends CoMap {
name = co.string;
age = co.number;
isActive = co.boolean;
}
```
### **2. Field Types**
- **Basic:** `co.string`, `co.number`, `co.boolean`
- **Optional:** `co.optional.string`, `co.optional.number`
- **Literals (Enums):** `co.literal("draft", "published", "archived")`
- **Dates:** `co.Date`
- **Custom Encoded:**
```typescript
customField = co.encoded({
encode: (v: string) => v.toUpperCase(),
decode: (v: unknown) => String(v).toLowerCase()
});
```
### **3. References to Other CoMaps**
```typescript
class Comment extends CoMap {
text = co.string;
createdAt = co.Date;
}
class Post extends CoMap {
title = co.string;
content = co.string;
mainComment = co.ref(Comment);
pinnedComment = co.optional.ref(Comment);
}
```
### **4. Lists with CoList**
```typescript
import { CoList, CoMap, co } from "jazz-tools";
class Task extends CoMap {
title = co.string;
completed = co.boolean;
}
class TaskList extends CoList.Of(co.ref(Task)) {}
class Project extends CoMap {
name = co.string;
tasks = co.ref(TaskList);
}
```
### **5. Validation & Custom Methods**
```typescript
class DraftPost extends CoMap {
title = co.optional.string;
content = co.optional.string;
validate() {
const errors: string[] = [];
if (!this.title) errors.push("Title is required");
if (!this.content) errors.push("Content is required");
return { errors };
}
get summary() { return this.content?.slice(0, 100) + "..."; }
}
```
### **Real-World Examples**
- **Chat Schema**
```typescript
class Message extends CoMap { text = co.string; image = co.optional.ref(ImageDefinition); }
class Chat extends CoList.Of(co.ref(Message)) {}
```
- **Organization Schema**
```typescript
class Project extends CoMap { name = co.string; }
class ListOfProjects extends CoList.Of(co.ref(Project)) {}
class Organization extends CoMap {
name = co.string;
projects = co.ref(ListOfProjects);
}
```
- **Issue Tracking**
```typescript
class Issue extends CoMap {
title = co.string;
description = co.string;
estimate = co.number;
status? = co.literal("backlog", "in progress", "done");
}
class ListOfIssues extends CoList.Of(co.ref(Issue)) {}
class Project extends CoMap {
name = co.string;
issues = co.ref(ListOfIssues);
}
```
### **Testing Example**
```typescript
class TestMap extends CoMap {
color = co.string;
_height = co.number;
birthday = co.Date;
name? = co.string;
nullable = co.optional.encoded<string | undefined>({
encode: (v: string | undefined) => v || null,
decode: (v: unknown) => (v as string) || undefined,
});
optionalDate = co.optional.encoded(Encoders.Date);
get roughColor() { return this.color + "ish"; }
}
```
### **Key Takeaways**
- Extend **CoMap** for schemas.
- Use `co.ref()` for references, `co.optional` for optional fields.
- Use `CoList.Of()` for collections.
- Fields auto-sync across clients.
- Add computed properties & validation methods.
---
## **CoList Overview**
**CoList** is a collaborative array in `jazz-tools`.
### **1. Basic Definition**
```typescript
import { CoList, co } from "jazz-tools";
class ColorList extends CoList.Of(co.string) {}
class NumberList extends CoList.Of(co.number) {}
class BooleanList extends CoList.Of(co.boolean) {}
```
### **2. Lists of CoMaps**
```typescript
import { CoList, CoMap, co } from "jazz-tools";
class Task extends CoMap { title = co.string; completed = co.boolean; }
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
```
### **3. CoList Operations**
```typescript
const taskList = ListOfTasks.create([], { owner: me });
taskList.push(Task.create({ title: "New task", completed: false }, { owner: me }));
const firstTask = taskList[0];
taskList.filter(task => !task.completed);
taskList.splice(1, 1);
```
### **4. Nested Lists**
```typescript
class Comment extends CoMap { text = co.string; createdAt = co.Date; }
class ListOfComments extends CoList.Of(co.ref(Comment)) {}
class Post extends CoMap {
title = co.string;
content = co.string;
comments = co.ref(ListOfComments);
}
class ListOfPosts extends CoList.Of(co.ref(Post)) {}
```
### **Real-World Examples**
- **Chat Schema**
```typescript
class Message extends CoMap { text = co.string; image = co.optional.ref(ImageDefinition); }
class Chat extends CoList.Of(co.ref(Message)) {}
```
- **Todo App Schema**
```typescript
class Task extends CoMap { done = co.boolean; text = co.string; }
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
```
- **Organization Schema**
```typescript
class Project extends CoMap { name = co.string; }
class ListOfProjects extends CoList.Of(co.ref(Project)) {}
class Organization extends CoMap {
name = co.string;
projects = co.ref(ListOfProjects);
}
```
### **5. Advanced Features**
```typescript
class TaskList extends CoList.Of(co.ref(Task)) {
getCompletedTasks() { return this.filter(task => task.completed); }
getPendingTasks() { return this.filter(task => !task.completed); }
}
```
### **Key Takeaways**
- `CoList.Of()` for list definitions.
- `co.ref()` for CoMap references.
- Acts like arrays with real-time sync.
- Supports custom methods & nested lists.
---
## **CoFeed Overview**
**CoFeed** is an append-only event stream, ideal for time-ordered data.
### **1. Basic Definition**
```typescript
import { CoFeed, co } from "jazz-tools";
class ActivityFeed extends CoFeed.Of(co.string) {}
class MetricsFeed extends CoFeed.Of(co.number) {}
```
### **2. Feeds with Complex Types**
```typescript
interface LogEvent {
timestamp: number;
level: "info" | "warn" | "error";
message: string;
}
class LogFeed extends CoFeed.Of(co.json<LogEvent>()) {}
```
### **3. Pet Reactions Example**
```typescript
export const ReactionTypes = ["aww","love","haha","wow","tiny","chonkers"] as const;
export class PetReactions extends CoFeed.Of(co.json<ReactionType>()) {}
```
### **4. Working with CoFeeds**
```typescript
const reactions = PetReactions.create({ owner: me });
reactions.post("love");
reactions.subscribe(feedId, me, {}, (feed) => console.log(feed.latest()));
```
### **5. Common Use Cases**
- **Activity Streams**
```typescript
class ActivityStream extends CoFeed.Of(co.json<{ type:"comment"|"like"|"share";userId:string;timestamp:number;}>) {}
```
- **Chat Messages**
```typescript
class ChatFeed extends CoFeed.Of(co.json<{ type:"message"|"join"|"leave";userId:string; content?:string;timestamp:number;}>) {}
```
- **Audit Logs**
```typescript
class AuditLog extends CoFeed.Of(co.json<{ action:string; user:string; details:Record<string,unknown>;timestamp:number;}>) {}
```
### **6. Differences: CoFeed vs. CoList**
| Feature | CoFeed (append-only) | CoList (mutable) |
|----------|----------------------|------------------|
| Order | Time-ordered | Arbitrary |
| Use Case | Logs, streams | Collections |
### **Key Takeaways**
- **CoFeed**: event logs, activity streams, append-only.
- **CoList**: modifiable lists.
- Real-time updates, easy to subscribe.
---
## **SchemaUnion Overview**
**SchemaUnion** handles runtime-discriminated union types of `CoMap` instances.
### **1. Basic Definition**
```typescript
import { SchemaUnion, CoMap, co } from "jazz-tools";
class BaseShape extends CoMap { type = co.string; }
class Circle extends BaseShape { type = co.literal("circle"); radius = co.number; }
class Rectangle extends BaseShape { type = co.literal("rectangle"); width = co.number; height = co.number; }
const Shape = SchemaUnion.Of<BaseShape>((raw) => {
switch (raw.get("type")) {
case "circle": return Circle;
case "rectangle": return Rectangle;
default: throw new Error("Unknown shape");
}
});
```
### **2. Nested Discriminators**
```typescript
class BaseButton extends CoMap { type = co.literal("button"); variant = co.string; }
class PrimaryButton extends BaseButton { variant = co.literal("primary"); label = co.string; size = co.literal("small","medium","large"); }
class SecondaryButton extends BaseButton { variant = co.literal("secondary"); label = co.string; outline = co.boolean; }
const Button = SchemaUnion.Of<BaseButton>((raw) => {
switch (raw.get("variant")) {
case "primary": return PrimaryButton;
case "secondary": return SecondaryButton;
default: throw new Error("Unknown variant");
}
});
```
### **3. Using SchemaUnion with CoLists**
```typescript
class BaseWidget extends CoMap { type = co.string; }
class ButtonWidget extends BaseWidget { type = co.literal("button"); label = co.string; }
class SliderWidget extends BaseWidget { type = co.literal("slider"); min = co.number; max = co.number; }
const Widget = SchemaUnion.Of<BaseWidget>((raw) => {
switch (raw.get("type")) {
case "button": return ButtonWidget;
case "slider": return SliderWidget;
default: throw new Error("Unknown widget");
}
});
class WidgetList extends CoList.Of(co.ref(Widget)) {}
```
### **4. Working with SchemaUnion Instances**
```typescript
const button = ButtonWidget.create({ type:"button", label:"Click me" }, { owner: me });
const widget = await loadCoValue(Widget, widgetId, me, {});
if (widget instanceof ButtonWidget) console.log(widget.label);
if (widget instanceof SliderWidget) console.log(widget.min, widget.max);
```
### **5. Validation Example**
```typescript
class BaseFormField extends CoMap {
type = co.string;
label = co.string;
required = co.boolean;
}
class TextField extends BaseFormField {
type = co.literal("text");
minLength = co.optional.number;
maxLength = co.optional.number;
validate(value: string){/* ... */}
}
class NumberField extends BaseFormField {
type = co.literal("number");
min = co.optional.number;
max = co.optional.number;
validate(value: number){/* ... */}
}
const FormField = SchemaUnion.Of<BaseFormField>((raw) => {
switch (raw.get("type")) {
case "text": return TextField;
case "number": return NumberField;
default: throw new Error("Unknown type");
}
});
```
### **Key Takeaways**
- **SchemaUnion** = polymorphic CoMaps.
- Use `co.literal()` for discriminators.
- `instanceof` for type-narrowing.
- Great for complex forms & dynamic components.
---
## **Groups, Accounts, Owners, Roles & Permissions in Jazz**
### **1. Ownership & Groups**
Every `CoValue` has an owner (an `Account` or `Group`):
```typescript
import { Account, Group, CoMap, co } from "jazz-tools";
const privateDoc = Document.create({ title:"Private" }, { owner: me });
const group = Group.create({ owner: me });
const sharedDoc = Document.create({ title:"Shared" }, { owner: group });
```
### **2. Roles & Permissions**
Built-in roles: `"admin"`, `"writer"`, `"reader"`, `"readerInvite"`, `"writerInvite"`.
```typescript
group.addMember(bob, "writer");
group.addMember(alice, "reader");
group.addMember("everyone","reader");
```
### **3. Organizations & Memberships**
```typescript
import { Account, CoMap, CoList, Group, co } from "jazz-tools";
class Project extends CoMap { name = co.string; description = co.string; }
class Organization extends CoMap {
name = co.string;
projects = co.ref(CoList.Of(co.ref(Project)));
static create(name: string, owner: Account) {
const group = Group.create({ owner });
return super.create({ name, projects: CoList.Of(co.ref(Project)).create([], { owner: group }) }, { owner: group });
}
addMember(account: Account, role: "admin"|"writer"|"reader") {
this._owner.castAs(Group).addMember(account, role);
}
}
```
### **4. Account Root & Migration Pattern**
```typescript
class TodoAccountRoot extends CoMap {
projects = co.ref(ListOfProjects);
}
export class UserProfile extends Profile { someProperty = co.string; }
class TodoAccount extends Account {
root = co.ref(TodoAccountRoot);
profile = co.ref(UserProfile);
migrate() {
if (!this._refs.root) {
this.root = TodoAccountRoot.create({ projects: ListOfProjects.create([], { owner: this }) }, { owner: this });
}
}
}
```
### **5. Public Sharing Example**
```typescript
class SharedFile extends CoMap {
name = co.string;
file = co.ref(FileStream);
createdAt = co.Date;
size = co.number;
}
class FileShareAccountRoot extends CoMap {
type = co.string;
sharedFiles = co.ref(ListOfSharedFiles);
publicGroup = co.ref(Group);
}
export class UserProfile extends Profile { someProperty = co.string; }
class FileShareAccount extends Account {
root = co.ref(FileShareAccountRoot);
profile = co.ref(UserProfile);
async migrate() {
await this._refs.root?.load();
if (!this.root || this.root.type !== "file-share-account") {
const publicGroup = Group.create({ owner: this });
publicGroup.addMember("everyone","reader");
this.root = FileShareAccountRoot.create({
type:"file-share-account",
sharedFiles: ListOfSharedFiles.create([], { owner: publicGroup }),
publicGroup
}, { owner: this });
}
}
}
```
### **6. Group Extensions**
```typescript
const parentGroup = Group.create({ owner: me });
parentGroup.addMember(bob, "reader");
const childGroup = Group.create({ owner: me });
childGroup.extend(parentGroup);
const doc = Document.create({ title:"Inherited Access" }, { owner: childGroup });
```
### **7. Checking Permissions**
```typescript
const group = document._owner.castAs(Group);
const myRole = group.myRole();
const hasWriteAccess = myRole === "admin" || myRole === "writer";
```
### **8. Invitation Pattern**
```typescript
class TeamInvite extends CoMap {
email = co.string;
role = co.literal("admin","writer","reader");
accepted = co.boolean;
}
class Team extends CoMap {
invites = co.ref(CoList.Of(co.ref(TeamInvite)));
async inviteMember(email: string, role: "admin"|"writer"|"reader") {
const group = this._owner.castAs(Group);
const invite = TeamInvite.create({ email, role, accepted:false }, { owner: group });
this.invites.push(invite);
group.addMember(email, (role+"Invite") as const);
}
acceptInvite(account: Account) {
const group = this._owner.castAs(Group);
const invite = this.invites.find(i => i.email === account.email);
if (invite) {
invite.accepted = true;
group.addMember(account, invite.role);
}
}
}
```
### **Key Takeaways**
- Every `CoValue` has an owner (Account or Group).
- Groups enable sharing/role-based access (`"admin"`, `"writer"`, `"reader"`, etc.).
- Groups can inherit permissions.
- Use account roots for private per-user data.
- Public sharing via `"everyone"` role.
- Invites allow controlled membership.
---
## **Inbox Pattern in Jazz**
Enables message exchange between accounts using `CoMap`, `CoList`, `Group`.
### **1. Basic Inbox Setup**
```typescript
import { CoMap, co, Group } from "jazz-tools";
class Message extends CoMap {
text = co.string;
createdAt = co.Date;
read = co.boolean;
}
class ChatInbox extends CoMap {
messages = co.ref(CoList.Of(co.ref(Message)));
lastReadAt = co.Date;
}
```
### **2. Sending Messages**
```typescript
async function sendMessage(sender: Account, receiverId: ID<Account>, text: string) {
const message = Message.create(
{ text, createdAt:new Date(), read:false },
{ owner: Group.create({ owner: sender }) }
);
const inboxSender = await InboxSender.load(receiverId, sender);
inboxSender.sendMessage(message);
}
```
### **3. Receiving Messages**
```typescript
async function setupInbox(receiver: Account) {
const inbox = await Inbox.load(receiver);
return inbox.subscribe(Message,(message,senderId)=>console.log("New:",message.text));
}
```
### **4. Chat Application Example**
```typescript
class ChatMessage extends CoMap { text = co.string; createdAt=co.Date; read=co.boolean; }
class ChatThread extends CoMap {
participants = co.json<string[]>();
messages = co.ref(CoList.Of(co.ref(ChatMessage)));
lastReadAt = co.optional.Date;
}
class ChatRoot extends CoMap {
threads = co.ref(CoList.Of(co.ref(ChatThread)));
inbox = co.ref(Inbox);
}
export class UserProfile extends Profile { someProperty=co.string; }
class ChatAccount extends Account {
root = co.ref(ChatRoot);
profile = co.ref(UserProfile);
async migrate() {
if(!this._refs.root) {
const group = Group.create({ owner:this });
this.root = ChatRoot.create({
threads: CoList.Of(co.ref(ChatThread)).create([], { owner: group }),
inbox: await Inbox.create(this)
},{ owner:this });
}
}
async sendMessage(to: ID<Account>, text:string) {
const message = ChatMessage.create({ text, createdAt:new Date(), read:false },
{ owner: Group.create({ owner:this }) });
const inboxSender=await InboxSender.load(to,this);
inboxSender.sendMessage(message);
}
async setupInboxListener() {
const inbox = await Inbox.load(this);
return inbox.subscribe(ChatMessage, async (message, senderId) => {
const thread = await this.findOrCreateThread(senderId);
thread.messages.push(message);
});
}
}
```
### **5. Testing the Inbox Pattern**
```typescript
describe("Inbox", () => {
it("should allow message exchange", async () => {
const { clientAccount:sender, serverAccount:receiver } = await setupTwoNodes();
const receiverInbox = await Inbox.load(receiver);
const message = Message.create({ text:"Hello" },{ owner:Group.create({ owner:sender }) });
const inboxSender = await InboxSender.load(receiver.id, sender);
inboxSender.sendMessage(message);
const receivedMessages: Message[] = [];
let senderAccountID: unknown;
const unsubscribe = receiverInbox.subscribe(Message, (msg, id) => {
senderAccountID=id; receivedMessages.push(msg);
});
await waitFor(() => receivedMessages.length===1);
expect(receivedMessages[0]?.text).toBe("Hello");
expect(senderAccountID).toBe(sender.id);
unsubscribe();
});
});
```
### **6. Message Status Tracking**
```typescript
class MessageStatus extends CoMap {
messageId = co.string;
delivered = co.boolean;
read = co.boolean;
readAt = co.optional.Date;
}
class EnhancedMessage extends CoMap {
text = co.string;
createdAt = co.Date;
status = co.ref(MessageStatus);
}
async function sendMessageWithStatus(sender: Account, receiverId: ID<Account>, text:string) {
const group = Group.create({ owner:sender });
const status = MessageStatus.create({ messageId:crypto.randomUUID(), delivered:false, read:false }, { owner:group });
const message = EnhancedMessage.create({ text, createdAt:new Date(), status }, { owner:group });
const inboxSender = await InboxSender.load(receiverId,sender);
inboxSender.sendMessage(message);
return message;
}
```
### **Key Takeaways**
- Messages owned by a `Group` from the sender.
- Use `InboxSender.load()` to send, `Inbox.load()` to receive.
- Subscribe for real-time updates.
- Append status tracking as needed.
---
## **Invite Pattern in Jazz**
Sharing access to `CoValues` with other users via invites.
### **1. Creating & Handling Invites**
```typescript
import { CoMap, Group, co, createInviteLink } from "jazz-tools";
class Project extends CoMap { name = co.string; members = co.ref(CoList.Of(co.ref(Member))); }
const group = Group.create({ owner: me });
const project = Project.create({ name:"New Project", members:CoList.Of(co.ref(Member)).create([],{owner:group}) }, { owner:group });
const readerInvite = createInviteLink(project,"reader");
const writerInvite = createInviteLink(project,"writer");
const adminInvite = createInviteLink(project,"admin");
```
### **2. Accepting Invites in UI**
```typescript
import { useAcceptInvite } from "jazz-react";
useAcceptInvite({
invitedObjectSchema:Project,
onAccept:(projectId)=>navigate(`/projects/${projectId}`)
});
```
### **3. Organization Example**
```typescript
class Organization extends CoMap {
name = co.string;
projects = co.ref(ListOfProjects);
createInvite(role:"reader"|"writer"|"admin"){ return createInviteLink(this,role); }
}
useAcceptInvite({
invitedObjectSchema:Organization,
onAccept:async(orgId)=>{/* ... */}
});
```
### **4. Value Hints in Invites**
```typescript
class Team extends CoMap {
name = co.string;
generateInvite(role:"reader"|"writer"|"admin"){ return createInviteLink(this, role, window.location.origin, "team"); }
}
useAcceptInvite({
invitedObjectSchema:Team,
forValueHint:"team",
onAccept:(teamId)=>navigate(`/teams/${teamId}`)
});
```
### **5. Testing Invites**
```typescript
describe("Invite Links", () => {
test("generate and parse invites", async () => {
const inviteLink = createInviteLink(group, "writer","https://example.com","myGroup");
const parsed = parseInviteLink(inviteLink);
expect(parsed?.valueID).toBe(group.id);
expect(parsed?.valueHint).toBe("myGroup");
});
test("accept invite", async () => {
const newAccount = await createJazzTestAccount();
const inviteLink = createInviteLink(group, "writer");
const result = await consumeInviteLink({ inviteURL: inviteLink, as:newAccount, invitedObjectSchema:Group });
expect(result?.valueID).toBe(group.id);
});
});
```
### **6. File Sharing with Invites**
```typescript
class SharedFile extends CoMap {
name = co.string;
sharedWith = co.ref(CoList.Of(co.ref(SharedWith)));
}
class SharedWith extends CoMap {
email = co.string;
role = co.literal("reader","writer");
acceptedAt = co.optional.Date;
}
class FileShareAccount extends Account {
async shareFile(file:SharedFile, email:string, role:"reader"|"writer") {
const inviteLink = createInviteLink(file,role);
file.sharedWith.push(SharedWith.create({ email, role, acceptedAt:null },{ owner:file._owner }));
await sendInviteEmail(email, inviteLink);
}
}
useAcceptInvite({
invitedObjectSchema:SharedFile,
onAccept:async(fileId)=>{
const file = await SharedFile.load(fileId,{});
const shareRecord = file.sharedWith.find(s=>s.email===currentUser.email);
if(shareRecord) shareRecord.acceptedAt=new Date();
navigate(`/files/${fileId}`);
}
});
```
### **Key Takeaways**
- Use Groups for shared ownership.
- `createInviteLink()` generates invites.
- `useAcceptInvite()` handles acceptance.
- Value hints (`forValueHint`) differentiate invite types.
---
## **CoValue Types & Patterns in Jazz**
### **1. CoMap**
- Use for structured data with named fields.
- Example:
```typescript
class UserProfile extends CoMap {
name = co.string;
email = co.string;
avatar = co.ref(FileStream);
preferences = co.json<{ theme:string; notifications:boolean }>();
}
class TagColors extends CoMap.Record(co.string) {}
```
### **2. CoList**
- Use for ordered, real-time collaborative arrays.
```typescript
class TodoList extends CoList.Of(co.ref(TodoItem)) {}
class StringList extends CoList.Of(co.string) {}
```
### **3. CoFeed**
- Use for append-only event/log data.
```typescript
class UserActivity extends CoFeed.Of(co.json<{ type:string; timestamp:number; text?:string }>) {}
```
### **4. SchemaUnion**
- Use for polymorphic objects with a runtime discriminator.
```typescript
class BaseWidget extends CoMap { type=co.string; }
class ButtonWidget extends BaseWidget { type=co.literal("button"); label=co.string; }
const Widget=SchemaUnion.Of<BaseWidget>(raw=>raw.get("type")==="button"?ButtonWidget:null);
```
### **5. Groups & Permissions**
- Owner can be an Account or Group.
- Roles: `"admin"|"writer"|"reader"|"readerInvite"|"writerInvite"`.
```typescript
const group=Group.create({owner:me});
group.addMember("everyone","reader");
```
### **6. Accounts**
- Per-user data storage with migrations.
```typescript
class JazzAccount extends Account {
root=co.ref(JazzAccountRoot);
profile=co.ref(UserProfile);
async migrate(){/*...*/}
}
```
### **7. Migrations**
- Update/initialize user data on account creation/login.
### **8. Invites**
- Role-based sharing through invite links.
### **Common Patterns**
- **Account Root Pattern**: store users top-level data.
- **Shared Document Pattern**: CoMap for doc, CoList for collaborators, CoFeed for history.
- **Draft Pattern**: CoMap with partial fields, validation.
- **Public Sharing**: set `Group.addMember("everyone","reader")`.
---
## **Examples**
1. **User Profile Storage (CoMap)**
**JSON**:
```json
{
"name":"John Doe","email":"john@example.com",
"avatar":{"url":"...","size":"..."},
"preferences":{"theme":"dark","notifications":true}
}
```
**Jazz**:
```typescript
class UserProfile extends CoMap {
name = co.string;
email = co.string;
avatar = co.ref(FileStream);
preferences = co.json<{theme:string;notifications:boolean}>();
}
```
2. **To-Do List (CoList)**
**JSON**:
```json
{"tasks":[{"id":1,"title":"Buy groceries","completed":false},{"id":2,"title":"Call mom","completed":true}]}
```
**Jazz**:
```typescript
class TodoItem extends CoMap { title=co.string; completed=co.boolean; }
class TodoList extends CoList.Of(co.ref(TodoItem)) {}
```
3. **Activity Feed (CoFeed)**
**JSON**:
```json
{"activities":[{"type":"login","timestamp":1700000000},{"type":"logout","timestamp":1700000500},{"type":"comment","timestamp":1700001000,"text":"Great post!"}]}
```
**Jazz**:
```typescript
class UserActivity extends CoFeed.Of(co.json<{type:string;timestamp:number;text?:string}>()) {}
```
4. **Polymorphic Widgets (SchemaUnion)**
**JSON**:
```json
{"widgets":[{"type":"button","label":"Click Me"},{"type":"slider","min":0,"max":100}]}
```
**Jazz**:
```typescript
class BaseWidget extends CoMap { type=co.string; }
class ButtonWidget extends BaseWidget { type=co.literal("button"); label=co.string; }
class SliderWidget extends BaseWidget { type=co.literal("slider"); min=co.number; max=co.number; }
const Widget=SchemaUnion.Of<BaseWidget>((raw)=>{...});
```
5. **Access Control via Groups**
**JSON**:
```json
{"group":{"owner":"user123","members":[{"id":"user456","role":"admin"},{"id":"user789","role":"writer"}]}}
```
**Jazz**:
```typescript
const group=Group.create({owner:user123});
group.addMember(user456,"admin");
group.addMember(user789,"writer");
```
6. **User Account with Root Data (Accounts)**
**JSON**:
```json
{"user":{"profile":{"name":"Jane Doe"},"documents":[{"title":"My Notes","content":"This is a note."}],"activities":[{"type":"login"}]}}
```
**Jazz**:
```typescript
class AppAccountRoot extends CoMap {
profile=co.ref(UserProfile);
documents=co.ref(CoList.Of(co.ref(Document)));
activities=co.ref(UserActivity);
}
class AppAccount extends Account {
root=co.ref(AppAccountRoot);
profile=co.ref(UserProfile);
}
```
7. **Document Collaboration**
**JSON**:
```json
{
"document":{
"title":"Project Plan","content":"Detailed...","collaborators":[{"id":"user1","role":"editor"},{"id":"user2","role":"viewer"}],
"history":[{"user":"user1","timestamp":1700000000,"change":"Edited content"}]
}
}
```
**Jazz**:
```typescript
class Document extends CoMap {
title=co.string;content=co.string;
collaborators=co.ref(CoList.Of(co.ref(UserProfile)));
history=co.ref(CoFeed.Of(co.json<{user:string;timestamp:number;change:string}>()));
}
```
8. **Draft System**
**JSON**:
```json
{"draft":{"name":"New Project","tasks":[],"valid":false,"errors":["Project name required"]}}
```
**Jazz**:
```typescript
class DraftProject extends CoMap {
name=co.optional.string;
tasks=co.ref(CoList.Of(co.ref(TodoItem)));
validate(){/*...*/}
}
```
9. **Public File Sharing**
**JSON**:
```json
{"file":{"name":"Presentation.pdf","size":2048,"uploadedAt":1700000000,"sharedWith":["everyone"]}}
```
**Jazz**:
```typescript
class SharedFile extends CoMap {
name=co.string;
file=co.ref(FileStream);
uploadedAt=co.Date;
}
const publicGroup=Group.create({owner:me});
publicGroup.addMember("everyone","reader");
```
10. **Invite System**
**JSON**:
```json
{"invites":[{"email":"user@example.com","role":"writer","status":"pending"}]}
```
**Jazz**:
```typescript
class Invite extends CoMap {
email=co.string;
role=co.literal("reader","writer","admin");
status=co.literal("pending","accepted");
}
const inviteLink=createInviteLink(project,"writer");
useAcceptInvite({ invitedObjectSchema:Project, onAccept:(id)=>navigate(`/projects/${id}`) });
```
---
Continue: 2_jazz_schema_template.md

View File

@@ -0,0 +1,216 @@
---
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
/**
* Represents a main data item in the apps domain.
*
* Properties:
* - name: Required field identifying the item.
* - metadata_field: Optional metadata (string).
* - container: Reference to a parent Container.
* - deleted: Soft delete flag for archiving/removing without permanent deletion.
*/
export class MainItem extends CoMap {
/** A required, identifying name. */
name = co.string;
/** An optional string field for metadata. */
metadata_field = co.optional.string;
/** Reference to the parent container. */
container = co.ref(Container);
/** Soft-delete flag: if true, treat this item as removed. */
deleted = co.boolean;
}
/**
* A list/array of MainItem references.
* Provides real-time collaboration features (insertion, removal, ordering).
*/
export class MainItemList extends CoList.Of(co.ref(MainItem)) {}
/**
* A container/organizational structure for grouping MainItem objects.
*
* Properties:
* - name: A human-friendly name for the container.
* - items: A CoList of MainItem references.
*/
export class Container extends CoMap {
/** Human-friendly name for this container. */
name = co.string;
/** A list of MainItems held by this container. */
items = co.ref(MainItemList);
}
/**
* The top-level structure in the users account, representing all stored data.
*
* Properties:
* - container: The default or root container for MainItems.
* - version: An optional version number for supporting migrations.
*/
export class AccountRoot extends CoMap {
/** A single container to hold or organize items. */
container = co.ref(Container);
/** Tracks schema version for migrations. */
version = co.optional.number;
}
/**
* Represents a users profile data.
*
* Properties:
* - email: Required email field for identification/contact.
*
* Static method:
* - validate: Enforces that both name and email are provided.
*/
export class UserProfile extends Profile {
/** Required user email. */
email = co.string;
/**
* Validate user profile data, ensuring both "name" and "email" exist and are non-empty.
*/
static validate(data: { name?: string; email?: string }) {
const errors: string[] = [];
if (!data.name?.trim()) {
errors.push("Please enter a name.");
}
if (!data.email?.trim()) {
errors.push("Please enter an email.");
}
return { errors };
}
}
/**
* The main Account class that holds the users data (AccountRoot) and profile.
* Handles initial migrations (setting up default Container, etc.) and can be extended
* to run future schema migrations.
*/
export class JazzAccount extends Account {
/** Reference to the users profile. */
profile = co.ref(UserProfile);
/** Reference to the account root data (container, version, etc.). */
root = co.ref(AccountRoot);
/**
* Migrate is run on creation and each login. If there is no root, creates initial data.
* Otherwise, you can add version-based migrations (below).
*/
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
if (!this._refs.root && creationProps) {
await this.initialMigration(creationProps);
return;
}
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
// uncomment this to add migrations
// Check the current version and run subsequent migrations
// const currentVersion = this.root?.version || 0;
// if (currentVersion < 1) {
// await this.migrationV1();
// }
// Add more version checks and migrations as needed
// if (currentVersion < 2) {
// await this.migrationV2();
// }
}
/**
* Executes the initial migration logic when the account is first created:
* - Validates the users profile data (name, email).
* - Sets up a public group (readable by "everyone") for the users profile.
* - Sets up a private group to own private resources.
* - Creates a default Container with a single MainItem.
*/
private async initialMigration(
creationProps: { name: string; other?: Record<string, unknown> }
) {
const { name, other } = creationProps;
// Validate profile data
const profileErrors = UserProfile.validate({ name, ...other });
if (profileErrors.errors.length > 0) {
throw new Error(
"Invalid profile data: " + profileErrors.errors.join(", "),
);
}
// Create a public group for the profile
const publicGroup = Group.create({ owner: this });
publicGroup.addMember("everyone", "reader");
// Create the user profile with validated data
this.profile = UserProfile.create(
{
name,
...other,
},
{ owner: publicGroup },
);
// Create a private group for data that should not be publicly readable
const privateGroup = Group.create({ owner: this });
// Create a default container with one default item
const defaultContainer = Container.create(
{
name: this.profile?.name
? \`\${this.profile.name}'s items\`
: "Your items",
items: MainItemList.create(
[
MainItem.create({ name: "Default item" }, privateGroup),
],
privateGroup,
),
},
privateGroup,
);
// Initialize the account root with version tracking
this.root = AccountRoot.create(
{
container: defaultContainer,
version: 0, // Start at version 0
},
{ owner: this },
);
}
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
// uncomment this to add migrations
// private async migrationV1() {
// Example migration logic:
// if (this.root) {
// // e.g., add a new field to all items
// // for (const container of this.root.containers || []) {
// // for (const item of container.items || []) {
// // item.newField = "default value";
// // }
// // }
// this.root.version = 1;
// }
// }
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
// uncomment this to add migrations
// private async migrationV2() {
// if (this.root) {
// // Future migration logic goes here
// this.root.version = 2;
// }
// }
}
---
Continue: 3_jazz_rules.md

View File

@@ -0,0 +1,55 @@
---
**Jazz Schema Rules:**
1. **User Profile and Account**
1.1. Define `export class UserProfile extends Profile` with exactly one property:
```ts
name = co.string;
```
1.2. Add a static `validate` method in `UserProfile` that checks `name` is present and non-empty.
1.3. Define `export class JazzAccount extends Account` with exactly two properties:
```ts
profile = co.ref(UserProfile);
root = co.ref(AccountRoot);
```
1.4. The `JazzAccount` class must have a `migrate(creationProps?: { name: string; other?: Record<string, unknown> })` method.
- Within `migrate`, if `this._refs.root` is undefined and `creationProps` is provided, run `initialMigration`.
- The `creationProps` **must** include a `name` property; `other` is optional but do not define more fields.
2. **Container, Root & Ownership**
2.1. The `AccountRoot` class (extending `CoMap`) **must** have a `container` property referencing a `Container`.
2.2. The `Container` class (extending `CoMap`) should contain the main domain entities of the app.
2.3. **Never** define a `name` field in the `Container` class. The template shows an example `name` property for a Container, but these rules override that.
2.4. Whenever the root structure is initialized, it is always owned by the current `JazzAccount`:
```ts
this.root = AccountRoot.create({ container: defaultContainer, version: 0 }, { owner: this });
```
3. **Groups & Ownership**
3.1. If the `UserProfile` is intended to be public, set its owner to a `publicGroup` that has `"everyone"` as `"reader"`. Otherwise, use a private group.
3.2. When creating a group, no need to explicitly pass `owner: this`. That is implicit if it's the same account.
3.3. **Do not** use properties like `user`, `users`, `group`, or `groups` in CoMaps or CoLists. Ownership is implicit.
4. **No Direct CoList Fields**
4.1. **Never** do:
```ts
co.ref(CoList.Of(co.ref(SomeClass)));
```
4.2. Instead, define a CoList class (e.g. `export class SomeClassList extends CoList.Of(co.ref(SomeClass)) {}`) and reference it.
5. **Schema Structure & Fields**
5.1. Follow the provided template patterns. **Do not** add extra entities or fields outside the users requirements or the template.
5.2. Do **not** use properties like `createdAt` or `updatedAt`; theyre implicit in CoValue.
5.3. If a property is optional, denote it with a question mark (`?`) in the field definition, or use `co.optional.*`.
5.4. Keep comments from the template, especially around migration blocks, intact.
5.5. Never set a property to "co.ref(UserProfile)".
6. **Output & Formatting**
6.1. Generate the final schema in TypeScript with no extra markdown or triple backticks.
6.2. Do **not** expand or alter the templates classes beyond what is required.
6.3. Avoid redundant or conflicting rules from the template; these revised rules take priority.
---
Continue: 4_1_jazz_example.md

View File

@@ -0,0 +1,229 @@
# Example app 1: A secure and organized password manager app that allows users to store, manage, and categorize their credentials in folders
```typescript
import { Account, CoList, CoMap, Group, Profile, co } from "jazz-tools";
/**
* Represents a password item in the Password Manager.
*
* Properties:
* - name: The required name identifying the password item.
* - username: Optional username.
* - username_input_selector: Optional selector for the username input field.
* - password: The required password.
* - password_input_selector: Optional selector for the password input.
* - uri: Optional URI associated with the item.
* - folder: Reference to the parent Folder.
* - deleted: Soft delete flag.
*/
export class PasswordItem extends CoMap {
name = co.string;
username = co.optional.string;
username_input_selector = co.optional.string;
password = co.string;
password_input_selector = co.optional.string;
uri = co.optional.string;
folder = co.ref(Folder);
deleted = co.boolean;
}
/**
* A list of PasswordItem references.
*/
export class PasswordList extends CoList.Of(co.ref(PasswordItem)) {}
/**
* Represents a folder that groups password items.
*
* Properties:
* - name: The folder's name.
* - items: A list of PasswordItems contained in the folder.
*/
export class Folder extends CoMap {
name = co.string;
items = co.ref(PasswordList);
}
/**
* A list of Folder references.
*/
export class FolderList extends CoList.Of(co.ref(Folder)) {}
/**
* Top-level container for the Password Manager.
* This container holds the main entities of the app.
*
* Properties:
* - folders: A list of Folder entities.
*/
export class Container extends CoMap {
folders = co.ref(FolderList);
}
/**
* The account root holds all user data.
*
* Properties:
* - container: The main container that organizes the apps data.
* - version: An optional version number used for migrations.
*/
export class PasswordManagerAccountRoot extends CoMap {
container = co.ref(Container);
version = co.optional.number;
}
/**
* Represents the user's profile.
*
* Properties:
* - name: The required user name.
*
* Static method:
* - validate: Ensures that a non-empty name is provided.
*/
export class UserProfile extends Profile {
name = co.string;
static validate(data: { name?: string; email?: string }) {
const errors: string[] = [];
if (!data.name?.trim()) {
errors.push("Please enter a name.");
}
// Note: In this schema, only 'name' is required.
return { errors };
}
}
/**
* Main account class for the Password Manager.
* Contains only the profile and root properties.
* Handles data initialization and migrations.
*/
export class PasswordManagerAccount extends Account {
profile = co.ref(UserProfile);
root = co.ref(PasswordManagerAccountRoot);
/**
* The migrate method is called on account creation and login.
* If the root is not initialized, it runs the initial migration.
* Otherwise, you can add version-based migrations as needed.
*/
async migrate(creationProps?: { name: string; other?: Record<string, unknown> }) {
if (!this._refs.root && creationProps) {
await this.initialMigration(creationProps);
return;
}
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
// Uncomment the following lines to add migrations:
// const currentVersion = this.root?.version || 0;
// if (currentVersion < 1) {
// await this.migrationV1();
// }
// if (currentVersion < 2) {
// await this.migrationV2();
// }
}
/**
* Executes the initial migration logic when the account is first created.
* - Validates the user's profile data.
* - Sets up a public group for the profile (accessible by "everyone").
* - Sets up a private group for private resources.
* - Creates a default Container with a default Folder and a default PasswordItem.
*/
private async initialMigration(creationProps: { name: string; other?: Record<string, unknown> }) {
const { name, other } = creationProps;
const profileErrors = UserProfile.validate({ name, ...other });
if (profileErrors.errors.length > 0) {
throw new Error("Invalid profile data: " + profileErrors.errors.join(", "));
}
// Create a public group for the user profile.
const publicGroup = Group.create({ owner: this });
publicGroup.addMember("everyone", "reader");
// Create the user profile with validated data.
this.profile = UserProfile.create(
{
name,
...other,
},
{ owner: publicGroup }
);
// Create a private group for private data.
const privateGroup = Group.create({ owner: this });
// Create a default Folder with one default PasswordItem.
const defaultFolder = Folder.create(
{
name: "Default",
items: PasswordList.create(
[
PasswordItem.create(
{
name: "Gmail",
username: "user@gmail.com",
password: "password123",
uri: "https://gmail.com",
// The folder reference will be set after defaultFolder creation.
folder: null as any,
deleted: false,
},
privateGroup
),
],
privateGroup
),
},
privateGroup
);
// Set the folder reference for the default PasswordItem.
defaultFolder.items[0].folder = defaultFolder;
// Create a default container that holds the FolderList.
const defaultContainer = Container.create(
{
folders: FolderList.create([defaultFolder], privateGroup),
},
privateGroup
);
// Initialize the account root with version tracking.
this.root = PasswordManagerAccountRoot.create(
{
container: defaultContainer,
version: 0, // Set initial version
},
{ owner: this }
);
}
// KEEP THESE COMMENTS IN THE GENERATED SCHEMA CODE
// Uncomment the following methods to add migrations:
// private async migrationV1() {
// if (this.root) {
// // Example migration logic: add a new field to all password items.
// // for (const folder of this.root.container.folders || []) {
// // for (const item of folder.items || []) {
// // item.newField = "default value";
// // }
// // }
// this.root.version = 1;
// }
// }
// private async migrationV2() {
// if (this.root) {
// // Future migration logic goes here.
// this.root.version = 2;
// }
// }
}
```
---
Continue: 4_2_jazz_example.md

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