jazz-tools 0.18.8 → 0.18.10
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/.turbo/turbo-build.log +42 -42
- package/CHANGELOG.md +20 -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/testing.js +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +2 -0
- package/dist/tools/implementation/ContextManager.d.ts.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/tools/implementation/ContextManager.ts +75 -32
- package/src/tools/tests/ContextManager.test.ts +252 -0
@@ -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
|
});
|