jazz-tools 0.19.20 → 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.
- package/.svelte-kit/__package__/server.d.ts.map +1 -1
- package/.svelte-kit/__package__/server.js +9 -7
- package/.turbo/turbo-build.log +52 -52
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js +4 -4
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/better-auth/database-adapter/index.js.map +1 -1
- package/dist/better-auth/database-adapter/repository/generic.d.ts +3 -3
- package/dist/better-auth/database-adapter/repository/session.d.ts +2 -2
- package/dist/better-auth/database-adapter/schema.d.ts +3 -3
- package/dist/better-auth/database-adapter/schema.d.ts.map +1 -1
- package/dist/{chunk-MI24YFCY.js → chunk-QCTQH5RS.js} +1 -1
- package/dist/chunk-QCTQH5RS.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/hooks.d.ts +1 -2
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.js +7 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +92 -1
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +126 -57
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useCoStates.test.d.ts +2 -0
- package/dist/react-core/tests/useCoStates.test.d.ts.map +1 -0
- package/dist/react-native/index.js +4 -0
- package/dist/react-native/index.js.map +1 -1
- 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 +4 -0
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/svelte/auth/ClerkAuth.svelte.d.ts +38 -0
- package/dist/svelte/auth/ClerkAuth.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/ClerkAuth.svelte.js +47 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts +67 -0
- package/dist/svelte/auth/JazzSvelteProviderWithClerk.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte +27 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts +17 -0
- package/dist/svelte/auth/RegisterClerkAuth.svelte.d.ts.map +1 -0
- package/dist/svelte/auth/index.d.ts +2 -0
- package/dist/svelte/auth/index.d.ts.map +1 -1
- package/dist/svelte/auth/index.js +2 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/ClerkAuth.svelte.test.js +202 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts +8 -0
- package/dist/svelte/tests/TestClerkAuthWrapper.svelte.d.ts.map +1 -0
- package/dist/svelte/tests/testUtils.d.ts +1 -0
- package/dist/svelte/tests/testUtils.d.ts.map +1 -1
- package/dist/svelte/tests/testUtils.js +3 -1
- package/dist/testing.js +1 -1
- package/dist/tools/auth/clerk/index.d.ts +1 -1
- package/dist/tools/auth/clerk/types.d.ts +1 -1
- package/dist/tools/auth/clerk/types.d.ts.map +1 -1
- package/dist/tools/subscribe/types.d.ts +1 -1
- package/dist/tools/subscribe/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/better-auth/auth/server.ts +9 -7
- package/src/better-auth/database-adapter/repository/generic.ts +3 -3
- package/src/better-auth/database-adapter/repository/session.ts +2 -2
- package/src/better-auth/database-adapter/schema.ts +5 -5
- package/src/react/hooks.tsx +4 -2
- package/src/react-core/hooks.ts +321 -76
- package/src/react-core/tests/useCoState.selector.test.ts +309 -22
- package/src/react-core/tests/useCoStates.test.tsx +414 -0
- package/src/react-native-core/hooks.tsx +2 -0
- package/src/svelte/auth/ClerkAuth.svelte.ts +67 -0
- package/src/svelte/auth/JazzSvelteProviderWithClerk.svelte +156 -0
- package/src/svelte/auth/RegisterClerkAuth.svelte +27 -0
- package/src/svelte/auth/index.ts +2 -0
- package/src/svelte/tests/ClerkAuth.svelte.test.ts +305 -0
- package/src/svelte/tests/TestClerkAuthWrapper.svelte +16 -0
- package/src/svelte/tests/testUtils.ts +4 -1
- package/src/tools/auth/clerk/types.ts +1 -1
- package/src/tools/subscribe/types.ts +1 -1
- package/src/tools/tests/inbox.test.ts +7 -7
- package/dist/chunk-MI24YFCY.js.map +0 -1
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { cojsonInternals } from "cojson";
|
|
4
|
+
import { Loaded, co, z } from "jazz-tools";
|
|
5
|
+
import { assertLoaded } from "jazz-tools/testing";
|
|
6
|
+
import { assert, beforeEach, describe, expect, expectTypeOf, it } from "vitest";
|
|
7
|
+
import React, { Suspense } from "react";
|
|
8
|
+
import { useCoStates, useSuspenseCoStates } from "../hooks.js";
|
|
9
|
+
import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
|
|
10
|
+
import { act, renderHook, waitFor } from "./testUtils.js";
|
|
11
|
+
|
|
12
|
+
const ProjectSchema = co.map({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
cojsonInternals.setCoValueLoadingRetryDelay(20);
|
|
18
|
+
|
|
19
|
+
await setupJazzTestSync({
|
|
20
|
+
asyncPeers: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await createJazzTestAccount({
|
|
24
|
+
isCurrentActiveAccount: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("useSuspenseCoStates", () => {
|
|
29
|
+
it("should return loaded values for all subscriptions", async () => {
|
|
30
|
+
const project1 = ProjectSchema.create({ name: "My Project 1" });
|
|
31
|
+
const project2 = ProjectSchema.create({ name: "My Project 2" });
|
|
32
|
+
|
|
33
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
34
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const { result } = await act(async () => {
|
|
38
|
+
return renderHook(
|
|
39
|
+
() =>
|
|
40
|
+
useSuspenseCoStates(ProjectSchema, [
|
|
41
|
+
project1.$jazz.id,
|
|
42
|
+
project2.$jazz.id,
|
|
43
|
+
]),
|
|
44
|
+
{
|
|
45
|
+
wrapper,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Wait for any async operations to complete
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(result.current).toBeDefined();
|
|
53
|
+
expect(result.current.length).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const [loadedProject1, loadedProject2] = result.current;
|
|
57
|
+
|
|
58
|
+
assert(loadedProject1);
|
|
59
|
+
expect(loadedProject1.name).toBe("My Project 1");
|
|
60
|
+
|
|
61
|
+
assert(loadedProject2);
|
|
62
|
+
expect(loadedProject2.name).toBe("My Project 2");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should have correct return types for each entry", async () => {
|
|
66
|
+
const project1 = ProjectSchema.create({ name: "Project 1" });
|
|
67
|
+
const project2 = ProjectSchema.create({ name: "Project 2" });
|
|
68
|
+
|
|
69
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
70
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const ids = [project1.$jazz.id, project2.$jazz.id] as const;
|
|
74
|
+
|
|
75
|
+
const { result } = await act(async () => {
|
|
76
|
+
return renderHook(() => useSuspenseCoStates(ProjectSchema, ids), {
|
|
77
|
+
wrapper,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(result.current).toBeDefined();
|
|
83
|
+
expect(result.current.length).toBe(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expectTypeOf(result.current).toEqualTypeOf<
|
|
87
|
+
Loaded<typeof ProjectSchema>[]
|
|
88
|
+
>();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should re-render when any value changes", async () => {
|
|
92
|
+
const project1 = ProjectSchema.create({ name: "Project 1" });
|
|
93
|
+
const project2 = ProjectSchema.create({ name: "Project 2" });
|
|
94
|
+
|
|
95
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
96
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const { result } = await act(async () => {
|
|
100
|
+
return renderHook(
|
|
101
|
+
() =>
|
|
102
|
+
useSuspenseCoStates(ProjectSchema, [
|
|
103
|
+
project1.$jazz.id,
|
|
104
|
+
project2.$jazz.id,
|
|
105
|
+
]),
|
|
106
|
+
{
|
|
107
|
+
wrapper,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(result.current).toBeDefined();
|
|
114
|
+
expect(result.current.length).toBe(2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert(result.current[0]);
|
|
118
|
+
assert(result.current[0]);
|
|
119
|
+
expect(result.current[0].name).toBe("Project 1");
|
|
120
|
+
|
|
121
|
+
// Update one of the values
|
|
122
|
+
act(() => {
|
|
123
|
+
project1.$jazz.set("name", "updated1");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
assert(result.current[0]);
|
|
127
|
+
expect(result.current[0].name).toBe("updated1");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should handle empty subscription array", async () => {
|
|
131
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
132
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const { result } = await act(async () => {
|
|
136
|
+
return renderHook(() => useSuspenseCoStates(ProjectSchema, []), {
|
|
137
|
+
wrapper,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(result.current).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.current).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("supports branching CoValues", async () => {
|
|
149
|
+
const project = ProjectSchema.create({ name: "My Project" });
|
|
150
|
+
|
|
151
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
152
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const branchName = "feature-branch";
|
|
156
|
+
const { result } = await act(async () => {
|
|
157
|
+
return renderHook(
|
|
158
|
+
() =>
|
|
159
|
+
useSuspenseCoStates(ProjectSchema, [project.$jazz.id], {
|
|
160
|
+
unstable_branch: { name: branchName },
|
|
161
|
+
}),
|
|
162
|
+
{
|
|
163
|
+
wrapper,
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
assert(result.current[0]);
|
|
169
|
+
expect(result.current[0].name).toBe("My Project");
|
|
170
|
+
|
|
171
|
+
// Updates on changes to the branched CoValue
|
|
172
|
+
act(() => {
|
|
173
|
+
result.current[0]!.$jazz.set("name", "My Project Updated");
|
|
174
|
+
});
|
|
175
|
+
assert(result.current[0]);
|
|
176
|
+
expect(result.current[0].name).toBe("My Project Updated");
|
|
177
|
+
|
|
178
|
+
// Does not update when changes are made to another branch
|
|
179
|
+
act(() => {
|
|
180
|
+
project.$jazz.set("name", "My Project Updated 2");
|
|
181
|
+
});
|
|
182
|
+
assert(result.current[0]);
|
|
183
|
+
expect(result.current[0].name).toBe("My Project Updated");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("useCoStates", () => {
|
|
188
|
+
it("should return MaybeLoaded values", async () => {
|
|
189
|
+
const project1 = ProjectSchema.create({ name: "My Project 1" });
|
|
190
|
+
const project2 = ProjectSchema.create({ name: "My Project 2" });
|
|
191
|
+
|
|
192
|
+
const { result } = renderHook(() =>
|
|
193
|
+
useCoStates(ProjectSchema, [project1.$jazz.id, project2.$jazz.id]),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
198
|
+
expect(result.current[1]?.$isLoaded).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const [loadedProject1, loadedProject2] = result.current;
|
|
202
|
+
|
|
203
|
+
assert(loadedProject1);
|
|
204
|
+
assert(loadedProject2);
|
|
205
|
+
assertLoaded(loadedProject1);
|
|
206
|
+
assertLoaded(loadedProject2);
|
|
207
|
+
expect(loadedProject1.name).toBe("My Project 1");
|
|
208
|
+
expect(loadedProject2.name).toBe("My Project 2");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should re-render when any value changes", async () => {
|
|
212
|
+
const project1 = ProjectSchema.create({ name: "Project 1" });
|
|
213
|
+
const project2 = ProjectSchema.create({ name: "Project 2" });
|
|
214
|
+
|
|
215
|
+
const { result } = renderHook(() =>
|
|
216
|
+
useCoStates(ProjectSchema, [project1.$jazz.id, project2.$jazz.id]),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await waitFor(() => {
|
|
220
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(result.current[0]).not.toBeNull();
|
|
224
|
+
if (result.current[0]) {
|
|
225
|
+
assertLoaded(result.current[0]);
|
|
226
|
+
expect(result.current[0].name).toBe("Project 1");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Update one of the values
|
|
230
|
+
act(() => {
|
|
231
|
+
project1.$jazz.set("name", "updated1");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
const val = result.current[0];
|
|
236
|
+
return val?.$isLoaded && val.name === "updated1";
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
assert(result.current[0]);
|
|
240
|
+
expect(result.current[0].name).toBe("updated1");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should handle empty subscription array", async () => {
|
|
244
|
+
const { result } = renderHook(() => useCoStates(ProjectSchema, []));
|
|
245
|
+
|
|
246
|
+
await waitFor(() => {
|
|
247
|
+
expect(result.current).not.toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(result.current).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should update when ids change", async () => {
|
|
254
|
+
const project1 = ProjectSchema.create({ name: "My Project 1" });
|
|
255
|
+
const project2 = ProjectSchema.create({ name: "My Project 2" });
|
|
256
|
+
|
|
257
|
+
let ids: string[] = [project1.$jazz.id, project2.$jazz.id];
|
|
258
|
+
const { result, rerender } = renderHook(
|
|
259
|
+
({ ids }: { ids: string[] }) => useCoStates(ProjectSchema, ids),
|
|
260
|
+
{
|
|
261
|
+
initialProps: { ids },
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
267
|
+
expect(result.current[1]?.$isLoaded).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const project3 = ProjectSchema.create({ name: "My Project 3" });
|
|
271
|
+
act(() => {
|
|
272
|
+
// Create a new array with updated IDs
|
|
273
|
+
ids = [project2.$jazz.id, project3.$jazz.id];
|
|
274
|
+
rerender({ ids });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
279
|
+
expect(result.current[1]?.$isLoaded).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
assert(result.current[0]);
|
|
283
|
+
assert(result.current[1]);
|
|
284
|
+
assertLoaded(result.current[0]);
|
|
285
|
+
assertLoaded(result.current[1]);
|
|
286
|
+
expect(result.current[0].name).toBe("My Project 2");
|
|
287
|
+
expect(result.current[1].name).toBe("My Project 3");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should not update when ids are the same", async () => {
|
|
291
|
+
const project1 = ProjectSchema.create({ name: "My Project 1" });
|
|
292
|
+
const project2 = ProjectSchema.create({ name: "My Project 2" });
|
|
293
|
+
|
|
294
|
+
let ids: string[] = [project1.$jazz.id, project2.$jazz.id];
|
|
295
|
+
const { result, rerender } = renderHook(
|
|
296
|
+
({ ids }: { ids: string[] }) => useCoStates(ProjectSchema, ids),
|
|
297
|
+
{
|
|
298
|
+
initialProps: { ids },
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
await waitFor(() => {
|
|
303
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
304
|
+
expect(result.current[1]?.$isLoaded).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const firstResult = result.current;
|
|
308
|
+
|
|
309
|
+
act(() => {
|
|
310
|
+
// Create a new array with the same IDs
|
|
311
|
+
ids = [...ids];
|
|
312
|
+
rerender({ ids });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// The result should be the same reference when IDs haven't changed
|
|
316
|
+
expect(result.current).toBe(firstResult);
|
|
317
|
+
expect(result.current[0]).toBe(firstResult[0]);
|
|
318
|
+
expect(result.current[1]).toBe(firstResult[1]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("supports branching CoValues", async () => {
|
|
322
|
+
const project = ProjectSchema.create({ name: "My Project" });
|
|
323
|
+
|
|
324
|
+
const { result } = renderHook(() =>
|
|
325
|
+
useCoStates(ProjectSchema, [project.$jazz.id], {
|
|
326
|
+
unstable_branch: { name: "feature-branch" },
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const loadedProject = result.current[0];
|
|
331
|
+
assert(loadedProject);
|
|
332
|
+
assertLoaded(loadedProject);
|
|
333
|
+
expect(loadedProject.name).toBe("My Project");
|
|
334
|
+
|
|
335
|
+
// Updates on changes to the branched CoValue
|
|
336
|
+
act(() => {
|
|
337
|
+
loadedProject.$jazz.set("name", "My Project Updated");
|
|
338
|
+
});
|
|
339
|
+
const loadedProject2 = result.current[0];
|
|
340
|
+
assert(loadedProject2);
|
|
341
|
+
assertLoaded(loadedProject2);
|
|
342
|
+
expect(loadedProject2.name).toBe("My Project Updated");
|
|
343
|
+
|
|
344
|
+
// Does not update when changes are made to another branch
|
|
345
|
+
act(() => {
|
|
346
|
+
project.$jazz.set("name", "My Project Updated 2");
|
|
347
|
+
});
|
|
348
|
+
const loadedProject3 = result.current[0];
|
|
349
|
+
assert(loadedProject3);
|
|
350
|
+
assertLoaded(loadedProject3);
|
|
351
|
+
expect(loadedProject3.name).toBe("My Project Updated");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should remove subscriptions for removed ids", async () => {
|
|
355
|
+
const project1 = ProjectSchema.create({ name: "My Project 1" });
|
|
356
|
+
const project2 = ProjectSchema.create({ name: "My Project 2" });
|
|
357
|
+
|
|
358
|
+
let ids = [project1.$jazz.id, project2.$jazz.id];
|
|
359
|
+
let renderCount = 0;
|
|
360
|
+
const { result, rerender } = renderHook(
|
|
361
|
+
({ ids }: { ids: string[] }) => {
|
|
362
|
+
renderCount++;
|
|
363
|
+
return useCoStates(ProjectSchema, ids);
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
initialProps: { ids },
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
372
|
+
expect(result.current[1]?.$isLoaded).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
assert(result.current[0]);
|
|
376
|
+
assertLoaded(result.current[0]);
|
|
377
|
+
const loadedProject1 = result.current[0];
|
|
378
|
+
|
|
379
|
+
act(() => {
|
|
380
|
+
ids.shift(); // Remove project1, keeping only project2
|
|
381
|
+
rerender({ ids });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(result.current.length).toBe(1);
|
|
386
|
+
expect(result.current[0]?.$isLoaded).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
assert(result.current[0]);
|
|
389
|
+
assertLoaded(result.current[0]);
|
|
390
|
+
expect(result.current[0].name).toBe("My Project 2");
|
|
391
|
+
|
|
392
|
+
expect(renderCount).toBe(2);
|
|
393
|
+
|
|
394
|
+
// Modify project1. The hook should NOT re-render because project1 is no longer subscribed
|
|
395
|
+
act(() => {
|
|
396
|
+
project1.$jazz.set("name", "Modified Project 1");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Wait a bit to ensure any potential updates would have occurred
|
|
400
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
401
|
+
|
|
402
|
+
// The hook didn't re-render
|
|
403
|
+
expect(renderCount).toBe(2);
|
|
404
|
+
|
|
405
|
+
// project2's name is still the same
|
|
406
|
+
assert(result.current[0]);
|
|
407
|
+
assertLoaded(result.current[0]);
|
|
408
|
+
expect(result.current[0].name).toBe("My Project 2");
|
|
409
|
+
|
|
410
|
+
// project1's subscription scope is no longer subscribed to
|
|
411
|
+
const project1SubscriptionScope = loadedProject1.$jazz._subscriptionScope;
|
|
412
|
+
expect(project1SubscriptionScope?.subscribers.size).toBe(0);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
@@ -6,6 +6,7 @@ import { Linking } from "react-native";
|
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
useCoState,
|
|
9
|
+
useCoStates,
|
|
9
10
|
experimental_useInboxSender,
|
|
10
11
|
useDemoAuth,
|
|
11
12
|
usePassphraseAuth,
|
|
@@ -20,6 +21,7 @@ export {
|
|
|
20
21
|
useAccountSubscription,
|
|
21
22
|
useSubscriptionSelector,
|
|
22
23
|
useSuspenseCoState,
|
|
24
|
+
useSuspenseCoStates,
|
|
23
25
|
useSuspenseAccount,
|
|
24
26
|
} from "jazz-tools/react-core";
|
|
25
27
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { JazzClerkAuth, type MinimalClerkClient } from "jazz-tools";
|
|
2
|
+
import { getAuthSecretStorage, getJazzContext } from "../jazz.svelte.js";
|
|
3
|
+
import { useIsAuthenticated } from "./useIsAuthenticated.svelte.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authentication state returned by {@link useClerkAuth}.
|
|
7
|
+
*
|
|
8
|
+
* - `"anonymous"`: User is not authenticated with Jazz (may or may not be signed into Clerk)
|
|
9
|
+
* - `"signedIn"`: User is authenticated with both Clerk and Jazz
|
|
10
|
+
*/
|
|
11
|
+
export type ClerkAuth = {
|
|
12
|
+
state: "anonymous" | "signedIn";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registers a Clerk authentication listener and provides reactive auth state.
|
|
17
|
+
*
|
|
18
|
+
* Must be used within a component that is a child of `JazzSvelteProvider`.
|
|
19
|
+
* Automatically syncs Clerk authentication state with Jazz.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <script>
|
|
24
|
+
* import { useClerkAuth } from "jazz-tools/svelte";
|
|
25
|
+
*
|
|
26
|
+
* const auth = useClerkAuth(clerk);
|
|
27
|
+
* </script>
|
|
28
|
+
*
|
|
29
|
+
* {#if auth.state === "signedIn"}
|
|
30
|
+
* <p>Welcome back!</p>
|
|
31
|
+
* {:else}
|
|
32
|
+
* <p>Please sign in</p>
|
|
33
|
+
* {/if}
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param clerk - The Clerk client instance
|
|
37
|
+
* @returns An object with a reactive `state` property
|
|
38
|
+
* @throws Error if used in guest mode
|
|
39
|
+
* @category Auth Providers
|
|
40
|
+
*/
|
|
41
|
+
export function useClerkAuth(clerk: MinimalClerkClient): ClerkAuth {
|
|
42
|
+
const context = getJazzContext();
|
|
43
|
+
const authSecretStorage = getAuthSecretStorage();
|
|
44
|
+
|
|
45
|
+
if ("guest" in context.current) {
|
|
46
|
+
throw new Error("Clerk auth is not supported in guest mode");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const authMethod = new JazzClerkAuth(
|
|
50
|
+
context.current.authenticate,
|
|
51
|
+
context.current.logOut,
|
|
52
|
+
authSecretStorage,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
return authMethod.registerListener(clerk);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const isAuthenticated = useIsAuthenticated();
|
|
60
|
+
const state = $derived(isAuthenticated.current ? "signedIn" : "anonymous");
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
get state() {
|
|
64
|
+
return state;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A pre-configured Jazz provider that integrates with Clerk authentication.
|
|
4
|
+
|
|
5
|
+
Use this component instead of `JazzSvelteProvider` when using Clerk for authentication.
|
|
6
|
+
It handles:
|
|
7
|
+
- Pre-loading Jazz credentials from Clerk before rendering children
|
|
8
|
+
- Registering Clerk auth state listeners
|
|
9
|
+
- Wiring up logout functionality to Clerk's signOut
|
|
10
|
+
|
|
11
|
+
@example
|
|
12
|
+
```svelte
|
|
13
|
+
<JazzSvelteProviderWithClerk
|
|
14
|
+
{clerk}
|
|
15
|
+
sync={{ peer: "wss://cloud.jazz.tools/?key=your-key" }}
|
|
16
|
+
AccountSchema={MyAccountSchema}
|
|
17
|
+
>
|
|
18
|
+
<App />
|
|
19
|
+
</JazzSvelteProviderWithClerk>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
@category Auth Providers
|
|
23
|
+
-->
|
|
24
|
+
<script
|
|
25
|
+
lang="ts"
|
|
26
|
+
generics="S extends (AccountClass<Account> & CoValueFromRaw<Account>) | AnyAccountSchema"
|
|
27
|
+
>
|
|
28
|
+
import {
|
|
29
|
+
Account,
|
|
30
|
+
type AccountClass,
|
|
31
|
+
type AnyAccountSchema,
|
|
32
|
+
type CoValueFromRaw,
|
|
33
|
+
InMemoryKVStore,
|
|
34
|
+
type InstanceOfSchema,
|
|
35
|
+
JazzClerkAuth,
|
|
36
|
+
KvStoreContext,
|
|
37
|
+
type MinimalClerkClient,
|
|
38
|
+
type SyncConfig,
|
|
39
|
+
} from "jazz-tools";
|
|
40
|
+
import {
|
|
41
|
+
type BaseBrowserContextOptions,
|
|
42
|
+
LocalStorageKVStore,
|
|
43
|
+
} from "jazz-tools/browser";
|
|
44
|
+
import type { Snippet } from "svelte";
|
|
45
|
+
import { JazzSvelteProvider } from "../jazz.svelte.js";
|
|
46
|
+
import RegisterClerkAuth from "./RegisterClerkAuth.svelte";
|
|
47
|
+
|
|
48
|
+
type Props = {
|
|
49
|
+
/** The Clerk client instance for authentication (can be null while Clerk is initializing) */
|
|
50
|
+
clerk: MinimalClerkClient | null;
|
|
51
|
+
/** Content to render when provider is initialized */
|
|
52
|
+
children?: Snippet;
|
|
53
|
+
/** Content to render while Clerk auth is loading */
|
|
54
|
+
fallback?: Snippet;
|
|
55
|
+
/** Content to render when authentication initialization fails */
|
|
56
|
+
errorFallback?: Snippet<[Error]>;
|
|
57
|
+
/** Callback when authentication initialization fails */
|
|
58
|
+
onAuthError?: (error: Error) => void;
|
|
59
|
+
/** Enable server-side rendering support with anonymous fallback */
|
|
60
|
+
enableSSR?: boolean;
|
|
61
|
+
/** Custom key for storing auth secrets in KvStore */
|
|
62
|
+
authSecretStorageKey?: string;
|
|
63
|
+
/** Jazz sync configuration (peer URL and key) */
|
|
64
|
+
sync: SyncConfig;
|
|
65
|
+
/** Custom storage implementation for auth secrets */
|
|
66
|
+
storage?: BaseBrowserContextOptions["storage"];
|
|
67
|
+
/** Account schema class for typed account access */
|
|
68
|
+
AccountSchema?: S;
|
|
69
|
+
/** Default profile name for new accounts */
|
|
70
|
+
defaultProfileName?: string;
|
|
71
|
+
/** Callback when an anonymous account is discarded during sign-in */
|
|
72
|
+
onAnonymousAccountDiscarded?: (
|
|
73
|
+
anonymousAccount: InstanceOfSchema<S>,
|
|
74
|
+
) => Promise<void>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let {
|
|
78
|
+
clerk,
|
|
79
|
+
children,
|
|
80
|
+
fallback,
|
|
81
|
+
errorFallback,
|
|
82
|
+
onAuthError,
|
|
83
|
+
...providerProps
|
|
84
|
+
}: Props = $props();
|
|
85
|
+
|
|
86
|
+
let isLoaded = $state(false);
|
|
87
|
+
let initError = $state<Error | null>(null);
|
|
88
|
+
|
|
89
|
+
function setupKvStore() {
|
|
90
|
+
const isSSR = typeof window === "undefined";
|
|
91
|
+
if (isSSR) {
|
|
92
|
+
console.debug("[Jazz] Using InMemoryKVStore for SSR context");
|
|
93
|
+
}
|
|
94
|
+
KvStoreContext.getInstance().initialize(
|
|
95
|
+
isSSR ? new InMemoryKVStore() : new LocalStorageKVStore(),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pre-loads Jazz authentication data from Clerk before mounting JazzSvelteProvider.
|
|
101
|
+
*
|
|
102
|
+
* For authenticated Clerk users with existing Jazz credentials, this populates the auth
|
|
103
|
+
* secret storage before rendering children, avoiding a flash of unauthenticated state.
|
|
104
|
+
* For unauthenticated users, the initialization completes immediately with no effect.
|
|
105
|
+
*/
|
|
106
|
+
$effect(() => {
|
|
107
|
+
setupKvStore();
|
|
108
|
+
|
|
109
|
+
if (!clerk) return;
|
|
110
|
+
|
|
111
|
+
let cancelled = false;
|
|
112
|
+
|
|
113
|
+
JazzClerkAuth.initializeAuth(clerk)
|
|
114
|
+
.then(() => {
|
|
115
|
+
if (!cancelled) {
|
|
116
|
+
isLoaded = true;
|
|
117
|
+
initError = null;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
console.error(
|
|
122
|
+
"[Jazz] Clerk authentication initialization failed:",
|
|
123
|
+
error,
|
|
124
|
+
);
|
|
125
|
+
if (!cancelled) {
|
|
126
|
+
const errorObj =
|
|
127
|
+
error instanceof Error ? error : new Error(String(error));
|
|
128
|
+
initError = errorObj;
|
|
129
|
+
onAuthError?.(errorObj);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
cancelled = true;
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
{#if initError}
|
|
140
|
+
{#if errorFallback}
|
|
141
|
+
{@render errorFallback(initError)}
|
|
142
|
+
{:else}
|
|
143
|
+
<div data-testid="jazz-clerk-auth-error">
|
|
144
|
+
Authentication initialization failed. Please refresh the page or try again
|
|
145
|
+
later.
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
148
|
+
{:else if isLoaded && clerk}
|
|
149
|
+
<JazzSvelteProvider {...providerProps} onLogOut={clerk.signOut}>
|
|
150
|
+
<RegisterClerkAuth {clerk}>
|
|
151
|
+
{@render children?.()}
|
|
152
|
+
</RegisterClerkAuth>
|
|
153
|
+
</JazzSvelteProvider>
|
|
154
|
+
{:else if fallback}
|
|
155
|
+
{@render fallback?.()}
|
|
156
|
+
{/if}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Internal component that registers the Clerk auth listener.
|
|
4
|
+
|
|
5
|
+
This component exists because `useClerkAuth` requires access to the Jazz context,
|
|
6
|
+
which is only available inside `JazzSvelteProvider`'s children. By placing the
|
|
7
|
+
hook call in this child component, we ensure the context is properly initialized.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import type { MinimalClerkClient } from "jazz-tools";
|
|
11
|
+
import type { Snippet } from "svelte";
|
|
12
|
+
import { useClerkAuth } from "./ClerkAuth.svelte.js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
clerk: MinimalClerkClient;
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { clerk, children }: Props = $props();
|
|
20
|
+
|
|
21
|
+
// Register the Clerk auth listener after JazzSvelteProvider context is available.
|
|
22
|
+
// The return value (auth state) is intentionally unused here - this component's
|
|
23
|
+
// sole purpose is to register the listener that syncs Clerk and Jazz auth state.
|
|
24
|
+
useClerkAuth(clerk);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{@render children?.()}
|
package/src/svelte/auth/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export * from "./PasskeyAuth.svelte.js";
|
|
2
2
|
export * from "./PassphraseAuth.svelte.js";
|
|
3
|
+
export * from "./ClerkAuth.svelte.js";
|
|
3
4
|
export { default as PasskeyAuthBasicUI } from "./PasskeyAuthBasicUI.svelte";
|
|
5
|
+
export { default as JazzSvelteProviderWithClerk } from "./JazzSvelteProviderWithClerk.svelte";
|