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.
Files changed (49) hide show
  1. package/.svelte-kit/__package__/jazz.svelte.d.ts +1 -1
  2. package/.svelte-kit/__package__/jazz.svelte.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/jazz.svelte.js +19 -26
  4. package/.turbo/turbo-build.log +43 -43
  5. package/CHANGELOG.md +31 -0
  6. package/dist/better-auth/auth/client.d.ts +1 -1
  7. package/dist/better-auth/auth/client.d.ts.map +1 -1
  8. package/dist/better-auth/auth/client.js.map +1 -1
  9. package/dist/{chunk-QF3R3C4N.js → chunk-RQHJFPIB.js} +56 -25
  10. package/dist/{chunk-QF3R3C4N.js.map → chunk-RQHJFPIB.js.map} +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/react/hooks.d.ts +1 -1
  13. package/dist/react/hooks.d.ts.map +1 -1
  14. package/dist/react/index.d.ts +1 -1
  15. package/dist/react/index.d.ts.map +1 -1
  16. package/dist/react/index.js +3 -1
  17. package/dist/react/index.js.map +1 -1
  18. package/dist/react-core/hooks.d.ts +56 -0
  19. package/dist/react-core/hooks.d.ts.map +1 -1
  20. package/dist/react-core/index.js +20 -0
  21. package/dist/react-core/index.js.map +1 -1
  22. package/dist/react-core/tests/useAccountWithSelector.test.d.ts +2 -0
  23. package/dist/react-core/tests/useAccountWithSelector.test.d.ts.map +1 -0
  24. package/dist/react-native-core/hooks.d.ts +1 -1
  25. package/dist/react-native-core/hooks.d.ts.map +1 -1
  26. package/dist/react-native-core/index.js +3 -1
  27. package/dist/react-native-core/index.js.map +1 -1
  28. package/dist/svelte/jazz.svelte.d.ts +1 -1
  29. package/dist/svelte/jazz.svelte.d.ts.map +1 -1
  30. package/dist/svelte/jazz.svelte.js +19 -26
  31. package/dist/testing.js +1 -1
  32. package/dist/tools/implementation/ContextManager.d.ts +2 -0
  33. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  34. package/dist/worker/index.d.ts +26 -0
  35. package/dist/worker/index.d.ts.map +1 -1
  36. package/dist/worker/index.js +29 -2
  37. package/dist/worker/index.js.map +1 -1
  38. package/package.json +4 -4
  39. package/src/better-auth/auth/client.ts +1 -1
  40. package/src/better-auth/auth/tests/client.test.ts +229 -0
  41. package/src/react/hooks.tsx +1 -0
  42. package/src/react/index.ts +1 -0
  43. package/src/react-core/hooks.ts +84 -0
  44. package/src/react-core/tests/useAccountWithSelector.test.ts +411 -0
  45. package/src/react-native-core/hooks.tsx +1 -0
  46. package/src/svelte/jazz.svelte.ts +23 -27
  47. package/src/tools/implementation/ContextManager.ts +75 -32
  48. package/src/tools/tests/ContextManager.test.ts +252 -0
  49. 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
  });
@@ -51,4 +51,5 @@ export {
51
51
  useJazzContext,
52
52
  useAccount,
53
53
  useCoStateWithSelector,
54
+ useAccountWithSelector,
54
55
  } from "jazz-tools/react-core";
@@ -8,6 +8,7 @@ export {
8
8
  useJazzContext,
9
9
  useAuthSecretStorage,
10
10
  useCoStateWithSelector,
11
+ useAccountWithSelector,
11
12
  } from "./hooks.js";
12
13
 
13
14
  export { createInviteLink, parseInviteLink } from "jazz-tools/browser";
@@ -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,