Compare commits

..

163 Commits

Author SHA1 Message Date
Guido D'Orsi
114898d8a9 Merge pull request #2643 from garden-co/changeset-release/main
Version Packages
2025-07-14 21:25:21 +02:00
github-actions[bot]
962213c712 Version Packages 2025-07-14 15:04:25 +00:00
Guido D'Orsi
427df8fcbb Merge pull request #2644 from garden-co/chore/lefthook-autoinstall
chore: auto-install lefthook using `postinstall` script
2025-07-14 17:02:16 +02:00
Matteo Manchi
c40aad55dc chore: auto-install lefthook using postinstall script 2025-07-14 14:38:48 +02:00
Guido D'Orsi
dfca5926de Merge pull request #2640 from garden-co/GCO-621-Adds-documentation-for-descriminatedUnion-workaround
Closes GCO-621 - Add documentation for discriminated union workaround
2025-07-14 12:50:16 +02:00
Guido D'Orsi
9815ec61f0 feat: export the z.ZodDiscriminatedUnion type and improve the recursive types docs 2025-07-14 11:14:59 +02:00
Guido D'Orsi
fca60d213e Merge pull request #2641 from garden-co/GCO-655-user-id-on-unauthorized
Expose current Account id in unauthorized error message
2025-07-14 11:12:49 +02:00
Matteo Manchi
b4fdab475b chore: add changeset 2025-07-14 11:03:59 +02:00
Matteo Manchi
958c122c36 chore(jazz-tools/tools): expose current user id in unauthorized error message 2025-07-12 21:01:44 +02:00
Margaret Culotta
5842838371 Add examples but remove twoslash because of limitations in how Twoslash parses advanced TypeScript types 2025-07-11 14:36:07 -05:00
Margaret Culotta
acd908fbc2 add docs for recursive connection 2025-07-11 10:41:50 -05:00
Margaret Culotta
4e61d1d191 add docs for recursive connections 2025-07-11 10:37:31 -05:00
Guido D'Orsi
9f6079b6c6 Merge pull request #2634 from garden-co/changeset-release/main
Version Packages
2025-07-10 16:14:58 +02:00
github-actions[bot]
4033d78fa6 Version Packages 2025-07-10 14:02:44 +00:00
Guido D'Orsi
83af94c850 Merge pull request #2575 from garden-co/feat/storage-api
feat: storage as load/store API
2025-07-10 16:00:33 +02:00
Guido D'Orsi
70fe856713 fix: don't wait for File streaming on SubscriptionScope 2025-07-10 14:54:00 +02:00
Guido D'Orsi
42e4afc42b test: add tests for markErrored 2025-07-10 14:35:51 +02:00
Guido D'Orsi
0e6797b222 chore: update lockfile 2025-07-10 14:28:10 +02:00
Guido D'Orsi
3634eaf8e9 chore: remove error logged on verify failures 2025-07-10 14:27:56 +02:00
Guido D'Orsi
58dfda3d0f Merge remote-tracking branch 'origin/main' into feat/storage-api 2025-07-10 14:16:16 +02:00
Guido D'Orsi
d304b0bcb5 Merge pull request #2622 from garden-co/feat/wait-for-streaming
feat: wait for the full streaming before return values in load and subscribe
2025-07-10 14:10:01 +02:00
Guido D'Orsi
44f5a3f5a2 test: add tests for loading/subscribing to large coValues 2025-07-10 14:06:20 +02:00
Sammii
ebb3ce1c25 Merge pull request #2623 from garden-co/feat/design-system-shadcn-integration
Feat/design system shadcn integration
2025-07-10 11:18:28 +01:00
Sammii
a67bba0dcf ensuring height consistency between buttons, inputs and dropdowns 2025-07-10 11:12:23 +01:00
Guido D'Orsi
4a72c26e42 chore: simplify toAddTransactions and tracking content from storage 2025-07-10 11:24:21 +02:00
Guido D'Orsi
084cb5936d perf: increase the ws messages batching to 5ms 2025-07-10 10:49:48 +02:00
Nico Rainhart
8a3be85e97 Merge pull request #2601 from garden-co/fix/chat-rn-example-not-running
Update pinned react version to 19.1.0
2025-07-09 09:44:49 -03:00
Brad Anderson
1a7f2b7379 fix: chat-rn build issues for android 2025-07-08 17:32:31 -04:00
Guido D'Orsi
caac82dffd chore: enable lazy load on ProjectScreen 2025-07-08 20:33:49 +02:00
Guido D'Orsi
27b48378e5 feat: wait for the full streaming before return values in load and subscribe 2025-07-08 20:19:24 +02:00
Guido D'Orsi
cfd3c3ca5c Merge remote-tracking branch 'origin/main' into feat/storage-api 2025-07-08 19:36:25 +02:00
Guido D'Orsi
41f26b7a4f chore: streamingTarget -> expectContentUntil 2025-07-08 19:18:37 +02:00
Guido D'Orsi
c57ebb1cea feat: add the streamingTarget information only on the first content message 2025-07-08 19:10:17 +02:00
Guido D'Orsi
259aded5cc chore: changeset 2025-07-08 19:10:12 +02:00
Guido D'Orsi
1f5e091dd7 Merge pull request #2602 from garden-co/fix/remove-storage-peers
feat: refactor Peer communication and schedule incoming messages on sync
2025-07-08 18:52:25 +02:00
Guido D'Orsi
bbb1c44977 fix: reduce delay on batch to 0 and add a config for incoming messages scheduling budget 2025-07-08 18:38:08 +02:00
Guido D'Orsi
4327ecbfdf Merge pull request #2619 from garden-co/changeset-release/main
Version Packages
2025-07-08 17:34:26 +02:00
Guido D'Orsi
114c10bc77 Merge pull request #2621 from garden-co/fix/invalid-signature
fix: fixes InvalidSignature errors that could happen during streaming
2025-07-08 17:33:18 +02:00
Guido D'Orsi
cecdf29721 test: add tests for invalid signatures coming from stale data updates 2025-07-08 17:32:23 +02:00
Sammii
bd717fc0d7 updating buttons for default default 2025-07-08 16:04:50 +01:00
Guido D'Orsi
739fff68b3 fix: fixes InvalidSignature errors that could happen during streaming 2025-07-08 16:50:30 +02:00
Sammii
d49cab0afa improving design systems integration with shadcn vars 2025-07-08 13:56:49 +01:00
Guido D'Orsi
ffebb4fdaf chore: remove console.log 2025-07-08 14:40:46 +02:00
github-actions[bot]
32565f0e53 Version Packages 2025-07-08 12:20:12 +00:00
Sammii
61a5889bea Merge pull request #2615 from garden-co/fix/team-update 2025-07-08 13:18:08 +01:00
Sammii
82bd3e1ea6 adding nico 2025-07-08 12:00:09 +01:00
Sammii
b800a6fba2 Merge pull request #2401 from garden-co/feat/snippet-improvements
Feat/snippet improvements
2025-07-08 11:54:03 +01:00
Sammii
1b6dbfdfff adjusting side nav item design 2025-07-08 11:45:29 +01:00
Sammii
061a70f1b3 responsive design for dropdown select 2025-07-08 11:45:18 +01:00
Sammii
f1c1e0dafd adding div's profile link 2025-07-08 10:43:54 +01:00
Guido D'Orsi
c3912fdb37 Merge pull request #2618 from garden-co/fix/inspector-element
fix: simplify definition of the AccountSchema type
2025-07-07 22:19:49 +02:00
Guido D'Orsi
356bfa4860 docs: add jsDoc for coAccountDefiner 2025-07-07 19:50:41 +02:00
Guido D'Orsi
38446668c4 fix: simplify definition of the AccountSchema type 2025-07-07 19:44:21 +02:00
Brad Anderson
e2bb3b8015 fix: chat-rn-expo works, canary bump 2025-07-07 12:33:12 -04:00
Guido D'Orsi
11dcfd703d Merge pull request #2616 from garden-co/changeset-release/main
Version Packages
2025-07-07 18:16:58 +02:00
Brad Anderson
0b09d23bd1 fix: chat-rn works w properly-hoisted RN dep 2025-07-07 12:09:16 -04:00
github-actions[bot]
879b726537 Version Packages 2025-07-07 16:06:32 +00:00
Guido D'Orsi
66bbd03262 Merge pull request #2614 from garden-co/fix/inspector-element
fix: react bundling in jazz-tools/inspector/register-custom-element
2025-07-07 18:04:30 +02:00
Guido D'Orsi
c09b63698f fix: react bundling in jazz-tools/inspector/register-custom-element 2025-07-07 18:03:18 +02:00
Sammii
bed7db0a33 team page updates 2025-07-07 16:51:22 +01:00
NicoR
8ff3e234c1 Upgrade examples' expo version to 54.0.0-canary 2025-07-07 12:44:21 -03:00
Sammii
296da5a5c4 design amends 2025-07-07 16:40:30 +01:00
Guido D'Orsi
700a4f1ba1 fix: restore sync url in todo main 2025-07-07 16:46:18 +02:00
Guido D'Orsi
6f6663d825 test: cover ws.terminate 2025-07-07 16:28:46 +02:00
Guido D'Orsi
844cdc907f Merge pull request #2612 from garden-co/chore/playwright-tests
perf(ci): batch the e2e tests execution in 2 workflow runs
2025-07-07 16:02:45 +02:00
Guido D'Orsi
9e32d4cb92 perf(ci): batch the e2e tests execution in 2 workflow runs 2025-07-07 16:01:12 +02:00
Guido D'Orsi
85dc6ba148 feat: add metrics on incoming messages and storage streaming operations 2025-07-07 15:41:33 +02:00
Sammii
16c4d27e00 code tidy 2025-07-07 14:21:24 +01:00
Sammii
69170fe0e0 style amendments 2025-07-07 14:14:28 +01:00
Sammii
a646ba54b3 component refactor 2025-07-07 14:14:16 +01:00
Sammii
45d60fc3c8 get started snippet select improvements 2025-07-07 14:02:22 +01:00
Sammii
6f0c399ccd Merge branch 'main' into feat/snippet-improvements 2025-07-07 13:50:59 +01:00
Guido D'Orsi
40e1ca7cb1 Merge pull request #2606 from garden-co/changeset-release/main
Version Packages
2025-07-07 11:30:02 +02:00
github-actions[bot]
80cf21e453 Version Packages 2025-07-07 09:27:27 +00:00
Guido D'Orsi
48c8a3d219 Merge pull request #2603 from garden-co/PR-template-v2-simplify
simplify PR template for ease of use
2025-07-07 11:25:24 +02:00
Guido D'Orsi
31bb1201fc Merge pull request #2611 from garden-co/gio/update-lockfile
chore: update lockfile
2025-07-07 11:15:25 +02:00
Giordano Ricci
08d1b05607 chore: update lockfile 2025-07-07 09:47:51 +01:00
Guido D'Orsi
d64a14210d Merge pull request #2608 from jeffgca/main
Error: Loading PostCSS Plugin failed: Cannot find module '@tailwindcss/postcss'
2025-07-07 10:35:06 +02:00
Guido D'Orsi
7e53d33e9b Merge pull request #2609 from jeffgca/user_age_calc_fix
User age calc fix
2025-07-07 10:34:31 +02:00
Jeff Griffiths
ea2b39cc30 fixed off-by-one error 2025-07-04 21:27:54 -07:00
Jeff Griffiths
6b835f95cf enhanced getUserAge to calculate the user's age in a more precise way. 2025-07-04 21:14:32 -07:00
Jeff Griffiths
a229ae5f70 changed postcss dependency to the tailwind plugin instead. 2025-07-04 20:49:46 -07:00
Giordano Ricci
84fdc1d8fd Merge pull request #2605 from garden-co/gio/cancel-pending-workflows-on-push 2025-07-04 17:19:27 +01:00
Guido D'Orsi
9b1d52d183 chore: document IncomingMessagesQueue 2025-07-04 18:15:08 +02:00
Giordano Ricci
14a8b32522 differentiate workflows 2025-07-04 17:10:44 +01:00
Guido D'Orsi
ddc09a0d6b Merge pull request #2604 from garden-co/gio/get-only-direct-members
feat: allow to get only the direct members of a group
2025-07-04 18:09:55 +02:00
Giordano Ricci
3b45a3f2fd chore: cancel pending workflows on push 2025-07-04 17:05:45 +01:00
Giordano Ricci
9034a45da0 forgot the role 2025-07-04 16:51:07 +01:00
Guido D'Orsi
6247fac6c5 Merge remote-tracking branch 'origin/feat/storage-api' into fix/remove-storage-peers 2025-07-04 17:49:17 +02:00
Giordano Ricci
a5ceaffb0c changeset, usemethod instead of getter, reuse logic 2025-07-04 16:47:19 +01:00
Giordano Ricci
dcee2f9b4e better test 2025-07-04 16:18:53 +01:00
Guido D'Orsi
f27a2c541e chore: cleanup code and add tests 2025-07-04 17:18:17 +02:00
Giordano Ricci
83fdc504ff feat: add directMembers get to get only the direct members of a given group 2025-07-04 16:07:30 +01:00
Guido D'Orsi
2317a23fd4 chore: refactor createWebSocketPeer 2025-07-04 16:26:08 +02:00
Guido D'Orsi
a34c0675cd Merge pull request #2599 from garden-co/changeset-release/main
Version Packages
2025-07-04 14:36:26 +02:00
Margaret Culotta
5a8a62b4a3 simplify PR template for ease of use 2025-07-02 13:23:56 -05:00
github-actions[bot]
325a554bd1 Version Packages 2025-07-02 17:07:22 +00:00
Guido D'Orsi
7422943e83 Merge pull request #2600 from garden-co/fix/react-native-peer-dependencies
Make all React Native deps in `jazz-tools` optional peer dependencies
2025-07-02 19:05:22 +02:00
NicoR
23bfea5861 Add changeset 2025-07-02 13:50:45 -03:00
NicoR
605a54eb11 Make react-native-fast-encoder an optional peer dependency 2025-07-02 13:49:04 -03:00
Brad Anderson
a7aaee51e6 Merge pull request #2587 from garden-co/feat/rn-betterauth
feat: add RN BetterAuth
2025-07-02 12:24:38 -04:00
Brad Anderson
4b8983858a chore: changeset 2025-07-02 12:07:25 -04:00
Brad Anderson
8a8c4d11e1 fix: small cleanup 2025-07-02 11:56:40 -04:00
Guido D'Orsi
26994684d7 feat: refactor Peer communication and schedule incoming messages on sync 2025-07-02 17:45:28 +02:00
NicoR
14a5e036a4 Update homepage to react 19.1.0 2025-07-02 11:03:40 -03:00
NicoR
5b1c1ca522 Update all examples to react 19.1.0 2025-07-02 10:44:31 -03:00
NicoR
a9c8458c51 Update chat-rn's Podfile.lock 2025-07-02 09:38:56 -03:00
NicoR
5f31d6cbe1 Update pinned react version 2025-07-02 09:38:38 -03:00
Guido D'Orsi
b774bb345d chore: changeset 2025-07-02 10:52:44 +02:00
Guido D'Orsi
7fd891d7b9 chore: fix formatting 2025-07-02 10:52:10 +02:00
Guido D'Orsi
27cac4a6d7 Merge pull request #2596 from satendra03/main
fix #1914
2025-07-02 10:51:23 +02:00
Brad Anderson
2b71ef1181 fix: PR feedback 2025-07-01 21:46:25 -04:00
NicoR
ae169c7b3a Revert change for react-native-fast-encoder 2025-07-01 13:57:17 -03:00
NicoR
d888c99d9a Add expo-sqlite dependency to Expo Project setup docs 2025-07-01 13:53:01 -03:00
NicoR
0b54917f19 Make all React Native deps in jazz-tools optional peer dependencies 2025-07-01 12:21:38 -03:00
Guido D'Orsi
c87b215b75 Merge pull request #2594 from garden-co/fix-RNQuickCrypto-type-error
fix: `RNQuickCrypto` type error
2025-07-01 17:15:01 +02:00
NicoR
e4ba23cbef Add changeset 2025-07-01 12:13:40 -03:00
Brad Anderson
98c005a6e0 feat: more RN BetterAuth 2025-07-01 08:49:13 -04:00
Guido D'Orsi
477fd8a62d feat: simple backpressure for sync storage 2025-07-01 12:55:56 +02:00
Guido D'Orsi
90999ee709 Merge pull request #2593 from garden-co/fix/remove-storage-peers
chore: remove storage peer
2025-07-01 12:48:34 +02:00
Guido D'Orsi
38065f0cdf Merge pull request #2590 from garden-co/fix/storage-streaming
fix: server subscription when streaming from storage
2025-07-01 12:47:52 +02:00
Guido D'Orsi
c77d16cdb3 chore: cleanup code 2025-07-01 12:31:24 +02:00
Guido D'Orsi
9410084e6a chore: cleanup code
Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
2025-07-01 12:30:08 +02:00
Guido D'Orsi
8528db4de4 Merge pull request #2595 from garden-co/fix/make-jazz-tools-rn-deps-peer-dependencies
fix: make `react-native-nitro-modules` and `react-native-quick-crypto` optional peer dependencies
2025-07-01 11:30:14 +02:00
satendra03
e0fe5a20b7 fix #1914 2025-07-01 04:21:26 +05:30
NicoR
e16e4d53d1 Keep file extension in relative import 2025-06-30 16:52:09 -03:00
NicoR
d904fae506 fix: make react-native-quick-crypto an optional peer dependency 2025-06-30 15:44:04 -03:00
NicoR
f67c0b3db3 fix: make react-native-nitro-modules an optional peer dependency 2025-06-30 15:43:30 -03:00
NicoR
283d7c6bf0 fix: RNQuickCrypto type error 2025-06-30 15:13:10 -03:00
Guido D'Orsi
e67c5838a9 chore: remove storage peer 2025-06-30 18:15:11 +02:00
Guido D'Orsi
0e7a7dbbc0 Merge pull request #2591 from garden-co/fix/ci-e2e-exit-code
fix(ci): listen to e2e-rn-test's exit code
2025-06-30 17:53:34 +02:00
Matteo Manchi
63c69b6b95 fix(ci): listen to e2e-rn-test's exit code 2025-06-30 17:41:57 +02:00
Guido D'Orsi
a141cbc7f7 chore: rename back to AvailableCoValueCore to facilitate review 2025-06-30 16:40:37 +02:00
Guido D'Orsi
6a5352cf3a test: remove TODO on browser integration tests 2025-06-30 15:31:38 +02:00
Guido D'Orsi
27762637ee fix: streaming from storage now correctly send the target known state in the load requests 2025-06-30 15:28:08 +02:00
Guido D'Orsi
dcebe34891 chore: remove unused id param from storage.store 2025-06-27 19:59:56 +02:00
Guido D'Orsi
99d510815f Merge remote-tracking branch 'origin/main' into feat/storage-api 2025-06-27 19:40:12 +02:00
Guido D'Orsi
928962c08b chore: add comments and improve tests 2025-06-27 19:37:43 +02:00
Guido D'Orsi
cdadd6db1d chore: revert CoJsonIDBTransaction 2025-06-27 18:58:36 +02:00
Guido D'Orsi
d45b8ae70b feat: improve the loading and add content streaming tests 2025-06-27 18:57:45 +02:00
Guido D'Orsi
445a58c864 chore: centralize loadFromStorage logic 2025-06-27 18:29:05 +02:00
Guido D'Orsi
1895b474ea Merge remote-tracking branch 'origin/main' into feat/storage-api 2025-06-27 18:15:49 +02:00
Guido D'Orsi
8990ff39a5 fix: initialize async sqlite db 2025-06-27 17:26:52 +02:00
Guido D'Orsi
71e4c97255 chore: update lockfile 2025-06-27 17:21:56 +02:00
Guido D'Orsi
577e960e28 Merge remote-tracking branch 'origin/main' into feat/storage-api 2025-06-27 17:19:07 +02:00
Guido D'Orsi
f232f75d40 feat: performance testing app 2025-06-27 17:18:57 +02:00
Guido D'Orsi
e1a7f829b4 feat: move storage inside cojson and add more tests 2025-06-26 18:18:30 +02:00
Guido D'Orsi
f82177b9da feat: indexed db 2025-06-25 15:26:08 +02:00
Guido D'Orsi
c1c553bad0 feat: storage as load/store API 2025-06-25 11:53:14 +02:00
Sammii
588ea02f63 refactoring Framework Select 2025-06-24 16:09:01 +01:00
Sammii
ddc69f2268 apply track on copy click to new snippet select component 2025-06-24 11:56:39 +01:00
Sammii
7c62689319 Merge branch 'main' into feat/snippet-improvements 2025-06-24 11:52:03 +01:00
Sammii
df7011167c making active dropdown item text primary 2025-06-02 17:14:45 +01:00
Sammii
28a785acb0 letting dropdown items be editable 2025-06-02 17:10:57 +01:00
Sammii
3ee557bfbe adding routerPush prop to framework select 2025-06-02 17:10:30 +01:00
Sammii
af94255166 updating HeroSection 2025-06-02 16:50:12 +01:00
Sammii
4a0dea3f75 create NpxCreateJazzApp.mdx 2025-06-02 16:49:46 +01:00
Sammii
6a42bc9655 creating GetStartedSnippetSelect component 2025-06-02 16:49:17 +01:00
Sammii
c6c8a7f6b7 amending Framework select 2025-06-02 16:48:41 +01:00
Sammii
133dd0e26d make dropdown classes last so you can edit them 2025-06-02 16:43:07 +01:00
Sammii
815339272f alter Feature Card styling 2025-06-02 14:47:30 +01:00
Sammii
9c1f340029 add new size to code group and amend copy button to be icon only on small 2025-06-02 13:54:29 +01:00
Sammii
b72ea9608d add new icon for clipboard success 2025-06-02 13:54:09 +01:00
225 changed files with 9917 additions and 8115 deletions

View File

@@ -6,7 +6,6 @@
"fixed": [
[
"cojson",
"cojson-storage",
"cojson-storage-indexeddb",
"cojson-storage-sqlite",
"cojson-transport-ws",

View File

@@ -1,24 +1,23 @@
### What this Does
Brief summary of the change, ideally framed in user or product terms.
# Description
<!-- Please include a summary of the change and which issue is fixed -->
<!-- Please also include relevant motivation and context -->
<!-- Include any links to documentation like RFCs if necessary -->
<!-- Add a link to to relevant preview environments or anything that would simplify visual review process -->
<!-- Supplemental screenshots and video are encouraged, but the primary description should be in text -->
### Why Are We Doing This?
Link to the shaped pitch or explain what problem it solves.
## Manual testing instructions
### Scope / Boundaries
Includes:
- [x] Core feature functionality
- [x] Tests or validation steps
<!-- Add any actions required to manually test the changes -->
Do NOT include:
- [ ] Related stretch features or follow-ups
## Tests
### Testing Instructions
How a reviewer or QA can verify behavior, offer step-by-step instructions if possible. Screenshots or recordings are welcome.
- [ ] Tests have been added and/or updated
- [ ] Tests have not been updated, because: <!-- Insert reason for not updating tests here -->
- [ ] I need help with writing tests
### Known Issues / Open Questions (if any)
- [ ] Note anything youd like review on or decided async
### Related Links
- GitHub issue
- Linear pitch
- Design links or references
## Checklist
- [ ] I've updated the part of the docs that are affected the PR changes
- [ ] I've generated a changeset, if a version bump is required
- [ ] I've updated the jsDoc comments to the public APIs I've modified, or added them when missing

View File

@@ -1,5 +1,11 @@
name: Code quality
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
branches:

View File

@@ -1,5 +1,11 @@
name: End-to-End Tests for React Native
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened]
@@ -61,7 +67,7 @@ jobs:
disable-animations: true
working-directory: ./examples/chat-rn-expo/
# killall due to this issue: https://github.com/ReactiveCircus/android-emulator-runner/issues/385
script: ./test/e2e/run.sh && killall -INT crashpad_handler || true
script: ./test/e2e/run.sh && ( killall -INT crashpad_handler || true )
- name: Copy Maestro Output
if: steps.e2e_test.outcome != 'success'

View File

@@ -1,5 +1,11 @@
name: Jazz Run Tests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
branches: ["main"]

View File

@@ -1,46 +0,0 @@
name: Playwright Tests
on:
push:
branches: ["main"]
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
timeout-minutes: 60
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Install root dependencies
run: pnpm install && pnpm exec turbo build --filter="./packages/*"
- name: Install project dependencies
run: pnpm install
working-directory: ./homepage/homepage
- name: Pnpm Build
run: pnpm exec turbo build
working-directory: ./homepage/homepage
- name: Install Playwright Browsers
run: pnpm exec playwright install
working-directory: ./homepage/homepage
- name: Run Playwright tests
run: pnpm exec playwright test
working-directory: ./homepage/homepage
- uses: actions/upload-artifact@v4
if: failure()
with:
name: homepage-playwright-report
path: ./homepage/homepage/playwright-report/
retention-days: 30

View File

@@ -1,5 +1,11 @@
name: Playwright Tests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
push:
branches: ["main"]
@@ -13,21 +19,7 @@ jobs:
continue-on-error: true
strategy:
matrix:
project: [
"tests/e2e",
"examples/chat",
"examples/chat-svelte",
"examples/clerk",
"examples/betterauth",
"examples/file-share-svelte",
"examples/form",
"examples/inspector",
"examples/music-player",
"examples/organization",
"starters/react-passkey-auth",
"starters/svelte-passkey-auth",
"tests/jazz-svelte"
]
shard: ["1/2", "2/2"]
steps:
- uses: actions/checkout@v4
@@ -37,25 +29,129 @@ jobs:
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: |
if [ -f .env.test ]; then
cp .env.test .env
fi
pnpm turbo build
working-directory: ./${{ matrix.project }}
- name: Install Playwright Browsers
run: pnpm exec playwright install
working-directory: ./${{ matrix.project }}
- name: Run Playwright tests
run: pnpm exec playwright test
working-directory: ./${{ matrix.project }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ hashFiles(format('{0}/package.json', matrix.project)) }}-playwright-report
path: ./${{ matrix.project }}/playwright-report/
retention-days: 30
- name: Run Playwright tests for shard ${{ matrix.shard }}
run: |
# Parse shard information (e.g., "1/2" -> shard_num=1, total_shards=2)
IFS='/' read -r shard_num total_shards <<< "${{ matrix.shard }}"
shard_index=$((shard_num - 1)) # Convert to 0-based index
# Debug: Print parsed values
echo "Parsed shard_num: $shard_num"
echo "Parsed total_shards: $total_shards"
echo "Calculated shard_index: $shard_index"
# Define all projects to test
all_projects=(
"tests/e2e"
"examples/chat"
"examples/chat-svelte"
"examples/clerk"
"examples/betterauth"
"examples/file-share-svelte"
"examples/form"
"examples/inspector"
"examples/music-player"
"examples/organization"
"starters/react-passkey-auth"
"starters/svelte-passkey-auth"
"tests/jazz-svelte"
)
# Calculate which projects this shard should run
shard_projects=()
for i in "${!all_projects[@]}"; do
if [ $((i % total_shards)) -eq $shard_index ]; then
shard_projects+=("${all_projects[i]}")
fi
done
# Track project results
overall_exit_code=0
failed_projects=()
passed_projects=()
echo "=== Running tests for shard ${{ matrix.shard }} ==="
echo "Projects in this shard:"
printf '%s\n' "${shard_projects[@]}"
echo
# Run tests for each project
for project in "${shard_projects[@]}"; do
echo "=== Testing project: $project ==="
# Check if project directory exists
if [ ! -d "$project" ]; then
echo "❌ FAILED: Project directory $project does not exist"
failed_projects+=("$project (directory not found)")
overall_exit_code=1
continue
fi
# Check if project has package.json
if [ ! -f "$project/package.json" ]; then
echo "❌ FAILED: No package.json found in $project"
failed_projects+=("$project (no package.json)")
overall_exit_code=1
continue
fi
# Build the project
echo "🔨 Building $project..."
cd "$project"
if [ -f .env.test ]; then
cp .env.test .env
fi
if ! pnpm turbo build; then
echo "❌ BUILD FAILED: $project"
failed_projects+=("$project (build failed)")
overall_exit_code=1
cd - > /dev/null
continue
fi
# Run Playwright tests
echo "🧪 Running Playwright tests for $project..."
if ! pnpm exec playwright test; then
echo "❌ TESTS FAILED: $project"
failed_projects+=("$project (tests failed)")
overall_exit_code=1
else
echo "✅ TESTS PASSED: $project"
passed_projects+=("$project")
fi
cd - > /dev/null
echo "=== Finished testing $project ==="
echo
done
# Print summary report
echo "=========================================="
echo "📊 TEST SUMMARY FOR SHARD ${{ matrix.shard }}"
echo "=========================================="
if [ ${#passed_projects[@]} -gt 0 ]; then
echo "✅ PASSED (${#passed_projects[@]}):"
printf ' - %s\n' "${passed_projects[@]}"
echo
fi
if [ ${#failed_projects[@]} -gt 0 ]; then
echo "❌ FAILED (${#failed_projects[@]}):"
printf ' - %s\n' "${failed_projects[@]}"
echo
fi
echo "Total projects in shard: ${#shard_projects[@]}"
echo "Passed: ${#passed_projects[@]}"
echo "Failed: ${#failed_projects[@]}"
echo "=========================================="
# Exit with overall status
exit $overall_exit_code

View File

@@ -1,4 +1,11 @@
name: Pre-Publish tagged Pull Requests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened, labeled]

View File

@@ -1,5 +1,11 @@
name: Unit Tests
concurrency:
# For pushes, this lets concurrent runs happen, so each push gets a result.
# But for other events (e.g. PRs), we can cancel the previous runs.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, synchronize, reopened]

View File

@@ -56,7 +56,7 @@
}
},
{
"include": ["packages/cojson-storage*/**", "cojson-transport-ws/**"],
"include": ["packages/cojson/src/storage/*/**", "cojson-transport-ws/**"],
"linter": {
"enabled": true,
"rules": {

View File

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

View File

@@ -11,11 +11,11 @@ react {
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
reactNativeDir = file("../../../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
codegenDir = file("../../../../node_modules/@react-native/codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
// cliFile = file("../../node_modules/react-native/cli.js")
cliFile = file("../../../../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to

View File

@@ -1,6 +1,6 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'ChatRN'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
includeBuild('../../../node_modules/@react-native/gradle-plugin')

View File

@@ -380,7 +380,7 @@
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
USE_HERMES = true;
@@ -452,7 +452,7 @@
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;

View File

@@ -2370,87 +2370,87 @@ PODS:
- Yoga (0.0.0)
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
- FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`)
- fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`)
- glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- "op-sqlite (from `../../../node_modules/@op-engineering/op-sqlite`)"
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
- React-Core (from `../node_modules/react-native/`)
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
- React-debug (from `../node_modules/react-native/ReactCommon/react/debug`)
- React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
- React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
- React-Fabric (from `../node_modules/react-native/ReactCommon`)
- React-FabricComponents (from `../node_modules/react-native/ReactCommon`)
- React-FabricImage (from `../node_modules/react-native/ReactCommon`)
- React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`)
- React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
- React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`)
- React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
- React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
- React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
- React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`)
- React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
- React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
- React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
- React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`)
- React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../../../node_modules/react-native/Libraries/Required`)
- RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../../../node_modules/react-native/`)
- React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`)
- React-Core (from `../../../node_modules/react-native/`)
- React-Core/RCTWebSocket (from `../../../node_modules/react-native/`)
- React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`)
- React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`)
- React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
- React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
- React-Fabric (from `../../../node_modules/react-native/ReactCommon`)
- React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`)
- React-FabricImage (from `../../../node_modules/react-native/ReactCommon`)
- React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`)
- React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
- React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`)
- React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`)
- React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
- React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
- React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`)
- React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`)
- React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)
- React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`)
- React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
- React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`)
- React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`)
- React-logger (from `../../../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`)
- react-native-mmkv (from `../../../node_modules/react-native-mmkv`)
- "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
- React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`)
- React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
- React-RCTFabric (from `../node_modules/react-native/React`)
- React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`)
- React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
- React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
- React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`)
- React-RCTRuntime (from `../node_modules/react-native/React/Runtime`)
- React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
- React-RCTText (from `../node_modules/react-native/Libraries/Text`)
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`)
- React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`)
- React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`)
- React-rncore (from `../node_modules/react-native/ReactCommon`)
- React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
- React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`)
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
- React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`)
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-timing (from `../node_modules/react-native/ReactCommon/react/timing`)
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`)
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
- React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`)
- React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`)
- React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`)
- React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`)
- React-RCTFabric (from `../../../node_modules/react-native/React`)
- React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`)
- React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`)
- React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`)
- React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`)
- React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`)
- React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`)
- React-RCTText (from `../../../node_modules/react-native/Libraries/Text`)
- React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`)
- React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`)
- React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`)
- React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`)
- React-rncore (from `../../../node_modules/react-native/ReactCommon`)
- React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
- React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
- React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`)
- React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`)
- React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`)
- React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`)
- ReactAppDependencyProvider (from `build/generated/ios`)
- ReactCodegen (from `build/generated/ios`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`)
- "RNCClipboard (from `../../../node_modules/@react-native-clipboard/clipboard`)"
- RNScreens (from `../node_modules/react-native-screens`)
- RNScreens (from `../../../node_modules/react-native-screens`)
- SocketRocket (~> 0.7.1)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
trunk:
@@ -2458,88 +2458,88 @@ SPEC REPOS:
EXTERNAL SOURCES:
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
:podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
:podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
fast_float:
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
:podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec"
FBLazyVector:
:path: "../node_modules/react-native/Libraries/FBLazyVector"
:path: "../../../node_modules/react-native/Libraries/FBLazyVector"
fmt:
:podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
:podspec: "../../../node_modules/react-native/third-party-podspecs/fmt.podspec"
glog:
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
:podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec"
hermes-engine:
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:tag: hermes-2025-05-06-RNv0.80.0-4eb6132a5bf0450bf4c6c91987675381d7ac8bca
op-sqlite:
:path: "../../../node_modules/@op-engineering/op-sqlite"
RCT-Folly:
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
:podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTDeprecation:
:path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
:path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
RCTRequired:
:path: "../node_modules/react-native/Libraries/Required"
:path: "../../../node_modules/react-native/Libraries/Required"
RCTTypeSafety:
:path: "../node_modules/react-native/Libraries/TypeSafety"
:path: "../../../node_modules/react-native/Libraries/TypeSafety"
React:
:path: "../node_modules/react-native/"
:path: "../../../node_modules/react-native/"
React-callinvoker:
:path: "../node_modules/react-native/ReactCommon/callinvoker"
:path: "../../../node_modules/react-native/ReactCommon/callinvoker"
React-Core:
:path: "../node_modules/react-native/"
:path: "../../../node_modules/react-native/"
React-CoreModules:
:path: "../node_modules/react-native/React/CoreModules"
:path: "../../../node_modules/react-native/React/CoreModules"
React-cxxreact:
:path: "../node_modules/react-native/ReactCommon/cxxreact"
:path: "../../../node_modules/react-native/ReactCommon/cxxreact"
React-debug:
:path: "../node_modules/react-native/ReactCommon/react/debug"
:path: "../../../node_modules/react-native/ReactCommon/react/debug"
React-defaultsnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
React-domnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom"
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom"
React-Fabric:
:path: "../node_modules/react-native/ReactCommon"
:path: "../../../node_modules/react-native/ReactCommon"
React-FabricComponents:
:path: "../node_modules/react-native/ReactCommon"
:path: "../../../node_modules/react-native/ReactCommon"
React-FabricImage:
:path: "../node_modules/react-native/ReactCommon"
:path: "../../../node_modules/react-native/ReactCommon"
React-featureflags:
:path: "../node_modules/react-native/ReactCommon/react/featureflags"
:path: "../../../node_modules/react-native/ReactCommon/react/featureflags"
React-featureflagsnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
React-graphics:
:path: "../node_modules/react-native/ReactCommon/react/renderer/graphics"
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics"
React-hermes:
:path: "../node_modules/react-native/ReactCommon/hermes"
:path: "../../../node_modules/react-native/ReactCommon/hermes"
React-idlecallbacksnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
React-ImageManager:
:path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
React-jserrorhandler:
:path: "../node_modules/react-native/ReactCommon/jserrorhandler"
:path: "../../../node_modules/react-native/ReactCommon/jserrorhandler"
React-jsi:
:path: "../node_modules/react-native/ReactCommon/jsi"
:path: "../../../node_modules/react-native/ReactCommon/jsi"
React-jsiexecutor:
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
:path: "../../../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern"
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern"
React-jsinspectorcdp:
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
React-jsinspectornetwork:
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network"
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network"
React-jsinspectortracing:
:path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
:path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
React-jsitooling:
:path: "../node_modules/react-native/ReactCommon/jsitooling"
:path: "../../../node_modules/react-native/ReactCommon/jsitooling"
React-jsitracing:
:path: "../node_modules/react-native/ReactCommon/hermes/executor/"
:path: "../../../node_modules/react-native/ReactCommon/hermes/executor/"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
:path: "../../../node_modules/react-native/ReactCommon/logger"
React-Mapbuffer:
:path: "../node_modules/react-native/ReactCommon"
:path: "../../../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-get-random-values:
:path: "../../../node_modules/react-native-get-random-values"
react-native-mmkv:
@@ -2547,75 +2547,75 @@ EXTERNAL SOURCES:
react-native-netinfo:
:path: "../../../node_modules/@react-native-community/netinfo"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
:path: "../../../node_modules/react-native-safe-area-context"
React-NativeModulesApple:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-oscompat:
:path: "../node_modules/react-native/ReactCommon/oscompat"
:path: "../../../node_modules/react-native/ReactCommon/oscompat"
React-perflogger:
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
:path: "../../../node_modules/react-native/ReactCommon/reactperflogger"
React-performancetimeline:
:path: "../node_modules/react-native/ReactCommon/react/performance/timeline"
:path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline"
React-RCTActionSheet:
:path: "../node_modules/react-native/Libraries/ActionSheetIOS"
:path: "../../../node_modules/react-native/Libraries/ActionSheetIOS"
React-RCTAnimation:
:path: "../node_modules/react-native/Libraries/NativeAnimation"
:path: "../../../node_modules/react-native/Libraries/NativeAnimation"
React-RCTAppDelegate:
:path: "../node_modules/react-native/Libraries/AppDelegate"
:path: "../../../node_modules/react-native/Libraries/AppDelegate"
React-RCTBlob:
:path: "../node_modules/react-native/Libraries/Blob"
:path: "../../../node_modules/react-native/Libraries/Blob"
React-RCTFabric:
:path: "../node_modules/react-native/React"
:path: "../../../node_modules/react-native/React"
React-RCTFBReactNativeSpec:
:path: "../node_modules/react-native/React"
:path: "../../../node_modules/react-native/React"
React-RCTImage:
:path: "../node_modules/react-native/Libraries/Image"
:path: "../../../node_modules/react-native/Libraries/Image"
React-RCTLinking:
:path: "../node_modules/react-native/Libraries/LinkingIOS"
:path: "../../../node_modules/react-native/Libraries/LinkingIOS"
React-RCTNetwork:
:path: "../node_modules/react-native/Libraries/Network"
:path: "../../../node_modules/react-native/Libraries/Network"
React-RCTRuntime:
:path: "../node_modules/react-native/React/Runtime"
:path: "../../../node_modules/react-native/React/Runtime"
React-RCTSettings:
:path: "../node_modules/react-native/Libraries/Settings"
:path: "../../../node_modules/react-native/Libraries/Settings"
React-RCTText:
:path: "../node_modules/react-native/Libraries/Text"
:path: "../../../node_modules/react-native/Libraries/Text"
React-RCTVibration:
:path: "../node_modules/react-native/Libraries/Vibration"
:path: "../../../node_modules/react-native/Libraries/Vibration"
React-rendererconsistency:
:path: "../node_modules/react-native/ReactCommon/react/renderer/consistency"
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency"
React-renderercss:
:path: "../node_modules/react-native/ReactCommon/react/renderer/css"
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/css"
React-rendererdebug:
:path: "../node_modules/react-native/ReactCommon/react/renderer/debug"
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug"
React-rncore:
:path: "../node_modules/react-native/ReactCommon"
:path: "../../../node_modules/react-native/ReactCommon"
React-RuntimeApple:
:path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
:path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
React-RuntimeCore:
:path: "../node_modules/react-native/ReactCommon/react/runtime"
:path: "../../../node_modules/react-native/ReactCommon/react/runtime"
React-runtimeexecutor:
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
:path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor"
React-RuntimeHermes:
:path: "../node_modules/react-native/ReactCommon/react/runtime"
:path: "../../../node_modules/react-native/ReactCommon/react/runtime"
React-runtimescheduler:
:path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
:path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
React-timing:
:path: "../node_modules/react-native/ReactCommon/react/timing"
:path: "../../../node_modules/react-native/ReactCommon/react/timing"
React-utils:
:path: "../node_modules/react-native/ReactCommon/react/utils"
:path: "../../../node_modules/react-native/ReactCommon/react/utils"
ReactAppDependencyProvider:
:path: build/generated/ios
ReactCodegen:
:path: build/generated/ios
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
:path: "../../../node_modules/react-native/ReactCommon"
RNCClipboard:
:path: "../../../node_modules/@react-native-clipboard/clipboard"
RNScreens:
:path: "../node_modules/react-native-screens"
:path: "../../../node_modules/react-native-screens"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
:path: "../../../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
@@ -2692,7 +2692,7 @@ SPEC CHECKSUMS:
React-timing: a275a1c2e6112dba17f8f7dd496d439213bbea0d
React-utils: 449a6e1fd53886510e284e80bdbb1b1c6db29452
ReactAppDependencyProvider: 3267432b637c9b38e86961b287f784ee1b08dde0
ReactCodegen: 5d41e1df061200130dd326e55cdfdf94b0289c6e
ReactCodegen: d82f538f70f00484d418803f74b5a0ea09cc8689
ReactCommon: b028d09a66e60ebd83ca59d8cc9a1216360db147
RNCClipboard: 54ff19965d7c816febbafe5f520c2c3e7b677a49
RNScreens: ee2abe7e0c548eed14e92742e81ed991165c56aa

View File

@@ -13,7 +13,7 @@
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bacons/text-decoder": "0.0.0",
"@op-engineering/op-sqlite": "14.1.0",
"@react-native-clipboard/clipboard": "1.16.2",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/native": "7.1.14",
"@react-navigation/native-stack": "7.3.19",
@@ -40,7 +40,7 @@
"@react-native/typescript-config": "0.80.0",
"@rnx-kit/metro-config": "^2.0.1",
"@rnx-kit/metro-resolver-symlinks": "^0.2.5",
"@types/react": "19.1.0",
"@types/react": "^19.1.0",
"eslint": "^8.19.0",
"pod-install": "^0.3.5",
"prettier": "2.8.8",

View File

@@ -1,5 +1,50 @@
# passkey-svelte
## 0.0.99
### Patch Changes
- Updated dependencies [9815ec6]
- Updated dependencies [b4fdab4]
- jazz-tools@0.15.10
## 0.0.98
### Patch Changes
- Updated dependencies [27b4837]
- jazz-tools@0.15.9
## 0.0.97
### Patch Changes
- Updated dependencies [3844666]
- jazz-tools@0.15.8
## 0.0.96
### Patch Changes
- Updated dependencies [c09b636]
- jazz-tools@0.15.7
## 0.0.95
### Patch Changes
- Updated dependencies [a5ceaff]
- jazz-tools@0.15.6
## 0.0.94
### Patch Changes
- Updated dependencies [23bfea5]
- Updated dependencies [e4ba23c]
- Updated dependencies [4b89838]
- jazz-tools@0.15.5
## 0.0.93
### Patch Changes

View File

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

View File

@@ -16,15 +16,15 @@
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "3.25.28"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"is-ci": "^3.0.1",
"postcss": "^8.4.40",
@@ -32,4 +32,4 @@
"typescript": "5.6.2",
"vite": "^6.3.5"
}
}
}

View File

@@ -14,15 +14,15 @@
"@bam.tech/react-native-image-resizer": "^3.0.11",
"@clerk/clerk-expo": "^2.13.1",
"@react-native-community/netinfo": "11.4.1",
"expo": "~53.0.9",
"expo": "54.0.0-canary-20250701-6a945c5",
"expo-crypto": "~14.1.5",
"expo-linking": "~7.1.5",
"expo-secure-store": "~14.2.3",
"expo-sqlite": "~15.2.10",
"expo-web-browser": "~14.2.0",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-native": "0.79.2",
"react": "19.1.0",
"react-native": "0.80.0",
"react-native-get-random-values": "^1.11.0",
"readable-stream": "^4.7.0"
},
@@ -32,4 +32,4 @@
"typescript": "~5.8.3"
},
"private": true
}
}

View File

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

View File

@@ -11,14 +11,14 @@
},
"dependencies": {
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",

View File

@@ -12,16 +12,16 @@
"dependencies": {
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@playwright/test": "^1.50.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",

View File

@@ -11,14 +11,14 @@
},
"dependencies": {
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "5.6.2",

View File

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

View File

@@ -10,8 +10,8 @@
"dependencies": {
"jazz-tools": "workspace:*",
"next": "15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.10",
@@ -21,4 +21,4 @@
"tailwindcss": "^4.1.10",
"typescript": "^5"
}
}
}

View File

@@ -24,15 +24,15 @@
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.485.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.17",
"tw-animate-css": "^1.2.5"
},
"devDependencies": {
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.3.4",
"jazz-run": "workspace:*",
"npm-run-all": "^4.1.5",

View File

@@ -13,14 +13,14 @@
"dependencies": {
"@react-spring/web": "^9.7.5",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "3.25.28"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",

View File

@@ -12,14 +12,14 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "5.6.2",

View File

@@ -23,8 +23,8 @@
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0"
@@ -32,8 +32,8 @@
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"postcss": "^8.4.27",
"tailwindcss": "^4.1.10",

View File

@@ -14,8 +14,8 @@
"dependencies": {
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0"
},
@@ -24,8 +24,8 @@
"@playwright/test": "^1.50.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"postcss": "^8.4.40",

View File

@@ -11,14 +11,14 @@
},
"dependencies": {
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "5.6.2",

View File

@@ -12,14 +12,14 @@
"dependencies": {
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"typescript": "5.6.2",

View File

@@ -19,15 +19,15 @@
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.39.1",
"react": "19.0.0",
"react-dom": "19.0.0"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",

View File

@@ -22,15 +22,15 @@
"clsx": "^2.1.1",
"jazz-tools": "workspace:*",
"lucide-react": "^0.509.0",
"react": "19.0.0",
"react-dom": "19.0.0"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@playwright/test": "^1.50.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"is-ci": "^3.0.1",

View File

@@ -10,7 +10,6 @@
"preview": "vite preview"
},
"dependencies": {
"@faker-js/faker": "^9.7.0",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-toast": "^1.2.14",
@@ -19,8 +18,8 @@
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
"qrcode": "^1.5.3",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"tailwind-merge": "^1.14.0",
@@ -30,8 +29,8 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.1.10",
"@types/qrcode": "^1.5.1",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react-swc": "^3.10.1",
"postcss": "^8.4.27",
"tailwindcss": "^4.1.10",

View File

@@ -17,12 +17,12 @@ import React from "react";
import { TodoAccount, TodoProject } from "./1_schema.ts";
import { NewProjectForm } from "./3_NewProjectForm.tsx";
import { ProjectTodoTable } from "./4_ProjectTodoTable.tsx";
import { apiKey } from "./apiKey.ts";
import {
Button,
ThemeProvider,
TitleAndLogo,
} from "./basicComponents/index.ts";
import { TaskGenerator } from "./components/TaskGenerator.tsx";
import { wordlist } from "./wordlist.ts";
/**
@@ -41,7 +41,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
return (
<JazzReactProvider
sync={{
peer: `ws://localhost:4200`,
peer: `wss://cloud.jazz.tools/?key=${apiKey}`,
}}
AccountSchema={TodoAccount}
>
@@ -92,10 +92,6 @@ export default function App() {
path: "/invite/*",
element: <p>Accepting invite...</p>,
},
{
path: "/generate",
element: <TaskGenerator />,
},
]);
// `useAcceptInvite()` is a hook that accepts an invite link from the URL hash,

View File

@@ -1,63 +0,0 @@
import { TodoAccount } from "@/1_schema";
import { FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import { generateRandomProject } from "../generate";
export function TaskGenerator() {
const [isGenerating, setIsGenerating] = useState(false);
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const numTasks = Math.max(
1,
parseInt(formData.get("numTasks") as string) || 1,
);
setIsGenerating(true);
const project = generateRandomProject(numTasks);
const { root } = await TodoAccount.getMe().ensureLoaded({
resolve: {
root: {
projects: true,
},
},
});
root.projects.push(project.value);
await project.done;
navigate(`/project/${project.value.id}`);
};
return (
<div className="p-4 border rounded-lg shadow-xs bg-white">
<h2 className="text-lg font-semibold mb-4">Generate Random Tasks</h2>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<label htmlFor="numTasks" className="text-sm font-medium">
Number of tasks:
</label>
<input
id="numTasks"
name="numTasks"
type="number"
min="1"
defaultValue={5}
className="w-20 px-2 py-1 border rounded"
/>
</div>
<button
type="submit"
disabled={isGenerating}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-blue-300"
>
{isGenerating ? "Generating..." : "Generate Tasks"}
</button>
</form>
</div>
);
}

View File

@@ -12,14 +12,14 @@
"dependencies": {
"@tailwindcss/forms": "^0.5.9",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0"
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"@vitejs/plugin-react": "^4.5.1",
"globals": "^15.11.0",
"tailwindcss": "^4.1.10",

View File

@@ -27,28 +27,26 @@ export default function ButtonsPage() {
return (
<>
<h3 className="text-lg mt-5 mb-2 font-bold">Variants</h3>
<p className="mb-3">
For compatibility the shadcn/ui variants are mapped to the design
system.
</p>
<p className="my-3">Buttons are styled with the variant prop.</p>
<div className="grid grid-cols-2 gap-2">
<Button variant="default">default</Button>
<Button variant="link">link</Button>
<Button variant="ghost">ghost</Button>
<Button variant="outline">outline</Button>
<Button variant="secondary">secondary</Button>
<Button variant="destructive">destructive</Button>
</div>
<h3 className="text-lg mt-5 mb-2 font-bold">Intents</h3>
<p>
We have extended the shadcn/ui variants to include more styles via the
intent prop.
<h3 className="text-lg mt-5 font-bold">Intents</h3>
<p className="my-3">
We have extended the variants to include more styles via the intent
prop.
</p>
<div className="grid grid-cols-2 gap-2">
{/* <Button intent="default">default</Button> */}
<Button intent="default">default</Button>
<Button intent="muted">muted</Button>
<Button intent="strong">strong</Button>
<Button intent="primary">primary</Button>
<Button intent="tip">tip</Button>
<Button intent="info">info</Button>
@@ -56,8 +54,6 @@ export default function ButtonsPage() {
<Button intent="warning">warning</Button>
<Button intent="alert">alert</Button>
<Button intent="danger">danger</Button>
<Button intent="muted">muted</Button>
<Button intent="strong">strong</Button>
</div>
<div className="flex justify-between items-center w-48 mt-10">
@@ -89,7 +85,7 @@ export default function ButtonsPage() {
<p className="text-sm mt-2 mb-5">
<strong>NB:</strong> Variants and styles are interchangeable. See the
intent on each variant with the dropdown
intent on each variant with the dropdown.
</p>
<div className="grid grid-cols-2 gap-2">
@@ -107,9 +103,19 @@ export default function ButtonsPage() {
</Button>
</div>
<p className="my-3">
For compatibility the shadcn/ui variants are mapped to the design
system.
</p>
<div className="grid grid-cols-2 gap-2">
<Button variant="secondary">secondary</Button>
<Button variant="destructive">destructive</Button>
</div>
<h3 className="text-lg font-bold mt-5">Icons</h3>
<p>Buttons can also contain an icon and text.</p>
<p className="my-3">Buttons can also contain an icon and text.</p>
<div className="grid grid-cols-2 gap-2">
<Button
@@ -130,7 +136,7 @@ export default function ButtonsPage() {
>
outline info with icon
</Button>
<p className="col-span-2">
<p className="col-span-2 my-2">
Or just use the icon prop with any of the button variants, style
variants and colors.
</p>
@@ -151,6 +157,7 @@ const buttonPropsTableData = {
{
prop: "intent?",
types: [
"default",
"primary",
"tip",
"info",
@@ -174,7 +181,7 @@ const buttonPropsTableData = {
"secondary",
"destructive",
],
default: "undefined",
default: "default",
},
{
prop: "icon?",

View File

@@ -159,8 +159,8 @@ const styleClasses = (intent: Style, variant: Variant | undefined) => {
inverted: `${styleToTextMap[intent]} ${colorToBgHoverMap30[styleToColorMap[intent] as VariantColor]} ${colorToBgMap[styleToColorMap[intent] as VariantColor]} ${colorToBgActiveMap50[styleToColorMap[intent] as VariantColor]} ${shadowClassesBase}`,
ghost: `bg-transparent ${styleToTextMap[intent]} ${colorToBgHoverMap10[styleToColorMap[intent] as VariantColor]} ${colorToBgActiveMap25[styleToColorMap[intent] as VariantColor]}`,
link: `bg-transparent ${styleToTextMap[intent]} underline underline-offset-2 p-0 hover:bg-transparent ${styleToTextHoverMap[intent]} ${styleToTextActiveMap[intent]} active:underline-stone-500`,
secondary: `bg-stone-300 ${styleToTextMap[intent]} hover:bg-stone-400/80 active:bg-stone-500/80`,
destructive: `bg-danger text-white hover:bg-red/80 active:bg-red/70`,
secondary: variantClass("muted"),
destructive: variantClass("danger"),
default: `${styleToBgGradientColorMap["default"]} ${styleToBgGradientHoverMap["default"]} ${textColorVariant("default")} ${styleToButtonStateMap["default"]} ${shadowClassesBase} shadow-stone-400/20`,
};
};

View File

@@ -12,6 +12,7 @@ import {
ChevronLeftIcon,
ChevronRight,
ChevronRightIcon,
ClipboardCheckIcon,
ClipboardIcon,
CodeIcon,
Eye,
@@ -66,6 +67,7 @@ export const icons = {
close: XIcon,
code: CodeIcon,
copy: ClipboardIcon,
copySuccess: ClipboardCheckIcon,
cursor: MousePointer2Icon,
darkTheme: MoonIcon,
delete: TrashIcon,

View File

@@ -11,7 +11,7 @@ export function CopyButton({
onCopy,
}: {
code: string;
size: "md" | "lg";
size: "sm" | "md" | "lg";
className?: string;
onCopy?: () => void;
}) {
@@ -32,13 +32,13 @@ export function CopyButton({
type="button"
className={clsx(
className,
"group/button absolute overflow-hidden rounded text-2xs font-medium md:opacity-0 backdrop-blur transition md:focus:opacity-100 group-hover:opacity-100",
"group/button absolute overflow-hidden rounded text-2xs font-medium md:opacity-0 backdrop-blur transition md:focus:opacity-100 group-hover:opacity-100 items-center align-middle p-0",
copied
? "bg-emerald-400/10 ring-1 ring-inset ring-emerald-400/20"
? "bg-blue-400/10 ring-1 ring-inset ring-blue-400/20"
: "bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5",
size == "md"
size === "md"
? "right-[8.5px] top-[8.5px] py-[2px] pl-1 pr-2"
: "right-2 top-2 py-1 pl-2 pr-3",
: "right-2 top-2 py-1 pl-2 pr-2",
)}
onClick={() => {
window.navigator.clipboard.writeText(code).then(() => {
@@ -60,18 +60,22 @@ export function CopyButton({
className={clsx(
size === "md" ? "size-3" : "size-4",
"stroke-stone-500 transition-colors group-hover/button:stroke-stone-600 dark:group-hover/button:stroke-stone-400",
copied && "stroke-primary",
)}
/>
Copy
{size !== "sm" && "Copy"}
</span>
<span
aria-hidden={!copied}
className={clsx(
"pointer-events-none absolute inset-0 flex items-center justify-center text-emerald-600 transition duration-300 dark:text-emerald-400",
"pointer-events-none absolute inset-0 flex items-center justify-center text-primary transition duration-300",
!copied && "translate-y-1.5 opacity-0",
)}
>
Copied!
{size === "sm" && (
<Icon name="copySuccess" size="xs" className="stroke-primary" />
)}
{size !== "sm" && "Copied!"}
</span>
</button>
);

View File

@@ -44,7 +44,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
: icon && iconPosition === "right";
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"w-full rounded-md border px-2.5 py-1 shadow-sm h-[36px]",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
);

View File

@@ -60,7 +60,7 @@ export function DropdownItem({
let classes = clsx(
className,
// Base styles
"group rounded-md space-x-2 focus:outline-none px-2.5 py-1.5",
"group rounded-md space-x-2 focus:outline-none px-2.5 py-1.5",
// Text styles
"text-left text-sm/6 dark:text-white forced-colors:text-[CanvasText]",
// Focus

View File

@@ -21,8 +21,8 @@ export type Style =
export const sizeClasses = {
sm: "text-sm py-1 px-2",
md: "py-1.5 px-3",
lg: "md:text-lg py-2 px-3 md:px-8 md:py-3",
md: "py-1.5 px-3 h-[36px]",
lg: "py-2 px-5 md:px-6 md:py-2.5",
};
export const styleToBorderMap = {

View File

@@ -42,15 +42,6 @@ export const team: Array<TeamMember> = [
linkedin: "giordanoricci",
image: "gio.jpg",
},
{
name: "Trisha Lim",
slug: "trisha",
titles: ["Frontend Dev", "Marketing"],
image: "trisha.png",
location: "Lisbon, Portugal ",
github: "trishalim",
website: "https://trishalim.com",
},
{
name: "Meg Culotta",
slug: "meg",
@@ -73,7 +64,7 @@ export const team: Array<TeamMember> = [
name: "Sammii Kellow",
slug: "sammii",
location: "London, UK",
titles: ["Design Engineer", "Marketing"],
titles: ["Frontend & Design Engineer", "Marketing"],
x: "SammiiHaylock",
github: "sammii-hk",
website: "https://sammii.dev",
@@ -91,4 +82,25 @@ export const team: Array<TeamMember> = [
linkedin: "boorad",
image: "brad.png",
},
{
name: "Divya S",
slug: "div",
location: "New York, US",
titles: ["Platform Engineer"],
x: "shortdiv",
github: "shortdiv",
website: "https://shortdiv.com",
bluesky: "shortdiv.bsky.social",
linkedin: "shortdiv",
image: "div.jpg",
},
{
name: "Nico Rainhart",
slug: "nico",
location: "Buenos Aires, Argentina",
titles: ["Full-Stack Dev", "Framework Engineer"],
image: "nico.jpeg",
github: "nrainhart",
linkedin: "nicolás-rainhart",
},
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

View File

@@ -4,6 +4,7 @@ import { ComingSoonSection } from "@/components/home/ComingSoonSection";
import { EarlyAdopterSection } from "@/components/home/EarlyAdopterSection";
import { EncryptionSection } from "@/components/home/EncryptionSection";
import { FeaturesSection } from "@/components/home/FeaturesSection";
import { GetStartedSnippetSelect } from "@/components/home/GetStartedSnippetSelect";
import { HeroSection } from "@/components/home/HeroSection";
import { HowJazzWorksSection } from "@/components/home/HowJazzWorksSection";
import { LocalFirstFeaturesSection } from "@/components/home/LocalFirstFeaturesSection";
@@ -16,7 +17,8 @@ export default function Home() {
<>
<HeroSection />
<div className="container flex flex-col gap-12 mt-12 lg:gap-20 lg:mt-20">
<div className="container flex flex-col gap-12 lg:gap-20">
<GetStartedSnippetSelect />
<SupportedEnvironmentsSection />
<HowJazzWorksSection />

View File

@@ -17,7 +17,7 @@ export function SideNavItem({
}) {
const classes = clsx(
className,
"py-1 px-2 -mx-2 group rounded-md flex items-center transition-colors",
"py-1 px-2 group rounded-md flex items-center transition-colors",
);
const path = usePathname();
@@ -28,7 +28,7 @@ export function SideNavItem({
className={clsx(
classes,
path === href
? "text-stone-900 font-medium bg-stone-100 dark:text-white dark:bg-stone-900"
? "text-stone-900 font-medium bg-stone-200/50 dark:text-white dark:bg-stone-800/50"
: "hover:text-stone-900 dark:hover:text-stone-200",
)}
>

View File

@@ -10,10 +10,21 @@ import {
DropdownItem,
DropdownMenu,
} from "@garden-co/design-system/src/components/organisms/Dropdown";
import clsx from "clsx";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
export function FrameworkSelect() {
export function FrameworkSelect({
onSelect,
size = "md",
routerPush = true,
className,
}: {
onSelect?: (framework: Framework) => void;
size?: "sm" | "md";
routerPush?: boolean;
className?: string;
}) {
const router = useRouter();
const defaultFramework = useFramework();
const [selectedFramework, setSelectedFramework] =
@@ -23,26 +34,26 @@ export function FrameworkSelect() {
const selectFramework = (newFramework: Framework) => {
setSelectedFramework(newFramework);
router.push(path.replace(defaultFramework, newFramework));
onSelect && onSelect(newFramework);
routerPush && router.push(path.replace(defaultFramework, newFramework));
};
return (
<Dropdown>
<DropdownButton
className="w-full justify-between"
className={clsx("w-full justify-between overflow-hidden text-nowrap", size === "sm" && "text-sm", className)}
as={Button}
variant="outline"
intent="default"
>
{frameworkNames[selectedFramework].label}
<span className="text-nowrap max-w-full overflow-hidden text-ellipsis">{frameworkNames[selectedFramework].label}</span>
<Icon name="chevronDown" size="sm" />
</DropdownButton>
<DropdownMenu className="w-[--button-width] z-50" anchor="bottom start">
{Object.entries(frameworkNames)
.filter(([_, framework]) => !framework.hidden)
.map(([key, framework]) => (
<DropdownItem
className="items-baseline"
className={clsx("items-baseline", size === "sm" && "text-xs text-nowrap", selectedFramework === key && "text-primary dark:text-primary")}
key={key}
onClick={() => selectFramework(key as Framework)}
>

View File

@@ -0,0 +1,43 @@
'use client'
import { Framework } from "@/content/framework";
import { useFramework } from "@/lib/use-framework";
import NpxCreateJazzApp from "@/components/home/NpxCreateJazzApp.mdx";
import { CopyButton } from "@garden-co/design-system/src/components/molecules/CodeGroup";
import { useState } from "react";
import { Button } from "@garden-co/design-system/src/components/atoms/Button";
import Link from "next/link";
import { FrameworkSelect } from "../docs/FrameworkSelect";
import clsx from "clsx";
import { track } from "@vercel/analytics";
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
export function GetStartedSnippetSelect() {
const defaultFramework = useFramework();
const [selectedFramework, setSelectedFramework] =
useState<Framework>(defaultFramework);
return (
<GappedGrid>
<div className="relative w-full col-span-2 lg:col-span-3 border-2 border-primary rounded-lg overflow-hidden">
<CopyButton
code="npx create-jazz-app@latest"
size="sm"
className={clsx("mt-0.5 mr-0.5 z-100 md:opacity-100 hidden md:block")}
onCopy={() => track("create-jazz-app command copied from hero")}
/>
<NpxCreateJazzApp />
</div>
<div className="col-span-2 lg:col-span-3 flex flex-row gap-2">
<div className="h-full items-center w-[175px]">
<FrameworkSelect onSelect={setSelectedFramework} size="md" routerPush={false} className="h-full md:px-4" />
</div>
<div className="flex h-full items-center">
<Button intent="primary" size="lg" className="w-full">
<Link className="my-[0.11rem]" href={`/docs/${selectedFramework}`}>Get started</Link>
</Button>
</div>
</div>
</GappedGrid>
);
}

View File

@@ -1,6 +1,5 @@
"use client";
import CreateJazzApp from "@/components/home/CreateJazzApp.mdx";
import { marketingCopy } from "@/content/marketingCopy";
import { H1 } from "@garden-co/design-system/src/components/atoms/Headings";
import {
@@ -8,11 +7,10 @@ import {
type IconName,
} from "@garden-co/design-system/src/components/atoms/Icon";
import { Kicker } from "@garden-co/design-system/src/components/atoms/Kicker";
import { CopyButton } from "@garden-co/design-system/src/components/molecules/CodeGroup";
import { Prose } from "@garden-co/design-system/src/components/molecules/Prose";
import { SectionHeader } from "@garden-co/design-system/src/components/molecules/SectionHeader";
import { track } from "@vercel/analytics";
import Link from "next/link";
import { GetStartedSnippetSelect } from "./GetStartedSnippetSelect";
const features: Array<{
title: string;
@@ -54,8 +52,8 @@ const features: Array<{
export function HeroSection() {
return (
<div className="container grid items-center gap-x-8 gap-y-12 my-12 md:my-16 lg:my-24 lg:gap-x-10 lg:grid-cols-3">
<div className="flex flex-col justify-center gap-5 lg:col-span-2 lg:gap-8">
<div className="container grid items-center gap-x-8 gap-y-12 my-12 md:my-16 lg:my-24 lg:gap-x-10 lg:grid-cols-12">
<div className="flex flex-col justify-center gap-5 lg:col-span-11 lg:gap-8">
<Kicker>Toolkit for backendless apps</Kicker>
<H1>
<span className="inline-block text-highlight">
@@ -94,31 +92,6 @@ export function HeroSection() {
))}
</div>
</div>
<div className="h-full group grid md:grid-cols-2 items-center lg:grid-cols-1 lg:pt-36">
<SectionHeader
className="md:col-span-2 lg:sr-only"
title="Get a Jazz app running in minutes."
/>
<div className="overflow-hidden sm:rounded-xl sm:border h-full sm:px-8 sm:pt-6 bg-stone-50 dark:bg-stone-950">
<div className="rounded-lg bg-white dark:bg-stone-925 sm:ring-4 ring-stone-400/20 sm:shadow-xl sm:shadow-blue/20 border relative sm:top-2 h-full w-full">
<div className="py-4 flex items-center gap-2.5 px-6 border-b">
<span className="rounded-full size-3 bg-stone-200 dark:bg-stone-900" />
<span className="rounded-full size-3 bg-stone-200 dark:bg-stone-900" />
<span className="rounded-full size-3 bg-stone-200 dark:bg-stone-900" />
<CopyButton
code="npx create-jazz-app@latest"
size="md"
className="mt-0.5 mr-0.5"
onCopy={() => track("create-jazz-app command copied from hero")}
/>
</div>
<div className="p-3">
<CreateJazzApp />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,3 @@
```sh
npx create-jazz-app@latest
```

View File

@@ -118,7 +118,7 @@ To use it, install the following Packages:
<CodeGroup>
```bash
pnpm add react-native-quick-crypto@1.0.0-beta.18 react-native-nitro-modules
pnpm add react-native-quick-crypto@1.0.0-beta.18 react-native-nitro-modules react-native-fast-encoder
```
</CodeGroup>

View File

@@ -116,7 +116,7 @@ To use it, install the following Packages:
<CodeGroup>
```bash
pnpm add react-native-quick-crypto@1.0.0-beta.18 react-native-nitro-modules
pnpm add react-native-quick-crypto@1.0.0-beta.18 react-native-nitro-modules react-native-fast-encoder
```
</CodeGroup>

View File

@@ -40,7 +40,7 @@ npx expo prebuild
<CodeGroup>
```bash
# Expo dependencies
npx expo install expo-linking expo-secure-store expo-file-system @react-native-community/netinfo @bam.tech/react-native-image-resizer
npx expo install expo-linking expo-secure-store expo-sqlite expo-file-system @react-native-community/netinfo @bam.tech/react-native-image-resizer
# React Native polyfills
npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values

View File

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

1543
homepage/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ packages:
- "gcmp"
catalog:
"react": "19.0.0"
"react-dom": "19.0.0"
"@types/react": "19.0.0"
"@types/react-dom": "19.0.0"
"react": "19.1.0"
"react-dom": "19.1.0"
"@types/react": "19.1.0"
"@types/react-dom": "19.1.0"

View File

@@ -23,8 +23,7 @@
"playwright": "^1.50.1",
"turbo": "^2.3.1",
"typedoc": "^0.25.13",
"vitest": "catalog:",
"yalc": "^1.0.0-pre.53"
"vitest": "catalog:"
},
"scripts": {
"dev": "turbo dev",
@@ -40,8 +39,8 @@
"changeset-version": "changeset version && pnpm i --no-frozen-lockfile",
"release": "turbo run build --filter='./packages/*' && pnpm changeset publish && git push --follow-tags",
"clean": "rm -rf ./packages/*/dist && rm -rf ./packages/*/node_modules && rm -rf ./examples/*/node_modules && rm -rf ./examples/*/dist",
"check-catalog-deps": "node scripts/check-catalog-deps.js",
"yalc:all": "for d in packages/*/; do echo $d; cd $d; pnpm yalc push --replace --sig; cd '../../'; done"
"postinstall": "lefthook install",
"check-catalog-deps": "node scripts/check-catalog-deps.js"
},
"version": "0.0.0",
"pnpm": {
@@ -52,10 +51,10 @@
"ignoreMissing": ["@babel/*", "expo-modules-*", "typescript"]
},
"overrides": {
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"vite": "6.3.5",
"esbuild": "0.24.0"
}

View File

@@ -1,5 +1,47 @@
# cojson-storage-indexeddb
## 0.15.10
### Patch Changes
- cojson@0.15.10
## 0.15.9
### Patch Changes
- Updated dependencies [27b4837]
- Updated dependencies [2776263]
- cojson@0.15.9
## 0.15.8
### Patch Changes
- cojson@0.15.8
- cojson-storage@0.15.8
## 0.15.7
### Patch Changes
- cojson@0.15.7
- cojson-storage@0.15.7
## 0.15.6
### Patch Changes
- cojson@0.15.6
- cojson-storage@0.15.6
## 0.15.5
### Patch Changes
- cojson@0.15.5
- cojson-storage@0.15.5
## 0.15.4
### Patch Changes

View File

@@ -1,13 +1,12 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.15.4",
"version": "0.15.10",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:*",
"cojson-storage": "workspace:*"
"cojson": "workspace:*"
},
"devDependencies": {
"typescript": "catalog:",

View File

@@ -7,7 +7,7 @@ import type {
StoredCoValueRow,
StoredSessionRow,
TransactionRow,
} from "cojson-storage";
} from "cojson";
import { CoJsonIDBTransaction } from "./CoJsonIDBTransaction.js";
export class IDBClient implements DBClientInterfaceAsync {

View File

@@ -1,10 +1,4 @@
import {
type IncomingSyncStream,
type OutgoingSyncQueue,
type Peer,
cojsonInternals,
} from "cojson";
import { StorageManagerAsync } from "cojson-storage";
import { StorageApiAsync } from "cojson";
import { IDBClient } from "./idbClient.js";
let DATABASE_NAME = "jazz-storage";
@@ -13,132 +7,50 @@ export function internal_setDatabaseName(name: string) {
DATABASE_NAME = name;
}
function createParallelOpsRunner() {
const ops = new Set<Promise<unknown>>();
export async function getIndexedDBStorage(name = DATABASE_NAME) {
const dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(name, 4);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result);
};
request.onupgradeneeded = async (ev) => {
const db = request.result;
if (ev.oldVersion === 0) {
const coValues = db.createObjectStore("coValues", {
autoIncrement: true,
keyPath: "rowID",
});
return {
add: (op: Promise<unknown>) => {
ops.add(op);
op.finally(() => {
ops.delete(op);
});
},
wait() {
return Promise.race(ops);
},
get size() {
return ops.size;
},
};
}
coValues.createIndex("coValuesById", "id", {
unique: true,
});
export class IDBNode {
private readonly dbClient: IDBClient;
private readonly syncManager: StorageManagerAsync;
const sessions = db.createObjectStore("sessions", {
autoIncrement: true,
keyPath: "rowID",
});
constructor(
db: IDBDatabase,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
this.dbClient = new IDBClient(db);
this.syncManager = new StorageManagerAsync(this.dbClient, toLocalNode);
sessions.createIndex("sessionsByCoValue", "coValue");
sessions.createIndex("uniqueSessions", ["coValue", "sessionID"], {
unique: true,
});
const processMessages = async () => {
const batch = createParallelOpsRunner();
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
if (msg.action === "content") {
await this.syncManager.handleSyncMessage(msg);
} else {
batch.add(this.syncManager.handleSyncMessage(msg));
}
if (batch.size > 10) {
await batch.wait();
}
} catch (e) {
console.error(e);
}
db.createObjectStore("transactions", {
keyPath: ["ses", "idx"],
});
}
if (ev.oldVersion <= 1) {
db.createObjectStore("signatureAfter", {
keyPath: ["ses", "idx"],
});
}
};
});
processMessages().catch((e) =>
console.error("Error in processMessages in IndexedDB", e),
);
}
const db = await dbPromise;
static async asPeer(
{ localNodeName = "local" }: { localNodeName?: string } | undefined = {
localNodeName: "local",
},
): Promise<Peer> {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"indexedDB",
{
peer1role: "client",
peer2role: "storage",
crashOnClose: true,
},
);
await IDBNode.open(localNodeAsPeer.incoming, localNodeAsPeer.outgoing);
return { ...storageAsPeer, priority: 100 };
}
static async open(
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
const dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DATABASE_NAME, 4);
request.onerror = () => {
reject(request.error);
};
request.onsuccess = () => {
resolve(request.result);
};
request.onupgradeneeded = async (ev) => {
const db = request.result;
if (ev.oldVersion === 0) {
const coValues = db.createObjectStore("coValues", {
autoIncrement: true,
keyPath: "rowID",
});
coValues.createIndex("coValuesById", "id", {
unique: true,
});
const sessions = db.createObjectStore("sessions", {
autoIncrement: true,
keyPath: "rowID",
});
sessions.createIndex("sessionsByCoValue", "coValue");
sessions.createIndex("uniqueSessions", ["coValue", "sessionID"], {
unique: true,
});
db.createObjectStore("transactions", {
keyPath: ["ses", "idx"],
});
}
if (ev.oldVersion <= 1) {
db.createObjectStore("signatureAfter", {
keyPath: ["ses", "idx"],
});
}
};
});
return new IDBNode(await dbPromise, fromLocalNode, toLocalNode);
}
return new StorageApiAsync(new IDBClient(db));
}

View File

@@ -1,5 +1,4 @@
export {
IDBNode,
IDBNode as IDBStorage,
internal_setDatabaseName,
getIndexedDBStorage,
} from "./idbNode.js";

View File

@@ -1,61 +0,0 @@
import { LocalNode } from "cojson";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { expect, test } from "vitest";
import { IDBStorage } from "../index.js";
const Crypto = await WasmCrypto.create();
test("Should be able to initialize and load from empty DB", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node.syncManager.addPeer(await IDBStorage.asPeer({}));
await new Promise((resolve) => setTimeout(resolve, 200));
expect(node.syncManager.peers.indexedDB).toBeDefined();
});
test("Should be able to sync data to database and then load that from a new node", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node1.syncManager.addPeer(
await IDBStorage.asPeer({ localNodeName: "node1" }),
);
const group = node1.createGroup();
const map = group.createMap();
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node2.syncManager.addPeer(
await IDBStorage.asPeer({ localNodeName: "node2" }),
);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(map2.get("hello")).toBe("world");
});

View File

@@ -1,8 +1,7 @@
import { LocalNode } from "cojson";
import { StorageManagerAsync } from "cojson-storage";
import { LocalNode, StorageApiAsync } from "cojson";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { IDBStorage } from "../index.js";
import { getIndexedDBStorage } from "../index.js";
import { toSimplifiedMessages } from "./messagesTestUtils.js";
import { trackMessages, waitFor } from "./testUtils.js";
@@ -17,22 +16,6 @@ afterEach(() => {
syncMessages.restore();
});
test("Should be able to initialize and load from empty DB", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node.syncManager.addPeer(await IDBStorage.asPeer());
await new Promise((resolve) => setTimeout(resolve, 200));
expect(node.syncManager.peers.indexedDB).toBeDefined();
});
test("should sync and load data from storage", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
@@ -41,18 +24,14 @@ test("should sync and load data from storage", async () => {
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const peer = await IDBStorage.asPeer();
node1.syncManager.addPeer(peer);
node1.setStorage(await getIndexedDBStorage());
const group = node1.createGroup();
const map = group.createMap();
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
expect(
toSimplifiedMessages(
@@ -65,9 +44,7 @@ test("should sync and load data from storage", async () => {
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
]
`);
@@ -80,9 +57,7 @@ test("should sync and load data from storage", async () => {
Crypto,
);
const peer2 = await IDBStorage.asPeer();
node2.syncManager.addPeer(peer2);
node2.setStorage(await getIndexedDBStorage());
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
@@ -103,9 +78,7 @@ test("should sync and load data from storage", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
]
`);
});
@@ -119,15 +92,12 @@ test("should send an empty content message if there is no content", async () =>
Crypto,
);
const peer = await IDBStorage.asPeer();
node1.syncManager.addPeer(peer);
node1.setStorage(await getIndexedDBStorage());
const group = node1.createGroup();
const map = group.createMap();
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
expect(
toSimplifiedMessages(
@@ -140,9 +110,7 @@ test("should send an empty content message if there is no content", async () =>
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: ",
"storage -> KNOWN Map sessions: header/0",
]
`);
@@ -155,9 +123,7 @@ test("should send an empty content message if there is no content", async () =>
Crypto,
);
const peer2 = await IDBStorage.asPeer();
node2.syncManager.addPeer(peer2);
node2.setStorage(await getIndexedDBStorage());
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
@@ -176,9 +142,7 @@ test("should send an empty content message if there is no content", async () =>
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: ",
"client -> KNOWN Map sessions: header/0",
]
`);
});
@@ -192,10 +156,7 @@ test("should load dependencies correctly (group inheritance)", async () => {
Crypto,
);
const peer = await IDBStorage.asPeer();
node1.syncManager.addPeer(peer);
node1.setStorage(await getIndexedDBStorage());
const group = node1.createGroup();
const parentGroup = node1.createGroup();
@@ -205,7 +166,7 @@ test("should load dependencies correctly (group inheritance)", async () => {
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
expect(
toSimplifiedMessages(
@@ -218,12 +179,9 @@ test("should load dependencies correctly (group inheritance)", async () => {
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"storage -> KNOWN ParentGroup sessions: header/4",
"client -> CONTENT Group header: true new: After: 0 New: 5",
"storage -> KNOWN Group sessions: header/5",
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
]
`);
@@ -236,9 +194,7 @@ test("should load dependencies correctly (group inheritance)", async () => {
Crypto,
);
const peer2 = await IDBStorage.asPeer();
node2.syncManager.addPeer(peer2);
node2.setStorage(await getIndexedDBStorage());
await node2.load(map.id);
@@ -259,11 +215,8 @@ test("should load dependencies correctly (group inheritance)", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
]
`);
});
@@ -277,9 +230,7 @@ test("should not send the same dependency value twice", async () => {
Crypto,
);
const peer = await IDBStorage.asPeer();
node1.syncManager.addPeer(peer);
node1.setStorage(await getIndexedDBStorage());
const group = node1.createGroup();
const parentGroup = node1.createGroup();
@@ -292,7 +243,8 @@ test("should not send the same dependency value twice", async () => {
map.set("hello", "world");
mapFromParent.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
await mapFromParent.core.waitForSync();
syncMessages.clear();
node1.gracefulShutdown();
@@ -303,9 +255,7 @@ test("should not send the same dependency value twice", async () => {
Crypto,
);
const peer2 = await IDBStorage.asPeer();
node2.syncManager.addPeer(peer2);
node2.setStorage(await getIndexedDBStorage());
await node2.load(map.id);
await node2.load(mapFromParent.id);
@@ -329,14 +279,10 @@ test("should not send the same dependency value twice", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
"client -> LOAD MapFromParent sessions: empty",
"storage -> CONTENT MapFromParent header: true new: After: 0 New: 1",
"client -> KNOWN MapFromParent sessions: header/1",
]
`);
});
@@ -350,9 +296,8 @@ test("should recover from data loss", async () => {
Crypto,
);
const peer = await IDBStorage.asPeer();
node1.syncManager.addPeer(peer);
const storage = await getIndexedDBStorage();
node1.setStorage(storage);
const group = node1.createGroup();
@@ -360,22 +305,25 @@ test("should recover from data loss", async () => {
map.set("0", 0);
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
const mock = vi
.spyOn(StorageManagerAsync.prototype, "handleSyncMessage")
.mockImplementation(() => Promise.resolve());
.spyOn(StorageApiAsync.prototype, "store")
.mockImplementation(() => Promise.resolve(undefined));
map.set("1", 1);
map.set("2", 2);
await new Promise((resolve) => setTimeout(resolve, 200));
const knownState = storage.getKnownState(map.id);
Object.assign(knownState, map.core.knownState());
mock.mockReset();
map.set("3", 3);
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
expect(
toSimplifiedMessages(
@@ -388,13 +336,10 @@ test("should recover from data loss", async () => {
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
"client -> CONTENT Map header: false new: After: 3 New: 1",
"storage -> KNOWN CORRECTION Map sessions: header/1",
"storage -> KNOWN CORRECTION Map sessions: header/4",
"client -> CONTENT Map header: false new: After: 1 New: 3",
"storage -> KNOWN Map sessions: header/4",
]
`);
@@ -407,9 +352,7 @@ test("should recover from data loss", async () => {
Crypto,
);
const peer2 = await IDBStorage.asPeer();
node2.syncManager.addPeer(peer2);
node2.setStorage(await getIndexedDBStorage());
const map2 = await node2.load(map.id);
@@ -436,9 +379,7 @@ test("should recover from data loss", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 4",
"client -> KNOWN Map sessions: header/4",
]
`);
});
@@ -452,7 +393,7 @@ test("should sync multiple sessions in a single content message", async () => {
Crypto,
);
node1.syncManager.addPeer(await IDBStorage.asPeer());
node1.setStorage(await getIndexedDBStorage());
const group = node1.createGroup();
@@ -460,7 +401,7 @@ test("should sync multiple sessions in a single content message", async () => {
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
await map.core.waitForSync();
node1.gracefulShutdown();
@@ -470,7 +411,7 @@ test("should sync multiple sessions in a single content message", async () => {
Crypto,
);
node2.syncManager.addPeer(await IDBStorage.asPeer());
node2.setStorage(await getIndexedDBStorage());
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
@@ -493,7 +434,7 @@ test("should sync multiple sessions in a single content message", async () => {
syncMessages.clear();
node3.syncManager.addPeer(await IDBStorage.asPeer());
node3.setStorage(await getIndexedDBStorage());
const map3 = await node3.load(map.id);
if (map3 === "unavailable") {
@@ -514,9 +455,7 @@ test("should sync multiple sessions in a single content message", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
"client -> KNOWN Map sessions: header/2",
]
`);
});
@@ -530,7 +469,7 @@ test("large coValue upload streaming", async () => {
Crypto,
);
node1.syncManager.addPeer(await IDBStorage.asPeer());
node1.setStorage(await getIndexedDBStorage());
const group = node1.createGroup();
const largeMap = group.createMap();
@@ -547,6 +486,7 @@ test("large coValue upload streaming", async () => {
largeMap.set(key, value, "trusting");
}
// TODO: Wait for storage to be updated
await largeMap.core.waitForSync();
const knownState = largeMap.core.knownState();
@@ -561,7 +501,7 @@ test("large coValue upload streaming", async () => {
syncMessages.clear();
node2.syncManager.addPeer(await IDBStorage.asPeer());
node2.setStorage(await getIndexedDBStorage());
const largeMapOnNode2 = await node2.load(largeMap.id);
@@ -586,15 +526,10 @@ test("large coValue upload streaming", async () => {
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> KNOWN Map sessions: header/200",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 97",
"client -> KNOWN Map sessions: header/97",
"storage -> CONTENT Map header: true new: After: 97 New: 97",
"client -> KNOWN Map sessions: header/194",
"storage -> CONTENT Map header: true new: After: 194 New: 6",
"client -> KNOWN Map sessions: header/200",
]
`);
});
@@ -605,7 +540,7 @@ test("should sync and load accounts from storage", async () => {
const { node: node1, accountID } = await LocalNode.withNewlyCreatedAccount({
crypto: Crypto,
initialAgentSecret: agentSecret,
peersToLoadFrom: [await IDBStorage.asPeer()],
storage: await getIndexedDBStorage(),
creationProps: {
name: "test",
},
@@ -615,8 +550,6 @@ test("should sync and load accounts from storage", async () => {
const profile = node1.expectProfileLoaded(accountID);
const profileGroup = profile.group;
await new Promise((resolve) => setTimeout(resolve, 200));
expect(
toSimplifiedMessages(
{
@@ -629,11 +562,8 @@ test("should sync and load accounts from storage", async () => {
).toMatchInlineSnapshot(`
[
"client -> CONTENT Account header: true new: After: 0 New: 4",
"storage -> KNOWN Account sessions: header/4",
"client -> CONTENT ProfileGroup header: true new: After: 0 New: 5",
"storage -> KNOWN ProfileGroup sessions: header/5",
"client -> CONTENT Profile header: true new: After: 0 New: 1",
"storage -> KNOWN Profile sessions: header/1",
]
`);
@@ -645,12 +575,11 @@ test("should sync and load accounts from storage", async () => {
crypto: Crypto,
accountSecret: agentSecret,
accountID,
peersToLoadFrom: [await IDBStorage.asPeer()],
peersToLoadFrom: [],
storage: await getIndexedDBStorage(),
sessionID: Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
});
await new Promise((resolve) => setTimeout(resolve, 200));
expect(
toSimplifiedMessages(
{
@@ -664,12 +593,9 @@ test("should sync and load accounts from storage", async () => {
[
"client -> LOAD Account sessions: empty",
"storage -> CONTENT Account header: true new: After: 0 New: 4",
"client -> KNOWN Account sessions: header/4",
"client -> LOAD Profile sessions: empty",
"storage -> CONTENT ProfileGroup header: true new: After: 0 New: 5",
"client -> KNOWN ProfileGroup sessions: header/5",
"storage -> CONTENT Profile header: true new: After: 0 New: 1",
"client -> KNOWN Profile sessions: header/1",
]
`);

View File

@@ -1,39 +1,63 @@
import type { LocalNode, SyncMessage } from "cojson";
import { cojsonInternals } from "cojson";
import { StorageManagerAsync } from "cojson-storage";
import type { RawCoID, SyncMessage } from "cojson";
import { StorageApiAsync } from "cojson";
import { onTestFinished } from "vitest";
const { SyncManager } = cojsonInternals;
export function trackMessages() {
const messages: {
from: "client" | "server" | "storage";
msg: SyncMessage;
}[] = [];
const originalHandleSyncMessage =
StorageManagerAsync.prototype.handleSyncMessage;
const originalNodeSyncMessage = SyncManager.prototype.handleSyncMessage;
const originalLoad = StorageApiAsync.prototype.load;
const originalStore = StorageApiAsync.prototype.store;
StorageManagerAsync.prototype.handleSyncMessage = async function (msg) {
StorageApiAsync.prototype.load = async function (id, callback, done) {
messages.push({
from: "client",
msg,
msg: {
action: "load",
id: id as RawCoID,
header: false,
sessions: {},
},
});
return originalHandleSyncMessage.call(this, msg);
return originalLoad.call(
this,
id,
(msg) => {
messages.push({
from: "storage",
msg,
});
callback(msg);
},
done,
);
};
SyncManager.prototype.handleSyncMessage = async function (msg, peer) {
messages.push({
from: "storage",
msg,
StorageApiAsync.prototype.store = async function (data, correctionCallback) {
for (const msg of data) {
messages.push({
from: "client",
msg,
});
}
return originalStore.call(this, data, (msg) => {
messages.push({
from: "storage",
msg: {
action: "known",
isCorrection: true,
...msg,
},
});
correctionCallback(msg);
});
return originalNodeSyncMessage.call(this, msg, peer);
};
const restore = () => {
StorageManagerAsync.prototype.handleSyncMessage = originalHandleSyncMessage;
SyncManager.prototype.handleSyncMessage = originalNodeSyncMessage;
StorageApiAsync.prototype.load = originalLoad;
StorageApiAsync.prototype.store = originalStore;
messages.length = 0;
};

View File

@@ -1,5 +1,47 @@
# cojson-storage-sqlite
## 0.15.10
### Patch Changes
- cojson@0.15.10
## 0.15.9
### Patch Changes
- Updated dependencies [27b4837]
- Updated dependencies [2776263]
- cojson@0.15.9
## 0.15.8
### Patch Changes
- cojson@0.15.8
- cojson-storage@0.15.8
## 0.15.7
### Patch Changes
- cojson@0.15.7
- cojson-storage@0.15.7
## 0.15.6
### Patch Changes
- cojson@0.15.6
- cojson-storage@0.15.6
## 0.15.5
### Patch Changes
- cojson@0.15.5
- cojson-storage@0.15.5
## 0.15.4
### Patch Changes

View File

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

View File

@@ -1,32 +0,0 @@
import Database, { type Database as DatabaseT } from "better-sqlite3";
import type { SQLiteDatabaseDriver } from "cojson-storage";
export class BetterSqliteDriver implements SQLiteDatabaseDriver {
private readonly db: DatabaseT;
constructor(filename: string) {
const db = new Database(filename);
this.db = db;
db.pragma("journal_mode = WAL");
}
run(sql: string, params: unknown[]) {
this.db.prepare(sql).run(params);
}
query<T>(sql: string, params: unknown[]): T[] {
return this.db.prepare(sql).all(params) as T[];
}
get<T>(sql: string, params: unknown[]): T | undefined {
return this.db.prepare(sql).get(params) as T | undefined;
}
transaction(callback: () => unknown) {
return this.db.transaction(callback)();
}
closeDb() {
this.db.close();
}
}

View File

@@ -1 +1,39 @@
export { SQLiteNode, SQLiteNode as SQLiteStorage } from "./sqliteNode.js";
import Database, { type Database as DatabaseT } from "better-sqlite3";
import type { SQLiteDatabaseDriver } from "cojson";
import { getSqliteStorage } from "cojson";
export class BetterSqliteDriver implements SQLiteDatabaseDriver {
private readonly db: DatabaseT;
constructor(filename: string) {
const db = new Database(filename);
this.db = db;
db.pragma("journal_mode = WAL");
}
run(sql: string, params: unknown[]) {
this.db.prepare(sql).run(params);
}
query<T>(sql: string, params: unknown[]): T[] {
return this.db.prepare(sql).all(params) as T[];
}
get<T>(sql: string, params: unknown[]): T | undefined {
return this.db.prepare(sql).get(params) as T | undefined;
}
transaction(callback: () => unknown) {
return this.db.transaction(callback)();
}
closeDb() {
this.db.close();
}
}
export function getBetterSqliteStorage(filename: string) {
const db = new BetterSqliteDriver(filename);
return getSqliteStorage(db);
}

View File

@@ -1,21 +0,0 @@
import type { Peer } from "cojson";
import { SQLiteNodeBase } from "cojson-storage";
import { BetterSqliteDriver } from "./betterSqliteDriver.js";
export class SQLiteNode extends SQLiteNodeBase {
static async asPeer({
filename,
localNodeName = "local",
}: {
filename: string;
localNodeName?: string;
}): Promise<Peer> {
const db = new BetterSqliteDriver(filename);
return SQLiteNodeBase.create({
db,
localNodeName,
maxBlockingTime: 500,
});
}
}

View File

@@ -2,18 +2,16 @@ import { randomUUID } from "node:crypto";
import { unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { LocalNode, cojsonInternals } from "cojson";
import { SQLiteNodeBase, StorageManagerSync } from "cojson-storage";
import { LocalNode, StorageApiSync, cojsonInternals } from "cojson";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { expect, onTestFinished, test, vi } from "vitest";
import { BetterSqliteDriver } from "../betterSqliteDriver.js";
import { SQLiteNode } from "../index.js";
import { getBetterSqliteStorage } from "../index.js";
import { toSimplifiedMessages } from "./messagesTestUtils.js";
import { trackMessages, waitFor } from "./testUtils.js";
const Crypto = await WasmCrypto.create();
async function createSQLiteStorage(defaultDbPath?: string) {
function createSQLiteStorage(defaultDbPath?: string) {
const dbPath = defaultDbPath ?? join(tmpdir(), `test-${randomUUID()}.db`);
if (!defaultDbPath) {
@@ -23,29 +21,11 @@ async function createSQLiteStorage(defaultDbPath?: string) {
}
return {
peer: await SQLiteNode.asPeer({
filename: dbPath,
}),
storage: getBetterSqliteStorage(dbPath),
dbPath,
};
}
test("Should be able to initialize and load from empty DB", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node.syncManager.addPeer((await createSQLiteStorage()).peer);
await new Promise((resolve) => setTimeout(resolve, 200));
expect(node.syncManager.peers.storage).toBeDefined();
});
test("should sync and load data from storage", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
@@ -55,11 +35,11 @@ test("should sync and load data from storage", async () => {
Crypto,
);
const node1Sync = trackMessages(node1);
const node1Sync = trackMessages();
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
@@ -80,9 +60,7 @@ test("should sync and load data from storage", async () => {
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
]
`);
@@ -94,11 +72,9 @@ test("should sync and load data from storage", async () => {
Crypto,
);
const node2Sync = trackMessages(node2);
const node2Sync = trackMessages();
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
@@ -119,9 +95,7 @@ test("should sync and load data from storage", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
]
`);
@@ -137,11 +111,11 @@ test("should send an empty content message if there is no content", async () =>
Crypto,
);
const node1Sync = trackMessages(node1);
const node1Sync = trackMessages();
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
@@ -160,9 +134,7 @@ test("should send an empty content message if there is no content", async () =>
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: ",
"storage -> KNOWN Map sessions: header/0",
]
`);
@@ -174,11 +146,9 @@ test("should send an empty content message if there is no content", async () =>
Crypto,
);
const node2Sync = trackMessages(node2);
const node2Sync = trackMessages();
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
@@ -197,9 +167,7 @@ test("should send an empty content message if there is no content", async () =>
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: ",
"client -> KNOWN Map sessions: header/0",
]
`);
@@ -215,11 +183,11 @@ test("should load dependencies correctly (group inheritance)", async () => {
Crypto,
);
const node1Sync = trackMessages(node1);
const node1Sync = trackMessages();
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
const parentGroup = node1.createGroup();
@@ -243,12 +211,9 @@ test("should load dependencies correctly (group inheritance)", async () => {
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"storage -> KNOWN ParentGroup sessions: header/4",
"client -> CONTENT Group header: true new: After: 0 New: 5",
"storage -> KNOWN Group sessions: header/5",
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
]
`);
@@ -260,11 +225,9 @@ test("should load dependencies correctly (group inheritance)", async () => {
Crypto,
);
const node2Sync = trackMessages(node2);
const node2Sync = trackMessages();
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
await node2.load(map.id);
@@ -285,11 +248,8 @@ test("should load dependencies correctly (group inheritance)", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
]
`);
});
@@ -303,11 +263,11 @@ test("should not send the same dependency value twice", async () => {
Crypto,
);
const node1Sync = trackMessages(node1);
const node1Sync = trackMessages();
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
const parentGroup = node1.createGroup();
@@ -330,11 +290,9 @@ test("should not send the same dependency value twice", async () => {
Crypto,
);
const node2Sync = trackMessages(node2);
const node2Sync = trackMessages();
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
await node2.load(map.id);
await node2.load(mapFromParent.id);
@@ -358,14 +316,10 @@ test("should not send the same dependency value twice", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
"client -> LOAD MapFromParent sessions: empty",
"storage -> CONTENT MapFromParent header: true new: After: 0 New: 1",
"client -> KNOWN MapFromParent sessions: header/1",
]
`);
});
@@ -379,11 +333,11 @@ test("should recover from data loss", async () => {
Crypto,
);
const node1Sync = trackMessages(node1);
const node1Sync = trackMessages();
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
@@ -394,8 +348,8 @@ test("should recover from data loss", async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
const mock = vi
.spyOn(StorageManagerSync.prototype, "handleSyncMessage")
.mockImplementation(() => Promise.resolve());
.spyOn(StorageApiSync.prototype, "store")
.mockImplementation(() => false);
map.set("1", 1);
map.set("2", 2);
@@ -419,13 +373,8 @@ test("should recover from data loss", async () => {
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
"client -> CONTENT Map header: false new: After: 3 New: 1",
"storage -> KNOWN CORRECTION Map sessions: header/1",
"client -> CONTENT Map header: false new: After: 1 New: 3",
"storage -> KNOWN Map sessions: header/4",
]
`);
@@ -437,11 +386,9 @@ test("should recover from data loss", async () => {
Crypto,
);
const node2Sync = trackMessages(node2);
const node2Sync = trackMessages();
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
const map2 = await node2.load(map.id);
@@ -468,9 +415,7 @@ test("should recover from data loss", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 4",
"client -> KNOWN Map sessions: header/4",
]
`);
});
@@ -501,24 +446,28 @@ test("should recover missing dependencies from storage", async () => {
node1.syncManager.addPeer(serverPeer);
serverNode.syncManager.addPeer(clientPeer);
const handleSyncMessage = StorageManagerSync.prototype.handleSyncMessage;
const store = StorageApiSync.prototype.store;
const mock = vi
.spyOn(StorageManagerSync.prototype, "handleSyncMessage")
.mockImplementation(function (this: StorageManagerSync, msg) {
.spyOn(StorageApiSync.prototype, "store")
.mockImplementation(function (
this: StorageApiSync,
data,
correctionCallback,
) {
if (
msg.action === "content" &&
[group.core.id, account.core.id].includes(msg.id)
data[0]?.id &&
[group.core.id, account.core.id as string].includes(data[0].id)
) {
return Promise.resolve();
return false;
}
return handleSyncMessage.call(this, msg);
return store.call(this, data, correctionCallback);
});
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
group.addMember("everyone", "writer");
@@ -549,9 +498,7 @@ test("should recover missing dependencies from storage", async () => {
node2.syncManager.addPeer(serverPeer2);
serverNode.syncManager.addPeer(clientPeer2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
const map2 = await node2.load(map.id);
@@ -573,9 +520,9 @@ test("should sync multiple sessions in a single content message", async () => {
Crypto,
);
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
@@ -593,7 +540,7 @@ test("should sync multiple sessions in a single content message", async () => {
Crypto,
);
node2.syncManager.addPeer((await createSQLiteStorage(dbPath)).peer);
node2.setStorage(createSQLiteStorage(dbPath).storage);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
@@ -614,9 +561,9 @@ test("should sync multiple sessions in a single content message", async () => {
Crypto,
);
const node3Sync = trackMessages(node3);
const node3Sync = trackMessages();
node3.syncManager.addPeer((await createSQLiteStorage(dbPath)).peer);
node3.setStorage(createSQLiteStorage(dbPath).storage);
const map3 = await node3.load(map.id);
if (map3 === "unavailable") {
@@ -637,9 +584,7 @@ test("should sync multiple sessions in a single content message", async () => {
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
"client -> KNOWN Map sessions: header/2",
]
`);
@@ -655,9 +600,9 @@ test("large coValue upload streaming", async () => {
Crypto,
);
const { peer, dbPath } = await createSQLiteStorage();
const { storage, dbPath } = createSQLiteStorage();
node1.syncManager.addPeer(peer);
node1.setStorage(storage);
const group = node1.createGroup();
const largeMap = group.createMap();
@@ -683,11 +628,9 @@ test("large coValue upload streaming", async () => {
Crypto,
);
const node2Sync = trackMessages(node2);
const node2Sync = trackMessages();
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
node2.setStorage(createSQLiteStorage(dbPath).storage);
const largeMapOnNode2 = await node2.load(largeMap.id);
@@ -714,51 +657,10 @@ test("large coValue upload streaming", async () => {
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> KNOWN Map sessions: header/200",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 97",
"client -> KNOWN Map sessions: header/97",
"storage -> CONTENT Map header: true new: After: 97 New: 97",
"client -> KNOWN Map sessions: header/194",
"storage -> CONTENT Map header: true new: After: 194 New: 6",
"client -> KNOWN Map sessions: header/200",
]
`);
});
test("should close the db when the node is closed", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const dbPath = join(tmpdir(), `test-${randomUUID()}.db`);
const db = new BetterSqliteDriver(dbPath);
const peer = SQLiteNodeBase.create({
db,
localNodeName: "test",
maxBlockingTime: 500,
});
const spy = vi.spyOn(db, "closeDb");
node1.syncManager.addPeer(peer);
await new Promise((resolve) => setTimeout(resolve, 10));
expect(spy).not.toHaveBeenCalled();
node1.gracefulShutdown();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(spy).toHaveBeenCalled();
unlinkSync(dbPath);
});

View File

@@ -1,36 +1,64 @@
import type { LocalNode, SyncMessage } from "cojson";
import { StorageManagerSync } from "cojson-storage";
import type { LocalNode, RawCoID, SyncMessage } from "cojson";
import { StorageApiSync } from "cojson";
import { onTestFinished } from "vitest";
export function trackMessages(node: LocalNode) {
export function trackMessages() {
const messages: {
from: "client" | "server" | "storage";
msg: SyncMessage;
}[] = [];
const originalHandleSyncMessage =
StorageManagerSync.prototype.handleSyncMessage;
const originalNodeSyncMessage = node.syncManager.handleSyncMessage;
const originalLoad = StorageApiSync.prototype.load;
const originalStore = StorageApiSync.prototype.store;
StorageManagerSync.prototype.handleSyncMessage = async function (msg) {
StorageApiSync.prototype.load = async function (id, callback, done) {
messages.push({
from: "client",
msg,
msg: {
action: "load",
id: id as RawCoID,
header: false,
sessions: {},
},
});
return originalHandleSyncMessage.call(this, msg);
return originalLoad.call(
this,
id,
(msg) => {
messages.push({
from: "storage",
msg,
});
callback(msg);
},
done,
);
};
node.syncManager.handleSyncMessage = async function (msg, peer) {
messages.push({
from: "storage",
msg,
StorageApiSync.prototype.store = function (data, correctionCallback) {
for (const msg of data) {
messages.push({
from: "client",
msg,
});
}
return originalStore.call(this, data, (msg) => {
messages.push({
from: "storage",
msg: {
action: "known",
isCorrection: true,
...msg,
},
});
correctionCallback(msg);
});
return originalNodeSyncMessage.call(this, msg, peer);
};
const restore = () => {
StorageManagerSync.prototype.handleSyncMessage = originalHandleSyncMessage;
node.syncManager.handleSyncMessage = originalNodeSyncMessage;
StorageApiSync.prototype.load = originalLoad;
StorageApiSync.prototype.store = originalStore;
messages.length = 0;
};
onTestFinished(() => {

View File

@@ -1,171 +0,0 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
.DS_Store

View File

@@ -1,2 +0,0 @@
coverage
node_modules

View File

@@ -1,609 +0,0 @@
# cojson-storage
## 0.15.4
### Patch Changes
- Updated dependencies [277e4d4]
- cojson@0.15.4
## 0.15.3
### Patch Changes
- cojson@0.15.3
## 0.15.2
### Patch Changes
- Updated dependencies [4b964ed]
- cojson@0.15.2
## 0.15.1
### Patch Changes
- Updated dependencies [b110f00]
- cojson@0.15.1
## 0.15.0
### Patch Changes
- cojson@0.15.0
## 0.14.28
### Patch Changes
- cojson@0.14.28
## 0.14.27
### Patch Changes
- cojson@0.14.27
## 0.14.26
### Patch Changes
- 680a2e2: Read in parallel up to 10 values on the async storage adapters to improve loading perf
- Updated dependencies [e74a077]
- cojson@0.14.26
## 0.14.25
### Patch Changes
- cojson@0.14.25
## 0.14.24
### Patch Changes
- cojson@0.14.24
## 0.14.23
### Patch Changes
- 5f42c97: Close the DB connection when the node/context is closed
- Updated dependencies [1ca9299]
- cojson@0.14.23
## 0.14.22
### Patch Changes
- Updated dependencies [57fb69f]
- cojson@0.14.22
## 0.14.21
### Patch Changes
- Updated dependencies [c3d8779]
- cojson@0.14.21
## 0.14.20
### Patch Changes
- cojson@0.14.20
## 0.14.19
### Patch Changes
- cojson@0.14.19
## 0.14.18
### Patch Changes
- be7c4c2: Incorporate SQLite sync/async adapters and make them more aligned
- Updated dependencies [0d5ee3e]
- cojson@0.14.18
## 0.14.16
### Patch Changes
- Updated dependencies [5e253cc]
- cojson@0.14.16
## 0.14.15
### Patch Changes
- 23daa7c: Align the processing of the group dependencies between LocalNode and Storage.
- Updated dependencies [23daa7c]
- cojson@0.14.15
## 0.14.1
### Patch Changes
- Updated dependencies [c8b33ad]
- cojson@0.14.1
## 0.14.0
### Patch Changes
- Updated dependencies [5835ed1]
- cojson@0.14.0
## 0.13.32
### Patch Changes
- 2bf9743: Implement content streaming for large CoValues on storage
## 0.13.31
### Patch Changes
- Updated dependencies [d63716a]
- Updated dependencies [d5edad7]
- cojson@0.13.31
## 0.13.30
### Patch Changes
- Updated dependencies [07dd2c5]
- cojson@0.13.30
## 0.13.29
### Patch Changes
- e2d6ba3: Create specialized Sync and Async storage managers
- Updated dependencies [eef1a5d]
- Updated dependencies [191ae38]
- Updated dependencies [daee7b9]
- cojson@0.13.29
## 0.13.28
### Patch Changes
- Updated dependencies [e7ccb2c]
- cojson@0.13.28
## 0.13.27
### Patch Changes
- Updated dependencies [6357052]
- cojson@0.13.27
## 0.13.25
### Patch Changes
- Updated dependencies [a846e07]
- cojson@0.13.25
## 0.13.23
### Patch Changes
- Updated dependencies [6b781cf]
- cojson@0.13.23
## 0.13.21
### Patch Changes
- Updated dependencies [e14e61f]
- cojson@0.13.21
## 0.13.20
### Patch Changes
- adfc9a6: Make waitForSync work on storage peers by handling optimistic/known states
- Updated dependencies [adfc9a6]
- Updated dependencies [1389207]
- Updated dependencies [d6e143e]
- Updated dependencies [3e6229d]
- cojson@0.13.20
## 0.13.18
### Patch Changes
- 8b2df0e: Optimized the dependency push from storage to send a given dependency only once
- Updated dependencies [9089252]
- Updated dependencies [b470f63]
- Updated dependencies [66373ba]
- Updated dependencies [f24cad1]
- cojson@0.13.18
## 0.13.17
### Patch Changes
- Updated dependencies [9fb98e2]
- Updated dependencies [0b89fad]
- cojson@0.13.17
## 0.13.16
### Patch Changes
- Updated dependencies [c6fb8dc]
- cojson@0.13.16
## 0.13.15
### Patch Changes
- Updated dependencies [c712ef2]
- cojson@0.13.15
## 0.13.14
### Patch Changes
- Updated dependencies [5c2c7d4]
- cojson@0.13.14
## 0.13.13
### Patch Changes
- Updated dependencies [ec9cb40]
- cojson@0.13.13
## 0.13.12
### Patch Changes
- Updated dependencies [65719f2]
- cojson@0.13.12
## 0.13.11
### Patch Changes
- Updated dependencies [17273a6]
- Updated dependencies [3396ed4]
- Updated dependencies [267ea4c]
- cojson@0.13.11
## 0.13.10
### Patch Changes
- Updated dependencies [f837cfe]
- cojson@0.13.10
## 0.13.7
### Patch Changes
- Updated dependencies [bc3d7bb]
- Updated dependencies [4e9aae1]
- Updated dependencies [21c935c]
- Updated dependencies [aa1c80e]
- Updated dependencies [13074be]
- cojson@0.13.7
## 0.13.5
### Patch Changes
- Updated dependencies [e090b39]
- cojson@0.13.5
## 0.13.2
### Patch Changes
- Updated dependencies [c551839]
- cojson@0.13.2
## 0.13.0
### Patch Changes
- Updated dependencies [a013538]
- Updated dependencies [bce3bcc]
- cojson@0.13.0
## 0.12.2
### Patch Changes
- Updated dependencies [c2f4827]
- cojson@0.12.2
## 0.12.1
### Patch Changes
- Updated dependencies [5a00fe0]
- cojson@0.12.1
## 0.12.0
### Patch Changes
- Updated dependencies [01523dc]
- Updated dependencies [01523dc]
- cojson@0.12.0
## 0.11.8
### Patch Changes
- Updated dependencies [6c86c4f]
- Updated dependencies [9d0c9dc]
- cojson@0.11.8
## 0.11.7
### Patch Changes
- Updated dependencies [2b94bc8]
- Updated dependencies [2957362]
- cojson@0.11.7
## 0.11.6
### Patch Changes
- Updated dependencies [8ed144e]
- cojson@0.11.6
## 0.11.5
### Patch Changes
- Updated dependencies [60f5b3f]
- cojson@0.11.5
## 0.11.4
### Patch Changes
- Updated dependencies [7f036c1]
- cojson@0.11.4
## 0.11.3
### Patch Changes
- 68b0242: Improve the error logging to have more information on errors leveraging the pino err serializer
- Updated dependencies [68b0242]
- cojson@0.11.3
## 0.11.0
### Patch Changes
- a4713df: Moving to the d.ts files for the exported type definitions
- Updated dependencies [b9d194a]
- Updated dependencies [a4713df]
- Updated dependencies [e22de9f]
- Updated dependencies [34cbdc3]
- Updated dependencies [0f67e0a]
- cojson@0.11.0
## 0.10.15
### Patch Changes
- Updated dependencies [f86e278]
- cojson@0.10.15
## 0.10.8
### Patch Changes
- Updated dependencies [153dc99]
- cojson@0.10.8
## 0.10.7
### Patch Changes
- 1e625f3: Improve rollback on error when failing to add new content
- Updated dependencies [0f83320]
- Updated dependencies [012022d]
- cojson@0.10.7
## 0.10.6
### Patch Changes
- Updated dependencies [5c76e37]
- cojson@0.10.6
## 0.10.4
### Patch Changes
- Updated dependencies [1af6072]
- cojson@0.10.4
## 0.10.2
### Patch Changes
- Updated dependencies [cae3a9e]
- cojson@0.10.2
## 0.10.1
### Patch Changes
- Updated dependencies [5a63cba]
- cojson@0.10.1
## 0.10.0
### Patch Changes
- Updated dependencies [b426342]
- Updated dependencies [498954f]
- Updated dependencies [8217981]
- Updated dependencies [ac3d9fa]
- Updated dependencies [610543c]
- cojson@0.10.0
## 0.9.23
### Patch Changes
- Updated dependencies [70c9a5d]
- cojson@0.9.23
## 0.9.19
### Patch Changes
- Updated dependencies [6ad0a9f]
- cojson@0.9.19
## 0.9.18
### Patch Changes
- Updated dependencies [8898b10]
- cojson@0.9.18
## 0.9.13
### Patch Changes
- 8d29e50: Restore the logger wrapper and adapt the API to pino
- Updated dependencies [8d29e50]
- cojson@0.9.13
## 0.9.12
### Patch Changes
- 15d4b2a: Revert the custom logger
- Updated dependencies [15d4b2a]
- cojson@0.9.12
## 0.9.11
### Patch Changes
- 5863bad: Wrap all the console logs with a logger class to make possible to customize the logger
- Updated dependencies [efbf3d8]
- Updated dependencies [5863bad]
- cojson@0.9.11
## 0.9.10
### Patch Changes
- Updated dependencies [4aa377d]
- cojson@0.9.10
## 0.9.9
### Patch Changes
- Updated dependencies [8eb9247]
- cojson@0.9.9
## 0.9.0
### Patch Changes
- Updated dependencies [8eda792]
- Updated dependencies [1ef3226]
- cojson@0.9.0
## 0.8.50
### Patch Changes
- Updated dependencies [43378ef]
- cojson@0.8.50
## 0.8.49
### Patch Changes
- Updated dependencies [25dfd90]
- cojson@0.8.49
## 0.8.48
### Patch Changes
- Updated dependencies [10ea733]
- cojson@0.8.48
## 0.8.45
### Patch Changes
- Updated dependencies [6f0bd7f]
- Updated dependencies [fca6a0b]
- Updated dependencies [88d7d9a]
- cojson@0.8.45
## 0.8.44
### Patch Changes
- Updated dependencies [5d20c81]
- cojson@0.8.44
## 0.8.41
### Patch Changes
- Updated dependencies [3252502]
- Updated dependencies [6370348]
- Updated dependencies [ac216b9]
- cojson@0.8.41
## 0.8.40
### Patch Changes
- e905c84: Stop the use of incremental streaming of large CoValue content from local storage peers that triggers sync protocol bug leading to redundant syncing from server peers.
## 0.8.39
### Patch Changes
- Updated dependencies [249eecb]
- Updated dependencies [3121551]
- cojson@0.8.39
## 0.8.38
### Patch Changes
- Updated dependencies [b00ee91]
- Updated dependencies [f488c09]
- cojson@0.8.38
## 0.8.37
### Patch Changes
- Updated dependencies [3d9f12e]
- cojson@0.8.37
## 0.8.36
### Patch Changes
- 1afbd2c: Refactor the SQLite and IndexedDB storage packages to extract common synchronization functionality into newly created cojson-storage package.
- Updated dependencies [441fe27]
- cojson@0.8.36

View File

@@ -1,19 +0,0 @@
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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.

View File

@@ -1,3 +0,0 @@
# CoJSON Storage IndexedDB
This implements persistence sync service for CoJSON / Jazz (see [jazz.tools](https://jazz.tools)).

View File

@@ -1,24 +0,0 @@
{
"name": "cojson-storage",
"version": "0.15.4",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:*"
},
"devDependencies": {
"libsql": "^0.5.10",
"typescript": "catalog:",
"vitest": "catalog:"
},
"scripts": {
"dev": "tsc --watch --sourceMap --outDir dist",
"test": "vitest --run --root ../../ --project cojson-storage",
"test:watch": "vitest --watch --root ../../ --project cojson-storage",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"build": "rm -rf ./dist && tsc --sourceMap --outDir dist"
}
}

View File

@@ -1,5 +0,0 @@
export * from "./types.js";
export { StorageManagerSync } from "./managerSync.js";
export { StorageManagerAsync } from "./managerAsync.js";
export * from "./sqlite/index.js";
export * from "./sqliteAsync/index.js";

View File

@@ -1,2 +0,0 @@
export { SQLiteNodeBase } from "./node.js";
export type { SQLiteDatabaseDriver } from "./types.js";

View File

@@ -1,104 +0,0 @@
import {
type IncomingSyncStream,
type OutgoingSyncQueue,
type Peer,
cojsonInternals,
logger,
} from "cojson";
import { StorageManagerSync } from "../managerSync.js";
import { SQLiteClient } from "./client.js";
import { getSQLiteMigrationQueries } from "./sqliteMigrations.js";
import type { SQLiteDatabaseDriver } from "./types.js";
export class SQLiteNodeBase {
private readonly syncManager: StorageManagerSync;
private readonly dbClient: SQLiteClient;
constructor(
db: SQLiteDatabaseDriver,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
maxBlockingTime: number,
) {
this.dbClient = new SQLiteClient(db);
this.syncManager = new StorageManagerSync(this.dbClient, toLocalNode);
const processMessages = async () => {
let lastTimer = performance.now();
let runningTimer = false;
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
if (!runningTimer) {
runningTimer = true;
lastTimer = performance.now();
setTimeout(() => {
runningTimer = false;
}, 10);
}
this.syncManager.handleSyncMessage(msg);
// Since the DB APIs are synchronous there may be the case
// where a bulk of messages are processed without interruptions
// which may block other peers from sending messages.
// To avoid this we schedule a timer to downgrade the priority of the storage peer work
if (performance.now() - lastTimer > maxBlockingTime) {
lastTimer = performance.now();
await new Promise((resolve) => setTimeout(resolve, 0));
}
} catch (e) {
logger.error("Error reading from localNode, handling msg", {
msg,
err: e,
});
}
}
db.closeDb();
};
processMessages().catch((e) =>
logger.error("Error in processMessages in sqlite", { err: e }),
);
}
static create({
db,
localNodeName = "local",
maxBlockingTime = 500,
}: {
db: SQLiteDatabaseDriver;
localNodeName?: string;
maxBlockingTime?: number;
}): Peer {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "storage", crashOnClose: true },
);
const rows = db.query<{ user_version: string }>("PRAGMA user_version", []);
const userVersion = Number(rows[0]?.user_version) ?? 0;
const migrations = getSQLiteMigrationQueries(userVersion);
for (const migration of migrations) {
db.run(migration, []);
}
new SQLiteNodeBase(
db,
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
maxBlockingTime,
);
return { ...storageAsPeer, priority: 100 };
}
}

View File

@@ -1,2 +0,0 @@
export { SQLiteNodeBaseAsync } from "./node.js";
export type { SQLiteDatabaseDriverAsync } from "./types.js";

View File

@@ -1,115 +0,0 @@
import {
type IncomingSyncStream,
type OutgoingSyncQueue,
type Peer,
cojsonInternals,
logger,
} from "cojson";
import { StorageManagerAsync } from "../managerAsync.js";
import { getSQLiteMigrationQueries } from "../sqlite/sqliteMigrations.js";
import { SQLiteClientAsync } from "./client.js";
import type { SQLiteDatabaseDriverAsync } from "./types.js";
function createParallelOpsRunner() {
const ops = new Set<Promise<unknown>>();
return {
add: (op: Promise<unknown>) => {
ops.add(op);
op.finally(() => {
ops.delete(op);
});
},
wait() {
return Promise.race(ops);
},
get size() {
return ops.size;
},
};
}
export class SQLiteNodeBaseAsync {
private readonly syncManager: StorageManagerAsync;
private readonly dbClient: SQLiteClientAsync;
constructor(
db: SQLiteDatabaseDriverAsync,
fromLocalNode: IncomingSyncStream,
toLocalNode: OutgoingSyncQueue,
) {
this.dbClient = new SQLiteClientAsync(db);
this.syncManager = new StorageManagerAsync(this.dbClient, toLocalNode);
const processMessages = async () => {
const batch = createParallelOpsRunner();
for await (const msg of fromLocalNode) {
try {
if (msg === "Disconnected" || msg === "PingTimeout") {
throw new Error("Unexpected Disconnected message");
}
if (msg.action === "content") {
await this.syncManager.handleSyncMessage(msg);
} else {
batch.add(this.syncManager.handleSyncMessage(msg));
}
if (batch.size > 10) {
await batch.wait();
}
} catch (e) {
logger.error("Error reading from localNode, handling msg", {
msg,
err: e,
});
}
}
db.closeDb().catch((e) =>
logger.error("Error closing sqlite", { err: e }),
);
};
processMessages().catch((e) =>
logger.error("Error in processMessages in sqlite", { err: e }),
);
}
static async create({
db,
localNodeName = "local",
}: {
db: SQLiteDatabaseDriverAsync;
localNodeName?: string;
}): Promise<Peer> {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "storage", crashOnClose: true },
);
await db.initialize();
const rows = await db.query<{ user_version: string }>(
"PRAGMA user_version",
[],
);
const userVersion = Number(rows[0]?.user_version) ?? 0;
const migrations = getSQLiteMigrationQueries(userVersion);
for (const migration of migrations) {
await db.run(migration, []);
}
new SQLiteNodeBaseAsync(
db,
localNodeAsPeer.incoming,
localNodeAsPeer.outgoing,
);
return { ...storageAsPeer, priority: 100 };
}
}

View File

@@ -1,95 +0,0 @@
export const fixtures = {
co_zKwG8NyfZ8GXqcjDHY4NS3SbU2m: {
getContent: ({ after = 0 }: { after?: number }) => ({
action: "content",
id: "co_zKwG8NyfZ8GXqcjDHY4NS3SbU2m",
header: {
type: "comap",
ruleset: {
type: "group",
initialAdmin:
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy",
},
meta: {
type: "account",
},
createdAt: null,
uniqueness: null,
},
new: {
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy_session_zbcBS6rHy8kA":
{
after,
lastSignature:
"signature_z2kcFHUPe1qGFYDY4ayvvFR2unFc4jeYph93nSCSjZYS14vnGN4uAw7pKZx1PEhwnspJcDizMRbLaFC8v13i6S79A",
newTransactions: [
{
privacy: "trusting",
madeAt: 1732368535089,
changes:
'[{"key":"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy","op":"set","value":"admin"}]',
},
{
privacy: "trusting",
madeAt: 1732368535096,
changes:
'[{"key":"key_z2YMuLXEfXG44Z2jGk_for_sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy","op":"set","value":"sealed_UAIpJTby8EovZW6WPtAqdaczA2_r6PEWRBuEtLN93-Dh9xDJFaGUNTXK1Cck61tjvA3GoGn9EyQdNN2fU6tnmWP2M09a83dG41Q=="}]',
},
{
privacy: "trusting",
madeAt: 1732368535099,
changes:
'[{"key":"readKey","op":"set","value":"key_z2YMuLXEfXG44Z2jGk"}]',
},
],
},
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy_session_zXgW54i2cCNA":
{
after,
lastSignature:
"signature_z5FsinkJCpqZfozVBkEMSchCQarsAjvMYpWN4d227PZtqCiM7KRBNukND3B25Q73idBLdY2MsghbmYFz5JHXk3d4D",
newTransactions: [
{
privacy: "trusting",
madeAt: 1732368535113,
changes:
'[{"key":"profile","op":"set","value":"co_zMKhQJs5rAeGjta3JX2qEdBS6hS"}]',
},
],
},
},
priority: 0,
}),
known: {
action: "known",
id: "co_zKwG8NyfZ8GXqcjDHY4NS3SbU2m",
header: true,
sessions: {
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy_session_zbcBS6rHy8kA": 3,
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy_session_zXgW54i2cCNA": 1,
},
},
sessionRecords: [
{
bytesSinceLastSignature: 479,
coValue: 2,
lastIdx: 3,
lastSignature:
"signature_z2kcFHUPe1qGFYDY4ayvvFR2unFc4jeYph93nSCSjZYS14vnGN4uAw7pKZx1PEhwnspJcDizMRbLaFC8v13i6S79A",
rowID: 2,
sessionID:
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy_session_zbcBS6rHy8kA",
},
{
bytesSinceLastSignature: 71,
coValue: 2,
lastIdx: 1,
lastSignature:
"signature_z5FsinkJCpqZfozVBkEMSchCQarsAjvMYpWN4d227PZtqCiM7KRBNukND3B25Q73idBLdY2MsghbmYFz5JHXk3d4D",
rowID: 3,
sessionID:
"sealer_zRKetKBH6tdGP8poA2rV9JDejXqTyAmpusCT4jRcXa4m/signer_z6bcctDRiWxtgmuqLRR6rVhM54DA3xJ2pWCEs6DVf4PSy_session_zXgW54i2cCNA",
},
],
},
};

View File

@@ -1,184 +0,0 @@
import type { CojsonInternalTypes, SessionID, Stringified } from "cojson";
import { describe, expect, it } from "vitest";
import { getDependedOnCoValues } from "../syncUtils.js";
function getMockedSessionID(accountId?: `co_z${string}`) {
return `${accountId ?? getMockedCoValueId()}_session_z${Math.random().toString(36).substring(2, 15)}`;
}
function getMockedCoValueId() {
return `co_z${Math.random().toString(36).substring(2, 15)}` as const;
}
function generateNewContentMessage(
privacy: "trusting" | "private",
changes: any[],
accountId: `co_z${string}`,
) {
return {
action: "content",
id: getMockedCoValueId(),
new: {
[getMockedSessionID(accountId)]: {
after: 0,
lastSignature: "signature_z123",
newTransactions: [
{
privacy,
madeAt: 0,
changes: JSON.stringify(changes) as any,
},
],
},
},
priority: 0,
} as CojsonInternalTypes.NewContentMessage;
}
describe("getDependedOnCoValues", () => {
it("should return dependencies for group ruleset", () => {
const coValueRow = {
id: "co_test",
header: {
ruleset: {
type: "group",
},
},
} as any;
const accountId = getMockedCoValueId();
const result = getDependedOnCoValues(
coValueRow.header,
generateNewContentMessage(
"trusting",
[
{ op: "set", key: "co_zabc123", value: "test" },
{ op: "set", key: "parent_co_zdef456", value: "test" },
{ op: "set", key: "normal_key", value: "test" },
],
accountId,
),
);
expect(result).toEqual(new Set([accountId, "co_zabc123", "co_zdef456"]));
});
it("should not throw on malformed JSON", () => {
const coValueRow = {
id: "co_test",
header: {
ruleset: {
type: "group",
},
},
} as any;
const accountId = getMockedCoValueId();
const message = generateNewContentMessage(
"trusting",
[{ op: "set", key: "co_zabc123", value: "test" }],
accountId,
);
message.new["invalid_session" as SessionID] = {
after: 0,
lastSignature: "signature_z123",
newTransactions: [
{
privacy: "trusting",
madeAt: 0,
changes: "}{-:)" as Stringified<CojsonInternalTypes.JsonObject[]>,
},
],
};
const result = getDependedOnCoValues(coValueRow.header, message);
expect(result).toEqual(new Set([accountId, "co_zabc123"]));
});
it("should return dependencies for ownedByGroup ruleset", () => {
const groupId = getMockedCoValueId();
const coValueRow = {
id: "co_owner",
header: {
ruleset: {
type: "ownedByGroup",
group: groupId,
},
},
} as any;
const accountId = getMockedCoValueId();
const message = generateNewContentMessage(
"trusting",
[
{ op: "set", key: "co_zabc123", value: "test" },
{ op: "set", key: "parent_co_zdef456", value: "test" },
{ op: "set", key: "normal_key", value: "test" },
],
accountId,
);
message.new["invalid_session" as SessionID] = {
after: 0,
lastSignature: "signature_z123",
newTransactions: [],
};
const result = getDependedOnCoValues(coValueRow.header, message);
expect(result).toEqual(new Set([groupId, accountId]));
});
it("should return empty array for other ruleset types", () => {
const coValueRow = {
id: "co_test",
header: {
ruleset: {
type: "other",
},
},
} as any;
const accountId = getMockedCoValueId();
const result = getDependedOnCoValues(
coValueRow.header,
generateNewContentMessage(
"trusting",
[
{ op: "set", key: "co_zabc123", value: "test" },
{ op: "set", key: "parent_co_zdef456", value: "test" },
{ op: "set", key: "normal_key", value: "test" },
],
accountId,
),
);
expect(result).toEqual(new Set([accountId]));
});
it("should ignore non-trusting transactions in group ruleset", () => {
const coValueRow = {
id: "co_test",
header: {
ruleset: {
type: "group",
},
},
} as any;
const accountId = getMockedCoValueId();
const result = getDependedOnCoValues(
coValueRow.header,
generateNewContentMessage(
"private",
[{ op: "set", key: "co_zabc123", value: "test" }],
accountId,
),
);
expect(result).toEqual(new Set([accountId]));
});
});

View File

@@ -1,72 +0,0 @@
import type { CoValueCore, CojsonInternalTypes, SyncMessage } from "cojson";
function simplifySessions(msg: CojsonInternalTypes.CoValueKnownState) {
const count = Object.values(msg.sessions).reduce(
(acc: number, session: number) => acc + session,
0,
);
if (msg.header) {
return `header/${count}`;
}
return "empty";
}
function simplifyNewContent(
content: CojsonInternalTypes.NewContentMessage["new"],
) {
if (!content) {
return undefined;
}
return Object.values(content)
.map((c) => `After: ${c.after} New: ${c.newTransactions.length}`)
.join(" | ");
}
export function toSimplifiedMessages(
coValues: Record<string, CoValueCore>,
messages: {
from: "client" | "server" | "storage";
msg: SyncMessage;
}[],
) {
function getCoValue(id: string) {
for (const [name, coValue] of Object.entries(coValues)) {
if (coValue.id === id) {
return name;
}
}
return `unknown/${id}`;
}
function toDebugString(
from: "client" | "server" | "storage",
msg: SyncMessage,
) {
switch (msg.action) {
case "known":
return `${from} -> KNOWN ${msg.isCorrection ? "CORRECTION " : ""}${getCoValue(msg.id)} sessions: ${simplifySessions(msg)}`;
case "load":
return `${from} -> LOAD ${getCoValue(msg.id)} sessions: ${simplifySessions(msg)}`;
case "done":
return `${from} -> DONE ${getCoValue(msg.id)}`;
case "content":
return `${from} -> CONTENT ${getCoValue(msg.id)} header: ${Boolean(msg.header)} new: ${simplifyNewContent(msg.new)}`;
}
}
return messages.map((m) => toDebugString(m.from, m.msg));
}
export function debugMessages(
coValues: Record<string, CoValueCore>,
messages: {
from: "client" | "server" | "storage";
msg: SyncMessage;
}[],
) {
console.log(toSimplifiedMessages(coValues, messages));
}

View File

@@ -1,798 +0,0 @@
import { randomUUID } from "node:crypto";
import { unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { LocalNode, cojsonInternals } from "cojson";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { expect, onTestFinished, test, vi } from "vitest";
import { toSimplifiedMessages } from "./messagesTestUtils.js";
import { trackMessages, waitFor } from "./testUtils.js";
const Crypto = await WasmCrypto.create();
import Database, { type Database as DatabaseT } from "libsql";
import { StorageManagerAsync } from "../managerAsync.js";
import { SQLiteNodeBaseAsync } from "../sqliteAsync/node.js";
import type { SQLiteDatabaseDriverAsync } from "../sqliteAsync/types.js";
class LibSQLSqliteDriver implements SQLiteDatabaseDriverAsync {
private readonly db: DatabaseT;
constructor(filename: string) {
this.db = new Database(filename, {});
}
async initialize() {
await this.db.pragma("journal_mode = WAL");
}
async run(sql: string, params: unknown[]) {
this.db.prepare(sql).run(params);
}
async query<T>(sql: string, params: unknown[]): Promise<T[]> {
return this.db.prepare(sql).all(params) as T[];
}
async get<T>(sql: string, params: unknown[]): Promise<T | undefined> {
return this.db.prepare(sql).get(params) as T | undefined;
}
async transaction(callback: () => unknown) {
await this.run("BEGIN TRANSACTION", []);
try {
await callback();
await this.run("COMMIT", []);
} catch (error) {
await this.run("ROLLBACK", []);
}
}
async closeDb() {
this.db.close();
}
}
async function createSQLiteStorage(defaultDbPath?: string) {
const dbPath = defaultDbPath ?? join(tmpdir(), `test-${randomUUID()}.db`);
if (!defaultDbPath) {
onTestFinished(() => {
unlinkSync(dbPath);
});
}
const db = new LibSQLSqliteDriver(dbPath);
return {
peer: await SQLiteNodeBaseAsync.create({
db,
}),
dbPath,
db,
};
}
test("Should be able to initialize and load from empty DB", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node.syncManager.addPeer((await createSQLiteStorage()).peer);
await new Promise((resolve) => setTimeout(resolve, 200));
expect(node.syncManager.peers.storage).toBeDefined();
});
test("should sync and load data from storage", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node1Sync = trackMessages(node1);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const map = group.createMap();
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node1Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
]
`);
node1Sync.restore();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node2Sync = trackMessages(node2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(map2.get("hello")).toBe("world");
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node2Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
]
`);
node2Sync.restore();
});
test("should send an empty content message if there is no content", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node1Sync = trackMessages(node1);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const map = group.createMap();
await new Promise((resolve) => setTimeout(resolve, 200));
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node1Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: ",
"storage -> KNOWN Map sessions: header/0",
]
`);
node1Sync.restore();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node2Sync = trackMessages(node2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node2Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: ",
"client -> KNOWN Map sessions: header/0",
]
`);
node2Sync.restore();
});
test("should load dependencies correctly (group inheritance)", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node1Sync = trackMessages(node1);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const parentGroup = node1.createGroup();
group.extend(parentGroup);
const map = group.createMap();
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
ParentGroup: parentGroup.core,
},
node1Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"storage -> KNOWN ParentGroup sessions: header/4",
"client -> CONTENT Group header: true new: After: 0 New: 5",
"storage -> KNOWN Group sessions: header/5",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
]
`);
node1Sync.restore();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node2Sync = trackMessages(node2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
await node2.load(map.id);
expect(node2.expectCoValueLoaded(map.id)).toBeTruthy();
expect(node2.expectCoValueLoaded(group.id)).toBeTruthy();
expect(node2.expectCoValueLoaded(parentGroup.id)).toBeTruthy();
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
ParentGroup: parentGroup.core,
},
node2Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
]
`);
});
test("should not send the same dependency value twice", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node1Sync = trackMessages(node1);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const parentGroup = node1.createGroup();
group.extend(parentGroup);
const mapFromParent = parentGroup.createMap();
const map = group.createMap();
map.set("hello", "world");
mapFromParent.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
node1Sync.restore();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node2Sync = trackMessages(node2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
await node2.load(map.id);
await node2.load(mapFromParent.id);
expect(node2.expectCoValueLoaded(map.id)).toBeTruthy();
expect(node2.expectCoValueLoaded(mapFromParent.id)).toBeTruthy();
expect(node2.expectCoValueLoaded(group.id)).toBeTruthy();
expect(node2.expectCoValueLoaded(parentGroup.id)).toBeTruthy();
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
ParentGroup: parentGroup.core,
MapFromParent: mapFromParent.core,
},
node2Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT ParentGroup header: true new: After: 0 New: 4",
"client -> KNOWN ParentGroup sessions: header/4",
"storage -> CONTENT Group header: true new: After: 0 New: 5",
"client -> KNOWN Group sessions: header/5",
"storage -> CONTENT Map header: true new: After: 0 New: 1",
"client -> KNOWN Map sessions: header/1",
"client -> LOAD MapFromParent sessions: empty",
"storage -> CONTENT MapFromParent header: true new: After: 0 New: 1",
"client -> KNOWN MapFromParent sessions: header/1",
]
`);
});
test("should recover from data loss", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node1Sync = trackMessages(node1);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const map = group.createMap();
map.set("0", 0);
await new Promise((resolve) => setTimeout(resolve, 200));
const mock = vi
.spyOn(StorageManagerAsync.prototype, "handleSyncMessage")
.mockImplementation(() => Promise.resolve());
map.set("1", 1);
map.set("2", 2);
await new Promise((resolve) => setTimeout(resolve, 200));
mock.mockReset();
map.set("3", 3);
await new Promise((resolve) => setTimeout(resolve, 200));
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node1Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> CONTENT Group header: true new: After: 0 New: 3",
"storage -> KNOWN Group sessions: header/3",
"client -> CONTENT Map header: true new: After: 0 New: 1",
"storage -> KNOWN Map sessions: header/1",
"client -> CONTENT Map header: false new: After: 3 New: 1",
"storage -> KNOWN CORRECTION Map sessions: header/1",
"client -> CONTENT Map header: false new: After: 1 New: 3",
"storage -> KNOWN Map sessions: header/4",
]
`);
node1Sync.restore();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node2Sync = trackMessages(node2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(map2.toJSON()).toEqual({
"0": 0,
"1": 1,
"2": 2,
"3": 3,
});
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node2Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 4",
"client -> KNOWN Map sessions: header/4",
]
`);
});
test("should recover missing dependencies from storage", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const account = LocalNode.internalCreateAccount({
crypto: Crypto,
});
const node1 = account.core.node;
const serverNode = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const [serverPeer, clientPeer] = cojsonInternals.connectedPeers(
node1.agentSecret,
serverNode.agentSecret,
{
peer1role: "server",
peer2role: "client",
},
);
node1.syncManager.addPeer(serverPeer);
serverNode.syncManager.addPeer(clientPeer);
const handleSyncMessage = StorageManagerAsync.prototype.handleSyncMessage;
const mock = vi
.spyOn(StorageManagerAsync.prototype, "handleSyncMessage")
.mockImplementation(function (this: StorageManagerAsync, msg) {
if (
msg.action === "content" &&
[group.core.id, account.core.id].includes(msg.id)
) {
return Promise.resolve();
}
return handleSyncMessage.call(this, msg);
});
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
group.addMember("everyone", "writer");
const map = group.createMap();
map.set("0", 0);
mock.mockReset();
await new Promise((resolve) => setTimeout(resolve, 200));
const node2 = new LocalNode(
Crypto.newRandomAgentSecret(),
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const [serverPeer2, clientPeer2] = cojsonInternals.connectedPeers(
node1.agentSecret,
serverNode.agentSecret,
{
peer1role: "server",
peer2role: "client",
},
);
node2.syncManager.addPeer(serverPeer2);
serverNode.syncManager.addPeer(clientPeer2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(map2.toJSON()).toEqual({
"0": 0,
});
});
test("should sync multiple sessions in a single content message", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const map = group.createMap();
map.set("hello", "world");
await new Promise((resolve) => setTimeout(resolve, 200));
node1.gracefulShutdown();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
node2.syncManager.addPeer((await createSQLiteStorage(dbPath)).peer);
const map2 = await node2.load(map.id);
if (map2 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(map2.get("hello")).toBe("world");
map2.set("hello", "world2");
await map2.core.waitForSync();
node2.gracefulShutdown();
const node3 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node3Sync = trackMessages(node3);
node3.syncManager.addPeer((await createSQLiteStorage(dbPath)).peer);
const map3 = await node3.load(map.id);
if (map3 === "unavailable") {
throw new Error("Map is unavailable");
}
expect(map3.get("hello")).toBe("world2");
expect(
toSimplifiedMessages(
{
Map: map.core,
Group: group.core,
},
node3Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 1 | After: 0 New: 1",
"client -> KNOWN Map sessions: header/2",
]
`);
node3Sync.restore();
});
test("large coValue upload streaming", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const { peer, dbPath } = await createSQLiteStorage();
node1.syncManager.addPeer(peer);
const group = node1.createGroup();
const largeMap = group.createMap();
const dataSize = 1 * 1024 * 200;
const chunkSize = 1024; // 1KB chunks
const chunks = dataSize / chunkSize;
const value = "a".repeat(chunkSize);
for (let i = 0; i < chunks; i++) {
const key = `key${i}`;
largeMap.set(key, value, "trusting");
}
await largeMap.core.waitForSync();
node1.gracefulShutdown();
const node2 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const node2Sync = trackMessages(node2);
const { peer: peer2 } = await createSQLiteStorage(dbPath);
node2.syncManager.addPeer(peer2);
const largeMapOnNode2 = await node2.load(largeMap.id);
if (largeMapOnNode2 === "unavailable") {
throw new Error("Map is unavailable");
}
await waitFor(() => {
expect(largeMapOnNode2.core.knownState()).toEqual(
largeMap.core.knownState(),
);
return true;
});
expect(
toSimplifiedMessages(
{
Map: largeMap.core,
Group: group.core,
},
node2Sync.messages,
),
).toMatchInlineSnapshot(`
[
"client -> LOAD Map sessions: empty",
"storage -> KNOWN Map sessions: header/200",
"storage -> CONTENT Group header: true new: After: 0 New: 3",
"client -> KNOWN Group sessions: header/3",
"storage -> CONTENT Map header: true new: After: 0 New: 97",
"client -> KNOWN Map sessions: header/97",
"storage -> CONTENT Map header: true new: After: 97 New: 97",
"client -> KNOWN Map sessions: header/194",
"storage -> CONTENT Map header: true new: After: 194 New: 6",
"client -> KNOWN Map sessions: header/200",
]
`);
});
test("should close the db when the node is closed", async () => {
const agentSecret = Crypto.newRandomAgentSecret();
const node1 = new LocalNode(
agentSecret,
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
Crypto,
);
const { peer, db } = await createSQLiteStorage();
const spy = vi.spyOn(db, "closeDb");
node1.syncManager.addPeer(peer);
await new Promise((resolve) => setTimeout(resolve, 10));
expect(spy).not.toHaveBeenCalled();
node1.gracefulShutdown();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(spy).toHaveBeenCalled();
});

View File

@@ -1,245 +0,0 @@
import {
type Mocked,
afterEach,
beforeEach,
describe,
expect,
test,
vi,
} from "vitest";
import type {
CojsonInternalTypes,
OutgoingSyncQueue,
SessionID,
SyncMessage,
} from "cojson";
import { StorageManagerAsync as SyncManager } from "../managerAsync.js";
import { getDependedOnCoValues } from "../syncUtils.js";
import type { DBClientInterfaceAsync as DBClientInterface } from "../types.js";
import { fixtures } from "./fixtureMessages.js";
type RawCoID = CojsonInternalTypes.RawCoID;
type NewContentMessage = CojsonInternalTypes.NewContentMessage;
vi.mock("../syncUtils");
const coValueIdToLoad = "co_zKwG8NyfZ8GXqcjDHY4NS3SbU2m";
const createEmptyLoadMsg = (id: string) =>
({
action: "load",
id,
header: false,
sessions: {},
}) as SyncMessage;
const sessionsData = fixtures[coValueIdToLoad].sessionRecords;
const coValueHeader = fixtures[coValueIdToLoad].getContent({ after: 0 }).header;
const incomingContentMessage = fixtures[coValueIdToLoad].getContent({
after: 0,
}) as SyncMessage;
describe("DB sync manager", () => {
let syncManager: SyncManager;
const queue: OutgoingSyncQueue = {} as unknown as OutgoingSyncQueue;
const DBClient = vi.fn();
DBClient.prototype.getCoValue = vi.fn();
DBClient.prototype.getCoValueSessions = vi.fn();
DBClient.prototype.getSingleCoValueSession = vi.fn();
DBClient.prototype.getNewTransactionInSession = vi.fn();
DBClient.prototype.addSessionUpdate = vi.fn();
DBClient.prototype.addTransaction = vi.fn();
DBClient.prototype.transaction = vi.fn((callback) => callback());
beforeEach(async () => {
const idbClient = new DBClient() as unknown as Mocked<DBClientInterface>;
syncManager = new SyncManager(idbClient, queue);
syncManager.sendStateMessage = vi.fn();
// No dependencies found
vi.mocked(getDependedOnCoValues).mockReturnValue(new Set());
});
afterEach(() => {
vi.clearAllMocks();
});
test("Incoming known messages are not processed", async () => {
await syncManager.handleSyncMessage({ action: "known" } as SyncMessage);
expect(syncManager.sendStateMessage).not.toBeCalled();
});
describe("Handle load incoming message", () => {
test("sends empty known message for unknown coValue", async () => {
const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
DBClient.prototype.getCoValue.mockResolvedValueOnce(undefined);
await syncManager.handleSyncMessage(loadMsg);
expect(syncManager.sendStateMessage).toBeCalledWith({
action: "known",
header: false,
id: coValueIdToLoad,
sessions: {},
});
});
test("Sends known and content message for known coValue with no sessions", async () => {
const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
DBClient.prototype.getCoValue.mockResolvedValueOnce({
id: coValueIdToLoad,
header: coValueHeader,
rowID: 3,
});
DBClient.prototype.getCoValueSessions.mockResolvedValueOnce([]);
await syncManager.handleSyncMessage(loadMsg);
expect(syncManager.sendStateMessage).toBeCalledTimes(1);
expect(syncManager.sendStateMessage).toBeCalledWith({
action: "content",
header: expect.objectContaining({
type: expect.any(String),
ruleset: expect.any(Object),
}),
id: coValueIdToLoad,
new: {},
priority: 0,
});
});
test("Sends messages for unique coValue dependencies only, leaving out circular dependencies", async () => {
const loadMsg = createEmptyLoadMsg(coValueIdToLoad);
const dependency1 = "co_zMKhQJs5rAeGjta3JX2qEdBS6hS";
const dependency2 = "co_zP51HdyAVCuRY9ptq5iu8DhMyAb";
const dependency3 = "co_zGyBniuJmKkcirCKYrccWpjQEFY";
const dependenciesTreeWithLoop: Record<RawCoID, RawCoID[]> = {
[coValueIdToLoad]: [dependency1, dependency2],
[dependency1]: [],
[dependency2]: [coValueIdToLoad, dependency3],
[dependency3]: [dependency1],
};
DBClient.prototype.getCoValue.mockImplementation(
(coValueId: RawCoID) => ({
id: coValueId,
header: coValueHeader,
rowID: 3,
}),
);
DBClient.prototype.getCoValueSessions.mockResolvedValue([]);
// Fetch dependencies of the current dependency for the future recursion iterations
vi.mocked(getDependedOnCoValues).mockImplementation(
(_, msg) => new Set(dependenciesTreeWithLoop[msg.id] || []),
);
await syncManager.handleSyncMessage(loadMsg);
// We send out pairs (known + content) messages only FOUR times - as many as the coValues number
// and less than amount of interconnected dependencies to loop through in dependenciesTreeWithLoop
expect(syncManager.sendStateMessage).toBeCalledTimes(4);
const contentExpected = {
action: "content",
header: expect.any(Object),
new: {},
priority: 0,
};
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(1, {
...contentExpected,
id: dependency1,
});
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(2, {
...contentExpected,
id: dependency3,
});
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(3, {
...contentExpected,
id: dependency2,
});
expect(syncManager.sendStateMessage).toHaveBeenNthCalledWith(4, {
...contentExpected,
id: coValueIdToLoad,
});
});
});
describe("Handle content incoming message", () => {
test("Sends correction message for unknown coValue", async () => {
DBClient.prototype.getCoValue.mockResolvedValueOnce(undefined);
await syncManager.handleSyncMessage({
...incomingContentMessage,
header: undefined,
} as SyncMessage);
expect(syncManager.sendStateMessage).toBeCalledWith({
action: "known",
header: false,
id: coValueIdToLoad,
isCorrection: true,
sessions: {},
});
});
test("Saves new transaction and sends an ack message as response", async () => {
DBClient.prototype.getCoValue.mockResolvedValueOnce({
id: coValueIdToLoad,
header: coValueHeader,
rowID: 3,
});
DBClient.prototype.getCoValueSessions.mockResolvedValueOnce([]);
const msg = {
...incomingContentMessage,
header: undefined,
} as NewContentMessage;
await syncManager.handleSyncMessage(msg);
const incomingTxCount = Object.keys(msg.new).reduce(
(acc, sessionID) =>
acc + msg.new[sessionID as SessionID]!.newTransactions.length,
0,
);
expect(DBClient.prototype.addTransaction).toBeCalledTimes(
incomingTxCount,
);
expect(syncManager.sendStateMessage).toBeCalledWith({
action: "known",
header: true,
id: coValueIdToLoad,
sessions: expect.any(Object),
});
});
test("Sends correction message when peer sends a message far ahead of our state due to invalid assumption", async () => {
DBClient.prototype.getCoValue.mockResolvedValueOnce({
id: coValueIdToLoad,
header: coValueHeader,
rowID: 3,
});
DBClient.prototype.getCoValueSessions.mockResolvedValueOnce(sessionsData);
const farAheadContentMessage = fixtures[coValueIdToLoad].getContent({
after: 10000,
});
await syncManager.handleSyncMessage(
farAheadContentMessage as SyncMessage,
);
expect(syncManager.sendStateMessage).toBeCalledWith({
action: "known",
header: true,
id: coValueIdToLoad,
isCorrection: true,
sessions: expect.any(Object),
});
});
});
});

View File

@@ -1,73 +0,0 @@
import type { LocalNode, SyncMessage } from "cojson";
import { onTestFinished } from "vitest";
import { StorageManagerAsync } from "../managerAsync";
export function trackMessages(node: LocalNode) {
const messages: {
from: "client" | "server" | "storage";
msg: SyncMessage;
}[] = [];
const originalHandleSyncMessage =
StorageManagerAsync.prototype.handleSyncMessage;
const originalNodeSyncMessage = node.syncManager.handleSyncMessage;
StorageManagerAsync.prototype.handleSyncMessage = async function (msg) {
messages.push({
from: "client",
msg,
});
return originalHandleSyncMessage.call(this, msg);
};
node.syncManager.handleSyncMessage = async function (msg, peer) {
messages.push({
from: "storage",
msg,
});
return originalNodeSyncMessage.call(this, msg, peer);
};
const restore = () => {
StorageManagerAsync.prototype.handleSyncMessage = originalHandleSyncMessage;
node.syncManager.handleSyncMessage = originalNodeSyncMessage;
};
onTestFinished(() => {
restore();
});
return {
messages,
restore,
};
}
export function waitFor(
callback: () => boolean | undefined | Promise<boolean | undefined>,
) {
return new Promise<void>((resolve, reject) => {
const checkPassed = async () => {
try {
return { ok: await callback(), error: null };
} catch (error) {
return { ok: false, error };
}
};
let retries = 0;
const interval = setInterval(async () => {
const { ok, error } = await checkPassed();
if (ok !== false) {
clearInterval(interval);
resolve();
}
if (++retries > 10) {
clearInterval(interval);
reject(error);
}
}, 100);
});
}

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"module": "esnext",
"target": "ES2020",
"moduleResolution": "bundler",
"moduleDetection": "force",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true
},
"include": ["./src/**/*"]
}

View File

@@ -1,5 +1,43 @@
# cojson-transport-nodejs-ws
## 0.15.10
### Patch Changes
- cojson@0.15.10
## 0.15.9
### Patch Changes
- Updated dependencies [27b4837]
- Updated dependencies [2776263]
- cojson@0.15.9
## 0.15.8
### Patch Changes
- cojson@0.15.8
## 0.15.7
### Patch Changes
- cojson@0.15.7
## 0.15.6
### Patch Changes
- cojson@0.15.6
## 0.15.5
### Patch Changes
- cojson@0.15.5
## 0.15.4
### Patch Changes

View File

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

View File

@@ -1,21 +1,106 @@
import type { SyncMessage } from "cojson";
import type { DisconnectedError, SyncMessage } from "cojson";
import type { Peer } from "cojson";
import {
type CojsonInternalTypes,
PriorityBasedMessageQueue,
cojsonInternals,
logger,
} from "cojson";
import { addMessageToBacklog } from "./serialization.js";
import type { AnyWebSocket } from "./types.js";
import {
hasWebSocketTooMuchBufferedData,
isWebSocketOpen,
waitForWebSocketBufferedAmount,
waitForWebSocketOpen,
} from "./utils.js";
const { CO_VALUE_PRIORITY } = cojsonInternals;
export const MAX_OUTGOING_MESSAGES_CHUNK_BYTES = 25_000;
export class BatchedOutgoingMessages {
export class BatchedOutgoingMessages
implements CojsonInternalTypes.OutgoingPeerChannel
{
private backlog = "";
private timeout: ReturnType<typeof setTimeout> | null = null;
private queue: PriorityBasedMessageQueue;
private processing = false;
private closed = false;
constructor(private send: (messages: string) => void) {}
constructor(
private websocket: AnyWebSocket,
private batching: boolean,
peerRole: Peer["role"],
) {
this.queue = new PriorityBasedMessageQueue(
CO_VALUE_PRIORITY.HIGH,
"outgoing",
{
peerRole: peerRole,
},
);
}
push(msg: SyncMessage) {
const payload = addMessageToBacklog(this.backlog, msg);
if (this.timeout) {
clearTimeout(this.timeout);
push(msg: SyncMessage | DisconnectedError) {
if (msg === "Disconnected") {
this.close();
return;
}
this.queue.push(msg);
if (this.processing) {
return;
}
this.processQueue().catch((e) => {
logger.error("Error while processing sendMessage queue", { err: e });
});
}
private async processQueue() {
const { websocket } = this;
this.processing = true;
// Delay the initiation of the queue processing to accumulate messages
// before sending them, in order to do prioritization and batching
await new Promise<void>((resolve) => setTimeout(resolve, 5));
let msg = this.queue.pull();
while (msg) {
if (this.closed) {
return;
}
if (!isWebSocketOpen(websocket)) {
await waitForWebSocketOpen(websocket);
}
if (hasWebSocketTooMuchBufferedData(websocket)) {
await waitForWebSocketBufferedAmount(websocket);
}
if (isWebSocketOpen(websocket)) {
this.processMessage(msg);
msg = this.queue.pull();
}
}
this.sendMessagesInBulk();
this.processing = false;
}
processMessage(msg: SyncMessage) {
if (!this.batching) {
this.websocket.send(JSON.stringify(msg));
return;
}
const payload = addMessageToBacklog(this.backlog, msg);
const maxChunkSizeReached =
payload.length >= MAX_OUTGOING_MESSAGES_CHUNK_BYTES;
const backlogExists = this.backlog.length > 0;
@@ -23,26 +108,49 @@ export class BatchedOutgoingMessages {
if (maxChunkSizeReached && backlogExists) {
this.sendMessagesInBulk();
this.backlog = addMessageToBacklog("", msg);
this.timeout = setTimeout(() => {
this.sendMessagesInBulk();
}, 0);
} else if (maxChunkSizeReached) {
this.backlog = payload;
this.sendMessagesInBulk();
} else {
this.backlog = payload;
this.timeout = setTimeout(() => {
this.sendMessagesInBulk();
}, 0);
}
}
sendMessagesInBulk() {
this.send(this.backlog);
this.backlog = "";
if (this.backlog.length > 0 && isWebSocketOpen(this.websocket)) {
this.websocket.send(this.backlog);
this.backlog = "";
}
}
setBatching(enabled: boolean) {
this.batching = enabled;
}
private closeListeners = new Set<() => void>();
onClose(callback: () => void) {
this.closeListeners.add(callback);
}
close() {
if (this.closed) {
return;
}
let msg = this.queue.pull();
while (msg) {
this.processMessage(msg);
msg = this.queue.pull();
}
this.closed = true;
this.sendMessagesInBulk();
for (const listener of this.closeListeners) {
listener();
}
this.closeListeners.clear();
}
}

View File

@@ -1,17 +1,9 @@
import {
type DisconnectedError,
type Peer,
type PingTimeoutError,
type SyncMessage,
cojsonInternals,
logger,
} from "cojson";
import { type Peer, type SyncMessage, cojsonInternals, logger } from "cojson";
import { BatchedOutgoingMessages } from "./BatchedOutgoingMessages.js";
import { deserializeMessages } from "./serialization.js";
import type { AnyWebSocket } from "./types.js";
export const BUFFER_LIMIT = 100_000;
export const BUFFER_LIMIT_POLLING_INTERVAL = 10;
const { ConnectedPeerChannel } = cojsonInternals;
export type CreateWebSocketPeerOpts = {
id: string;
@@ -52,70 +44,6 @@ function createPingTimeoutListener(
};
}
function waitForWebSocketOpen(websocket: AnyWebSocket) {
return new Promise<void>((resolve) => {
if (websocket.readyState === 1) {
resolve();
} else {
websocket.addEventListener("open", () => resolve(), { once: true });
}
});
}
function createOutgoingMessagesManager(
websocket: AnyWebSocket,
batchingByDefault: boolean,
) {
let closed = false;
const outgoingMessages = new BatchedOutgoingMessages((messages) => {
if (websocket.readyState === 1) {
websocket.send(messages);
}
});
let batchingEnabled = batchingByDefault;
async function sendMessage(msg: SyncMessage) {
if (closed) {
return Promise.reject(new Error("WebSocket closed"));
}
if (websocket.readyState !== 1) {
await waitForWebSocketOpen(websocket);
}
while (
websocket.bufferedAmount > BUFFER_LIMIT &&
websocket.readyState === 1
) {
await new Promise<void>((resolve) =>
setTimeout(resolve, BUFFER_LIMIT_POLLING_INTERVAL),
);
}
if (websocket.readyState !== 1) {
return;
}
if (!batchingEnabled) {
websocket.send(JSON.stringify(msg));
} else {
outgoingMessages.push(msg);
}
}
return {
sendMessage,
setBatchingEnabled(enabled: boolean) {
batchingEnabled = enabled;
},
close() {
closed = true;
outgoingMessages.close();
},
};
}
function createClosedEventEmitter(callback = () => {}) {
let disconnected = false;
@@ -137,17 +65,11 @@ export function createWebSocketPeer({
onSuccess,
onClose,
}: CreateWebSocketPeerOpts): Peer {
const incoming = new cojsonInternals.Channel<
SyncMessage | DisconnectedError | PingTimeoutError
>();
const incoming = new ConnectedPeerChannel();
const emitClosedEvent = createClosedEventEmitter(onClose);
function handleClose() {
incoming
.push("Disconnected")
.catch((e) =>
logger.error("Error while pushing disconnect msg", { err: e }),
);
incoming.push("Disconnected");
emitClosedEvent();
}
@@ -166,18 +88,19 @@ export function createWebSocketPeer({
expectPings,
pingTimeout,
() => {
incoming
.push("PingTimeout")
.catch((e) =>
logger.error("Error while pushing ping timeout", { err: e }),
);
incoming.push("Disconnected");
logger.error("Ping timeout from peer", {
peerId: id,
peerRole: role,
});
emitClosedEvent();
},
);
const outgoingMessages = createOutgoingMessagesManager(
const outgoing = new BatchedOutgoingMessages(
websocket,
batchingByDefault,
role,
);
let isFirstMessage = true;
@@ -206,50 +129,42 @@ export function createWebSocketPeer({
if (messages.length > 1) {
// If more than one message is received, the other peer supports batching
outgoingMessages.setBatchingEnabled(true);
outgoing.setBatching(true);
}
for (const msg of messages) {
if (msg && "action" in msg) {
incoming
.push(msg)
.catch((e) =>
logger.error("Error while pushing incoming msg", { err: e }),
);
incoming.push(msg);
}
}
}
websocket.addEventListener("message", handleIncomingMsg);
outgoing.onClose(() => {
websocket.removeEventListener("message", handleIncomingMsg);
websocket.removeEventListener("close", handleClose);
pingTimeoutListener.clear();
emitClosedEvent();
if (websocket.readyState === 0) {
websocket.addEventListener(
"open",
function handleClose() {
websocket.close();
},
{ once: true },
);
} else if (websocket.readyState === 1) {
websocket.close();
}
});
return {
id,
incoming,
outgoing: {
push: outgoingMessages.sendMessage,
close() {
outgoingMessages.close();
websocket.removeEventListener("message", handleIncomingMsg);
websocket.removeEventListener("close", handleClose);
pingTimeoutListener.clear();
emitClosedEvent();
if (websocket.readyState === 0) {
websocket.addEventListener(
"open",
function handleClose() {
websocket.close();
},
{ once: true },
);
} else if (websocket.readyState === 1) {
websocket.close();
}
},
},
outgoing,
role,
crashOnClose: false,
deletePeerStateOnClose,
};
}

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