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
@@ -0,0 +1,411 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { Account, RefsToResolve, co, z } from "jazz-tools";
4
+ import { beforeEach, describe, expect, it } from "vitest";
5
+ import { useAccountWithSelector, useJazzContextManager } from "../hooks.js";
6
+ import { useIsAuthenticated } from "../index.js";
7
+ import {
8
+ createJazzTestAccount,
9
+ createJazzTestGuest,
10
+ setupJazzTestSync,
11
+ } from "../testing.js";
12
+ import { act, renderHook } from "./testUtils.js";
13
+ import { useRef } from "react";
14
+
15
+ beforeEach(async () => {
16
+ await setupJazzTestSync();
17
+ });
18
+
19
+ const useRenderCount = <T>(hook: () => T) => {
20
+ const renderCountRef = useRef(0);
21
+ const result = hook();
22
+ renderCountRef.current = renderCountRef.current + 1;
23
+ return {
24
+ renderCount: renderCountRef.current,
25
+ result,
26
+ };
27
+ };
28
+
29
+ describe("useAccountWithSelector", () => {
30
+ it("should return the correct selected value", async () => {
31
+ const AccountRoot = co.map({
32
+ value: z.string(),
33
+ });
34
+
35
+ const AccountSchema = co
36
+ .account({
37
+ root: AccountRoot,
38
+ profile: co.profile(),
39
+ })
40
+ .withMigration((account, creationProps) => {
41
+ if (!account.$jazz.refs.root) {
42
+ account.$jazz.set("root", { value: "123" });
43
+ }
44
+ });
45
+
46
+ const account = await createJazzTestAccount({
47
+ AccountSchema,
48
+ });
49
+
50
+ const { result } = renderHook(
51
+ () =>
52
+ useAccountWithSelector(AccountSchema, {
53
+ resolve: {
54
+ root: true,
55
+ },
56
+ select: (account) => account?.root.value ?? "Loading...",
57
+ }),
58
+ {
59
+ account,
60
+ },
61
+ );
62
+
63
+ expect(result.current).toBe("123");
64
+ });
65
+
66
+ it("should load nested values if requested", async () => {
67
+ const AccountRoot = co.map({
68
+ value: z.string(),
69
+ nested: co.map({
70
+ nestedValue: z.string(),
71
+ }),
72
+ });
73
+
74
+ const AccountSchema = co
75
+ .account({
76
+ root: AccountRoot,
77
+ profile: co.profile(),
78
+ })
79
+ .withMigration((account, creationProps) => {
80
+ if (!account.$jazz.refs.root) {
81
+ const root = AccountRoot.create({
82
+ value: "123",
83
+ nested: co
84
+ .map({
85
+ nestedValue: z.string(),
86
+ })
87
+ .create({
88
+ nestedValue: "456",
89
+ }),
90
+ });
91
+ account.$jazz.set("root", root);
92
+ }
93
+ });
94
+
95
+ const account = await createJazzTestAccount({
96
+ AccountSchema,
97
+ });
98
+
99
+ const { result } = renderHook(
100
+ () =>
101
+ useAccountWithSelector(AccountSchema, {
102
+ resolve: {
103
+ root: {
104
+ nested: true,
105
+ },
106
+ },
107
+ select: (account) => account?.root.nested.nestedValue ?? "Loading...",
108
+ }),
109
+ {
110
+ account,
111
+ },
112
+ );
113
+
114
+ expect(result.current).toBe("456");
115
+ });
116
+
117
+ it("should not re-render when a nested coValue is updated and not selected", async () => {
118
+ const AccountRoot = co.map({
119
+ value: z.string(),
120
+ get nested() {
121
+ return co
122
+ .map({
123
+ nestedValue: z.string(),
124
+ })
125
+ .optional();
126
+ },
127
+ });
128
+
129
+ const AccountSchema = co
130
+ .account({
131
+ root: AccountRoot,
132
+ profile: co.profile(),
133
+ })
134
+ .withMigration((account, creationProps) => {
135
+ if (!account.$jazz.refs.root) {
136
+ const root = AccountRoot.create({
137
+ value: "1",
138
+ nested: co
139
+ .map({
140
+ nestedValue: z.string(),
141
+ })
142
+ .create({
143
+ nestedValue: "1",
144
+ }),
145
+ });
146
+ account.$jazz.set("root", root);
147
+ }
148
+ });
149
+
150
+ const account = await createJazzTestAccount({
151
+ AccountSchema,
152
+ });
153
+
154
+ const { result } = renderHook(
155
+ () =>
156
+ useRenderCount(() =>
157
+ useAccountWithSelector(AccountSchema, {
158
+ resolve: {
159
+ root: {
160
+ nested: true,
161
+ },
162
+ },
163
+ select: (account) => account?.root.value ?? "Loading...",
164
+ }),
165
+ ),
166
+ {
167
+ account,
168
+ },
169
+ );
170
+
171
+ await act(async () => {
172
+ // Update nested value that is not selected
173
+ account.root.nested?.$jazz.set("nestedValue", "100");
174
+ await account.$jazz.waitForAllCoValuesSync();
175
+ });
176
+
177
+ expect(result.current.result).toEqual("1");
178
+ expect(result.current.renderCount).toEqual(1);
179
+ });
180
+
181
+ it("should re-render when a nested coValue is updated and selected", async () => {
182
+ const AccountRoot = co.map({
183
+ value: z.string(),
184
+ get nested() {
185
+ return co
186
+ .map({
187
+ nestedValue: z.string(),
188
+ })
189
+ .optional();
190
+ },
191
+ });
192
+
193
+ const AccountSchema = co
194
+ .account({
195
+ root: AccountRoot,
196
+ profile: co.profile(),
197
+ })
198
+ .withMigration((account, creationProps) => {
199
+ if (!account.$jazz.refs.root) {
200
+ const root = AccountRoot.create({
201
+ value: "1",
202
+ nested: co
203
+ .map({
204
+ nestedValue: z.string(),
205
+ })
206
+ .create({
207
+ nestedValue: "1",
208
+ }),
209
+ });
210
+ account.$jazz.set("root", root);
211
+ }
212
+ });
213
+
214
+ const account = await createJazzTestAccount({
215
+ AccountSchema,
216
+ });
217
+
218
+ const { result } = renderHook(
219
+ () =>
220
+ useRenderCount(() =>
221
+ useAccountWithSelector(AccountSchema, {
222
+ resolve: {
223
+ root: {
224
+ nested: true,
225
+ },
226
+ },
227
+ select: (account) =>
228
+ account?.root?.nested?.nestedValue ?? "Loading...",
229
+ }),
230
+ ),
231
+ {
232
+ account,
233
+ },
234
+ );
235
+
236
+ await act(async () => {
237
+ // Update nested value that is selected
238
+ account.root?.nested?.$jazz.set("nestedValue", "100");
239
+ await account.$jazz.waitForAllCoValuesSync();
240
+ });
241
+
242
+ expect(result.current.result).toEqual("100");
243
+ expect(result.current.renderCount).toEqual(2); // Initial render + update
244
+ });
245
+
246
+ it("should not re-render when equalityFn always returns true", async () => {
247
+ const AccountRoot = co.map({
248
+ value: z.string(),
249
+ get nested() {
250
+ return co
251
+ .map({
252
+ nestedValue: z.string(),
253
+ })
254
+ .optional();
255
+ },
256
+ });
257
+
258
+ const AccountSchema = co
259
+ .account({
260
+ root: AccountRoot,
261
+ profile: co.profile(),
262
+ })
263
+ .withMigration((account, creationProps) => {
264
+ if (!account.$jazz.refs.root) {
265
+ const root = AccountRoot.create({
266
+ value: "1",
267
+ nested: co
268
+ .map({
269
+ nestedValue: z.string(),
270
+ })
271
+ .create({
272
+ nestedValue: "1",
273
+ }),
274
+ });
275
+ account.$jazz.set("root", root);
276
+ }
277
+ });
278
+
279
+ const account = await createJazzTestAccount({
280
+ AccountSchema,
281
+ });
282
+
283
+ const { result } = renderHook(
284
+ () =>
285
+ useRenderCount(() =>
286
+ useAccountWithSelector(AccountSchema, {
287
+ resolve: {
288
+ root: {
289
+ nested: true,
290
+ },
291
+ },
292
+ select: (account) =>
293
+ account?.root?.nested?.nestedValue ?? "Loading...",
294
+ equalityFn: () => true, // Always return true to prevent re-renders
295
+ }),
296
+ ),
297
+ {
298
+ account,
299
+ },
300
+ );
301
+
302
+ await act(async () => {
303
+ // Update nested value that is selected
304
+ account.root?.nested?.$jazz.set("nestedValue", "100");
305
+ await account.$jazz.waitForAllCoValuesSync();
306
+ });
307
+
308
+ expect(result.current.result).toEqual("1"); // Should still be "1" due to equalityFn
309
+ expect(result.current.renderCount).toEqual(1); // Should not re-render
310
+ });
311
+
312
+ it("should not load nested values if the account is a guest", async () => {
313
+ const AccountRoot = co.map({
314
+ value: z.string(),
315
+ });
316
+
317
+ const AccountSchema = co
318
+ .account({
319
+ root: AccountRoot,
320
+ profile: co.profile(),
321
+ })
322
+ .withMigration((account, creationProps) => {
323
+ if (!account.$jazz.refs.root) {
324
+ account.$jazz.set("root", { value: "123" });
325
+ }
326
+ });
327
+
328
+ const account = await createJazzTestGuest();
329
+
330
+ const { result } = renderHook(
331
+ () =>
332
+ useAccountWithSelector(AccountSchema, {
333
+ resolve: {
334
+ root: true,
335
+ },
336
+ select: (account) => account?.root?.value ?? "Guest",
337
+ }),
338
+ {
339
+ account,
340
+ },
341
+ );
342
+
343
+ expect(result.current).toBe("Guest");
344
+ });
345
+
346
+ it("should handle undefined account gracefully", async () => {
347
+ const account = await createJazzTestGuest();
348
+
349
+ const { result } = renderHook(
350
+ () =>
351
+ useAccountWithSelector(Account, {
352
+ select: (account) => account?.$jazz.id ?? "No account",
353
+ }),
354
+ {
355
+ account,
356
+ },
357
+ );
358
+
359
+ expect(result.current).toBe("No account");
360
+ });
361
+
362
+ it("should re-render when selector result changes due to external prop changes", async () => {
363
+ const AccountRoot = co.map({
364
+ value: z.string(),
365
+ });
366
+
367
+ const AccountSchema = co
368
+ .account({
369
+ root: AccountRoot,
370
+ profile: co.profile(),
371
+ })
372
+ .withMigration((account, creationProps) => {
373
+ if (!account.$jazz.refs.root) {
374
+ account.$jazz.set("root", { value: "initial" });
375
+ }
376
+ });
377
+
378
+ const account = await createJazzTestAccount({
379
+ AccountSchema,
380
+ });
381
+
382
+ let externalProp = "suffix1";
383
+
384
+ const { result, rerender } = renderHook(
385
+ () =>
386
+ useRenderCount(() =>
387
+ useAccountWithSelector(AccountSchema, {
388
+ resolve: {
389
+ root: true,
390
+ },
391
+ select: (account) => {
392
+ const baseValue = account?.root?.value ?? "loading";
393
+ return `${baseValue}-${externalProp}`;
394
+ },
395
+ }),
396
+ ),
397
+ {
398
+ account,
399
+ },
400
+ );
401
+
402
+ expect(result.current.result).toEqual("initial-suffix1");
403
+ expect(result.current.renderCount).toEqual(1);
404
+
405
+ // Change external prop and rerender
406
+ externalProp = "suffix2";
407
+ rerender();
408
+
409
+ expect(result.current.result).toEqual("initial-suffix2");
410
+ });
411
+ });
@@ -14,6 +14,7 @@ export {
14
14
  useIsAuthenticated,
15
15
  useAccount,
16
16
  useCoStateWithSelector,
17
+ useAccountWithSelector,
17
18
  } from "jazz-tools/react-core";
18
19
 
19
20
  export function useAcceptInviteNative<S extends CoValueClassOrSchema>({
@@ -8,7 +8,7 @@ import type {
8
8
  } from "jazz-tools";
9
9
  import { Account } from "jazz-tools";
10
10
  import { consumeInviteLinkFromWindowLocation } from "jazz-tools/browser";
11
- import { getContext, untrack } from "svelte";
11
+ import { getContext, onDestroy, untrack } from "svelte";
12
12
  import Provider from "./Provider.svelte";
13
13
 
14
14
  export { Provider as JazzSvelteProvider };
@@ -73,37 +73,33 @@ export class InviteListener<V extends CoValueClassOrSchema> {
73
73
  forValueHint,
74
74
  }: {
75
75
  invitedObjectSchema: V;
76
- onAccept: (projectID: ID<V>) => void;
76
+ onAccept: (coValueID: ID<V>) => void;
77
77
  forValueHint?: string;
78
78
  }) {
79
- // TODO Listen to the hashchange event
80
79
  const _onAccept = onAccept;
80
+ const ctx = getJazzContext<InstanceOfSchema<AccountClass<Account>>>();
81
81
 
82
- // Subscribe to the onAccept function.
82
+ const tryConsume = () => {
83
+ if (!ctx.current || !("me" in ctx.current)) return;
84
+
85
+ consumeInviteLinkFromWindowLocation({
86
+ as: ctx.current.me,
87
+ invitedObjectSchema,
88
+ forValueHint,
89
+ })
90
+ .then((result) => result && _onAccept(result.valueID))
91
+ .catch((e) => console.error("Failed to accept invite", e));
92
+ };
93
+
94
+ // run once when instantiated
83
95
  $effect(() => {
84
- const ctx = getJazzContext<InstanceOfSchema<AccountClass<Account>>>();
85
-
86
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
87
- _onAccept;
88
- // Subscribe to the onAccept function.
89
- untrack(() => {
90
- // If there is no context, return.
91
- if (!ctx.current) return;
92
- if (!("me" in ctx.current)) return;
93
-
94
- // Consume the invite link from the window location.
95
- const result = consumeInviteLinkFromWindowLocation({
96
- as: ctx.current.me,
97
- invitedObjectSchema,
98
- forValueHint,
99
- });
100
- // If the result is valid, call the onAccept function.
101
- result
102
- .then((result) => result && _onAccept(result?.valueID))
103
- .catch((e) => {
104
- console.error("Failed to accept invite", e);
105
- });
106
- });
96
+ untrack(tryConsume);
97
+ });
98
+
99
+ window.addEventListener("hashchange", tryConsume);
100
+
101
+ onDestroy(() => {
102
+ window.removeEventListener("hashchange", tryConsume);
107
103
  });
108
104
  }
109
105
  }
@@ -68,10 +68,9 @@ export class JazzContextManager<
68
68
  protected authSecretStorage = new AuthSecretStorage();
69
69
  protected keepContextOpen = false;
70
70
  contextPromise: Promise<void> | undefined;
71
+ protected authenticatingAccountID: string | null = null;
71
72
 
72
- constructor(opts?: {
73
- useAnonymousFallback?: boolean;
74
- }) {
73
+ constructor(opts?: { useAnonymousFallback?: boolean }) {
75
74
  KvStoreContext.getInstance().initialize(this.getKvStore());
76
75
 
77
76
  if (opts?.useAnonymousFallback) {
@@ -163,11 +162,17 @@ export class JazzContextManager<
163
162
  return this.authSecretStorage;
164
163
  }
165
164
 
165
+ getAuthenticatingAccountID() {
166
+ return this.authenticatingAccountID;
167
+ }
168
+
166
169
  logOut = async () => {
167
170
  if (!this.context || !this.props) {
168
171
  return;
169
172
  }
170
173
 
174
+ this.authenticatingAccountID = null;
175
+
171
176
  await this.props.onLogOut?.();
172
177
 
173
178
  if (this.props.logOutReplacement) {
@@ -206,17 +211,44 @@ export class JazzContextManager<
206
211
  throw new Error("Props required");
207
212
  }
208
213
 
209
- const prevContext = this.context;
210
- const migratingAnonymousAccount =
211
- await this.shouldMigrateAnonymousAccount();
214
+ if (
215
+ this.authenticatingAccountID &&
216
+ this.authenticatingAccountID === credentials.accountID
217
+ ) {
218
+ console.info(
219
+ "Authentication already in progress for account",
220
+ credentials.accountID,
221
+ "skipping duplicate request",
222
+ );
223
+ return;
224
+ }
225
+
226
+ if (
227
+ this.authenticatingAccountID &&
228
+ this.authenticatingAccountID !== credentials.accountID
229
+ ) {
230
+ throw new Error(
231
+ `Authentication already in progress for different account (${this.authenticatingAccountID}), cannot authenticate ${credentials.accountID}`,
232
+ );
233
+ }
234
+
235
+ this.authenticatingAccountID = credentials.accountID;
236
+
237
+ try {
238
+ const prevContext = this.context;
239
+ const migratingAnonymousAccount =
240
+ await this.shouldMigrateAnonymousAccount();
212
241
 
213
- this.keepContextOpen = migratingAnonymousAccount;
214
- await this.createContext(this.props, { credentials }).finally(() => {
215
- this.keepContextOpen = false;
216
- });
242
+ this.keepContextOpen = migratingAnonymousAccount;
243
+ await this.createContext(this.props, { credentials }).finally(() => {
244
+ this.keepContextOpen = false;
245
+ });
217
246
 
218
- if (migratingAnonymousAccount) {
219
- await this.handleAnonymousAccountMigration(prevContext);
247
+ if (migratingAnonymousAccount) {
248
+ await this.handleAnonymousAccountMigration(prevContext);
249
+ }
250
+ } finally {
251
+ this.authenticatingAccountID = null;
220
252
  }
221
253
  };
222
254
 
@@ -228,29 +260,40 @@ export class JazzContextManager<
228
260
  throw new Error("Props required");
229
261
  }
230
262
 
231
- const prevContext = this.context;
232
- const migratingAnonymousAccount =
233
- await this.shouldMigrateAnonymousAccount();
234
-
235
- this.keepContextOpen = migratingAnonymousAccount;
236
- await this.createContext(this.props, {
237
- newAccountProps: {
238
- secret: accountSecret,
239
- creationProps,
240
- },
241
- }).finally(() => {
242
- this.keepContextOpen = false;
243
- });
244
-
245
- if (migratingAnonymousAccount) {
246
- await this.handleAnonymousAccountMigration(prevContext);
263
+ if (this.authenticatingAccountID) {
264
+ throw new Error("Authentication already in progress");
247
265
  }
248
266
 
249
- if (this.context && "me" in this.context) {
250
- return this.context.me.$jazz.id;
251
- }
267
+ // For registration, we don't know the account ID yet, so we'll set it to "register"
268
+ this.authenticatingAccountID = "register";
252
269
 
253
- throw new Error("The registration hasn't created a new account");
270
+ try {
271
+ const prevContext = this.context;
272
+ const migratingAnonymousAccount =
273
+ await this.shouldMigrateAnonymousAccount();
274
+
275
+ this.keepContextOpen = migratingAnonymousAccount;
276
+ await this.createContext(this.props, {
277
+ newAccountProps: {
278
+ secret: accountSecret,
279
+ creationProps,
280
+ },
281
+ }).finally(() => {
282
+ this.keepContextOpen = false;
283
+ });
284
+
285
+ if (migratingAnonymousAccount) {
286
+ await this.handleAnonymousAccountMigration(prevContext);
287
+ }
288
+
289
+ if (this.context && "me" in this.context) {
290
+ return this.context.me.$jazz.id;
291
+ }
292
+
293
+ throw new Error("The registration hasn't created a new account");
294
+ } finally {
295
+ this.authenticatingAccountID = null;
296
+ }
254
297
  };
255
298
 
256
299
  private async handleAnonymousAccountMigration(