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.
- package/.svelte-kit/__package__/jazz.svelte.d.ts +1 -1
- package/.svelte-kit/__package__/jazz.svelte.d.ts.map +1 -1
- package/.svelte-kit/__package__/jazz.svelte.js +19 -26
- package/.turbo/turbo-build.log +43 -43
- package/CHANGELOG.md +31 -0
- package/dist/better-auth/auth/client.d.ts +1 -1
- package/dist/better-auth/auth/client.d.ts.map +1 -1
- package/dist/better-auth/auth/client.js.map +1 -1
- package/dist/{chunk-QF3R3C4N.js → chunk-RQHJFPIB.js} +56 -25
- package/dist/{chunk-QF3R3C4N.js.map → chunk-RQHJFPIB.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +56 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +20 -0
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useAccountWithSelector.test.d.ts +2 -0
- package/dist/react-core/tests/useAccountWithSelector.test.d.ts.map +1 -0
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +3 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/svelte/jazz.svelte.d.ts +1 -1
- package/dist/svelte/jazz.svelte.d.ts.map +1 -1
- package/dist/svelte/jazz.svelte.js +19 -26
- package/dist/testing.js +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +2 -0
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/dist/worker/index.d.ts +26 -0
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +29 -2
- package/dist/worker/index.js.map +1 -1
- package/package.json +4 -4
- package/src/better-auth/auth/client.ts +1 -1
- package/src/better-auth/auth/tests/client.test.ts +229 -0
- package/src/react/hooks.tsx +1 -0
- package/src/react/index.ts +1 -0
- package/src/react-core/hooks.ts +84 -0
- package/src/react-core/tests/useAccountWithSelector.test.ts +411 -0
- package/src/react-native-core/hooks.tsx +1 -0
- package/src/svelte/jazz.svelte.ts +23 -27
- package/src/tools/implementation/ContextManager.ts +75 -32
- package/src/tools/tests/ContextManager.test.ts +252 -0
- 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
|
});
|
package/src/worker/index.ts
CHANGED
@@ -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
|
}
|