jazz-tools 0.18.8 → 0.18.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.svelte-kit/__package__/jazz.svelte.d.ts +1 -1
  2. package/.svelte-kit/__package__/jazz.svelte.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/jazz.svelte.js +19 -26
  4. package/.turbo/turbo-build.log +43 -43
  5. package/CHANGELOG.md +31 -0
  6. package/dist/better-auth/auth/client.d.ts +1 -1
  7. package/dist/better-auth/auth/client.d.ts.map +1 -1
  8. package/dist/better-auth/auth/client.js.map +1 -1
  9. package/dist/{chunk-QF3R3C4N.js → chunk-RQHJFPIB.js} +56 -25
  10. package/dist/{chunk-QF3R3C4N.js.map → chunk-RQHJFPIB.js.map} +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/react/hooks.d.ts +1 -1
  13. package/dist/react/hooks.d.ts.map +1 -1
  14. package/dist/react/index.d.ts +1 -1
  15. package/dist/react/index.d.ts.map +1 -1
  16. package/dist/react/index.js +3 -1
  17. package/dist/react/index.js.map +1 -1
  18. package/dist/react-core/hooks.d.ts +56 -0
  19. package/dist/react-core/hooks.d.ts.map +1 -1
  20. package/dist/react-core/index.js +20 -0
  21. package/dist/react-core/index.js.map +1 -1
  22. package/dist/react-core/tests/useAccountWithSelector.test.d.ts +2 -0
  23. package/dist/react-core/tests/useAccountWithSelector.test.d.ts.map +1 -0
  24. package/dist/react-native-core/hooks.d.ts +1 -1
  25. package/dist/react-native-core/hooks.d.ts.map +1 -1
  26. package/dist/react-native-core/index.js +3 -1
  27. package/dist/react-native-core/index.js.map +1 -1
  28. package/dist/svelte/jazz.svelte.d.ts +1 -1
  29. package/dist/svelte/jazz.svelte.d.ts.map +1 -1
  30. package/dist/svelte/jazz.svelte.js +19 -26
  31. package/dist/testing.js +1 -1
  32. package/dist/tools/implementation/ContextManager.d.ts +2 -0
  33. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  34. package/dist/worker/index.d.ts +26 -0
  35. package/dist/worker/index.d.ts.map +1 -1
  36. package/dist/worker/index.js +29 -2
  37. package/dist/worker/index.js.map +1 -1
  38. package/package.json +4 -4
  39. package/src/better-auth/auth/client.ts +1 -1
  40. package/src/better-auth/auth/tests/client.test.ts +229 -0
  41. package/src/react/hooks.tsx +1 -0
  42. package/src/react/index.ts +1 -0
  43. package/src/react-core/hooks.ts +84 -0
  44. package/src/react-core/tests/useAccountWithSelector.test.ts +411 -0
  45. package/src/react-native-core/hooks.tsx +1 -0
  46. package/src/svelte/jazz.svelte.ts +23 -27
  47. package/src/tools/implementation/ContextManager.ts +75 -32
  48. package/src/tools/tests/ContextManager.test.ts +252 -0
  49. package/src/worker/index.ts +28 -1
@@ -540,4 +540,256 @@ describe("ContextManager", () => {
540
540
  manager.register(secret, { name: "Test User" }),
541
541
  ).rejects.toThrow("Props required");
542
542
  });
543
+
544
+ describe("Race condition handling", () => {
545
+ test("prevents concurrent authentication attempts", async () => {
546
+ const account = await createJazzTestAccount();
547
+ const onAnonymousAccountDiscarded = vi.fn();
548
+
549
+ // Create initial anonymous context
550
+ await manager.createContext({ onAnonymousAccountDiscarded });
551
+
552
+ const credentials = {
553
+ accountID: account.$jazz.id,
554
+ accountSecret: account.$jazz.localNode.getCurrentAgent().agentSecret,
555
+ provider: "test",
556
+ };
557
+
558
+ // Start multiple concurrent authentication attempts
559
+ const promises = [];
560
+ for (let i = 0; i < 5; i++) {
561
+ promises.push(manager.authenticate(credentials));
562
+ }
563
+
564
+ await Promise.all(promises);
565
+
566
+ // onAnonymousAccountDiscarded should only be called once despite multiple authenticate calls
567
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(1);
568
+ });
569
+
570
+ test("prevents concurrent registration attempts", async () => {
571
+ const onAnonymousAccountDiscarded = vi.fn();
572
+ await manager.createContext({ onAnonymousAccountDiscarded });
573
+
574
+ const secret = Crypto.newRandomAgentSecret();
575
+
576
+ // Start multiple concurrent registration attempts
577
+ const promises = [];
578
+ for (let i = 0; i < 3; i++) {
579
+ promises.push(manager.register(secret, { name: "Test User" }));
580
+ }
581
+
582
+ await Promise.allSettled(promises);
583
+
584
+ // onAnonymousAccountDiscarded should only be called once
585
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(1);
586
+ });
587
+
588
+ test("allows authentication after logout", async () => {
589
+ const account = await createJazzTestAccount();
590
+ const onAnonymousAccountDiscarded = vi.fn();
591
+
592
+ // Create initial context and authenticate
593
+ await manager.createContext({ onAnonymousAccountDiscarded });
594
+
595
+ await manager.authenticate({
596
+ accountID: account.$jazz.id,
597
+ accountSecret: account.$jazz.localNode.getCurrentAgent().agentSecret,
598
+ provider: "test",
599
+ });
600
+
601
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(1);
602
+
603
+ await manager.logOut();
604
+
605
+ // Should be able to authenticate again
606
+ const account2 = await createJazzTestAccount();
607
+
608
+ await manager.authenticate({
609
+ accountID: account2.$jazz.id,
610
+ accountSecret: account2.$jazz.localNode.getCurrentAgent().agentSecret,
611
+ provider: "test",
612
+ });
613
+
614
+ // Should be called again since we logged out and reset the migration state
615
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(2);
616
+ });
617
+
618
+ test("handles authentication in progress correctly", async () => {
619
+ const account = await createJazzTestAccount();
620
+ const onAnonymousAccountDiscarded = vi.fn();
621
+
622
+ await manager.createContext({ onAnonymousAccountDiscarded });
623
+
624
+ const credentials = {
625
+ accountID: account.$jazz.id,
626
+ accountSecret: account.$jazz.localNode.getCurrentAgent().agentSecret,
627
+ provider: "test",
628
+ };
629
+
630
+ // Start first authentication
631
+ const firstAuth = manager.authenticate(credentials);
632
+
633
+ // Try to authenticate while first is in progress
634
+ await manager.authenticate(credentials);
635
+
636
+ // Wait for first to complete
637
+ await firstAuth;
638
+
639
+ // Should only have been called once
640
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(1);
641
+ });
642
+
643
+ test("prevents duplicate onAnonymousAccountDiscarded calls during complex migration", async () => {
644
+ const AccountRoot = co.map({
645
+ value: z.string(),
646
+ get transferredRoot(): co.Optional<typeof AccountRoot> {
647
+ return co.optional(AccountRoot);
648
+ },
649
+ });
650
+
651
+ const CustomAccount = co
652
+ .account({
653
+ root: AccountRoot,
654
+ profile: co.profile(),
655
+ })
656
+ .withMigration(async (account) => {
657
+ account.$jazz.set(
658
+ "root",
659
+ AccountRoot.create(
660
+ { value: "Hello" },
661
+ Group.create(this).makePublic(),
662
+ ),
663
+ );
664
+ });
665
+
666
+ const customManager = new TestJazzContextManager<
667
+ InstanceOfSchema<typeof CustomAccount>
668
+ >();
669
+
670
+ const onAnonymousAccountDiscarded = vi
671
+ .fn()
672
+ .mockImplementation(async (anonymousAccount) => {
673
+ // Simulate complex migration work
674
+ await new Promise((resolve) => setTimeout(resolve, 10));
675
+
676
+ const anonymousAccountWithRoot =
677
+ await anonymousAccount.$jazz.ensureLoaded({
678
+ resolve: { root: true },
679
+ });
680
+
681
+ const me = await CustomAccount.getMe().$jazz.ensureLoaded({
682
+ resolve: { root: true },
683
+ });
684
+
685
+ me.root.$jazz.set("transferredRoot", anonymousAccountWithRoot.root);
686
+ });
687
+
688
+ await customManager.createContext({
689
+ AccountSchema: coValueClassFromCoValueClassOrSchema(CustomAccount),
690
+ onAnonymousAccountDiscarded,
691
+ });
692
+
693
+ const account = (
694
+ customManager.getCurrentValue() as JazzAuthContext<
695
+ InstanceOfSchema<typeof CustomAccount>
696
+ >
697
+ ).me;
698
+
699
+ // Start multiple concurrent authentication attempts
700
+ const promises = [];
701
+ for (let i = 0; i < 3; i++) {
702
+ promises.push(
703
+ customManager.authenticate({
704
+ accountID: account.$jazz.id,
705
+ accountSecret:
706
+ account.$jazz.localNode.getCurrentAgent().agentSecret,
707
+ provider: "test",
708
+ }),
709
+ );
710
+ }
711
+
712
+ await Promise.all(promises);
713
+
714
+ // Migration should only happen once
715
+ expect(onAnonymousAccountDiscarded).toHaveBeenCalledTimes(1);
716
+
717
+ const me = await CustomAccount.getMe().$jazz.ensureLoaded({
718
+ resolve: { root: { transferredRoot: true } },
719
+ });
720
+
721
+ expect(me.root.transferredRoot?.value).toBe("Hello");
722
+ });
723
+
724
+ test("fails fast when trying to authenticate different accounts concurrently", async () => {
725
+ const account1 = await createJazzTestAccount();
726
+ const account2 = await createJazzTestAccount();
727
+
728
+ await manager.createContext({});
729
+
730
+ const credentials1 = {
731
+ accountID: account1.$jazz.id,
732
+ accountSecret: account1.$jazz.localNode.getCurrentAgent().agentSecret,
733
+ provider: "test",
734
+ };
735
+
736
+ const credentials2 = {
737
+ accountID: account2.$jazz.id,
738
+ accountSecret: account2.$jazz.localNode.getCurrentAgent().agentSecret,
739
+ provider: "test",
740
+ };
741
+
742
+ const auth1Promise = manager.authenticate(credentials1);
743
+
744
+ // Try to authenticate account2 while account1 is in progress
745
+ await expect(manager.authenticate(credentials2)).rejects.toThrow();
746
+
747
+ // First authentication should still complete successfully
748
+ await expect(auth1Promise).resolves.toBeUndefined();
749
+
750
+ // After first auth completes, second account should be able to authenticate
751
+ await expect(manager.authenticate(credentials2)).resolves.toBeUndefined();
752
+ });
753
+
754
+ test("throws error when authenticating with different credentials simultaneously", async () => {
755
+ const account1 = await createJazzTestAccount();
756
+ const account2 = await createJazzTestAccount();
757
+
758
+ await manager.createContext({});
759
+
760
+ const credentials1 = {
761
+ accountID: account1.$jazz.id,
762
+ accountSecret: account1.$jazz.localNode.getCurrentAgent().agentSecret,
763
+ provider: "test",
764
+ };
765
+
766
+ const credentials2 = {
767
+ accountID: account2.$jazz.id,
768
+ accountSecret: account2.$jazz.localNode.getCurrentAgent().agentSecret,
769
+ provider: "test",
770
+ };
771
+
772
+ const results = await Promise.allSettled([
773
+ manager.authenticate(credentials1),
774
+ manager.authenticate(credentials2),
775
+ ]);
776
+
777
+ // One should succeed and one should fail
778
+ const successCount = results.filter(
779
+ (r) => r.status === "fulfilled",
780
+ ).length;
781
+ const failureCount = results.filter(
782
+ (r) => r.status === "rejected",
783
+ ).length;
784
+
785
+ expect(successCount).toBe(1);
786
+ expect(failureCount).toBe(1);
787
+
788
+ // Verify the successful authentication resulted in a valid context
789
+ const currentAccount = getCurrentValue().me;
790
+ expect([credentials1.accountID, credentials2.accountID]).toContain(
791
+ currentAccount.$jazz.id,
792
+ );
793
+ });
794
+ });
543
795
  });
@@ -91,7 +91,6 @@ export async function startWorker<
91
91
  secret: accountSecret as AgentSecret,
92
92
  },
93
93
  AccountSchema,
94
- // TODO: locked sessions similar to browser
95
94
  sessionProvider: randomSessionProvider,
96
95
  peersToLoadFrom,
97
96
  crypto: options.crypto ?? (await WasmCrypto.create()),
@@ -123,10 +122,23 @@ export async function startWorker<
123
122
  };
124
123
 
125
124
  return {
125
+ /**
126
+ * The worker account instance.
127
+ */
126
128
  worker: context.account as Loaded<S>,
127
129
  experimental: {
130
+ /**
131
+ * API to subscribe to the inbox messages.
132
+ *
133
+ * More info on the Inbox API: https://jazz.tools/docs/react/server-side/inbox
134
+ */
128
135
  inbox: inboxPublicApi,
129
136
  },
137
+ /**
138
+ * Wait for the connection to the sync server to be established.
139
+ *
140
+ * If already connected, it will resolve immediately.
141
+ */
130
142
  waitForConnection() {
131
143
  return wsPeer.waitUntilConnected();
132
144
  },
@@ -137,6 +149,21 @@ export async function startWorker<
137
149
  wsPeer.unsubscribe(listener);
138
150
  };
139
151
  },
152
+ /**
153
+ * Waits for all CoValues to sync and then shuts down the worker.
154
+ *
155
+ * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()
156
+ *
157
+ * @deprecated Use shutdownWorker
158
+ */
140
159
  done,
160
+ /**
161
+ * Waits for all CoValues to sync and then shuts down the worker.
162
+ *
163
+ * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()
164
+ */
165
+ shutdownWorker() {
166
+ return done();
167
+ },
141
168
  };
142
169
  }