jazz-tools 0.19.19 → 0.19.21

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 (127) hide show
  1. package/.svelte-kit/__package__/client.d.ts.map +1 -1
  2. package/.svelte-kit/__package__/client.js +3 -1
  3. package/.svelte-kit/__package__/server.d.ts.map +1 -1
  4. package/.svelte-kit/__package__/server.js +9 -7
  5. package/.svelte-kit/__package__/tests/client.test.js +48 -0
  6. package/.turbo/turbo-build.log +70 -66
  7. package/dist/better-auth/auth/client.d.ts.map +1 -1
  8. package/dist/better-auth/auth/client.js +1 -1
  9. package/dist/better-auth/auth/client.js.map +1 -1
  10. package/dist/better-auth/auth/server.d.ts.map +1 -1
  11. package/dist/better-auth/auth/server.js +4 -4
  12. package/dist/better-auth/auth/server.js.map +1 -1
  13. package/dist/better-auth/database-adapter/index.js.map +1 -1
  14. package/dist/better-auth/database-adapter/repository/generic.d.ts +3 -3
  15. package/dist/better-auth/database-adapter/repository/session.d.ts +2 -2
  16. package/dist/better-auth/database-adapter/schema.d.ts +3 -3
  17. package/dist/better-auth/database-adapter/schema.d.ts.map +1 -1
  18. package/dist/{chunk-PEHQ7TN2.js → chunk-QCTQH5RS.js} +31 -4
  19. package/dist/chunk-QCTQH5RS.js.map +1 -0
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/hooks.d.ts +1 -2
  23. package/dist/react/hooks.d.ts.map +1 -1
  24. package/dist/react/index.js +7 -2
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/react-core/hooks.d.ts +94 -3
  27. package/dist/react-core/hooks.d.ts.map +1 -1
  28. package/dist/react-core/index.js +130 -135
  29. package/dist/react-core/index.js.map +1 -1
  30. package/dist/react-core/tests/useCoStates.test.d.ts +2 -0
  31. package/dist/react-core/tests/useCoStates.test.d.ts.map +1 -0
  32. package/dist/react-native/chunk-DGUM43GV.js +11 -0
  33. package/dist/react-native/chunk-DGUM43GV.js.map +1 -0
  34. package/dist/react-native/crypto.js +2 -0
  35. package/dist/react-native/crypto.js.map +1 -1
  36. package/dist/react-native/index.js +544 -29
  37. package/dist/react-native/index.js.map +1 -1
  38. package/dist/react-native-core/auth/PasskeyAuth.d.ts +123 -0
  39. package/dist/react-native-core/auth/PasskeyAuth.d.ts.map +1 -0
  40. package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts +34 -0
  41. package/dist/react-native-core/auth/PasskeyAuthBasicUI.d.ts.map +1 -0
  42. package/dist/react-native-core/auth/auth.d.ts +3 -0
  43. package/dist/react-native-core/auth/auth.d.ts.map +1 -1
  44. package/dist/react-native-core/auth/passkey-utils.d.ts +16 -0
  45. package/dist/react-native-core/auth/passkey-utils.d.ts.map +1 -0
  46. package/dist/react-native-core/auth/usePasskeyAuth.d.ts +48 -0
  47. package/dist/react-native-core/auth/usePasskeyAuth.d.ts.map +1 -0
  48. package/dist/react-native-core/chunk-DGUM43GV.js +11 -0
  49. package/dist/react-native-core/chunk-DGUM43GV.js.map +1 -0
  50. package/dist/react-native-core/crypto.js +2 -0
  51. package/dist/react-native-core/crypto.js.map +1 -1
  52. package/dist/react-native-core/hooks.d.ts +1 -1
  53. package/dist/react-native-core/hooks.d.ts.map +1 -1
  54. package/dist/react-native-core/index.js +539 -24
  55. package/dist/react-native-core/index.js.map +1 -1
  56. package/dist/react-native-core/tests/PasskeyAuth.test.d.ts +2 -0
  57. package/dist/react-native-core/tests/PasskeyAuth.test.d.ts.map +1 -0
  58. package/dist/react-native-core/tests/passkey-utils.test.d.ts +2 -0
  59. package/dist/react-native-core/tests/passkey-utils.test.d.ts.map +1 -0
  60. package/dist/svelte/auth/ClerkAuth.svelte.d.ts +38 -0
  61. package/dist/svelte/auth/ClerkAuth.svelte.d.ts.map +1 -0
  62. package/dist/svelte/auth/ClerkAuth.svelte.js +47 -0
  63. package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
  64. package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts +67 -0
  65. package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts.map +1 -0
  66. package/dist/svelte/auth/RegisterClerkAuth.svelte +27 -0
  67. package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts +17 -0
  68. package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts.map +1 -0
  69. package/dist/svelte/auth/index.d.ts +2 -0
  70. package/dist/svelte/auth/index.d.ts.map +1 -1
  71. package/dist/svelte/auth/index.js +2 -0
  72. package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts +2 -0
  73. package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts.map +1 -0
  74. package/dist/svelte/tests/ClerkAuth.svelte.test.js +202 -0
  75. package/dist/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
  76. package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts +8 -0
  77. package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts.map +1 -0
  78. package/dist/svelte/tests/testUtils.d.ts +1 -0
  79. package/dist/svelte/tests/testUtils.d.ts.map +1 -1
  80. package/dist/svelte/tests/testUtils.js +3 -1
  81. package/dist/testing.js +1 -1
  82. package/dist/tools/auth/clerk/index.d.ts +1 -1
  83. package/dist/tools/auth/clerk/types.d.ts +1 -1
  84. package/dist/tools/auth/clerk/types.d.ts.map +1 -1
  85. package/dist/tools/coValues/account.d.ts +5 -1
  86. package/dist/tools/coValues/account.d.ts.map +1 -1
  87. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts +30 -1
  88. package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
  89. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  90. package/dist/tools/subscribe/types.d.ts +1 -1
  91. package/dist/tools/subscribe/types.d.ts.map +1 -1
  92. package/dist/tools/testing.d.ts.map +1 -1
  93. package/package.json +8 -4
  94. package/src/better-auth/auth/client.ts +3 -1
  95. package/src/better-auth/auth/server.ts +9 -7
  96. package/src/better-auth/auth/tests/client.test.ts +66 -2
  97. package/src/better-auth/database-adapter/repository/generic.ts +3 -3
  98. package/src/better-auth/database-adapter/repository/session.ts +2 -2
  99. package/src/better-auth/database-adapter/schema.ts +5 -5
  100. package/src/react/hooks.tsx +4 -2
  101. package/src/react-core/hooks.ts +332 -178
  102. package/src/react-core/tests/useCoState.selector.test.ts +309 -22
  103. package/src/react-core/tests/useCoStates.test.tsx +414 -0
  104. package/src/react-native-core/auth/PasskeyAuth.ts +316 -0
  105. package/src/react-native-core/auth/PasskeyAuthBasicUI.tsx +284 -0
  106. package/src/react-native-core/auth/auth.ts +3 -0
  107. package/src/react-native-core/auth/passkey-utils.ts +47 -0
  108. package/src/react-native-core/auth/usePasskeyAuth.tsx +85 -0
  109. package/src/react-native-core/hooks.tsx +2 -0
  110. package/src/react-native-core/tests/PasskeyAuth.test.ts +463 -0
  111. package/src/react-native-core/tests/passkey-utils.test.ts +144 -0
  112. package/src/svelte/auth/ClerkAuth.svelte.ts +67 -0
  113. package/src/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
  114. package/src/svelte/auth/RegisterClerkAuth.svelte +27 -0
  115. package/src/svelte/auth/index.ts +2 -0
  116. package/src/svelte/tests/ClerkAuth.svelte.test.ts +305 -0
  117. package/src/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
  118. package/src/svelte/tests/testUtils.ts +4 -1
  119. package/src/tools/auth/clerk/types.ts +1 -1
  120. package/src/tools/coValues/account.ts +11 -3
  121. package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +27 -1
  122. package/src/tools/subscribe/types.ts +1 -1
  123. package/src/tools/tests/account.test.ts +2 -1
  124. package/src/tools/tests/inbox.test.ts +7 -7
  125. package/testSetup.ts +4 -0
  126. package/vitest.config.ts +1 -0
  127. package/dist/chunk-PEHQ7TN2.js.map +0 -1
@@ -23,7 +23,6 @@ import {
23
23
  Loaded,
24
24
  MaybeLoaded,
25
25
  NotLoaded,
26
- RefsToResolve,
27
26
  ResolveQuery,
28
27
  ResolveQueryStrict,
29
28
  SchemaResolveQuery,
@@ -103,94 +102,107 @@ export function useCoValueSubscription<
103
102
  resolve?: ResolveQueryStrict<S, R>;
104
103
  unstable_branch?: BranchDefinition;
105
104
  },
106
- ) {
105
+ ): CoValueSubscription<S, R> | null {
106
+ const resolve = getResolveQuery(Schema, options?.resolve);
107
+ const subscriptions = useCoValueSubscriptions(
108
+ Schema,
109
+ [id],
110
+ resolve,
111
+ options?.unstable_branch,
112
+ );
113
+ return (subscriptions[0] ?? null) as CoValueSubscription<S, R> | null;
114
+ }
115
+
116
+ /**
117
+ * Tracked state for the entire subscriptions array.
118
+ * If any of the dependencies change, the subscriptions are recreated.
119
+ */
120
+ interface SubscriptionsState {
121
+ subscriptions: (SubscriptionScope<CoValue> | null)[];
122
+ schema: CoValueClassOrSchema;
123
+ ids: readonly (string | undefined | null)[];
124
+ resolve: ResolveQuery<any>;
125
+ contextManager: ReturnType<typeof useJazzContextManager>;
126
+ agent: AnonymousJazzAgent | Loaded<any, true>;
127
+ branchName?: string;
128
+ branchOwnerId?: string;
129
+ }
130
+
131
+ /**
132
+ * Internal hook that manages an array of SubscriptionScope instances.
133
+ *
134
+ * - Uses a ref to track subscriptions by index
135
+ * - Detects changes by comparing schema/ids/resolve/branch
136
+ * - Creates new subscriptions via SubscriptionScopeCache.getOrCreate()
137
+ * - Returns null for entries with undefined/null IDs or invalid branches
138
+ */
139
+ function useCoValueSubscriptions(
140
+ schema: CoValueClassOrSchema,
141
+ ids: readonly (string | undefined | null)[],
142
+ resolve: ResolveQuery<any>,
143
+ branch?: BranchDefinition,
144
+ ): (SubscriptionScope<CoValue> | null)[] {
107
145
  const contextManager = useJazzContextManager();
108
146
  const agent = useAgent();
109
147
 
110
- const callerStack = React.useRef<Error | undefined>(undefined);
148
+ const callerStack = useMemo(() => captureStack(), []);
111
149
 
112
- if (!callerStack.current) {
113
- callerStack.current = captureStack();
114
- }
150
+ const createAllSubscriptions = (): SubscriptionsState => {
151
+ const node = contextManager.getCurrentValue()!.node;
152
+ const cache = contextManager.getSubscriptionScopeCache();
115
153
 
116
- const createSubscription = () => {
117
- if (!id) {
118
- return {
119
- subscription: null,
120
- contextManager,
121
- id,
122
- Schema,
123
- };
124
- }
154
+ const subscriptions = ids.map((id) => {
155
+ if (id === undefined || id === null) {
156
+ return null;
157
+ }
125
158
 
126
- if (options?.unstable_branch?.owner === null) {
127
- return {
128
- subscription: null,
129
- contextManager,
159
+ const subscription = cache.getOrCreate(
160
+ node,
161
+ schema,
130
162
  id,
131
- Schema,
132
- };
133
- }
134
-
135
- const resolve = getResolveQuery(Schema, options?.resolve);
136
-
137
- const node = contextManager.getCurrentValue()!.node;
138
- const cache = contextManager.getSubscriptionScopeCache();
139
- const subscription = cache.getOrCreate(
140
- node,
141
- Schema,
142
- id,
143
- resolve,
144
- false,
145
- false,
146
- options?.unstable_branch,
147
- );
163
+ resolve,
164
+ false,
165
+ false,
166
+ branch,
167
+ );
168
+
169
+ if (callerStack) {
170
+ subscription.callerStack = callerStack;
171
+ }
148
172
 
149
- // Set callerStack on returned subscription after retrieval
150
- if (callerStack.current) {
151
- subscription.callerStack = callerStack.current;
152
- }
173
+ return subscription;
174
+ });
153
175
 
154
176
  return {
155
- value: subscription,
177
+ subscriptions,
178
+ schema,
179
+ ids,
180
+ resolve,
156
181
  contextManager,
157
- id,
158
- Schema,
159
- branchName: options?.unstable_branch?.name,
160
- branchOwnerId: options?.unstable_branch?.owner?.$jazz.id,
161
182
  agent,
183
+ branchName: branch?.name,
184
+ branchOwnerId: branch?.owner?.$jazz.id,
162
185
  };
163
186
  };
164
187
 
165
- const subscriptionRef = React.useRef<null | ReturnType<
166
- typeof createSubscription
167
- >>(null);
188
+ const stateRef = React.useRef<SubscriptionsState | null>(null);
189
+ const newSubscriptions = createAllSubscriptions();
168
190
 
169
- if (!subscriptionRef.current) {
170
- subscriptionRef.current = createSubscription();
171
- }
191
+ const state = stateRef.current;
172
192
 
173
- const branchName = options?.unstable_branch?.name;
174
- const branchOwnerId = options?.unstable_branch?.owner?.$jazz.id;
193
+ // Avoid recreating the subscriptions array if all subscriptions are already cached
194
+ const anySubscriptionChanged =
195
+ newSubscriptions.subscriptions.length !== state?.subscriptions.length ||
196
+ newSubscriptions.subscriptions.some(
197
+ (newSubscriptions, index) =>
198
+ newSubscriptions !== state.subscriptions[index],
199
+ );
175
200
 
176
- let subscription = subscriptionRef.current;
177
-
178
- // Check if the subscription needs to be updated
179
- // because one of the dependencies has changed
180
- if (
181
- subscription.contextManager !== contextManager ||
182
- subscription.id !== id ||
183
- subscription.Schema !== Schema ||
184
- subscription.branchName !== branchName ||
185
- subscription.branchOwnerId !== branchOwnerId ||
186
- subscription.agent !== agent
187
- ) {
188
- subscriptionRef.current = createSubscription();
189
- subscription = subscriptionRef.current;
201
+ if (anySubscriptionChanged) {
202
+ stateRef.current = newSubscriptions;
190
203
  }
191
204
 
192
- // Subscribe to the context manager to react to auth changes
193
- return subscription.value as CoValueSubscription<S, R>;
205
+ return stateRef.current!.subscriptions;
194
206
  }
195
207
 
196
208
  function useImportCoValueContent<V>(
@@ -417,31 +429,8 @@ export function useCoState<
417
429
  },
418
430
  ): TSelectorReturn {
419
431
  useImportCoValueContent(id, options?.preloaded);
420
-
421
432
  const subscription = useCoValueSubscription(Schema, id, options);
422
- const getCurrentValue = useGetCurrentValue(subscription);
423
-
424
- const value = useSyncExternalStoreWithSelector<
425
- MaybeLoaded<Loaded<S, R>>,
426
- TSelectorReturn
427
- >(
428
- React.useCallback(
429
- (callback) => {
430
- if (!subscription) {
431
- return () => {};
432
- }
433
-
434
- return subscription.subscribe(callback);
435
- },
436
- [subscription],
437
- ),
438
- getCurrentValue,
439
- getCurrentValue,
440
- options?.select ?? ((value) => value as TSelectorReturn),
441
- options?.equalityFn ?? Object.is,
442
- );
443
-
444
- return value;
433
+ return useSubscriptionSelector(subscription, options);
445
434
  }
446
435
 
447
436
  export function useSuspenseCoState<
@@ -491,50 +480,34 @@ export function useSuspenseCoState<
491
480
 
492
481
  use(subscription.getCachedPromise());
493
482
 
494
- const getCurrentValue = () => {
495
- const value = subscription.getCurrentValue();
496
-
497
- if (!value.$isLoaded) {
498
- throw new Error("CoValue must be loaded in a suspense context");
499
- }
500
-
501
- return value;
502
- };
503
-
504
- const value = useSyncExternalStoreWithSelector<Loaded<S, R>, TSelectorReturn>(
505
- React.useCallback(
506
- (callback) => {
507
- return subscription.subscribe(callback);
508
- },
509
- [subscription],
510
- ),
511
- getCurrentValue,
512
- getCurrentValue,
513
- options?.select ?? ((value) => value as TSelectorReturn),
514
- options?.equalityFn ?? Object.is,
515
- );
516
-
517
- return value;
483
+ return useSubscriptionSelector(subscription, options);
518
484
  }
519
485
 
486
+ /**
487
+ * Returns a subscription's current value.
488
+ * Allows to optionally select a subset of the subscription's value.
489
+ *
490
+ * This is the single-value counterpart to {@link useSubscriptionsSelector}.
491
+ * Keeping it separate for performance reasons.
492
+ */
520
493
  export function useSubscriptionSelector<
521
494
  S extends CoValueClassOrSchema,
522
495
  // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
523
496
  const R extends ResolveQuery<S> = SchemaResolveQuery<S>,
524
- TSelectorReturn = MaybeLoaded<Loaded<S, R>>,
497
+ // Selector input can be an already loaded or a maybe-loaded value,
498
+ // depending on whether a suspense hook is used or not, respectively.
499
+ TSelectorInput = MaybeLoaded<Loaded<S, R>>,
500
+ TSelectorReturn = TSelectorInput,
525
501
  >(
526
502
  subscription: CoValueSubscription<S, R>,
527
503
  options?: {
528
- select?: (value: MaybeLoaded<Loaded<S, R>>) => TSelectorReturn;
504
+ select?: (value: TSelectorInput) => TSelectorReturn;
529
505
  equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
530
506
  },
531
- ) {
507
+ ): TSelectorReturn {
532
508
  const getCurrentValue = useGetCurrentValue(subscription);
533
509
 
534
- return useSyncExternalStoreWithSelector<
535
- MaybeLoaded<Loaded<S, R>>,
536
- TSelectorReturn
537
- >(
510
+ return useSyncExternalStoreWithSelector(
538
511
  React.useCallback(
539
512
  (callback) => {
540
513
  if (!subscription) {
@@ -547,7 +520,7 @@ export function useSubscriptionSelector<
547
520
  ),
548
521
  getCurrentValue,
549
522
  getCurrentValue,
550
- options?.select ?? ((value) => value as TSelectorReturn),
523
+ options?.select ?? ((value) => value as unknown as TSelectorReturn),
551
524
  options?.equalityFn ?? Object.is,
552
525
  );
553
526
  }
@@ -566,10 +539,7 @@ export function useAccountSubscription<
566
539
  const contextManager = useJazzContextManager();
567
540
 
568
541
  // Capture stack trace at hook call time
569
- const callerStack = React.useRef<Error | undefined>(undefined);
570
- if (!callerStack.current) {
571
- callerStack.current = captureStack();
572
- }
542
+ const callerStack = useMemo(() => captureStack(), []);
573
543
 
574
544
  const createSubscription = () => {
575
545
  const agent = getCurrentAccountFromContextManager(contextManager);
@@ -597,8 +567,8 @@ export function useAccountSubscription<
597
567
  );
598
568
 
599
569
  // Set callerStack on returned subscription after retrieval
600
- if (callerStack.current) {
601
- subscription.callerStack = callerStack.current;
570
+ if (callerStack) {
571
+ subscription.callerStack = callerStack;
602
572
  }
603
573
 
604
574
  return {
@@ -759,27 +729,7 @@ export function useAccount<
759
729
  },
760
730
  ): TSelectorReturn {
761
731
  const subscription = useAccountSubscription(AccountSchema, options);
762
- const getCurrentValue = useGetCurrentValue(subscription);
763
-
764
- return useSyncExternalStoreWithSelector<
765
- MaybeLoaded<Loaded<A, R>>,
766
- TSelectorReturn
767
- >(
768
- React.useCallback(
769
- (callback) => {
770
- if (!subscription) {
771
- return () => {};
772
- }
773
-
774
- return subscription.subscribe(callback);
775
- },
776
- [subscription],
777
- ),
778
- getCurrentValue,
779
- getCurrentValue,
780
- options?.select ?? ((value) => value as TSelectorReturn),
781
- options?.equalityFn ?? Object.is,
782
- );
732
+ return useSubscriptionSelector(subscription, options);
783
733
  }
784
734
 
785
735
  export function useSuspenseAccount<
@@ -826,32 +776,7 @@ export function useSuspenseAccount<
826
776
 
827
777
  use(subscription.getCachedPromise());
828
778
 
829
- const getCurrentValue = () => {
830
- const value = subscription.getCurrentValue();
831
-
832
- if (!value.$isLoaded) {
833
- throw new Error("Account must be loaded in a suspense context");
834
- }
835
-
836
- return value;
837
- };
838
-
839
- return useSyncExternalStoreWithSelector<Loaded<A, R>, TSelectorReturn>(
840
- React.useCallback(
841
- (callback) => {
842
- if (!subscription) {
843
- return () => {};
844
- }
845
-
846
- return subscription.subscribe(callback);
847
- },
848
- [subscription],
849
- ),
850
- getCurrentValue,
851
- getCurrentValue,
852
- options?.select ?? ((value) => value as TSelectorReturn),
853
- options?.equalityFn ?? Object.is,
854
- );
779
+ return useSubscriptionSelector(subscription, options);
855
780
  }
856
781
 
857
782
  /**
@@ -973,3 +898,232 @@ function getResolveQuery(
973
898
  }
974
899
  return true;
975
900
  }
901
+
902
+ /**
903
+ * Internal hook that suspends until all values are loaded.
904
+ *
905
+ * - Creates a Promise.all from individual getCachedPromise() calls
906
+ * - Returns Promise.resolve(null) for null subscriptions (undefined/null IDs)
907
+ * - Suspends via the use() hook until all values are loaded
908
+ */
909
+ function useSuspendUntilLoaded(
910
+ subscriptions: (SubscriptionScope<CoValue> | null)[],
911
+ ): void {
912
+ const combinedPromise = useMemo(() => {
913
+ const promises = subscriptions.map((sub) => {
914
+ if (!sub) {
915
+ // For null subscriptions (undefined/null IDs), resolve immediately with null
916
+ return Promise.resolve(null);
917
+ }
918
+ return sub.getCachedPromise();
919
+ });
920
+
921
+ return Promise.all(promises);
922
+ }, [subscriptions]);
923
+
924
+ use(combinedPromise);
925
+ }
926
+
927
+ /**
928
+ * Internal hook that uses useSyncExternalStore to subscribe to multiple SubscriptionScopes.
929
+ *
930
+ * - Creates a combined subscribe function that subscribes to all scopes
931
+ * - Returns an array of current values from each scope
932
+ * - Maintains stable references for unchanged values
933
+ *
934
+ * @param subscriptions - Array of SubscriptionScope instances (or null for skipped entries)
935
+ * @returns Array of loaded CoValues (or null for skipped entries)
936
+ */
937
+ function useSubscriptionsSelector<
938
+ T extends CoValue[] | MaybeLoaded<CoValue>[],
939
+ // Selector input can be an already loaded or a maybe-loaded value,
940
+ // depending on whether a suspense hook is used or not, respectively.
941
+ TSelectorInput = T[number],
942
+ TSelectorReturn = TSelectorInput,
943
+ >(
944
+ subscriptions: SubscriptionScope<CoValue>[],
945
+ options?: {
946
+ select?: (value: TSelectorInput) => TSelectorReturn;
947
+ equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
948
+ },
949
+ ): TSelectorReturn[] {
950
+ // Combined subscribe function that subscribes to all scopes
951
+ const subscribe = useCallback(
952
+ (callback: () => void) => {
953
+ const unsubscribes = subscriptions.map((sub) => sub.subscribe(callback));
954
+
955
+ return () => {
956
+ unsubscribes.forEach((unsub) => unsub());
957
+ };
958
+ },
959
+ [subscriptions],
960
+ );
961
+
962
+ // Cache current values to avoid infinite loops
963
+ const cachedCurrentValuesRef = useRef<T>([] as unknown as T);
964
+ const getCurrentValues = useCallback(() => {
965
+ const newValues = subscriptions.map((sub) => sub.getCurrentValue());
966
+
967
+ // Check if values have changed by comparing each element
968
+ const cached = cachedCurrentValuesRef.current;
969
+ const hasChanged =
970
+ cached.length !== newValues.length ||
971
+ newValues.some((value, index) => value !== cached[index]);
972
+
973
+ if (hasChanged) {
974
+ cachedCurrentValuesRef.current = newValues as T;
975
+ }
976
+
977
+ return cachedCurrentValuesRef.current as unknown as TSelectorInput[];
978
+ }, [subscriptions]);
979
+
980
+ const selectFn = useMemo(() => {
981
+ if (!options?.select) {
982
+ return (values: TSelectorInput[]) =>
983
+ values as unknown as TSelectorReturn[];
984
+ }
985
+ return (values: TSelectorInput[]) =>
986
+ values.map((value) => options.select!(value));
987
+ }, [options?.select]);
988
+
989
+ const elementEqualityFn = useMemo(
990
+ () => options?.equalityFn ?? Object.is,
991
+ [options?.equalityFn],
992
+ );
993
+ const equalityFn = useMemo(() => {
994
+ return (a: TSelectorReturn[], b: TSelectorReturn[]) =>
995
+ a.length === b.length &&
996
+ a.every((value, index) => elementEqualityFn(value, b[index]));
997
+ }, [elementEqualityFn]);
998
+
999
+ return useSyncExternalStoreWithSelector(
1000
+ subscribe,
1001
+ getCurrentValues,
1002
+ getCurrentValues,
1003
+ selectFn,
1004
+ equalityFn,
1005
+ );
1006
+ }
1007
+
1008
+ /**
1009
+ * Subscribe to multiple CoValues with unified Suspense handling.
1010
+ *
1011
+ * This hook accepts a list of CoValue IDs and returns an array of loaded values,
1012
+ * suspending until all values are available.
1013
+ *
1014
+ * @param Schema - The CoValue schema or class constructor
1015
+ * @param ids - Array of CoValue IDs to subscribe to
1016
+ * @param options - Optional configuration, including resolve query
1017
+ * @returns An array of loaded CoValues in the same order as the input IDs
1018
+ */
1019
+ export function useSuspenseCoStates<
1020
+ S extends CoValueClassOrSchema,
1021
+ // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
1022
+ const R extends ResolveQuery<S> = SchemaResolveQuery<S>,
1023
+ TSelectorReturn = Loaded<S, R>,
1024
+ >(
1025
+ Schema: S,
1026
+ ids: readonly string[],
1027
+ options?: {
1028
+ /** Resolve query to specify which nested CoValues to load */
1029
+ resolve?: ResolveQueryStrict<S, R>;
1030
+ /** Select which value to return. Applies to each element individually. */
1031
+ select?: (value: Loaded<S, R>) => TSelectorReturn;
1032
+ /** Equality function to determine if a selected value has changed, defaults to `Object.is` */
1033
+ equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
1034
+ /**
1035
+ * Create or load a branch for isolated editing.
1036
+ *
1037
+ * Branching lets you take a snapshot of the current state and start modifying it without affecting the canonical/shared version.
1038
+ * It's a fork of your data graph: the same schema, but with diverging values.
1039
+ *
1040
+ * The checkout of the branch is applied on all the resolved values.
1041
+ *
1042
+ * @param name - A unique name for the branch. This identifies the branch
1043
+ * and can be used to switch between different branches of the same CoValue.
1044
+ * @param owner - The owner of the branch. Determines who can access and modify
1045
+ * the branch. If not provided, the branch is owned by the current user.
1046
+ *
1047
+ * For more info see the [branching](https://jazz.tools/docs/react/using-covalues/version-control) documentation.
1048
+ */
1049
+ unstable_branch?: BranchDefinition;
1050
+ },
1051
+ ): TSelectorReturn[] {
1052
+ const resolve = getResolveQuery(Schema, options?.resolve);
1053
+ const subscriptionScopes = useCoValueSubscriptions(
1054
+ Schema,
1055
+ ids,
1056
+ resolve,
1057
+ options?.unstable_branch,
1058
+ ) as SubscriptionScope<CoValue>[];
1059
+ useSuspendUntilLoaded(subscriptionScopes);
1060
+ return useSubscriptionsSelector(subscriptionScopes, options);
1061
+ }
1062
+
1063
+ /**
1064
+ * Subscribe to multiple CoValues without Suspense.
1065
+ *
1066
+ * This hook accepts a list of CoValue IDs and returns an array of maybe-loaded values.
1067
+ * Unlike `useSuspenseCoStates`, this hook does not suspend and returns loading/unavailable
1068
+ * states that can be checked via the `$isLoaded` property.
1069
+ *
1070
+ * @param Schema - The CoValue schema or class constructor
1071
+ * @param ids - Array of CoValue IDs to subscribe to
1072
+ * @param options - Optional configuration, including resolve query
1073
+ * @returns An array of MaybeLoaded CoValues in the same order as the input IDs
1074
+ *
1075
+ * @example
1076
+ * ```typescript
1077
+ * const [project1, project2] = useCoStates(
1078
+ * ProjectSchema,
1079
+ * [projectId1, projectId2],
1080
+ * { resolve: { assignee: true } }
1081
+ * );
1082
+ *
1083
+ * if (!project1.$isLoaded || !project2.$isLoaded) {
1084
+ * return <Loading />;
1085
+ * }
1086
+ * ```
1087
+ */
1088
+ export function useCoStates<
1089
+ S extends CoValueClassOrSchema,
1090
+ // @ts-expect-error we can't statically enforce the schema's resolve query is a valid resolve query, but in practice it is
1091
+ const R extends ResolveQuery<S> = SchemaResolveQuery<S>,
1092
+ TSelectorReturn = MaybeLoaded<Loaded<S, R>>,
1093
+ >(
1094
+ Schema: S,
1095
+ ids: readonly string[],
1096
+ options?: {
1097
+ /** Resolve query to specify which nested CoValues to load */
1098
+ resolve?: ResolveQueryStrict<S, R>;
1099
+ /** Select which value to return. Applies to each element individually. */
1100
+ select?: (value: MaybeLoaded<Loaded<S, R>>) => TSelectorReturn;
1101
+ /** Equality function to determine if a selected value has changed, defaults to `Object.is` */
1102
+ equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
1103
+ /**
1104
+ * Create or load a branch for isolated editing.
1105
+ *
1106
+ * Branching lets you take a snapshot of the current state and start modifying it without affecting the canonical/shared version.
1107
+ * It's a fork of your data graph: the same schema, but with diverging values.
1108
+ *
1109
+ * The checkout of the branch is applied on all the resolved values.
1110
+ *
1111
+ * @param name - A unique name for the branch. This identifies the branch
1112
+ * and can be used to switch between different branches of the same CoValue.
1113
+ * @param owner - The owner of the branch. Determines who can access and modify
1114
+ * the branch. If not provided, the branch is owned by the current user.
1115
+ *
1116
+ * For more info see the [branching](https://jazz.tools/docs/react/using-covalues/version-control) documentation.
1117
+ */
1118
+ unstable_branch?: BranchDefinition;
1119
+ },
1120
+ ): TSelectorReturn[] {
1121
+ const resolve = getResolveQuery(Schema, options?.resolve);
1122
+ const subscriptionScopes = useCoValueSubscriptions(
1123
+ Schema,
1124
+ ids,
1125
+ resolve,
1126
+ options?.unstable_branch,
1127
+ ) as SubscriptionScope<CoValue>[];
1128
+ return useSubscriptionsSelector(subscriptionScopes, options);
1129
+ }