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.
- package/.turbo/turbo-build.log +35 -35
- 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/better-auth/auth/server.js +1 -1
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/{chunk-CFAY3FMQ.js → chunk-RQHJFPIB.js} +61 -26
- package/dist/{chunk-CFAY3FMQ.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 +25 -1
- 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/coValues/CoFieldInit.d.ts +5 -5
- package/dist/tools/coValues/CoFieldInit.d.ts.map +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +2 -0
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +5 -3
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/better-auth/auth/client.ts +1 -1
- package/src/better-auth/auth/server.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/coValues/CoFieldInit.ts +5 -5
- package/src/tools/implementation/ContextManager.ts +75 -32
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +22 -8
- package/src/tools/tests/ContextManager.test.ts +252 -0
- 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
|
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<
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
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({
|