Compare commits

..

43 Commits

Author SHA1 Message Date
Guido D'Orsi
4ef43e3477 Merge pull request #1321 from garden-co/changeset-release/main
Version Packages
2025-02-10 11:34:24 +01:00
github-actions[bot]
ab378b3d16 Version Packages 2025-02-06 09:41:56 +00:00
Guido D'Orsi
bc2e531c05 Merge pull request #1323 from garden-co/fix/garden-copyright
Update copyright to 2025
2025-02-06 10:40:41 +01:00
Trisha Lim
14bdd496f7 Update copyright to 2025 2025-02-05 18:36:15 +07:00
Guido D'Orsi
91fcb3f6b6 Merge pull request #1309 from garden-co/fix/rotate-before-revoke
fix: move access revoke after key rotation
2025-02-05 11:19:18 +01:00
Guido D'Orsi
b5962b4f81 chore(ci): fix syntax errors 2025-02-04 11:19:07 +01:00
Guido D'Orsi
f038c2d378 Merge pull request #1315 from garden-co/jazz-686-use-latest-corepack
Uses latest corepack as part of CI build
2025-02-04 10:10:06 +01:00
Benjamin S. Leveritt
febafecfdd Uses latest corepack as part of CI build 2025-02-04 09:08:23 +00:00
Guido D'Orsi
70c9a5db88 fix: move access revoke after key rotation 2025-01-30 19:01:28 +01:00
Trisha Lim
0cf789622c Remove dropdown component 2025-01-30 12:02:45 +08:00
Trisha Lim
d63f5eec5e Move kicker to a separate component 2025-01-30 12:02:45 +08:00
Trisha Lim
42bd8b76a1 Create a more flexible component for headings 2025-01-30 12:02:45 +08:00
Trisha Lim
73742656ae Design system improvements 2025-01-30 12:02:45 +08:00
Guido D'Orsi
f71f26d1dc Merge pull request #1303 from garden-co/changeset-release/main
Version Packages
2025-01-29 19:43:35 +01:00
github-actions[bot]
95822300d7 Version Packages 2025-01-29 18:39:50 +00:00
Guido D'Orsi
679fe040cf Merge pull request #1302 from garden-co/handler-websocket-errors
fix: handle websocket errors and add onSuccess callback
2025-01-29 19:38:38 +01:00
Guido D'Orsi
14b6149f50 fix: handle websocket errors and add onSuccess callback 2025-01-29 18:58:56 +01:00
Guido D'Orsi
4446738940 chore: disable formatter on package.json 2025-01-29 18:57:48 +01:00
Giordano Ricci
bbbd773c3f Merge pull request #1299 from garden-co/changeset-release/main
Version Packages
2025-01-29 16:53:17 +00:00
github-actions[bot]
7d5f4e5881 Version Packages 2025-01-29 16:40:51 +00:00
Giordano Ricci
29c44538dc Merge pull request #1298 from garden-co/gio/is-it-really-me
Change isMe to indicate whether an account is the currently active account
2025-01-29 16:39:46 +00:00
Giordano Ricci
198f995b21 update changeset 2025-01-29 16:31:33 +00:00
Giordano Ricci
1be017d69d make isMe indicate whether an account is the currently active one, add isLocalNodeOwner to reproduce previous behavior 2025-01-29 16:20:14 +00:00
Benjamin S. Leveritt
5574be699b Merge pull request #1290 from garden-co/jazz-682-update-nix-flake-to-node-22
Chore: Update nix flake for node22
2025-01-27 15:25:16 +00:00
Benjamin S. Leveritt
b3142cb8ed Chore: Update nix flake for node22 2025-01-27 15:02:20 +00:00
Benjamin S. Leveritt
cb08d9d1a3 Merge pull request #1240 from garden-co/jazz-662-add-llmstxt
Adds LLMs.txt generator
2025-01-27 14:55:10 +00:00
Guido D'Orsi
4d01cbae03 Merge pull request #1288 from garden-co/sync-tests-improvements
Improve sync tests and add some utils to track the messages exchanges
2025-01-27 15:47:03 +01:00
Guido D'Orsi
13f6389559 chore: format text 2025-01-27 15:38:34 +01:00
Guido D'Orsi
f6ea9bcc50 test: remove flaky test 2025-01-27 15:37:02 +01:00
Guido D'Orsi
f6f2766eac test: cover the initial messages exchanges 2025-01-27 15:35:35 +01:00
Guido D'Orsi
f8f63bface test: simplify sync tests with the test sync pattern 2025-01-27 15:35:34 +01:00
Guido D'Orsi
5ec561fec4 Merge pull request #1281 from garden-co/changeset-release/main
Version Packages
2025-01-27 11:07:35 +01:00
github-actions[bot]
277787cbd2 Version Packages 2025-01-27 10:03:17 +00:00
Guido D'Orsi
dda4336721 Merge pull request #1252 from garden-co/fix-ts-symbol-error
fix(typescript): replace private symbols with prefixed strings
2025-01-27 11:01:37 +01:00
Benjamin S. Leveritt
4dab70eea3 Adds docs to llms-full 2025-01-24 10:51:32 +00:00
Guido D'Orsi
b01cc1fe9d fix(typescript): fix type error on ItemsSym 2025-01-23 19:21:29 +01:00
Benjamin S. Leveritt
68e9c85d8f Adds docs to llms.txt 2025-01-23 16:44:44 +00:00
Benjamin S. Leveritt
1b124ff5c7 Changes extension for use in nodeJS 2025-01-23 16:44:29 +00:00
Benjamin S. Leveritt
ab2645765d Generates two llms.txt, listing and full 2025-01-23 15:13:45 +00:00
Benjamin S. Leveritt
2dec852ce2 Adds API ref links to methods 2025-01-23 13:14:19 +00:00
Benjamin S. Leveritt
8cb09379f4 Adds ids for methods 2025-01-23 13:13:41 +00:00
Benjamin S. Leveritt
9fb2e61e01 Extracts more information 2025-01-23 11:39:52 +00:00
Benjamin S. Leveritt
794927cac3 Adds LLMs.txt generator 2025-01-22 17:09:29 +00:00
138 changed files with 3080 additions and 813 deletions

View File

@@ -27,8 +27,13 @@ jobs:
with:
submodules: true
- name: Enable corepack
run: corepack enable
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3

View File

@@ -18,8 +18,13 @@ jobs:
with:
submodules: true
- name: Enable corepack
run: corepack enable
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3

View File

@@ -16,8 +16,13 @@ jobs:
with:
submodules: true
- name: Enable corepack
run: corepack enable
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3

View File

@@ -20,8 +20,13 @@ jobs:
with:
submodules: true
- name: Enable corepack
run: corepack enable
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3

View File

@@ -22,8 +22,13 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Enable corepack
run: corepack enable
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v3

View File

@@ -15,8 +15,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- name: Enable latestcorepack
run: |
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
npm install -g corepack@latest
echo "After : corepack version => $(corepack --version)"
corepack enable
pnpm --version
- name: Install Node.js
uses: actions/setup-node@v4

View File

@@ -1,4 +1,4 @@
Copyright 2024, Garden Computing, Inc.
Copyright 2025, Garden Computing, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@@ -17,4 +17,4 @@ For community and support, please join our [Discord](https://discord.gg/utDMjHYg
- Community & support: [Discord](https://discord.gg/utDMjHYg42)
- Updates: [X](https://x.com/jazz_tools) & [Email](https://garden.co/news)
Copyright 2024 — Garden Computing, Inc.
Copyright 2025 — Garden Computing, Inc.

View File

@@ -42,6 +42,15 @@
}
},
"overrides": [
{
"include": ["**/package.json"],
"linter": {
"enabled": false
},
"formatter": {
"enabled": false
}
},
{
"include": ["packages/**/src/**"],
"linter": {

View File

@@ -1,5 +1,41 @@
# chat-rn-clerk
## 1.0.64
### Patch Changes
- jazz-react-native@0.9.23
- jazz-react-native-auth-clerk@0.9.23
- jazz-tools@0.9.23
- jazz-react-native-media-images@0.9.23
## 1.0.63
### Patch Changes
- jazz-react-native@0.9.22
- jazz-react-native-auth-clerk@0.9.22
## 1.0.62
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react-native@0.9.21
- jazz-react-native-auth-clerk@0.9.21
- jazz-react-native-media-images@0.9.21
## 1.0.61
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react-native@0.9.20
- jazz-react-native-auth-clerk@0.9.20
- jazz-react-native-media-images@0.9.20
## 1.0.60
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# chat-rn
## 1.0.61
### Patch Changes
- jazz-react-native@0.9.23
- jazz-tools@0.9.23
## 1.0.60
### Patch Changes
- jazz-react-native@0.9.22
## 1.0.59
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react-native@0.9.21
## 1.0.58
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react-native@0.9.20
## 1.0.57
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "chat-rn",
"version": "1.0.57",
"version": "1.0.61",
"main": "index.js",
"scripts": {
"build": "expo export -p ios",

View File

@@ -1,5 +1,38 @@
# chat-vue
## 0.0.48
### Patch Changes
- jazz-browser@0.9.23
- jazz-tools@0.9.23
- jazz-vue@0.9.23
## 0.0.47
### Patch Changes
- jazz-browser@0.9.22
- jazz-vue@0.9.22
## 0.0.46
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser@0.9.21
- jazz-vue@0.9.21
## 0.0.45
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser@0.9.20
- jazz-vue@0.9.20
## 0.0.44
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# jazz-example-chat
## 0.0.144
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.143
### Patch Changes
- jazz-browser-media-images@0.9.22
- jazz-react@0.9.22
## 0.0.142
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser-media-images@0.9.21
- jazz-react@0.9.21
## 0.0.141
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser-media-images@0.9.20
- jazz-react@0.9.20
## 0.0.140
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# minimal-auth-clerk
## 0.0.43
### Patch Changes
- jazz-react@0.9.23
- jazz-react-auth-clerk@0.9.23
- jazz-tools@0.9.23
## 0.0.42
### Patch Changes
- jazz-react@0.9.22
- jazz-react-auth-clerk@0.9.22
## 0.0.41
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react@0.9.21
- jazz-react-auth-clerk@0.9.21
## 0.0.40
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react@0.9.20
- jazz-react-auth-clerk@0.9.20
## 0.0.39
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "clerk",
"private": true,
"version": "0.0.39",
"version": "0.0.43",
"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.9.19",
"jazz-react-auth-clerk": "workspace:0.9.23",
"jazz-tools": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"

View File

@@ -1,5 +1,34 @@
# file-share-svelte
## 0.0.28
### Patch Changes
- jazz-svelte@0.9.23
- jazz-tools@0.9.23
## 0.0.27
### Patch Changes
- jazz-svelte@0.9.22
## 0.0.26
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-svelte@0.9.21
## 0.0.25
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-svelte@0.9.20
## 0.0.24
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# form
## 0.0.39
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.38
### Patch Changes
- jazz-browser-media-images@0.9.22
- jazz-react@0.9.22
## 0.0.37
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser-media-images@0.9.21
- jazz-react@0.9.21
## 0.0.36
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser-media-images@0.9.20
- jazz-react@0.9.20
## 0.0.35
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# image-upload
## 0.0.41
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.40
### Patch Changes
- jazz-browser-media-images@0.9.22
- jazz-react@0.9.22
## 0.0.39
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser-media-images@0.9.21
- jazz-react@0.9.21
## 0.0.38
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser-media-images@0.9.20
- jazz-react@0.9.20
## 0.0.37
### Patch Changes

View File

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

View File

@@ -1,5 +1,20 @@
# jazz-example-inspector
## 0.0.102
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
- cojson-transport-ws@0.9.23
## 0.0.101
### Patch Changes
- Updated dependencies [14b6149]
- cojson-transport-ws@0.9.22
## 0.0.100
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-inspector-app",
"private": true,
"version": "0.0.100",
"version": "0.0.102",
"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.9.19",
"cojson-transport-ws": "workspace:0.9.19",
"cojson": "workspace:0.9.23",
"cojson-transport-ws": "workspace:0.9.23",
"hash-slash": "workspace:0.2.1",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",

View File

@@ -1,5 +1,37 @@
# jazz-example-musicplayer
## 0.0.65
### Patch Changes
- jazz-inspector@0.9.23
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.64
### Patch Changes
- jazz-react@0.9.22
## 0.0.63
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-inspector@0.9.22
- jazz-react@0.9.21
## 0.0.62
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-inspector@0.9.21
- jazz-react@0.9.20
## 0.0.61
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-music-player",
"private": true,
"version": "0.0.61",
"version": "0.0.65",
"type": "module",
"scripts": {
"dev": "vite",
@@ -18,8 +18,8 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-react": "workspace:0.9.19",
"jazz-tools": "workspace:0.9.19",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"jazz-inspector": "workspace:*",
"lucide-react": "^0.274.0",
"react": "^18.3.1",

View File

@@ -1,5 +1,38 @@
# jazz-example-onboarding
## 0.0.45
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.44
### Patch Changes
- jazz-browser-media-images@0.9.22
- jazz-react@0.9.22
## 0.0.43
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser-media-images@0.9.21
- jazz-react@0.9.21
## 0.0.42
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser-media-images@0.9.20
- jazz-react@0.9.20
## 0.0.41
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-onboarding",
"private": true,
"version": "0.0.41",
"version": "0.0.45",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,34 @@
# organization
## 0.0.37
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.36
### Patch Changes
- jazz-react@0.9.22
## 0.0.35
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react@0.9.21
## 0.0.34
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react@0.9.20
## 0.0.33
### Patch Changes

View File

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

View File

@@ -1,5 +1,29 @@
# passkey-svelte
## 0.0.32
### Patch Changes
- jazz-svelte@0.9.23
## 0.0.31
### Patch Changes
- jazz-svelte@0.9.22
## 0.0.30
### Patch Changes
- jazz-svelte@0.9.21
## 0.0.29
### Patch Changes
- jazz-svelte@0.9.20
## 0.0.28
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# minimal-auth-passkey
## 0.0.42
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.41
### Patch Changes
- jazz-react@0.9.22
## 0.0.40
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react@0.9.21
## 0.0.39
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react@0.9.20
## 0.0.38
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# jazz-password-manager
## 0.0.63
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.62
### Patch Changes
- jazz-react@0.9.22
## 0.0.61
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react@0.9.21
## 0.0.60
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react@0.9.20
## 0.0.59
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-password-manager",
"private": true,
"version": "0.0.59",
"version": "0.0.63",
"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.9.19",
"jazz-tools": "workspace:0.9.19",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.41.5",

View File

@@ -1,5 +1,38 @@
# jazz-example-pets
## 0.0.161
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.160
### Patch Changes
- jazz-browser-media-images@0.9.22
- jazz-react@0.9.22
## 0.0.159
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser-media-images@0.9.21
- jazz-react@0.9.21
## 0.0.158
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser-media-images@0.9.20
- jazz-react@0.9.20
## 0.0.157
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-pets",
"private": true,
"version": "0.0.157",
"version": "0.0.161",
"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.9.19",
"jazz-react": "workspace:0.9.19",
"jazz-tools": "workspace:0.9.19",
"jazz-browser-media-images": "workspace:0.9.23",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"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.9.19",
"jazz-run": "workspace:0.9.23",
"postcss": "^8.4.27",
"tailwindcss": "^3.4.15",
"typescript": "~5.6.2",

View File

@@ -1,5 +1,38 @@
# reactions
## 0.0.41
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
- jazz-browser-media-images@0.9.23
## 0.0.40
### Patch Changes
- jazz-browser-media-images@0.9.22
- jazz-react@0.9.22
## 0.0.39
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser-media-images@0.9.21
- jazz-react@0.9.21
## 0.0.38
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser-media-images@0.9.20
- jazz-react@0.9.20
## 0.0.37
### Patch Changes

View File

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

View File

@@ -1,5 +1,38 @@
# todo-vue
## 0.0.46
### Patch Changes
- jazz-browser@0.9.23
- jazz-tools@0.9.23
- jazz-vue@0.9.23
## 0.0.45
### Patch Changes
- jazz-browser@0.9.22
- jazz-vue@0.9.22
## 0.0.44
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser@0.9.21
- jazz-vue@0.9.21
## 0.0.43
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser@0.9.20
- jazz-vue@0.9.20
## 0.0.42
### Patch Changes

View File

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

View File

@@ -1,5 +1,34 @@
# jazz-example-todo
## 0.0.160
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.159
### Patch Changes
- jazz-react@0.9.22
## 0.0.158
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react@0.9.21
## 0.0.157
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react@0.9.20
## 0.0.156
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "jazz-example-todo",
"private": true,
"version": "0.0.156",
"version": "0.0.160",
"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.9.19",
"jazz-tools": "workspace:0.9.19",
"jazz-react": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "^18.3.1",

View File

@@ -1,5 +1,34 @@
# version-history
## 0.0.38
### Patch Changes
- jazz-react@0.9.23
- jazz-tools@0.9.23
## 0.0.37
### Patch Changes
- jazz-react@0.9.22
## 0.0.36
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-react@0.9.21
## 0.0.35
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-react@0.9.20
## 0.0.34
### Patch Changes

View File

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

12
flake.lock generated
View File

@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1730785428,
"narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=",
"lastModified": 1737885589,
"narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7",
"rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8",
"type": "github"
},
"original": {

View File

@@ -18,12 +18,15 @@
buildInputs = with pkgs; [
nodejs_22
nodePackages.pnpm
git
];
shellHook = ''
echo ""
echo "Welcome to the Jazz development environment!"
echo "Run 'pnpm install' to install the dependencies."
echo ""
'';
};
});
}
}

View File

@@ -1,11 +1,11 @@
import { clsx } from "clsx";
import Link from "next/link";
import { forwardRef } from "react";
import { Icon } from "../atoms/Icon";
import { Icon } from "./Icon";
import { Spinner } from "./Spinner";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "tertiary";
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
size?: "sm" | "md" | "lg";
href?: string;
newTab?: boolean;
@@ -42,6 +42,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
loading,
loadingText,
icon,
type = "button",
...buttonProps
},
ref,
@@ -58,16 +59,21 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
secondary:
"text-stone-900 border font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
tertiary: "text-blue underline underline-offset-4",
destructive:
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
};
const classNames = clsx(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
const classNames =
variant === "plain"
? className
: clsx(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
if (href) {
return (
@@ -95,6 +101,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{...buttonProps}
disabled={disabled || loading}
className={classNames}
type={type}
>
<ButtonIcon icon={icon} loading={loading} />

View File

@@ -0,0 +1,13 @@
export function Copyright({
className,
companyName = "Garden Computing, Inc.",
}: {
companyName?: string;
className?: string;
}) {
return (
<p className={className}>
© {new Date().getFullYear()} {companyName}
</p>
);
}

View File

@@ -0,0 +1,48 @@
import clsx from "clsx";
type HeadingProps = {
level?: 1 | 2 | 3 | 4 | 5 | 6;
size?: 1 | 2 | 3 | 4 | 5 | 6;
} & React.ComponentPropsWithoutRef<"h1" | "h2" | "h3" | "h4" | "h5" | "h6">;
const classes = {
1: [
"font-display",
"text-stone-950 dark:text-white",
"text-5xl lg:text-6xl",
"mb-3",
"font-medium",
"tracking-tighter",
],
2: [
"font-display",
"text-stone-950 dark:text-white",
"text-2xl md:text-4xl",
"mb-2",
"font-semibold",
"tracking-tight",
],
3: [
"font-display",
"text-stone-950 dark:text-white",
"text-xl md:text-2xl",
"mb-2",
"font-semibold",
"tracking-tight",
],
4: ["text-bold"],
5: [],
6: [],
};
export function Heading({
className,
level = 1,
size: customSize,
...props
}: HeadingProps) {
let Element: `h${typeof level}` = `h${level}`;
const size = customSize || level;
return <Element {...props} className={clsx(classes[size])} />;
}

View File

@@ -1,85 +1,38 @@
import clsx from "clsx";
import { Heading } from "./Heading";
interface HeadingProps {
children: React.ReactNode;
className?: string;
id?: string;
export function H1(
props: React.ComponentPropsWithoutRef<"h1"> & React.PropsWithChildren,
) {
return <Heading level={1} {...props} />;
}
export function H1({ children, className, id }: HeadingProps) {
return (
<h1
id={id}
className={clsx(
className,
"font-display",
"text-stone-950 dark:text-white",
"text-5xl lg:text-6xl",
"mb-3",
"font-medium",
"tracking-tighter",
)}
>
{children}
</h1>
);
export function H2(
props: React.ComponentPropsWithoutRef<"h2"> & React.PropsWithChildren,
) {
return <Heading level={2} {...props} />;
}
export function H2({ children, className, id }: HeadingProps) {
return (
<h2
id={id}
className={clsx(
className,
"font-display",
"text-stone-950 dark:text-white",
"text-2xl md:text-4xl",
"mb-2",
"font-semibold",
"tracking-tight",
)}
>
{children}
</h2>
);
export function H3(
props: React.ComponentPropsWithoutRef<"h3"> & React.PropsWithChildren,
) {
return <Heading level={3} {...props} />;
}
export function H3({ children, className, id }: HeadingProps) {
return (
<h3
id={id}
className={clsx(
className,
"font-display",
"text-stone-950 dark:text-white",
"text-xl md:text-2xl",
"mb-2",
"font-semibold",
"tracking-tight",
)}
>
{children}
</h3>
);
export function H4(
props: React.ComponentPropsWithoutRef<"h4"> & React.PropsWithChildren,
) {
return <Heading level={4} {...props} />;
}
export function H4({ children, className, id }: HeadingProps) {
return (
<h4 id={id} className={clsx(className, "text-bold")}>
{children}
</h4>
);
export function H5(
props: React.ComponentPropsWithoutRef<"h5"> & React.PropsWithChildren,
) {
return <Heading level={5} {...props} />;
}
export function Kicker({ children, className }: HeadingProps) {
return (
<p
className={clsx(
className,
"uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400",
)}
>
{children}
</p>
);
export function H6(
props: React.ComponentPropsWithoutRef<"h6"> & React.PropsWithChildren,
) {
return <Heading level={6} {...props} />;
}

View File

@@ -0,0 +1,21 @@
import clsx from "clsx";
export function Kicker({
children,
className,
as,
}: React.ComponentPropsWithoutRef<"p"> & {
as?: React.ElementType;
}) {
const Element = as ?? "p";
return (
<Element
className={clsx(
className,
"uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400",
)}
>
{children}
</Element>
);
}

View File

@@ -1,32 +1,33 @@
import { clsx } from "clsx";
import { useId } from "react";
import { forwardRef, useId } from "react";
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
// label is required for a11y, but you can hide it with a "label:sr-only" className
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
// label can be hidden with a "label:sr-only" className
label: string;
type?: "text" | "email" | "number";
className?: string;
id?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, className, id: customId, ...inputProps }, ref) => {
const generatedId = useId();
const id = customId || generatedId;
export function Input(props: Props) {
const { label, id: customId, className, type = "text" } = props;
const generatedId = useId();
const id = customId || generatedId;
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
);
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white",
);
const containerClassName = clsx("grid gap-1", className);
const containerClassName = clsx("grid gap-1", className);
return (
<div className={containerClassName}>
<label htmlFor={id} className="text-stone-600 dark:text-stone-300">
{label}
</label>
return (
<div className={containerClassName}>
<label htmlFor={id} className="text-stone-600 dark:text-stone-300">
{label}
</label>
<input {...props} type={type} id={id} className={inputClassName} />
</div>
);
}
<input ref={ref} {...inputProps} id={id} className={inputClassName} />
</div>
);
},
);

View File

@@ -1,6 +1,7 @@
import clsx from "clsx";
import { ReactNode } from "react";
import { H2, Kicker } from "../atoms/Headings";
import { H2 } from "../atoms/Headings";
import { Kicker } from "../atoms/Kicker";
import { Prose } from "./Prose";
function H2Sub({ children }: { children: React.ReactNode }) {

View File

@@ -14,10 +14,9 @@ export function Select(
const containerClassName = clsx("grid gap-1", className);
const selectClassName = clsx(
"g-select",
"w-full rounded-md border shadow-sm px-2 py-1.5 text-sm",
"font-medium text-stone-900",
"dark:text-white",
"dark:text-white dark:bg-stone-925",
"appearance-none",
);

View File

@@ -4,6 +4,7 @@ import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ComponentType, ReactNode } from "react";
import { Copyright } from "../atoms/Copyright";
import { NewsletterForm } from "./NewsletterForm";
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
@@ -18,29 +19,13 @@ type FooterSection = {
type FooterProps = {
logo: ReactNode;
companyName: string;
sections: FooterSection[];
socials: SocialLinksProps;
themeToggle: ComponentType<{ className?: string }>;
};
function Copyright({
className,
companyName,
}: {
companyName: string;
className?: string;
}) {
return (
<p className={clsx(className, "text-sm")}>
© {new Date().getFullYear()} {companyName}
</p>
);
}
export function Footer({
logo,
companyName,
sections,
socials,
themeToggle: ThemeToggle,
@@ -82,10 +67,7 @@ export function Footer({
</div>
))}
<Copyright
className="order-last col-span-full self-center md:col-span-10 md:order-none"
companyName={companyName}
/>
<Copyright className="text-sm order-last col-span-full self-center md:col-span-10 md:order-none" />
<div className="col-span-full flex items-center justify-between gap-6 md:col-span-2">
<SocialLinks {...socials}></SocialLinks>

View File

@@ -9,6 +9,7 @@ import { GcmpNav } from "@/components/Nav";
import { ThemeToggle } from "@/components/ThemeToggle";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Copyright } from "gcmp-design-system/src/app/components/atoms/Copyright";
// If loading a variable font, you don't need to specify the font weight
const manrope = Manrope({
@@ -98,7 +99,7 @@ export default function RootLayout({
<GcmpNav />
<main className="flex-1 w-full">{children}</main>
<footer className="py-8 text-sm flex justify-between gap-3 w-full container mt-12 md:mt-20">
<p>©2024 Garden Computing, Inc.</p>
<Copyright />
<ThemeToggle className="hidden md:block" />
</footer>

View File

@@ -40,3 +40,6 @@ codeSamples
.turbo
.env
# LLM docs
public/llms*.txt

View File

@@ -1,7 +1,7 @@
import DocsLayout from "@/app/docs/[framework]/(others)/layout";
import { TableOfContents } from "@/components/docs/TableOfContents";
import ComingSoonPage from "@/components/docs/coming-soon.mdx";
import { docNavigationItems } from "@/lib/docNavigationItems";
import { docNavigationItems } from "@/lib/docNavigationItems.js";
import { Framework, frameworks } from "@/lib/framework";
import type { Toc } from "@stefanprobst/rehype-extract-toc";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";

View File

@@ -39,7 +39,20 @@ Sync and persist your data by setting up a [sync and storage infrastructure](/do
Learn how to structure your data using [collaborative values](/docs/schemas/covalues).
## API Reference
Many of the packages provided are documented in the [API Reference](/api-reference).
## LLM Docs
We support the [llms.txt](https://llmstxt.org/) convention for making documentation available to large language models and the applications that make use of them.
We currently have:
- [/llms.txt](/llms.txt) - A overview listing of the available packages and their documentation
- [/llms-full.txt](/llms-full.txt) - Full documentation for our packages
## Get support
If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42).
We would love to help you get started.
We would love to help you get started.

View File

@@ -3,7 +3,7 @@
import { SideNav } from "@/components/SideNav";
import { SideNavHeader } from "@/components/SideNavHeader";
import { FrameworkSelect } from "@/components/docs/FrameworkSelect";
import { docNavigationItems } from "@/lib/docNavigationItems";
import { docNavigationItems } from "@/lib/docNavigationItems.js";
import { useFramework } from "@/lib/use-framework";
import { clsx } from "clsx";

View File

@@ -156,8 +156,14 @@ export function FnDecl({
doc: ReactNode;
example: ReactNode;
}) {
// Extract the method name from the signature (everything before the first parenthesis or type parameter)
const methodName = signature.match(/^[^(<]+/)?.[0];
return (
<div className="text-sm flex flex-col gap-3 my-2 p-3 rounded bg-stone-50 dark:bg-stone-925">
<div
id={methodName}
className="text-sm flex flex-col gap-3 my-2 p-3 rounded bg-stone-50 dark:bg-stone-925"
>
<div className="flex flex-col gap-2">
<div>
{<Highlight>{signature + ":"}</Highlight>}{" "}

View File

@@ -7,7 +7,6 @@ export function JazzFooter() {
return (
<Footer
logo={<GcmpLogo monochrome className="w-36" />}
companyName="Garden Computing, Inc."
socials={socials}
themeToggle={ThemeToggle}
sections={[

View File

@@ -1,3 +1,4 @@
import { H1 } from "gcmp-design-system/src/app/components/atoms/Headings";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
import Link from "next/link";
@@ -44,10 +45,10 @@ export function HeroSection() {
<p className="uppercase text-blue tracking-widest text-sm font-medium dark:text-stone-400">
Local-first development toolkit
</p>
<h1 className="font-display text-stone-950 dark:text-white text-4xl md:text-5xl lg:text-6xl font-medium tracking-tighter">
<H1>
<span className="inline-block">Ship top-tier apps</span>{" "}
<span className="inline-block">at high tempo.</span>
</h1>
</H1>
<Prose size="lg" className="text-pretty max-w-2xl dark:text-stone-200">
<p>

View File

@@ -0,0 +1,511 @@
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";
function formatType(type) {
if (!type) return "unknown";
// Handle type aliases and references
if (type.type === "reference") {
const name = type.package ? `${type.package}.${type.name}` : type.name;
return (
name +
(type.typeArguments
? `<${type.typeArguments.map(formatType).join(", ")}>`
: "")
);
}
// Handle union types
if (type.type === "union") {
return type.types.map(formatType).join(" | ");
}
// Handle array types
if (type.type === "array") {
return `${formatType(type.elementType)}[]`;
}
// Handle basic types
if (type.type === "intrinsic" || type.type === "literal") {
return typeof type.value !== "undefined"
? JSON.stringify(type.value)
: type.name;
}
// Handle tuple types
if (type.type === "tuple") {
return `[${type.elements.map(formatType).join(", ")}]`;
}
// Handle intersection types
if (type.type === "intersection") {
return type.types.map(formatType).join(" & ");
}
// Handle template literal types
if (type.type === "template-literal") {
return `\`${type.head}${type.tail.map((t) => `\${${formatType(t[0])}}${t[1]}`).join("")}\``;
}
// Handle reflection types (object types and function types)
if (type.type === "reflection") {
if (type.declaration.signatures) {
const sig = type.declaration.signatures[0];
const params =
sig.parameters
?.map(
(p) =>
`${p.name}${p.flags?.isOptional ? "?" : ""}: ${formatType(p.type)}`,
)
.join(", ") || "";
return `(${params}) => ${formatType(sig.type)}`;
}
if (type.declaration.children) {
return (
"{ " +
type.declaration.children
.map((child) => {
const optional = child.flags?.isOptional ? "?" : "";
return `${child.name}${optional}: ${formatType(child.type)}`;
})
.join("; ") +
" }"
);
}
}
// Handle query types
if (type.type === "query") {
return `typeof ${formatType(type.queryType)}`;
}
// Handle conditional types
if (type.type === "conditional") {
return `${formatType(type.checkType)} extends ${formatType(type.extendsType)} ? ${formatType(type.trueType)} : ${formatType(type.falseType)}`;
}
// Handle index access types
if (type.type === "indexedAccess") {
return `${formatType(type.objectType)}[${formatType(type.indexType)}]`;
}
// Handle mapped types
if (type.type === "mapped") {
const readonly = type.readonlyModifier === "+" ? "readonly " : "";
const optional = type.optionalModifier === "+" ? "?" : "";
return `{ ${readonly}[${type.parameter} in ${formatType(type.parameterType)}]${optional}: ${formatType(type.templateType)} }`;
}
// Handle type operators
if (type.type === "typeOperator") {
return `${type.operator} ${formatType(type.target)}`;
}
// Handle predicate types
if (type.type === "predicate") {
return `${type.name} is ${formatType(type.targetType)}`;
}
// Handle inferred types
if (type.type === "inferred") {
return `infer ${type.name}`;
}
// Handle rest types
if (type.type === "rest") {
return `...${formatType(type.elementType)}`;
}
// Handle unknown types with more detail
if (type.toString) {
return type.toString();
}
return "unknown";
}
function formatComment(comment) {
if (!comment) return "";
let text =
comment.summary
?.map((part) => part.text)
.join("")
.trim() || "";
// Add parameter descriptions if available
if (comment.blockTags) {
const params = comment.blockTags
.filter((tag) => tag.tag === "@param")
.map((tag) => {
const paramName = tag.param;
let description = "";
let codeExample = "";
tag.content.forEach((part) => {
if (part.kind === "code") {
// Don't wrap in code blocks since examples are already wrapped
codeExample += "\n" + part.text + "\n";
} else {
description += part.text;
}
});
return `- ${paramName}: ${description.trim()}${codeExample}`;
});
if (params.length > 0) {
text += "\n\nParameters:\n" + params.join("\n");
}
// Add remarks if available
const remarks = comment.blockTags
.filter((tag) => tag.tag === "@remarks")
.map((tag) =>
tag.content
.map((part) => part.text)
.join("")
.trim(),
);
if (remarks.length > 0) {
text += "\n\nRemarks:\n" + remarks.join("\n");
}
// Add examples
const examples = comment.blockTags
.filter((tag) => tag.tag === "@example")
.map((tag) =>
tag.content
.map((part) => {
if (part.kind === "code") {
// Don't wrap in code blocks since examples are already wrapped
return "\n" + part.text + "\n";
}
return part.text;
})
.join("")
.trim(),
);
if (examples.length > 0) {
text += "\n\nExamples:\n" + examples.join("\n");
}
}
return text;
}
async function readMdxContent(url) {
try {
// Special case for the introduction
if (url === "/docs") {
const introPath = path.join(
process.cwd(),
"components/docs/docs-intro.mdx",
);
try {
const content = await fs.readFile(introPath, "utf8");
// Remove imports and exports
return content
.replace(/^import[^\n]*\n/gm, "")
.replace(/export const metadata[^;]*;/, "")
.trim();
} catch (err) {
if (err.code !== "ENOENT") throw err;
}
}
// Convert URL to file path
// Remove leading slash and 'docs' from URL
const relativePath = url.replace(/^\/docs\/?/, "");
// Base directory for docs
const baseDir = path.join(process.cwd(), "app/docs/[framework]/[...slug]");
// If it's a directory, try to read all framework variants
const fullPath = path.join(baseDir, relativePath);
try {
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
// Read all MDX files in the directory
const files = await fs.readdir(fullPath);
const mdxFiles = files.filter((f) => f.endsWith(".mdx"));
if (mdxFiles.length === 0) return null;
// Combine content from all framework variants
const contents = await Promise.all(
mdxFiles.map(async (file) => {
const content = await fs.readFile(
path.join(fullPath, file),
"utf8",
);
// Remove imports and exports
const cleanContent = content
.replace(/^import[^\n]*\n/gm, "")
.replace(/export const metadata[^;]*;/, "")
.trim();
return `### ${path.basename(file, ".mdx")} Implementation\n\n${cleanContent}`;
}),
);
return contents.join("\n\n---\n\n");
}
} catch (err) {
if (err.code !== "ENOENT") throw err;
}
// Try as a single MDX file
const mdxPath = fullPath + ".mdx";
try {
const content = await fs.readFile(mdxPath, "utf8");
// Remove imports and exports
return content
.replace(/^import[^\n]*\n/gm, "")
.replace(/export const metadata[^;]*;/, "")
.trim();
} catch (err) {
if (err.code !== "ENOENT") throw err;
}
console.warn(`Could not find MDX content for ${url} at ${fullPath}`);
return null;
} catch (error) {
console.warn(`Error reading MDX content for ${url}:`, error);
return null;
}
}
async function generateDetailedDocs(docs) {
const output = [];
const deserializer = new Deserializer();
// Project title
output.push("# Jazz\n");
// Documentation sections with full content
output.push("## Documentation\n");
for (const section of DOC_SECTIONS) {
output.push(`### ${section.title}\n`);
for (const page of section.pages) {
output.push(`#### ${page.title}\n`);
const content = await readMdxContent(page.url);
console.log(content);
if (content) {
// If the content contains framework-specific implementations, they're already properly formatted
// Otherwise, just add the content directly
output.push(content + "\n");
}
output.push("\n");
}
}
// API Reference by package
output.push("## API Reference\n\n");
// for (const [packageName, packageDocs] of Object.entries(docs)) {
// const project = deserializer.reviveProject(packageDocs, packageName);
// // Add package heading with description
// output.push(`### ${packageName}\n`);
// output.push(`${getPackageDescription(packageName)}\n\n`);
// output.push(
// `[API Reference](https://jazz.tools/api-reference/${packageName})\n\n`,
// );
// // Process each category
// project.categories?.forEach((category) => {
// output.push(`#### ${category.title}\n`);
// category.children.forEach((child) => {
// // Add name, kind, and API reference link
// const apiLink = `[API Reference](https://jazz.tools/api-reference/${packageName}#${child.name})`;
// output.push(
// `##### ${child.name} (${ReflectionKind[child.kind]}) ${apiLink}\n`,
// );
// // Add description if available
// const description = formatComment(child.comment);
// if (description) {
// output.push(`${description}\n`);
// }
// output.push("\n");
// // Add properties for classes/interfaces
// if (child.children) {
// output.push("Properties:\n");
// // Group overloaded methods by name
// const methodGroups = new Map();
// child.children.forEach((prop) => {
// if (prop.signatures?.length > 0) {
// const existing = methodGroups.get(prop.name) || [];
// methodGroups.set(prop.name, [...existing, prop]);
// }
// });
// child.children.forEach((prop) => {
// // Skip if this is an overloaded method that we'll handle later
// if (
// prop.signatures?.length > 0 &&
// methodGroups.get(prop.name)?.length > 1
// ) {
// return;
// }
// const type = formatType(prop.type);
// const description = formatComment(prop.comment);
// // Output the property name and type, but skip the type for methods since we'll show signatures
// if (prop.signatures?.length > 0) {
// output.push(
// `- ${prop.name}${description ? ` - ${description}` : ""}\n`,
// );
// } else {
// output.push(
// `- ${prop.name}: ${type}${description ? ` - ${description}` : ""}\n`,
// );
// }
// // Handle method signatures with proper indentation
// if (prop.signatures) {
// prop.signatures.forEach((sig) => {
// const params = sig.parameters
// ?.map((p) => {
// const paramType = formatType(p.type);
// return `${p.name}: ${paramType}`;
// })
// .join(", ");
// output.push(
// ` Method signature: \`(${params || ""}) => ${formatType(sig.type)}\`\n`,
// );
// // Add API reference URL for the method
// output.push(
// ` [API Reference](https://jazz.tools/api-reference/${packageName}#${child.name}.${prop.name})\n`,
// );
// const methodDesc = formatComment(sig.comment);
// if (methodDesc) {
// // Indent each line of the description
// const indentedDesc = methodDesc
// .split("\n")
// .map((line) => ` ${line}`)
// .join("\n");
// output.push(`${indentedDesc}\n`);
// }
// });
// }
// });
// // Handle overloaded methods
// methodGroups.forEach((props, name) => {
// if (props.length <= 1) return;
// const firstProp = props[0];
// const description = formatComment(firstProp.comment);
// output.push(`- ${name}${description ? ` - ${description}` : ""}\n`);
// // Combine all signatures with proper indentation
// const allSignatures = props.flatMap((p) => p.signatures || []);
// allSignatures.forEach((sig) => {
// const params = sig.parameters
// ?.map((p) => {
// const paramType = formatType(p.type);
// return `${p.name}: ${paramType}`;
// })
// .join(", ");
// output.push(
// ` Method signature: \`(${params || ""}) => ${formatType(sig.type)}\`\n`,
// );
// // Add API reference URL for the overloaded method
// output.push(
// ` [API Reference](https://jazz.tools/api-reference/${packageName}#${child.name}.${name})\n`,
// );
// const methodDesc = formatComment(sig.comment);
// if (methodDesc) {
// // Indent each line of the description
// const indentedDesc = methodDesc
// .split("\n")
// .map((line) => ` ${line}`)
// .join("\n");
// output.push(`${indentedDesc}\n`);
// }
// });
// });
// }
// // Add signatures for functions/methods
// if (child.signatures) {
// child.signatures.forEach((sig) => {
// const params = sig.parameters
// ?.map((p) => {
// const type = formatType(p.type);
// const desc = formatComment(p.comment);
// return `${p.name}: ${type}${desc ? ` - ${desc}` : ""}`;
// })
// .join(", ");
// output.push(`Signature: ${child.name}(${params || ""})`);
// if (sig.type) {
// output.push(`Returns: ${formatType(sig.type)}`);
// if (sig.comment?.returns) {
// output.push(
// `Return description: ${sig.comment.returns
// .map((part) => part.text)
// .join("")
// .trim()}`,
// );
// }
// }
// const sigComment = formatComment(sig.comment);
// if (sigComment) {
// output.push(`\n${sigComment}`);
// }
// output.push("\n");
// });
// }
// output.push("\n");
// });
// });
// }
// Optional section for additional resources
output.push("## Resources\n\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-full.txt", output.join("\n"));
}
// Main execution
async function main() {
console.log("Generating detailed LLM docs...");
const docs = await loadTypedocFiles();
await generateDetailedDocs(docs);
}
main().catch(console.error);

View File

@@ -0,0 +1,80 @@
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

@@ -1,21 +1,7 @@
import { Application } from "typedoc";
import { PACKAGES } from "./utils/config.mjs";
for (const { packageName, entryPoint, tsconfig, typedocOptions } of [
{
packageName: "jazz-tools",
entryPoint: "exports.ts",
},
{
packageName: "jazz-react",
entryPoint: "index.ts",
typedocOptions: {
skipErrorChecking: true, // TODO: remove this. Temporary workaround
},
},
{ packageName: "jazz-browser" },
{ packageName: "jazz-browser-media-images" },
{ packageName: "jazz-nodejs" },
]) {
for (const { packageName, entryPoint, tsconfig, typedocOptions } of PACKAGES) {
const app = await Application.bootstrapWithPlugins({
entryPoints: [
`../../packages/${packageName}/src/${entryPoint || "index.ts"}`,

View File

@@ -0,0 +1,47 @@
import { docNavigationItems } from "../../lib/docNavigationItems.js";
// Transform docNavigationItems into the format we need
function transformNavItems() {
return docNavigationItems
.map((section) => ({
title: section.name,
pages: section.items
.filter((item) => item.done !== 0) // Skip not-yet-written docs
.map((item) => ({
title: item.name,
url: item.href,
})),
}))
.filter((section) => section.pages.length > 0); // Only include sections with pages
}
export const DOC_SECTIONS = transformNavItems();
export const PACKAGES = [
{
packageName: "jazz-tools",
entryPoint: "exports.ts",
description:
"The base implementation for Jazz, a framework for distributed state. Provides a high-level API around the CoJSON protocol.",
},
{
packageName: "jazz-react",
entryPoint: "index.ts",
description: "React bindings for Jazz, a framework for distributed state.",
typedocOptions: {
skipErrorChecking: true, // TODO: remove this. Temporary workaround
},
},
{
packageName: "jazz-browser",
description: "Browser (Vanilla JavaScript) bindings for Jazz",
},
{
packageName: "jazz-browser-media-images",
description: "Image handling utilities for Jazz in the browser",
},
{
packageName: "jazz-nodejs",
description: "NodeJS/Bun server worker bindings for Jazz",
},
];

View File

@@ -0,0 +1,58 @@
import path from "path";
import fs from "fs/promises";
import { PACKAGES } from "./config.mjs";
// Common configuration
export const PACKAGE_DESCRIPTIONS = {
"jazz-tools":
"The base implementation for Jazz, a framework for distributed state. Provides a high-level API around the CoJSON protocol.",
"jazz-react": "React bindings for Jazz, a framework for distributed state.",
"jazz-browser": "Browser (Vanilla JavaScript) bindings for Jazz",
"jazz-browser-media-images":
"Image handling utilities for Jazz in the browser",
"jazz-nodejs": "NodeJS/Bun server worker bindings for Jazz",
};
// Helper functions
export async function loadTypedocFiles() {
const docs = {};
for (const { packageName } of PACKAGES) {
docs[packageName] = JSON.parse(
await fs.readFile(
path.join(process.cwd(), "typedoc", packageName + ".json"),
"utf-8",
),
);
}
return docs;
}
export function getPackageDescription(packageName) {
const pkg = PACKAGES.find((p) => p.packageName === packageName);
return pkg?.description || "";
}
export function cleanDescription(description) {
if (!description) return "";
return (
description
.map((part) => part.text)
.join(" ")
.trim()
// Remove code blocks
.replace(/```[\s\S]*?```/g, "")
// Remove line breaks and extra spaces
.replace(/\s+/g, " ")
// Clean up backticks
.replace(/\`/g, "'")
);
}
export async function writeDocsFile(filename, content) {
await fs.writeFile(
path.join(process.cwd(), "public", filename),
content,
"utf8",
);
console.log(`Documentation generated at 'public/${filename}'`);
}

View File

@@ -5,11 +5,15 @@
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=8192 next dev",
"build:generate-docs": "node genDocs.mjs --build",
"build:generate-docs": "pnpm run generate:docs && pnpm run generate:llm-docs:all",
"build": "next build",
"start": "next start",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write"
"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"
},
"packageManager": "pnpm@9.14.0",
"dependencies": {

View File

@@ -27,7 +27,8 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"mdx.d.ts"
"mdx.d.ts",
"lib/docNavigationItems.js"
],
"exclude": ["node_modules"]
}

View File

@@ -1,5 +1,13 @@
# cojson-storage-indexeddb
## 0.9.23
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
- cojson-storage@0.9.23
## 0.9.19
### Patch Changes

View File

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

View File

@@ -1,5 +1,13 @@
# cojson-storage-sqlite
## 0.8.59
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
- cojson-storage@0.9.23
## 0.8.58
### Patch Changes

View File

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

View File

@@ -1,5 +1,13 @@
# cojson-storage-sqlite
## 0.9.23
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
- cojson-storage@0.9.23
## 0.9.19
### Patch Changes

View File

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

View File

@@ -1,5 +1,12 @@
# cojson-storage
## 0.9.23
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
## 0.9.19
### Patch Changes

View File

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

View File

@@ -1,5 +1,18 @@
# cojson-transport-nodejs-ws
## 0.9.23
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
## 0.9.22
### Patch Changes
- 14b6149: Handle websocket errors and add an onSuccess callback
## 0.9.19
### Patch Changes

View File

@@ -1,12 +1,12 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.9.19",
"version": "0.9.23",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.9.19",
"cojson": "workspace:*",
"typescript": "~5.6.2"
},
"scripts": {
@@ -19,6 +19,7 @@
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@types/ws": "8.5.10"
"@types/ws": "8.5.10",
"ws": "^8.14.2"
}
}

View File

@@ -21,6 +21,7 @@ export type CreateWebSocketPeerOpts = {
batchingByDefault?: boolean;
deletePeerStateOnClose?: boolean;
onClose?: () => void;
onSuccess?: () => void;
};
function createPingTimeoutListener(enabled: boolean, callback: () => void) {
@@ -127,6 +128,7 @@ export function createWebSocketPeer({
expectPings = true,
batchingByDefault = true,
deletePeerStateOnClose = false,
onSuccess,
onClose,
}: CreateWebSocketPeerOpts): Peer {
const incoming = new cojsonInternals.Channel<
@@ -142,6 +144,13 @@ export function createWebSocketPeer({
}
websocket.addEventListener("close", handleClose);
websocket.addEventListener("error" as any, (err) => {
logger.warn(err.message);
if (err.message.includes("ECONNREFUSED")) {
websocket.close();
}
});
const pingTimeout = createPingTimeoutListener(expectPings, () => {
incoming
@@ -154,6 +163,7 @@ export function createWebSocketPeer({
websocket,
batchingByDefault,
);
let isFirstMessage = true;
function handleIncomingMsg(event: { data: unknown }) {
if (event.data === "") {
@@ -169,6 +179,13 @@ export function createWebSocketPeer({
return;
}
if (isFirstMessage) {
// The only way to know that the connection has been correctly established with our sync server
// is to track that we got a message from the server.
onSuccess?.();
isFirstMessage = false;
}
const { messages } = result;
if (messages.length > 1) {

View File

@@ -170,6 +170,31 @@ describe("createWebSocketPeer", () => {
);
});
test("should call onSuccess handler after receiving first message", () => {
const onSuccess = vi.fn();
const { listeners } = setup({ onSuccess });
const messageHandler = listeners.get("message");
const message: SyncMessage = {
action: "known",
id: "co_ztest",
header: false,
sessions: {},
};
// First message should trigger onSuccess
messageHandler?.(
new MessageEvent("message", { data: JSON.stringify(message) }),
);
expect(onSuccess).toHaveBeenCalledTimes(1);
// Subsequent messages should not trigger onSuccess again
messageHandler?.(
new MessageEvent("message", { data: JSON.stringify(message) }),
);
expect(onSuccess).toHaveBeenCalledTimes(1);
});
describe("batchingByDefault = true", () => {
test("should batch outgoing messages", async () => {
const { peer, mockWebSocket } = setup();

View File

@@ -0,0 +1,132 @@
import { ControlledAgent, LocalNode, WasmCrypto } from "cojson";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { WebSocket } from "ws";
import { createWebSocketPeer } from "../index";
import { startSyncServer } from "./syncServer";
describe("WebSocket Peer Integration", () => {
let server: any;
let syncServerUrl: string;
let crypto: WasmCrypto;
beforeEach(async () => {
crypto = await WasmCrypto.create();
const result = await startSyncServer();
server = result;
syncServerUrl = result.syncServer;
});
afterEach(() => {
server.close();
});
test("should establish connection between client and server nodes", async () => {
// Create client node
const clientAgent = crypto.newRandomAgentSecret();
const clientNode = new LocalNode(
new ControlledAgent(clientAgent, crypto),
crypto.newRandomSessionID(crypto.getAgentID(clientAgent)),
crypto,
);
// Create WebSocket connection
const ws = new WebSocket(syncServerUrl);
// Track connection success
let connectionEstablished = false;
// Create peer and add to client node
const peer = createWebSocketPeer({
id: "test-client",
websocket: ws,
role: "server",
onSuccess: () => {
connectionEstablished = true;
},
});
clientNode.syncManager.addPeer(peer);
// Wait for connection to establish
await new Promise<void>((resolve) => {
const checkConnection = setInterval(() => {
if (connectionEstablished) {
clearInterval(checkConnection);
resolve();
}
}, 100);
});
expect(connectionEstablished).toBe(true);
expect(clientNode.syncManager.getPeers()).toHaveLength(1);
});
test("should sync data between nodes through WebSocket connection", async () => {
const clientAgent = crypto.newRandomAgentSecret();
const clientNode = new LocalNode(
new ControlledAgent(clientAgent, crypto),
crypto.newRandomSessionID(crypto.getAgentID(clientAgent)),
crypto,
);
const ws = new WebSocket(syncServerUrl);
const peer = createWebSocketPeer({
id: "test-client",
websocket: ws,
role: "server",
});
clientNode.syncManager.addPeer(peer);
// Create a test group
const group = clientNode.createGroup();
const map = group.createMap();
map.set("testKey", "testValue", "trusting");
// Wait for sync
await map.core.waitForSync();
console.log("synced");
// Verify data reached the server
const serverNode = server.localNode;
const serverMap = await serverNode.load(map.id);
expect(serverMap.get("testKey")).toBe("testValue");
});
test("should handle disconnection and cleanup", async () => {
const clientAgent = crypto.newRandomAgentSecret();
const clientNode = new LocalNode(
new ControlledAgent(clientAgent, crypto),
crypto.newRandomSessionID(crypto.getAgentID(clientAgent)),
crypto,
);
const ws = new WebSocket(syncServerUrl);
let disconnectCalled = false;
const peer = createWebSocketPeer({
id: "test-client",
websocket: ws,
role: "server",
onClose: () => {
disconnectCalled = true;
},
});
clientNode.syncManager.addPeer(peer);
// Wait for connection to establish
await new Promise((resolve) => setTimeout(resolve, 200));
// Close the server
server.close();
// Wait for disconnect handling
await new Promise((resolve) => setTimeout(resolve, 200));
expect(disconnectCalled).toBe(true);
expect(ws.readyState).toBe(WebSocket.CLOSED);
});
});

View File

@@ -0,0 +1,89 @@
import { createServer } from "http";
import { ControlledAgent, LocalNode, WasmCrypto } from "cojson";
import { WebSocket, WebSocketServer } from "ws";
import { createWebSocketPeer } from "../index";
export const startSyncServer = async (port?: number) => {
const crypto = await WasmCrypto.create();
const server = createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200);
res.end("ok");
}
});
const wss = new WebSocketServer({ noServer: true });
const agentSecret = crypto.newRandomAgentSecret();
const agentID = crypto.getAgentID(agentSecret);
const localNode = new LocalNode(
new ControlledAgent(agentSecret, crypto),
crypto.newRandomSessionID(agentID),
crypto,
);
const connections = new Set<WebSocket>();
wss.on("connection", function connection(ws, req) {
connections.add(ws);
const sendPing = () => {
ws.send(
JSON.stringify({
type: "ping",
time: Date.now(),
dc: "unknown",
}),
);
};
// ping/pong for the connection liveness
const pinging = setInterval(sendPing, 100);
sendPing(); // Immediately send a ping to the client to signal that the connection is established
ws.on("close", () => {
clearInterval(pinging);
connections.delete(ws);
});
const clientId = new Date().toISOString();
localNode.syncManager.addPeer(
createWebSocketPeer({
id: clientId,
role: "client",
websocket: ws,
expectPings: false,
batchingByDefault: false,
deletePeerStateOnClose: true,
}),
);
ws.on("error", (e) => console.error(`Error on connection ${clientId}:`, e));
});
server.on("upgrade", function upgrade(req, socket, head) {
if (req.url !== "/health") {
wss.handleUpgrade(req, socket, head, function done(ws) {
wss.emit("connection", ws, req);
});
}
});
server.listen(port ?? 0);
port = (server.address() as { port: number }).port;
const syncServer = `ws://localhost:${port}`;
return {
close: () => {
connections.forEach((ws) => ws.close());
server.close();
},
syncServer,
port,
localNode,
};
};

View File

@@ -1,5 +1,11 @@
# cojson
## 0.9.23
### Patch Changes
- 70c9a5d: Rotate keys before revoking access, so when admins remove themselves the keys are successfully rotated
## 0.9.19
### Patch Changes

View File

@@ -24,7 +24,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.9.19",
"version": "0.9.23",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^1.29.0",
"@types/jest": "^29.5.3",

View File

@@ -383,8 +383,10 @@ export class RawGroup<
}
/** @internal */
rotateReadKey() {
const memberKeys = this.getMemberKeys();
rotateReadKey(removedMemberKey?: RawAccountID | AgentID | "everyone") {
const memberKeys = this.getMemberKeys().filter(
(key) => key !== removedMemberKey,
);
const currentlyPermittedReaders = memberKeys.filter((key) => {
const role = this.get(key);
@@ -522,7 +524,7 @@ export class RawGroup<
continue;
}
child.rotateReadKey();
child.rotateReadKey(removedMemberKey);
}
}
@@ -617,8 +619,9 @@ export class RawGroup<
account: RawAccount | ControlledAccountOrAgent | AgentID | Everyone,
) {
const memberKey = typeof account === "string" ? account : account.id;
this.rotateReadKey(memberKey);
this.set(memberKey, "revoked", "trusting");
this.rotateReadKey();
}
/**

View File

@@ -1,43 +1,36 @@
import { describe, expect, onTestFinished, test, vi } from "vitest";
import { beforeEach, describe, expect, onTestFinished, test, vi } from "vitest";
import {
GlobalSyncStateListenerCallback,
PeerSyncStateListenerCallback,
} from "../SyncStateManager.js";
import { accountHeaderForInitialAgentSecret } from "../coValues/account.js";
import { connectedPeers } from "../streamUtils.js";
import { emptyKnownState } from "../sync.js";
import {
blockMessageTypeOnOutgoingPeer,
connectNodeToSyncServer,
createTestNode,
createTwoConnectedNodes,
loadCoValueOrFail,
setupSyncServer,
waitFor,
} from "./testUtils.js";
let jazzCloud = setupSyncServer();
beforeEach(async () => {
jazzCloud = setupSyncServer();
});
describe("SyncStateManager", () => {
test("subscribeToUpdates receives updates when peer state changes", async () => {
// Setup nodes
const client = createTestNode();
const jazzCloud = createTestNode();
const { nodeToServerPeer } = connectNodeToSyncServer(client);
// Create test data
const group = client.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
// Connect nodes
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
"clientConnection",
"jazzCloudConnection",
{
peer1role: "client",
peer2role: "server",
},
);
client.syncManager.addPeer(jazzCloudAsPeer);
jazzCloud.syncManager.addPeer(clientAsPeer);
const subscriptionManager = client.syncManager.syncState;
const updateSpy: GlobalSyncStateListenerCallback = vi.fn();
@@ -46,21 +39,21 @@ describe("SyncStateManager", () => {
await client.syncManager.actuallySyncCoValue(map.core);
expect(updateSpy).toHaveBeenCalledWith(
"jazzCloudConnection",
nodeToServerPeer.id,
emptyKnownState(map.core.id),
{ uploaded: false },
);
await waitFor(() => {
return subscriptionManager.getCurrentSyncState(
"jazzCloudConnection",
nodeToServerPeer.id,
map.core.id,
).uploaded;
});
expect(updateSpy).toHaveBeenCalledWith(
"jazzCloudConnection",
client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
nodeToServerPeer.id,
client.syncManager.peers[nodeToServerPeer.id]!.knownStates.get(
map.core.id,
)!,
{ uploaded: true },
@@ -73,42 +66,30 @@ describe("SyncStateManager", () => {
test("subscribeToPeerUpdates receives updates only for specific peer", async () => {
// Setup nodes
const client = createTestNode();
const jazzCloud = createTestNode();
const { nodeToServerPeer } = connectNodeToSyncServer(client);
// Create test data
const group = client.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
// Connect nodes
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
"clientConnection",
"jazzCloudConnection",
{
peer1role: "client",
peer2role: "server",
},
);
const [clientStoragePeer] = connectedPeers("clientStorage", "unusedPeer", {
peer1role: "client",
peer2role: "server",
});
client.syncManager.addPeer(jazzCloudAsPeer);
client.syncManager.addPeer(clientStoragePeer);
jazzCloud.syncManager.addPeer(clientAsPeer);
const subscriptionManager = client.syncManager.syncState;
const updateToJazzCloudSpy: PeerSyncStateListenerCallback = vi.fn();
const updateToStorageSpy: PeerSyncStateListenerCallback = vi.fn();
const unsubscribe1 = subscriptionManager.subscribeToPeerUpdates(
"jazzCloudConnection",
nodeToServerPeer.id,
updateToJazzCloudSpy,
);
const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
"clientStorage",
clientStoragePeer.id,
updateToStorageSpy,
);
@@ -126,13 +107,13 @@ describe("SyncStateManager", () => {
await waitFor(() => {
return subscriptionManager.getCurrentSyncState(
"jazzCloudConnection",
nodeToServerPeer.id,
map.core.id,
).uploaded;
});
expect(updateToJazzCloudSpy).toHaveBeenLastCalledWith(
client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
client.syncManager.peers[nodeToServerPeer.id]!.knownStates.get(
map.core.id,
)!,
{ uploaded: true },
@@ -147,80 +128,50 @@ describe("SyncStateManager", () => {
test("getIsCoValueFullyUploadedIntoPeer returns correct status", async () => {
// Setup nodes
const client = createTestNode();
const jazzCloud = createTestNode();
const { nodeToServerPeer } = connectNodeToSyncServer(client);
// Create test data
const group = client.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
// Connect nodes
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
"clientConnection",
"jazzCloudConnection",
{
peer1role: "client",
peer2role: "server",
},
);
client.syncManager.addPeer(jazzCloudAsPeer);
jazzCloud.syncManager.addPeer(clientAsPeer);
await client.syncManager.actuallySyncCoValue(map.core);
const subscriptionManager = client.syncManager.syncState;
expect(
subscriptionManager.getCurrentSyncState(
"jazzCloudConnection",
map.core.id,
).uploaded,
subscriptionManager.getCurrentSyncState(nodeToServerPeer.id, map.core.id)
.uploaded,
).toBe(false);
await waitFor(() => {
return subscriptionManager.getCurrentSyncState(
"jazzCloudConnection",
nodeToServerPeer.id,
map.core.id,
).uploaded;
});
expect(
subscriptionManager.getCurrentSyncState(
"jazzCloudConnection",
map.core.id,
).uploaded,
subscriptionManager.getCurrentSyncState(nodeToServerPeer.id, map.core.id)
.uploaded,
).toBe(true);
});
test("unsubscribe stops receiving updates", async () => {
// Setup nodes
const client = createTestNode();
const jazzCloud = createTestNode();
const { nodeToServerPeer } = connectNodeToSyncServer(client);
// Create test data
const group = client.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
// Connect nodes
const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
"clientConnection",
"jazzCloudConnection",
{
peer1role: "client",
peer2role: "server",
},
);
client.syncManager.addPeer(jazzCloudAsPeer);
jazzCloud.syncManager.addPeer(clientAsPeer);
const subscriptionManager = client.syncManager.syncState;
const anyUpdateSpy = vi.fn();
const unsubscribe1 = subscriptionManager.subscribeToUpdates(anyUpdateSpy);
const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
"jazzCloudConnection",
nodeToServerPeer.id,
anyUpdateSpy,
);
@@ -233,7 +184,7 @@ describe("SyncStateManager", () => {
await waitFor(() => {
return subscriptionManager.getCurrentSyncState(
"jazzCloudConnection",
nodeToServerPeer.id,
map.core.id,
).uploaded;
});
@@ -243,22 +194,21 @@ describe("SyncStateManager", () => {
test("getCurrentSyncState should return the correct state", async () => {
// Setup nodes
const clientNode = createTestNode();
const serverNode = jazzCloud;
const {
node1: clientNode,
node2: serverNode,
node1ToNode2Peer: clientToServerPeer,
node2ToNode1Peer: serverToClientPeer,
} = await createTwoConnectedNodes("client", "server");
nodeToServerPeer: clientToServerPeer,
serverToNodePeer: serverToClientPeer,
} = connectNodeToSyncServer(clientNode);
// Create test data
const group = clientNode.node.createGroup();
const group = clientNode.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
group.addMember("everyone", "writer");
// Initially should not be synced
expect(
clientNode.node.syncManager.syncState.getCurrentSyncState(
clientNode.syncManager.syncState.getCurrentSyncState(
clientToServerPeer.id,
map.core.id,
),
@@ -268,13 +218,13 @@ describe("SyncStateManager", () => {
await map.core.waitForSync();
expect(
clientNode.node.syncManager.syncState.getCurrentSyncState(
clientNode.syncManager.syncState.getCurrentSyncState(
clientToServerPeer.id,
map.core.id,
),
).toEqual({ uploaded: true });
const mapOnServer = await loadCoValueOrFail(serverNode.node, map.id);
const mapOnServer = await loadCoValueOrFail(serverNode, map.id);
// Block the content messages so the client won't fully sync immediately
const outgoing = blockMessageTypeOnOutgoingPeer(
@@ -285,14 +235,14 @@ describe("SyncStateManager", () => {
mapOnServer.set("key2", "value2", "trusting");
expect(
clientNode.node.syncManager.syncState.getCurrentSyncState(
clientNode.syncManager.syncState.getCurrentSyncState(
clientToServerPeer.id,
map.core.id,
),
).toEqual({ uploaded: true });
expect(
serverNode.node.syncManager.syncState.getCurrentSyncState(
serverNode.syncManager.syncState.getCurrentSyncState(
serverToClientPeer.id,
map.core.id,
),
@@ -304,14 +254,14 @@ describe("SyncStateManager", () => {
await mapOnServer.core.waitForSync();
expect(
clientNode.node.syncManager.syncState.getCurrentSyncState(
clientNode.syncManager.syncState.getCurrentSyncState(
clientToServerPeer.id,
map.core.id,
),
).toEqual({ uploaded: true });
expect(
serverNode.node.syncManager.syncState.getCurrentSyncState(
serverNode.syncManager.syncState.getCurrentSyncState(
serverToClientPeer.id,
map.core.id,
),

View File

@@ -1,4 +1,4 @@
import { expect, test } from "vitest";
import { expect, test, vi } from "vitest";
import { expectMap } from "../coValue.js";
import { ControlledAgent } from "../coValues/account.js";
import { WasmCrypto } from "../crypto/WasmCrypto.js";
@@ -2908,3 +2908,61 @@ test("extend cycles should not break the keys rotation", () => {
expect(map.get("test")).toEqual("Hello!");
});
test("Admin can remove themselves from a group", async () => {
const warnSpy = vi.spyOn(console, "warn");
const { group, admin } = newGroupHighLevel();
// Admin removes themselves
await group.removeMember(admin);
expect(group.myRole()).toBeUndefined();
expect(warnSpy).not.toHaveBeenCalled();
});
test("Can revoke read permission from 'everyone'", async () => {
const { group } = newGroupHighLevel();
const childObject = group.createMap();
// Give everyone read access
group.addMember("everyone", "reader");
childObject.set("foo", "bar", "private");
expect(childObject.get("foo")).toEqual("bar");
// Create a new account to verify access
const newAccount = new ControlledAgent(Crypto.newRandomAgentSecret(), Crypto);
const childContent = expectMap(
childObject.core
.testWithDifferentAccount(
newAccount,
Crypto.newRandomSessionID(newAccount.currentAgentID()._unsafeUnwrap()),
)
.getCurrentContent(),
);
// Verify the new account can read
expect(childContent.get("foo")).toEqual("bar");
// Revoke everyone's access
await group.removeMember("everyone");
childObject.set("foo", "updated after revoke", "private");
// Create another new account to verify access is revoked
const newAccount2 = new ControlledAgent(
Crypto.newRandomAgentSecret(),
Crypto,
);
const childContent2 = expectMap(
childObject.core
.testWithDifferentAccount(
newAccount2,
Crypto.newRandomSessionID(newAccount2.currentAgentID()._unsafeUnwrap()),
)
.getCurrentContent(),
);
// Verify the new account cannot read after revocation
expect(childContent2.get("foo")).toEqual("bar");
});

View File

@@ -1,4 +1,4 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { expectMap } from "../coValue.js";
import type { CoValueHeader } from "../coValueCore.js";
import type { RawAccountID } from "../coValues/account.js";
@@ -12,17 +12,27 @@ import { connectedPeers, newQueuePair } from "../streamUtils.js";
import type { SyncMessage } from "../sync.js";
import {
blockMessageTypeOnOutgoingPeer,
connectNodeToSyncServer,
connectTwoPeers,
createConnectedTestAgentNode,
createConnectedTestNode,
createTestMetricReader,
createTestNode,
loadCoValueOrFail,
randomAnonymousAccountAndSessionID,
setupSyncServer,
tearDownTestMetricReader,
waitFor,
} from "./testUtils.js";
const Crypto = await WasmCrypto.create();
let jazzCloud = setupSyncServer();
beforeEach(async () => {
jazzCloud = setupSyncServer();
});
test("Node replies with initial tx and header to empty subscribe", async () => {
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session, Crypto);
@@ -861,111 +871,34 @@ test.skip("When loading a coValue on one node, the server node it is requested f
});
test("Can sync a coValue through a server to another client", async () => {
const [admin, session] = randomAnonymousAccountAndSessionID();
const client1 = new LocalNode(admin, session, Crypto);
const { node: client1 } = await createConnectedTestNode();
const group = client1.createGroup();
const map = group.createMap();
map.set("hello", "world", "trusting");
const [serverUser, serverSession] = randomAnonymousAccountAndSessionID();
const { node: client2 } = await createConnectedTestNode();
const server = new LocalNode(serverUser, serverSession, Crypto);
const mapOnClient2 = await loadCoValueOrFail(client2, map.id);
const [serverAsPeerForClient1, client1AsPeer] = connectedPeers(
"serverFor1",
"client1",
{
peer1role: "server",
peer2role: "client",
// trace: true,
},
);
client1.syncManager.addPeer(serverAsPeerForClient1);
server.syncManager.addPeer(client1AsPeer);
const client2 = new LocalNode(
admin,
Crypto.newRandomSessionID(admin.id),
Crypto,
);
const [serverAsPeerForClient2, client2AsPeer] = connectedPeers(
"serverFor2",
"client2",
{
peer1role: "server",
peer2role: "client",
// trace: true,
},
);
client2.syncManager.addPeer(serverAsPeerForClient2);
server.syncManager.addPeer(client2AsPeer);
const mapOnClient2 = await client2.loadCoValueCore(map.core.id);
if (mapOnClient2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(expectMap(mapOnClient2.getCurrentContent()).get("hello")).toEqual(
"world",
);
expect(mapOnClient2.get("hello")).toEqual("world");
});
test("Can sync a coValue with private transactions through a server to another client", async () => {
const [admin, session] = randomAnonymousAccountAndSessionID();
const client1 = new LocalNode(admin, session, Crypto);
const { node: client1 } = await createConnectedTestNode();
const group = client1.createGroup();
const map = group.createMap();
map.set("hello", "world", "private");
group.addMember("everyone", "reader");
const [serverUser, serverSession] = randomAnonymousAccountAndSessionID();
const { node: client2 } = await createConnectedTestNode();
const server = new LocalNode(serverUser, serverSession, Crypto);
const mapOnClient2 = await loadCoValueOrFail(client2, map.id);
const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
// trace: true,
peer1role: "server",
peer2role: "client",
});
client1.syncManager.addPeer(serverAsPeer);
server.syncManager.addPeer(client1AsPeer);
const client2 = new LocalNode(
admin,
client1.crypto.newRandomSessionID(admin.id),
Crypto,
);
const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
"server",
"client2",
{
// trace: true,
peer1role: "server",
peer2role: "client",
},
);
client2.syncManager.addPeer(serverAsOtherPeer);
server.syncManager.addPeer(client2AsPeer);
const mapOnClient2 = await client2.loadCoValueCore(map.core.id);
if (mapOnClient2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(expectMap(mapOnClient2.getCurrentContent()).get("hello")).toEqual(
"world",
);
expect(mapOnClient2.get("hello")).toEqual("world");
});
test.skip("When a peer's incoming/readable stream closes, we remove the peer", async () => {
@@ -1023,111 +956,32 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
*/
});
test.skip("When a peer's outgoing/writable stream closes, we remove the peer", async () => {
/*
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session, Crypto);
const group = node.createGroup();
const [inRx] = await Effect.runPromise(newStreamPair());
const [outRx, outTx] = await Effect.runPromise(newStreamPair());
node.syncManager.addPeer({
id: "test",
incoming: inRx,
outgoing: outTx,
role: "server",
});
// expect(yield* Queue.take(outRxQ)).toMatchObject({
// action: "load",
// id: admin.id,
// });
expect(yield * Queue.take(outRxQ)).toMatchObject({
action: "load",
id: group.core.id,
});
const map = group.createMap();
const mapSubscribeMsg = await reader.read();
expect(mapSubscribeMsg.value).toEqual({
action: "load",
...map.core.knownState(),
} satisfies SyncMessage);
// expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
expect(yield * Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
const mapContentMsg = await reader.read();
expect(mapContentMsg.value).toEqual({
action: "content",
id: map.core.id,
header: map.core.header,
new: {},
} satisfies SyncMessage);
reader.releaseLock();
await outRx.cancel();
map.set("hello", "world", "trusting");
await new Promise((resolve) => setTimeout(resolve, 100));
expect(node.syncManager.peers["test"]).toBeUndefined();
*/
});
test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
const [admin, session] = randomAnonymousAccountAndSessionID();
const node1 = new LocalNode(admin, session, Crypto);
const { node: node1 } = await createConnectedTestNode();
const group = node1.createGroup();
const map = group.createMap();
map.set("hello", "world", "trusting");
const node2 = new LocalNode(
admin,
Crypto.newRandomSessionID(admin.id),
Crypto,
);
const node2 = createTestNode();
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
peer1role: "server",
peer2role: "client",
// trace: true,
});
node1.syncManager.addPeer(node2asPeer);
const mapOnNode2Promise = node2.loadCoValueCore(map.core.id);
const mapOnNode2Promise = loadCoValueOrFail(node2, map.id);
expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("unknown");
node2.syncManager.addPeer(node1asPeer);
connectNodeToSyncServer(node2);
const mapOnNode2 = await mapOnNode2Promise;
if (mapOnNode2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual(
"world",
);
expect(mapOnNode2.get("hello")).toEqual("world");
});
test("should keep the peer state when the peer closes", async () => {
const {
client,
jazzCloud,
jazzCloudConnectionAsPeer,
connectionWithClientAsPeer,
} = createTwoConnectedNodes();
const client = createTestNode();
const { nodeToServerPeer, serverToNodePeer } =
connectNodeToSyncServer(client);
const group = jazzCloud.createGroup();
const map = group.createMap();
@@ -1136,25 +990,23 @@ test("should keep the peer state when the peer closes", async () => {
await client.loadCoValueCore(map.core.id);
const syncManager = client.syncManager;
const peerState = syncManager.peers[jazzCloudConnectionAsPeer.id];
const peerState = syncManager.peers[nodeToServerPeer.id];
// @ts-expect-error Simulating a peer closing, leveraging the direct connection between the client/server peers
await connectionWithClientAsPeer.outgoing.push("Disconnected");
await serverToNodePeer.outgoing.push("Disconnected");
await waitFor(() => peerState?.closed);
expect(syncManager.peers[jazzCloudConnectionAsPeer.id]).not.toBeUndefined();
expect(syncManager.peers[nodeToServerPeer.id]).not.toBeUndefined();
});
test("should delete the peer state when the peer closes if deletePeerStateOnClose is true", async () => {
const {
client,
jazzCloud,
jazzCloudConnectionAsPeer,
connectionWithClientAsPeer,
} = createTwoConnectedNodes();
const client = createTestNode();
jazzCloudConnectionAsPeer.deletePeerStateOnClose = true;
const { nodeToServerPeer, serverToNodePeer } =
connectNodeToSyncServer(client);
nodeToServerPeer.deletePeerStateOnClose = true;
const group = jazzCloud.createGroup();
const map = group.createMap();
@@ -1164,14 +1016,14 @@ test("should delete the peer state when the peer closes if deletePeerStateOnClos
const syncManager = client.syncManager;
const peerState = syncManager.peers[jazzCloudConnectionAsPeer.id];
const peerState = syncManager.peers[nodeToServerPeer.id];
// @ts-expect-error Simulating a peer closing, leveraging the direct connection between the client/server peers
await connectionWithClientAsPeer.outgoing.push("Disconnected");
await serverToNodePeer.outgoing.push("Disconnected");
await waitFor(() => peerState?.closed);
expect(syncManager.peers[jazzCloudConnectionAsPeer.id]).toBeUndefined();
expect(syncManager.peers[nodeToServerPeer.id]).toBeUndefined();
});
describe("sync - extra tests", () => {
@@ -1624,29 +1476,6 @@ describe("sync - extra tests", () => {
});
});
function createTwoConnectedNodes() {
// Setup nodes
const client = createTestNode();
const jazzCloud = createTestNode();
// Connect nodes initially
const [connectionWithClientAsPeer, jazzCloudConnectionAsPeer] =
connectedPeers("connectionWithClient", "jazzCloudConnection", {
peer1role: "client",
peer2role: "server",
});
client.syncManager.addPeer(jazzCloudConnectionAsPeer);
jazzCloud.syncManager.addPeer(connectionWithClientAsPeer);
return {
client,
jazzCloud,
connectionWithClientAsPeer,
jazzCloudConnectionAsPeer,
};
}
test("a value created on one node can be loaded on anotehr node even if not directly connected", async () => {
const userA = createTestNode();
const userB = createTestNode();
@@ -1671,7 +1500,7 @@ test("a value created on one node can be loaded on anotehr node even if not dire
describe("SyncManager - knownStates vs optimisticKnownStates", () => {
test("knownStates and optimisticKnownStates are the same when the coValue is fully synced", async () => {
const { client, jazzCloud } = createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
// Create test data
const group = client.createGroup();
@@ -1683,9 +1512,8 @@ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
// Wait for the full sync to complete
await mapOnClient.core.waitForSync();
const peerStateClient = client.syncManager.peers["jazzCloudConnection"]!;
const peerStateJazzCloud =
jazzCloud.syncManager.peers["connectionWithClient"]!;
const peerStateClient = client.syncManager.getPeers()[0]!;
const peerStateJazzCloud = jazzCloud.syncManager.getPeers()[0]!;
// The optimisticKnownStates should be the same as the knownStates after the full sync is complete
expect(
@@ -1699,7 +1527,7 @@ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
});
test("optimisticKnownStates is updated as new transactions are sent, while knownStates only when the updates are acknowledged", async () => {
const { client, jazzCloudConnectionAsPeer } = createTwoConnectedNodes();
const { node: client, nodeToServerPeer } = await createConnectedTestNode();
// Create test data and sync the first change
// We want that both the nodes know about the coValue so we can test
@@ -1717,7 +1545,7 @@ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
// while knownStates is only updated when we receive the "known" messages
// that are acknowledging the receipt of the content messages
const outgoing = blockMessageTypeOnOutgoingPeer(
jazzCloudConnectionAsPeer,
nodeToServerPeer,
"content",
);
@@ -1725,8 +1553,7 @@ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
await client.syncManager.actuallySyncCoValue(map.core);
const peerState = client.syncManager.peers["jazzCloudConnection"]!;
const peerState = client.syncManager.peers[nodeToServerPeer.id]!;
expect(peerState.optimisticKnownStates.get(map.core.id)).not.toEqual(
peerState.knownStates.get(map.core.id),
);
@@ -1747,7 +1574,7 @@ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
describe("SyncManager.addPeer", () => {
test("new peer gets a copy of previous peer's knownStates when replacing it", async () => {
const { client } = createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
// Create test data
const group = client.createGroup();
@@ -1759,26 +1586,24 @@ describe("SyncManager.addPeer", () => {
// Wait for initial sync
await map.core.waitForSync();
const firstPeerState = client.syncManager.getPeers()[0]!;
// Store the initial known states
const initialKnownStates =
client.syncManager.peers["jazzCloudConnection"]!.knownStates;
const initialKnownStates = firstPeerState.knownStates;
// Create new connection with same ID
const [jazzCloudConnectionAsPeer2] = connectedPeers(
"jazzCloudConnection",
"unusedPeer",
{
peer1role: "server",
peer2role: "client",
},
);
const [secondPeer] = connectedPeers(firstPeerState.id, "unusedPeer", {
peer1role: "server",
peer2role: "client",
});
// Add new peer with same ID
client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
client.syncManager.addPeer(secondPeer);
const newPeerState = client.syncManager.getPeers()[0]!;
// Verify that the new peer has a copy of the previous known states
const newPeerKnownStates =
client.syncManager.peers["jazzCloudConnection"]!.knownStates;
const newPeerKnownStates = newPeerState.knownStates;
expect(newPeerKnownStates).not.toBe(initialKnownStates); // Should be a different instance
expect(newPeerKnownStates.get(map.core.id)).toEqual(
@@ -1787,7 +1612,7 @@ describe("SyncManager.addPeer", () => {
});
test("new peer with new ID starts with empty knownStates", async () => {
const { client } = createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
// Create test data
const group = client.createGroup();
@@ -1815,23 +1640,19 @@ describe("SyncManager.addPeer", () => {
});
test("when adding a peer with the same ID as a previous peer, the previous peer is closed", async () => {
const { client } = createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
// Store reference to first peer
const firstPeer = client.syncManager.peers["jazzCloudConnection"]!;
const firstPeer = client.syncManager.getPeers()[0]!;
const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
// Create and add replacement peer
const [jazzCloudConnectionAsPeer2] = connectedPeers(
"jazzCloudConnection",
"unusedPeer",
{
peer1role: "server",
peer2role: "client",
},
);
const [secondPeer] = connectedPeers(firstPeer.id, "unusedPeer", {
peer1role: "server",
peer2role: "client",
});
client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
client.syncManager.addPeer(secondPeer);
// Verify thet the first peer had ben closed correctly
expect(closeSpy).toHaveBeenCalled();
@@ -1839,25 +1660,21 @@ describe("SyncManager.addPeer", () => {
});
test("when adding a peer with the same ID as a previous peer and the previous peer is closed, do not attempt to close it again", async () => {
const { client } = createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
// Store reference to first peer
const firstPeer = client.syncManager.peers["jazzCloudConnection"]!;
const firstPeer = client.syncManager.getPeers()[0]!;
firstPeer.gracefulShutdown();
const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
// Create and add replacement peer
const [jazzCloudConnectionAsPeer2] = connectedPeers(
"jazzCloudConnection",
"unusedPeer",
{
peer1role: "server",
peer2role: "client",
},
);
const [secondPeer] = connectedPeers(firstPeer.id, "unusedPeer", {
peer1role: "server",
peer2role: "client",
});
client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
client.syncManager.addPeer(secondPeer);
// Verify thet the first peer had not been closed again
expect(closeSpy).not.toHaveBeenCalled();
@@ -1865,24 +1682,15 @@ describe("SyncManager.addPeer", () => {
});
test("when adding a server peer the local coValues should be sent to it", async () => {
// Setup nodes
const client = createTestNode();
const jazzCloud = createTestNode();
// Connect nodes initially
const [connectionWithClientAsPeer, jazzCloudConnectionAsPeer] =
connectedPeers("connectionWithClient", "jazzCloudConnection", {
peer1role: "client",
peer2role: "server",
});
jazzCloud.syncManager.addPeer(connectionWithClientAsPeer);
const { node: client, addServerPeer } = await createConnectedTestNode({
connected: false,
});
const group = client.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
client.syncManager.addPeer(jazzCloudConnectionAsPeer);
addServerPeer();
await map.core.waitForSync();
@@ -1892,18 +1700,8 @@ describe("SyncManager.addPeer", () => {
describe("loadCoValueCore with retry", () => {
test("should load the value if available on the server", async () => {
const { client, jazzCloud } = createTwoConnectedNodes();
const anotherClient = createTestNode();
const [
connectionWithAnotherClientAsPeer,
jazzCloudConnectionAsPeerForAnotherClient,
] = connectedPeers("connectionWithAnotherClient", "jazzCloudConnection", {
peer1role: "client",
peer2role: "server",
});
jazzCloud.syncManager.addPeer(connectionWithAnotherClientAsPeer);
const { node: client } = await createConnectedTestNode();
const { node: anotherClient } = await createConnectedTestNode();
const group = anotherClient.createGroup();
const map = group.createMap();
@@ -1911,25 +1709,12 @@ describe("loadCoValueCore with retry", () => {
const promise = client.loadCoValueCore(map.id);
anotherClient.syncManager.addPeer(
jazzCloudConnectionAsPeerForAnotherClient,
);
await expect(promise).resolves.not.toBe("unavailable");
});
test("should handle correctly two subsequent loads", async () => {
const { client, jazzCloud } = createTwoConnectedNodes();
const anotherClient = createTestNode();
const [
connectionWithAnotherClientAsPeer,
jazzCloudConnectionAsPeerForAnotherClient,
] = connectedPeers("connectionWithAnotherClient", "jazzCloudConnection", {
peer1role: "client",
peer2role: "server",
});
jazzCloud.syncManager.addPeer(connectionWithAnotherClientAsPeer);
const { node: client } = await createConnectedTestNode();
const { node: anotherClient } = await createConnectedTestNode();
const group = anotherClient.createGroup();
const map = group.createMap();
@@ -1938,10 +1723,6 @@ describe("loadCoValueCore with retry", () => {
const promise1 = client.loadCoValueCore(map.id);
const promise2 = client.loadCoValueCore(map.id);
anotherClient.syncManager.addPeer(
jazzCloudConnectionAsPeerForAnotherClient,
);
await expect(promise1).resolves.not.toBe("unavailable");
await expect(promise2).resolves.not.toBe("unavailable");
});
@@ -1949,8 +1730,7 @@ describe("loadCoValueCore with retry", () => {
describe("waitForSyncWithPeer", () => {
test("should resolve when the coValue is fully uploaded into the peer", async () => {
const { client, jazzCloudConnectionAsPeer: peer } =
createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
// Create test data
const group = client.createGroup();
@@ -1959,21 +1739,32 @@ describe("waitForSyncWithPeer", () => {
await client.syncManager.actuallySyncCoValue(map.core);
const peer = client.syncManager.getPeers()[0];
if (!peer) {
throw new Error("No peer found");
}
await expect(
client.syncManager.waitForSyncWithPeer(peer.id, map.core.id, 100),
).resolves.toBe(true);
});
test("should not resolve when the coValue is not synced", async () => {
const { client, jazzCloudConnectionAsPeer: peer } =
createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
const peer = client.syncManager.getPeers()[0];
if (!peer) {
throw new Error("No peer found");
}
// Create test data
const group = client.createGroup();
const map = group.createMap();
map.set("key1", "value1", "trusting");
vi.spyOn(peer.outgoing, "push").mockImplementation(async () => {
vi.spyOn(peer, "pushOutgoingMessage").mockImplementation(async () => {
return Promise.resolve();
});
@@ -1986,7 +1777,7 @@ describe("waitForSyncWithPeer", () => {
});
test("Should not crash when syncing an unknown coValue type", async () => {
const { client, jazzCloud } = createTwoConnectedNodes();
const { node: client } = await createConnectedTestNode();
const coValue = client.createCoValue({
type: "ooops" as any,
@@ -1997,8 +1788,10 @@ test("Should not crash when syncing an unknown coValue type", async () => {
await coValue.waitForSync();
const { node: anotherClient } = await createConnectedTestNode();
const coValueOnTheOtherNode = await loadCoValueOrFail(
jazzCloud,
anotherClient,
coValue.getCurrentContent().id,
);
expect(coValueOnTheOtherNode.id).toBe(coValue.id);
@@ -2092,6 +1885,163 @@ describe("metrics", () => {
});
});
describe("sync protocol", () => {
test("should have the correct messages exchanged between client and server", async () => {
// Creating the account from agent to simplify the messages exchange
const { node: client, messages } = await createConnectedTestAgentNode();
const group = client.createGroup();
const map = group.createMap();
map.set("hello", "world", "trusting");
const mapOnJazzCloud = await loadCoValueOrFail(jazzCloud, map.id);
expect(mapOnJazzCloud.get("hello")).toEqual("world");
expect(messages).toEqual([
{
from: "client",
msg: {
action: "load",
header: true,
id: group.id,
sessions: {
[client.currentSessionID]: 3,
},
},
},
{
from: "server",
msg: {
action: "load",
header: false,
id: group.id,
sessions: {},
},
},
{
from: "client",
msg: {
action: "load",
header: true,
id: map.id,
sessions: {
[client.currentSessionID]: 1,
},
},
},
{
from: "server",
msg: {
action: "load",
header: false,
id: map.id,
sessions: {},
},
},
{
from: "client",
msg: {
action: "content",
header: {
createdAt: expect.any(String),
meta: null,
ruleset: {
initialAdmin: client.account.id,
type: "group",
},
type: "comap",
uniqueness: expect.any(String),
},
id: group.id,
new: {
[client.currentSessionID]: {
after: 0,
lastSignature: expect.any(String),
newTransactions: expect.any(Array),
},
},
priority: 0,
},
},
{
from: "client",
msg: {
action: "content",
header: {
createdAt: expect.any(String),
meta: null,
ruleset: {
group: group.id,
type: "ownedByGroup",
},
type: "comap",
uniqueness: expect.any(String),
},
id: map.id,
new: {
[client.currentSessionID]: {
after: 0,
lastSignature: expect.any(String),
newTransactions: expect.any(Array),
},
},
priority: 3,
},
},
{
from: "server",
msg: {
action: "known",
header: true,
id: group.id,
sessions: {
[client.currentSessionID]: 3,
},
},
},
{
// TODO: This is a redundant message, we should remove it
from: "client",
msg: {
action: "content",
header: {
createdAt: expect.any(String),
meta: null,
ruleset: {
group: group.id,
type: "ownedByGroup",
},
type: "comap",
uniqueness: expect.any(String),
},
id: map.id,
new: {
[client.currentSessionID]: {
after: 0,
lastSignature: expect.any(String),
newTransactions: expect.any(Array),
},
},
priority: 3,
},
},
{
// TODO: This is a redundant message, we should remove it
from: "server",
msg: {
action: "known",
asDependencyOf: undefined,
header: true,
id: group.id,
sessions: {
[client.currentSessionID]: 3,
},
},
},
]);
});
});
function groupContentEx(group: RawGroup) {
return {
action: "content",
@@ -2099,27 +2049,9 @@ function groupContentEx(group: RawGroup) {
};
}
function _admContEx(adminID: RawAccountID) {
return {
action: "content",
id: adminID,
};
}
function groupStateEx(group: RawGroup) {
return {
action: "known",
id: group.core.id,
};
}
function _admStateEx(adminID: RawAccountID) {
return {
action: "known",
id: adminID,
};
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -17,6 +17,12 @@ import { expectGroup } from "../typeUtils/expectGroup.js";
const Crypto = await WasmCrypto.create();
const syncServer: {
current: undefined | LocalNode;
} = {
current: undefined,
};
export function randomAnonymousAccountAndSessionID(): [
ControlledAgent,
SessionID,
@@ -33,25 +39,6 @@ export function createTestNode() {
return new LocalNode(admin, session, Crypto);
}
export function connectTwoPeers(
a: LocalNode,
b: LocalNode,
aRole: "client" | "server",
bRole: "client" | "server",
) {
const [aAsPeer, bAsPeer] = connectedPeers(
"peer:" + a.account.id,
"peer:" + b.account.id,
{
peer1role: aRole,
peer2role: bRole,
},
);
a.syncManager.addPeer(bAsPeer);
b.syncManager.addPeer(aAsPeer);
}
export async function createTwoConnectedNodes(
node1Role: Peer["role"],
node2Role: Peer["role"],
@@ -149,6 +136,25 @@ export async function createThreeConnectedNodes(
};
}
export function connectTwoPeers(
a: LocalNode,
b: LocalNode,
aRole: "client" | "server",
bRole: "client" | "server",
) {
const [aAsPeer, bAsPeer] = connectedPeers(
"peer:" + a.account.id,
"peer:" + b.account.id,
{
peer1role: aRole,
peer2role: bRole,
},
);
a.syncManager.addPeer(bAsPeer);
b.syncManager.addPeer(aAsPeer);
}
export function newGroup() {
const [admin, sessionID] = randomAnonymousAccountAndSessionID();
@@ -368,3 +374,90 @@ export function createTestMetricReader() {
export function tearDownTestMetricReader() {
metrics.disable();
}
export function setupSyncServer() {
syncServer.current = createTestNode();
return syncServer.current;
}
export async function createConnectedTestAgentNode(opts = { connected: true }) {
if (!syncServer.current) {
throw new Error("Sync server not initialized");
}
const [admin, session] = randomAnonymousAccountAndSessionID();
const node = new LocalNode(admin, session, Crypto);
const { nodeToServerPeer, serverToNodePeer, messages, addServerPeer } =
connectNodeToSyncServer(node, opts.connected);
return { node, nodeToServerPeer, serverToNodePeer, messages, addServerPeer };
}
export async function createConnectedTestNode(opts = { connected: true }) {
if (!syncServer.current) {
throw new Error("Sync server not initialized");
}
const ctx = await LocalNode.withNewlyCreatedAccount({
peersToLoadFrom: [],
crypto: Crypto,
creationProps: { name: "Client" },
});
const { nodeToServerPeer, serverToNodePeer, messages, addServerPeer } =
connectNodeToSyncServer(ctx.node, opts.connected);
return {
node: ctx.node,
accountID: ctx.accountID,
nodeToServerPeer,
serverToNodePeer,
messages,
addServerPeer,
};
}
export function connectNodeToSyncServer(node: LocalNode, connected = true) {
if (!syncServer.current) {
throw new Error("Sync server not initialized");
}
const [nodeToServerPeer, serverToNodePeer] = connectedPeers(
syncServer.current.account.id,
node.account.id,
{
peer1role: "server",
peer2role: "client",
},
);
const messages: {
from: "client" | "server";
msg: SyncMessage;
}[] = [];
const serverPush = serverToNodePeer.outgoing.push;
serverToNodePeer.outgoing.push = (msg) => {
messages.push({ from: "server", msg });
return serverPush.call(serverToNodePeer.outgoing, msg);
};
const clientPush = nodeToServerPeer.outgoing.push;
nodeToServerPeer.outgoing.push = (msg) => {
messages.push({ from: "client", msg });
return clientPush.call(nodeToServerPeer.outgoing, msg);
};
syncServer.current.syncManager.addPeer(serverToNodePeer);
if (connected) {
node.syncManager.addPeer(nodeToServerPeer);
}
return {
nodeToServerPeer,
serverToNodePeer,
messages,
addServerPeer: () => node.syncManager.addPeer(nodeToServerPeer),
};
}

View File

@@ -1,5 +1,36 @@
# jazz-browser-media-images
## 0.9.23
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
- jazz-browser@0.9.23
- jazz-tools@0.9.23
## 0.9.22
### Patch Changes
- jazz-browser@0.9.22
## 0.9.21
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser@0.9.21
## 0.9.20
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser@0.9.20
## 0.9.19
### Patch Changes

View File

@@ -1,14 +1,14 @@
{
"name": "jazz-browser-auth-clerk",
"version": "0.9.19",
"version": "0.9.23",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.9.19",
"jazz-browser": "workspace:0.9.19",
"jazz-tools": "workspace:0.9.19"
"cojson": "workspace:0.9.23",
"jazz-browser": "workspace:0.9.23",
"jazz-tools": "workspace:0.9.23"
},
"scripts": {
"format-and-lint": "biome check .",

View File

@@ -1,5 +1,34 @@
# jazz-browser-media-images
## 0.9.23
### Patch Changes
- jazz-browser@0.9.23
- jazz-tools@0.9.23
## 0.9.22
### Patch Changes
- jazz-browser@0.9.22
## 0.9.21
### Patch Changes
- Updated dependencies [1be017d]
- jazz-tools@0.9.21
- jazz-browser@0.9.21
## 0.9.20
### Patch Changes
- Updated dependencies [b01cc1f]
- jazz-tools@0.9.20
- jazz-browser@0.9.20
## 0.9.19
### Patch Changes

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