jazz-tools 0.18.7 → 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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +35 -35
  2. package/CHANGELOG.md +31 -0
  3. package/dist/better-auth/auth/client.d.ts +1 -1
  4. package/dist/better-auth/auth/client.d.ts.map +1 -1
  5. package/dist/better-auth/auth/client.js.map +1 -1
  6. package/dist/better-auth/auth/server.js +1 -1
  7. package/dist/better-auth/auth/server.js.map +1 -1
  8. package/dist/{chunk-CFAY3FMQ.js → chunk-RQHJFPIB.js} +61 -26
  9. package/dist/{chunk-CFAY3FMQ.js.map → chunk-RQHJFPIB.js.map} +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/react/hooks.d.ts +1 -1
  12. package/dist/react/hooks.d.ts.map +1 -1
  13. package/dist/react/index.d.ts +1 -1
  14. package/dist/react/index.d.ts.map +1 -1
  15. package/dist/react/index.js +3 -1
  16. package/dist/react/index.js.map +1 -1
  17. package/dist/react-core/hooks.d.ts +56 -0
  18. package/dist/react-core/hooks.d.ts.map +1 -1
  19. package/dist/react-core/index.js +25 -1
  20. package/dist/react-core/index.js.map +1 -1
  21. package/dist/react-core/tests/useAccountWithSelector.test.d.ts +2 -0
  22. package/dist/react-core/tests/useAccountWithSelector.test.d.ts.map +1 -0
  23. package/dist/react-native-core/hooks.d.ts +1 -1
  24. package/dist/react-native-core/hooks.d.ts.map +1 -1
  25. package/dist/react-native-core/index.js +3 -1
  26. package/dist/react-native-core/index.js.map +1 -1
  27. package/dist/testing.js +1 -1
  28. package/dist/tools/coValues/CoFieldInit.d.ts +5 -5
  29. package/dist/tools/coValues/CoFieldInit.d.ts.map +1 -1
  30. package/dist/tools/implementation/ContextManager.d.ts +2 -0
  31. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  32. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +5 -3
  33. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  34. package/package.json +4 -4
  35. package/src/better-auth/auth/client.ts +1 -1
  36. package/src/better-auth/auth/server.ts +1 -1
  37. package/src/better-auth/auth/tests/client.test.ts +229 -0
  38. package/src/react/hooks.tsx +1 -0
  39. package/src/react/index.ts +1 -0
  40. package/src/react-core/hooks.ts +84 -0
  41. package/src/react-core/tests/useAccountWithSelector.test.ts +411 -0
  42. package/src/react-native-core/hooks.tsx +1 -0
  43. package/src/tools/coValues/CoFieldInit.ts +5 -5
  44. package/src/tools/implementation/ContextManager.ts +75 -32
  45. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +22 -8
  46. package/src/tools/tests/ContextManager.test.ts +252 -0
  47. package/src/tools/tests/coMap.test.ts +37 -0
@@ -180,7 +180,9 @@ export interface CoMapSchema<
180
180
  *
181
181
  * @returns A new CoMap schema with all fields optional.
182
182
  */
183
- partial(): CoMapSchema<PartialShape<Shape>, CatchAll, Owner>;
183
+ partial<Keys extends keyof Shape = keyof Shape>(
184
+ keys?: { [key in Keys]: true },
185
+ ): CoMapSchema<PartialShape<Shape, Keys>, CatchAll, Owner>;
184
186
  }
185
187
 
186
188
  export function createCoreCoMapSchema<
@@ -281,10 +283,17 @@ export function enrichCoMapSchema<
281
283
 
282
284
  return coMapDefiner(pickedShape);
283
285
  },
284
- partial: () => {
286
+ partial: <Keys extends keyof Shape = keyof Shape>(
287
+ keys?: { [key in Keys]: true },
288
+ ) => {
285
289
  const partialShape: Record<string, AnyZodOrCoValueSchema> = {};
286
290
 
287
291
  for (const [key, value] of Object.entries(coValueSchema.shape)) {
292
+ if (keys && !keys[key as Keys]) {
293
+ partialShape[key] = value;
294
+ continue;
295
+ }
296
+
288
297
  if (isAnyCoValueSchema(value)) {
289
298
  partialShape[key] = coOptionalDefiner(value);
290
299
  } else {
@@ -341,10 +350,15 @@ export type CoMapInstanceCoValuesNullable<Shape extends z.core.$ZodLooseShape> =
341
350
  >;
342
351
  };
343
352
 
344
- export type PartialShape<Shape extends z.core.$ZodLooseShape> = Simplify<{
345
- -readonly [key in keyof Shape]: Shape[key] extends AnyZodSchema
346
- ? z.ZodOptional<Shape[key]>
347
- : Shape[key] extends CoreCoValueSchema
348
- ? CoOptionalSchema<Shape[key]>
349
- : never;
353
+ export type PartialShape<
354
+ Shape extends z.core.$ZodLooseShape,
355
+ PartialKeys extends keyof Shape = keyof Shape,
356
+ > = Simplify<{
357
+ -readonly [key in keyof Shape]: key extends PartialKeys
358
+ ? Shape[key] extends AnyZodSchema
359
+ ? z.ZodOptional<Shape[key]>
360
+ : Shape[key] extends CoreCoValueSchema
361
+ ? CoOptionalSchema<Shape[key]>
362
+ : never
363
+ : Shape[key];
350
364
  }>;
@@ -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
  });
@@ -2646,6 +2646,43 @@ describe("co.map schema", () => {
2646
2646
  expect(draftPerson.pet).toEqual(rex);
2647
2647
  });
2648
2648
 
2649
+ test("creates a new CoMap schema by making some properties optional", () => {
2650
+ const Dog = co.map({
2651
+ name: z.string(),
2652
+ breed: z.string(),
2653
+ });
2654
+ const Person = co.map({
2655
+ name: z.string(),
2656
+ age: z.number(),
2657
+ pet: Dog,
2658
+ });
2659
+
2660
+ const DraftPerson = Person.partial({
2661
+ pet: true,
2662
+ });
2663
+
2664
+ const draftPerson = DraftPerson.create({
2665
+ name: "John",
2666
+ age: 20,
2667
+ });
2668
+
2669
+ expect(draftPerson.$jazz.has("pet")).toBe(false);
2670
+
2671
+ const rex = Dog.create({ name: "Rex", breed: "Labrador" });
2672
+ draftPerson.$jazz.set("pet", rex);
2673
+
2674
+ expect(draftPerson.pet).toEqual(rex);
2675
+
2676
+ expect(draftPerson.$jazz.has("pet")).toBe(true);
2677
+
2678
+ draftPerson.$jazz.delete("pet");
2679
+
2680
+ expect(draftPerson.$jazz.has("pet")).toBe(false);
2681
+
2682
+ // @ts-expect-error - should not allow deleting required properties
2683
+ draftPerson.$jazz.delete("age");
2684
+ });
2685
+
2649
2686
  test("the new schema includes catchall properties", () => {
2650
2687
  const Person = co
2651
2688
  .map({