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
@@ -2,6 +2,7 @@ import { createAuthClient } from "better-auth/client";
|
|
2
2
|
import type { Account, AuthSecretStorage } from "jazz-tools";
|
3
3
|
import {
|
4
4
|
TestJazzContextManager,
|
5
|
+
createJazzTestAccount,
|
5
6
|
setActiveAccount,
|
6
7
|
setupJazzTestSync,
|
7
8
|
} from "jazz-tools/testing";
|
@@ -334,4 +335,232 @@ describe("Better-Auth client plugin", () => {
|
|
334
335
|
}),
|
335
336
|
);
|
336
337
|
});
|
338
|
+
|
339
|
+
describe("Race condition handling", () => {
|
340
|
+
it("should handle multiple concurrent get-session calls without errors", async () => {
|
341
|
+
const credentials = await authSecretStorage.get();
|
342
|
+
assert(credentials, "Jazz credentials are not available");
|
343
|
+
|
344
|
+
// Mock multiple get-session responses with fresh Response objects
|
345
|
+
customFetchImpl.mockImplementation(() =>
|
346
|
+
Promise.resolve(
|
347
|
+
new Response(
|
348
|
+
JSON.stringify({
|
349
|
+
user: {
|
350
|
+
id: "YW5kcmVpYnVkb2k",
|
351
|
+
email: "test@jazz.dev",
|
352
|
+
name: "andreibudoi",
|
353
|
+
accountID: credentials.accountID,
|
354
|
+
},
|
355
|
+
jazzAuth: {
|
356
|
+
accountID: credentials.accountID,
|
357
|
+
secretSeed: credentials.secretSeed,
|
358
|
+
accountSecret: credentials.accountSecret,
|
359
|
+
provider: "better-auth",
|
360
|
+
},
|
361
|
+
}),
|
362
|
+
),
|
363
|
+
),
|
364
|
+
);
|
365
|
+
|
366
|
+
// Simulate multiple concurrent get-session calls (like during OAuth)
|
367
|
+
const promises = [];
|
368
|
+
for (let i = 0; i < 5; i++) {
|
369
|
+
promises.push(authClient.$fetch("/get-session", { method: "GET" }));
|
370
|
+
}
|
371
|
+
|
372
|
+
// Should complete without errors due to race condition handling
|
373
|
+
await expect(Promise.all(promises)).resolves.toBeDefined();
|
374
|
+
|
375
|
+
// Should have been called multiple times by better-auth
|
376
|
+
expect(customFetchImpl).toHaveBeenCalledTimes(5);
|
377
|
+
|
378
|
+
// User should be authenticated
|
379
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
380
|
+
});
|
381
|
+
|
382
|
+
it("should handle credentials mismatch scenario without errors", async () => {
|
383
|
+
const originalCredentials = await authSecretStorage.get();
|
384
|
+
assert(originalCredentials, "Jazz credentials are not available");
|
385
|
+
|
386
|
+
// Create a test account for the mismatch scenario
|
387
|
+
const testAccount = await createJazzTestAccount();
|
388
|
+
|
389
|
+
// Mock get-session response with different account using fresh Response objects
|
390
|
+
customFetchImpl.mockImplementation(() =>
|
391
|
+
Promise.resolve(
|
392
|
+
new Response(
|
393
|
+
JSON.stringify({
|
394
|
+
user: {
|
395
|
+
id: "RGlmZmVyZW50IFVzZXI",
|
396
|
+
email: "different@jazz.dev",
|
397
|
+
name: "Different User",
|
398
|
+
accountID: testAccount.$jazz.id,
|
399
|
+
},
|
400
|
+
jazzAuth: {
|
401
|
+
accountID: testAccount.$jazz.id,
|
402
|
+
secretSeed: new Uint8Array([4, 5, 6]),
|
403
|
+
accountSecret:
|
404
|
+
testAccount.$jazz.localNode.getCurrentAgent().agentSecret,
|
405
|
+
provider: "better-auth",
|
406
|
+
},
|
407
|
+
}),
|
408
|
+
),
|
409
|
+
),
|
410
|
+
);
|
411
|
+
|
412
|
+
// Simulate multiple concurrent get-session calls with mismatched credentials
|
413
|
+
const promises = [];
|
414
|
+
for (let i = 0; i < 3; i++) {
|
415
|
+
promises.push(authClient.$fetch("/get-session", { method: "GET" }));
|
416
|
+
}
|
417
|
+
|
418
|
+
// Should complete without errors despite credential mismatch
|
419
|
+
await expect(Promise.all(promises)).resolves.toBeDefined();
|
420
|
+
|
421
|
+
// Should have been called multiple times by better-auth
|
422
|
+
expect(customFetchImpl).toHaveBeenCalledTimes(3);
|
423
|
+
|
424
|
+
// Should be authenticated with the new account
|
425
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
426
|
+
const currentCredentials = await authSecretStorage.get();
|
427
|
+
expect(currentCredentials?.accountID).toBe(testAccount.$jazz.id);
|
428
|
+
});
|
429
|
+
|
430
|
+
it("should allow authentication after sign out without being blocked", async () => {
|
431
|
+
const credentials = await authSecretStorage.get();
|
432
|
+
assert(credentials, "Jazz credentials are not available");
|
433
|
+
|
434
|
+
const getSessionResponseData = {
|
435
|
+
user: {
|
436
|
+
id: "123",
|
437
|
+
accountID: credentials.accountID,
|
438
|
+
},
|
439
|
+
jazzAuth: {
|
440
|
+
accountID: credentials.accountID,
|
441
|
+
secretSeed: credentials.secretSeed,
|
442
|
+
accountSecret: credentials.accountSecret,
|
443
|
+
provider: "better-auth",
|
444
|
+
},
|
445
|
+
};
|
446
|
+
|
447
|
+
// First authenticate
|
448
|
+
customFetchImpl.mockResolvedValueOnce(
|
449
|
+
new Response(JSON.stringify(getSessionResponseData)),
|
450
|
+
);
|
451
|
+
|
452
|
+
await authClient.$fetch("/get-session", { method: "GET" });
|
453
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
454
|
+
|
455
|
+
// Then sign out
|
456
|
+
customFetchImpl.mockResolvedValueOnce(
|
457
|
+
new Response(JSON.stringify({ success: true })),
|
458
|
+
);
|
459
|
+
|
460
|
+
await authClient.signOut();
|
461
|
+
expect(authSecretStorage.isAuthenticated).toBe(false);
|
462
|
+
|
463
|
+
// Authenticating again should work without being blocked
|
464
|
+
customFetchImpl.mockResolvedValueOnce(
|
465
|
+
new Response(JSON.stringify(getSessionResponseData)),
|
466
|
+
);
|
467
|
+
|
468
|
+
// Should complete without hanging or errors
|
469
|
+
await expect(
|
470
|
+
authClient.$fetch("/get-session", { method: "GET" }),
|
471
|
+
).resolves.toBeDefined();
|
472
|
+
|
473
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
474
|
+
});
|
475
|
+
|
476
|
+
it("should fail fast when trying to authenticate different accounts concurrently", async () => {
|
477
|
+
const originalCredentials = await authSecretStorage.get();
|
478
|
+
assert(originalCredentials, "Jazz credentials are not available");
|
479
|
+
|
480
|
+
const testAccount1 = await createJazzTestAccount();
|
481
|
+
const testAccount2 = await createJazzTestAccount();
|
482
|
+
const testAccount3 = await createJazzTestAccount();
|
483
|
+
|
484
|
+
const accounts = [testAccount1, testAccount2, testAccount3];
|
485
|
+
let callCount = 0;
|
486
|
+
|
487
|
+
customFetchImpl.mockImplementation(() => {
|
488
|
+
const accountIndex = callCount % 3;
|
489
|
+
const account = accounts[accountIndex]!;
|
490
|
+
callCount++;
|
491
|
+
|
492
|
+
return Promise.resolve(
|
493
|
+
new Response(
|
494
|
+
JSON.stringify({
|
495
|
+
user: {
|
496
|
+
id: `user-${accountIndex + 1}`,
|
497
|
+
email: `user${accountIndex + 1}@jazz.dev`,
|
498
|
+
name: `User ${accountIndex + 1}`,
|
499
|
+
accountID: account.$jazz.id,
|
500
|
+
},
|
501
|
+
jazzAuth: {
|
502
|
+
accountID: account.$jazz.id,
|
503
|
+
secretSeed: new Uint8Array([
|
504
|
+
accountIndex + 1,
|
505
|
+
accountIndex + 2,
|
506
|
+
accountIndex + 3,
|
507
|
+
]),
|
508
|
+
accountSecret:
|
509
|
+
account.$jazz.localNode.getCurrentAgent().agentSecret,
|
510
|
+
provider: "better-auth",
|
511
|
+
},
|
512
|
+
}),
|
513
|
+
),
|
514
|
+
);
|
515
|
+
});
|
516
|
+
|
517
|
+
const promises = [];
|
518
|
+
for (let i = 0; i < 3; i++) {
|
519
|
+
promises.push(authClient.$fetch("/get-session", { method: "GET" }));
|
520
|
+
}
|
521
|
+
|
522
|
+
await expect(Promise.all(promises)).rejects.toThrow();
|
523
|
+
|
524
|
+
expect(customFetchImpl).toHaveBeenCalledTimes(3);
|
525
|
+
});
|
526
|
+
|
527
|
+
it("should deduplicate auth requests for the same account", async () => {
|
528
|
+
const credentials = await authSecretStorage.get();
|
529
|
+
assert(credentials, "Jazz credentials are not available");
|
530
|
+
|
531
|
+
customFetchImpl.mockImplementation(() =>
|
532
|
+
Promise.resolve(
|
533
|
+
new Response(
|
534
|
+
JSON.stringify({
|
535
|
+
user: {
|
536
|
+
id: "test-user",
|
537
|
+
email: "test@jazz.dev",
|
538
|
+
name: "Test User",
|
539
|
+
accountID: credentials.accountID,
|
540
|
+
},
|
541
|
+
jazzAuth: {
|
542
|
+
accountID: credentials.accountID,
|
543
|
+
secretSeed: credentials.secretSeed,
|
544
|
+
accountSecret: credentials.accountSecret,
|
545
|
+
provider: "better-auth",
|
546
|
+
},
|
547
|
+
}),
|
548
|
+
),
|
549
|
+
),
|
550
|
+
);
|
551
|
+
|
552
|
+
const promises = [];
|
553
|
+
for (let i = 0; i < 3; i++) {
|
554
|
+
promises.push(authClient.$fetch("/get-session", { method: "GET" }));
|
555
|
+
}
|
556
|
+
|
557
|
+
await expect(Promise.all(promises)).resolves.toBeDefined();
|
558
|
+
|
559
|
+
expect(customFetchImpl).toHaveBeenCalledTimes(3);
|
560
|
+
|
561
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
562
|
+
const finalCredentials = await authSecretStorage.get();
|
563
|
+
expect(finalCredentials?.accountID).toBe(credentials.accountID);
|
564
|
+
});
|
565
|
+
});
|
337
566
|
});
|
package/src/react/hooks.tsx
CHANGED
package/src/react/index.ts
CHANGED
package/src/react-core/hooks.ts
CHANGED
@@ -580,6 +580,90 @@ export function useAccount<
|
|
580
580
|
};
|
581
581
|
}
|
582
582
|
|
583
|
+
/**
|
584
|
+
* React hook for accessing the current user's account with selective data extraction and custom equality checking.
|
585
|
+
*
|
586
|
+
* This hook extends `useAccount` by allowing you to select only specific parts of the account data
|
587
|
+
* through a selector function, which helps reduce unnecessary re-renders by narrowing down the
|
588
|
+
* returned data. Additionally, you can provide a custom equality function to further optimize
|
589
|
+
* performance by controlling when the component should re-render based on the selected data.
|
590
|
+
*
|
591
|
+
* The hook automatically handles the subscription lifecycle and supports deep loading of nested
|
592
|
+
* CoValues through resolve queries, just like `useAccount`.
|
593
|
+
*
|
594
|
+
* @returns The result of the selector function applied to the loaded account data
|
595
|
+
*
|
596
|
+
* @example
|
597
|
+
* ```tsx
|
598
|
+
* // Select only specific fields to reduce re-renders
|
599
|
+
* const MyAppAccount = co.account({
|
600
|
+
* profile: co.profile(),
|
601
|
+
* root: co.map({
|
602
|
+
* name: z.string(),
|
603
|
+
* email: z.string(),
|
604
|
+
* lastLogin: z.date(),
|
605
|
+
* }),
|
606
|
+
* });
|
607
|
+
*
|
608
|
+
* function UserProfile({ accountId }: { accountId: string }) {
|
609
|
+
* // Only re-render when the profile name changes, not other fields
|
610
|
+
* const profileName = useAccountWithSelector(
|
611
|
+
* MyAppAccount,
|
612
|
+
* {
|
613
|
+
* resolve: {
|
614
|
+
* profile: true,
|
615
|
+
* root: true,
|
616
|
+
* },
|
617
|
+
* select: (account) => account?.profile?.name ?? "Loading...",
|
618
|
+
* }
|
619
|
+
* );
|
620
|
+
*
|
621
|
+
* return <h1>{profileName}</h1>;
|
622
|
+
* }
|
623
|
+
* ```
|
624
|
+
*
|
625
|
+
* For more examples, see the [subscription and deep loading](https://jazz.tools/docs/react/using-covalues/subscription-and-loading) documentation.
|
626
|
+
*/
|
627
|
+
export function useAccountWithSelector<
|
628
|
+
A extends AccountClass<Account> | AnyAccountSchema,
|
629
|
+
TSelectorReturn,
|
630
|
+
R extends ResolveQuery<A> = true,
|
631
|
+
>(
|
632
|
+
/** The account schema to use. Defaults to the base Account schema */
|
633
|
+
AccountSchema: A = Account as unknown as A,
|
634
|
+
/** Configuration for the subscription and selection */
|
635
|
+
options: {
|
636
|
+
/** Resolve query to specify which nested CoValues to load from the account */
|
637
|
+
resolve?: ResolveQueryStrict<A, R>;
|
638
|
+
/** Select which value to return from the account data */
|
639
|
+
select: (account: Loaded<A, R> | undefined | null) => TSelectorReturn;
|
640
|
+
/** Equality function to determine if the selected value has changed, defaults to `Object.is` */
|
641
|
+
equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
|
642
|
+
},
|
643
|
+
): TSelectorReturn {
|
644
|
+
const subscription = useAccountSubscription(AccountSchema, options);
|
645
|
+
|
646
|
+
return useSyncExternalStoreWithSelector<
|
647
|
+
Loaded<A, R> | undefined | null,
|
648
|
+
TSelectorReturn
|
649
|
+
>(
|
650
|
+
React.useCallback(
|
651
|
+
(callback) => {
|
652
|
+
if (!subscription) {
|
653
|
+
return () => {};
|
654
|
+
}
|
655
|
+
|
656
|
+
return subscription.subscribe(callback);
|
657
|
+
},
|
658
|
+
[subscription],
|
659
|
+
),
|
660
|
+
() => (subscription ? subscription.getCurrentValue() : null),
|
661
|
+
() => (subscription ? subscription.getCurrentValue() : null),
|
662
|
+
options.select,
|
663
|
+
options.equalityFn ?? Object.is,
|
664
|
+
);
|
665
|
+
}
|
666
|
+
|
583
667
|
export function experimental_useInboxSender<
|
584
668
|
I extends CoValue,
|
585
669
|
O extends CoValue | undefined,
|