jazz-tools 0.10.6 → 0.10.8

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.
@@ -13,12 +13,12 @@ KvStoreContext.getInstance().initialize(kvStore);
13
13
  let authSecretStorage = new AuthSecretStorage();
14
14
 
15
15
  describe("AuthSecretStorage", () => {
16
- beforeEach(() => {
17
- kvStore.clearAll();
18
- authSecretStorage = new AuthSecretStorage();
19
- });
20
-
21
16
  describe("migrate", () => {
17
+ beforeEach(() => {
18
+ kvStore.clearAll();
19
+ authSecretStorage = new AuthSecretStorage();
20
+ });
21
+
22
22
  it("should migrate demo auth secret", async () => {
23
23
  const demoSecret = JSON.stringify({
24
24
  accountID: "demo123",
@@ -79,6 +79,11 @@ describe("AuthSecretStorage", () => {
79
79
  });
80
80
 
81
81
  describe("get", () => {
82
+ beforeEach(() => {
83
+ kvStore.clearAll();
84
+ authSecretStorage = new AuthSecretStorage();
85
+ });
86
+
82
87
  it("should return null when no data exists", async () => {
83
88
  expect(await authSecretStorage.get()).toBeNull();
84
89
  });
@@ -132,6 +137,11 @@ describe("AuthSecretStorage", () => {
132
137
  });
133
138
 
134
139
  describe("set", () => {
140
+ beforeEach(() => {
141
+ kvStore.clearAll();
142
+ authSecretStorage = new AuthSecretStorage();
143
+ });
144
+
135
145
  it("should set credentials with secretSeed", async () => {
136
146
  const payload = {
137
147
  accountID: "test123" as ID<Account>,
@@ -165,61 +175,14 @@ describe("AuthSecretStorage", () => {
165
175
  const stored = JSON.parse((await kvStore.get("jazz-logged-in-secret"))!);
166
176
  expect(stored).toEqual(payload);
167
177
  });
168
-
169
- it("should emit update event when setting credentials", async () => {
170
- const handler = vi.fn();
171
- authSecretStorage.onUpdate(handler);
172
-
173
- await authSecretStorage.set({
174
- accountID: "test123" as ID<Account>,
175
- accountSecret:
176
- "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
177
- provider: "passphrase",
178
- });
179
-
180
- expect(handler).toHaveBeenCalled();
181
- });
182
178
  });
183
179
 
184
- describe("isAuthenticated", () => {
185
- it("should return false when no data exists", async () => {
186
- expect(authSecretStorage.isAuthenticated).toBe(false);
187
- });
188
-
189
- it("should return false for anonymous credentials", async () => {
190
- await authSecretStorage.set({
191
- accountID: "test123" as ID<Account>,
192
- accountSecret:
193
- "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
194
- secretSeed: new Uint8Array([1, 2, 3]),
195
- provider: "anonymous",
196
- });
197
- expect(authSecretStorage.isAuthenticated).toBe(false);
198
- });
199
-
200
- it("should return true for non-anonymous credentials", async () => {
201
- await authSecretStorage.set({
202
- accountID: "test123" as ID<Account>,
203
- accountSecret:
204
- "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
205
- secretSeed: new Uint8Array([1, 2, 3]),
206
- provider: "demo",
207
- });
208
- expect(authSecretStorage.isAuthenticated).toBe(true);
209
- });
210
-
211
- it("should return true when the provider is missing", async () => {
212
- await authSecretStorage.set({
213
- accountID: "test123" as ID<Account>,
214
- accountSecret:
215
- "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
216
- secretSeed: new Uint8Array([1, 2, 3]),
217
- } as any);
218
- expect(authSecretStorage.isAuthenticated).toBe(true);
180
+ describe("onUpdate", () => {
181
+ beforeEach(() => {
182
+ kvStore.clearAll();
183
+ authSecretStorage = new AuthSecretStorage();
219
184
  });
220
- });
221
185
 
222
- describe("onUpdate", () => {
223
186
  it("should add and remove event listener", () => {
224
187
  const handler = vi.fn();
225
188
 
@@ -275,6 +238,11 @@ describe("AuthSecretStorage", () => {
275
238
  });
276
239
 
277
240
  describe("clear", () => {
241
+ beforeEach(() => {
242
+ kvStore.clearAll();
243
+ authSecretStorage = new AuthSecretStorage();
244
+ });
245
+
278
246
  it("should remove stored credentials", async () => {
279
247
  await authSecretStorage.set({
280
248
  accountID: "test123" as ID<Account>,
@@ -287,21 +255,179 @@ describe("AuthSecretStorage", () => {
287
255
 
288
256
  expect(await authSecretStorage.get()).toBeNull();
289
257
  });
258
+ });
290
259
 
291
- it("should emit update event when clearing", async () => {
292
- await authSecretStorage.set({
293
- accountID: "test123" as ID<Account>,
294
- accountSecret:
295
- "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
296
- provider: "passphrase",
260
+ describe("notify=true", () => {
261
+ beforeEach(() => {
262
+ kvStore.clearAll();
263
+ authSecretStorage = new AuthSecretStorage();
264
+ authSecretStorage.notify = true;
265
+ });
266
+
267
+ describe("set", () => {
268
+ it("should emit update event when setting credentials", async () => {
269
+ const handler = vi.fn();
270
+ authSecretStorage.onUpdate(handler);
271
+
272
+ await authSecretStorage.set({
273
+ accountID: "test123" as ID<Account>,
274
+ accountSecret:
275
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
276
+ provider: "passphrase",
277
+ });
278
+
279
+ expect(handler).toHaveBeenCalled();
297
280
  });
281
+ });
298
282
 
299
- const handler = vi.fn();
300
- authSecretStorage.onUpdate(handler);
283
+ describe("isAuthenticated", () => {
284
+ it("should return false when no data exists", async () => {
285
+ expect(authSecretStorage.isAuthenticated).toBe(false);
286
+ });
301
287
 
302
- await authSecretStorage.clear();
288
+ it("should return false for anonymous credentials", async () => {
289
+ await authSecretStorage.set({
290
+ accountID: "test123" as ID<Account>,
291
+ accountSecret:
292
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
293
+ secretSeed: new Uint8Array([1, 2, 3]),
294
+ provider: "anonymous",
295
+ });
296
+ expect(authSecretStorage.isAuthenticated).toBe(false);
297
+ });
298
+
299
+ it("should return true for non-anonymous credentials", async () => {
300
+ await authSecretStorage.set({
301
+ accountID: "test123" as ID<Account>,
302
+ accountSecret:
303
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
304
+ secretSeed: new Uint8Array([1, 2, 3]),
305
+ provider: "demo",
306
+ });
307
+ expect(authSecretStorage.isAuthenticated).toBe(true);
308
+ });
309
+
310
+ it("should return true when the provider is missing", async () => {
311
+ await authSecretStorage.set({
312
+ accountID: "test123" as ID<Account>,
313
+ accountSecret:
314
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
315
+ secretSeed: new Uint8Array([1, 2, 3]),
316
+ } as any);
317
+ expect(authSecretStorage.isAuthenticated).toBe(true);
318
+ });
319
+ });
320
+
321
+ describe("clear", () => {
322
+ it("should emit update event when clearing", async () => {
323
+ await authSecretStorage.set({
324
+ accountID: "test123" as ID<Account>,
325
+ accountSecret:
326
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
327
+ provider: "passphrase",
328
+ });
329
+
330
+ const handler = vi.fn();
331
+ authSecretStorage.onUpdate(handler);
332
+
333
+ await authSecretStorage.clear();
334
+
335
+ expect(handler).toHaveBeenCalled();
336
+ });
337
+ });
338
+ });
339
+
340
+ describe("notify=false", () => {
341
+ beforeEach(() => {
342
+ kvStore.clearAll();
343
+ authSecretStorage = new AuthSecretStorage();
344
+ });
345
+
346
+ describe("set", () => {
347
+ it("should not emit update event when setting credentials", async () => {
348
+ const handler = vi.fn();
349
+ authSecretStorage.onUpdate(handler);
350
+
351
+ await authSecretStorage.set({
352
+ accountID: "test123" as ID<Account>,
353
+ accountSecret:
354
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
355
+ provider: "passphrase",
356
+ });
303
357
 
304
- expect(handler).toHaveBeenCalled();
358
+ expect(handler).not.toHaveBeenCalled();
359
+ });
360
+ });
361
+
362
+ describe("isAuthenticated", () => {
363
+ it("should return false when no data exists", async () => {
364
+ expect(authSecretStorage.isAuthenticated).toBe(false);
365
+ });
366
+
367
+ it("should return false for anonymous credentials", async () => {
368
+ await authSecretStorage.set({
369
+ accountID: "test123" as ID<Account>,
370
+ accountSecret:
371
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
372
+ secretSeed: new Uint8Array([1, 2, 3]),
373
+ provider: "anonymous",
374
+ });
375
+ expect(authSecretStorage.isAuthenticated).toBe(false);
376
+ });
377
+
378
+ it("should return true for non-anonymous credentials", async () => {
379
+ await authSecretStorage.set({
380
+ accountID: "test123" as ID<Account>,
381
+ accountSecret:
382
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
383
+ secretSeed: new Uint8Array([1, 2, 3]),
384
+ provider: "demo",
385
+ });
386
+ expect(authSecretStorage.isAuthenticated).toBe(false);
387
+ authSecretStorage.emitUpdate({
388
+ accountID: "test123" as ID<Account>,
389
+ accountSecret:
390
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
391
+ secretSeed: new Uint8Array([1, 2, 3]),
392
+ provider: "demo",
393
+ });
394
+ expect(authSecretStorage.isAuthenticated).toBe(true);
395
+ });
396
+
397
+ it("should return true when the provider is missing", async () => {
398
+ await authSecretStorage.set({
399
+ accountID: "test123" as ID<Account>,
400
+ accountSecret:
401
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
402
+ secretSeed: new Uint8Array([1, 2, 3]),
403
+ } as any);
404
+ expect(authSecretStorage.isAuthenticated).toBe(false);
405
+ authSecretStorage.emitUpdate({
406
+ accountID: "test123" as ID<Account>,
407
+ accountSecret:
408
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
409
+ secretSeed: new Uint8Array([1, 2, 3]),
410
+ } as any);
411
+ expect(authSecretStorage.isAuthenticated).toBe(true);
412
+ });
413
+ });
414
+
415
+ describe("clear", () => {
416
+ it("should not emit update event when clearing", async () => {
417
+ await authSecretStorage.set({
418
+ accountID: "test123" as ID<Account>,
419
+ accountSecret:
420
+ "secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
421
+ provider: "passphrase",
422
+ });
423
+
424
+ const handler = vi.fn();
425
+ authSecretStorage.onUpdate(handler);
426
+
427
+ await authSecretStorage.clear();
428
+
429
+ expect(handler).not.toHaveBeenCalled();
430
+ });
305
431
  });
306
432
  });
307
433
  });
@@ -26,6 +26,7 @@ import {
26
26
  setupJazzTestSync,
27
27
  } from "../testing";
28
28
 
29
+ // @ts-ignore Typescript in VSCode doesn't like top level await
29
30
  const Crypto = await WasmCrypto.create();
30
31
 
31
32
  class TestJazzContextManager<Acc extends Account> extends JazzContextManager<
@@ -53,16 +54,20 @@ class TestJazzContextManager<Acc extends Account> extends JazzContextManager<
53
54
  AccountSchema: props.AccountSchema,
54
55
  });
55
56
 
56
- this.updateContext(props, {
57
- me: context.account,
58
- node: context.node,
59
- done: () => {
60
- context.done();
57
+ await this.updateContext(
58
+ props,
59
+ {
60
+ me: context.account,
61
+ node: context.node,
62
+ done: () => {
63
+ context.done();
64
+ },
65
+ logOut: async () => {
66
+ await context.logOut();
67
+ },
61
68
  },
62
- logOut: async () => {
63
- await context.logOut();
64
- },
65
- });
69
+ authProps,
70
+ );
66
71
  }
67
72
  }
68
73
 
@@ -1,5 +1,5 @@
1
1
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
2
- import { describe, expect, expectTypeOf, test } from "vitest";
2
+ import { describe, expect, expectTypeOf, test, vi } from "vitest";
3
3
  import { Group, randomSessionProvider } from "../exports.js";
4
4
  import {
5
5
  Account,
@@ -10,7 +10,7 @@ import {
10
10
  createJazzContextFromExistingCredentials,
11
11
  isControlledAccount,
12
12
  } from "../index.js";
13
- import { setupTwoNodes } from "./utils.js";
13
+ import { setupTwoNodes, waitFor } from "./utils.js";
14
14
 
15
15
  const connectedPeers = cojsonInternals.connectedPeers;
16
16
 
@@ -1,6 +1,6 @@
1
1
  import { cojsonInternals } from "cojson";
2
2
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
3
- import { describe, expect, expectTypeOf, test } from "vitest";
3
+ import { describe, expect, expectTypeOf, test, vi } from "vitest";
4
4
  import {
5
5
  Account,
6
6
  CoFeed,
@@ -14,6 +14,7 @@ import {
14
14
  isControlledAccount,
15
15
  } from "../index.js";
16
16
  import { randomSessionProvider } from "../internal.js";
17
+ import { waitFor } from "./utils.js";
17
18
 
18
19
  const Crypto = await WasmCrypto.create();
19
20
  const { connectedPeers } = cojsonInternals;
@@ -303,3 +304,86 @@ test("Deep loading a record-like coMap", async () => {
303
304
  expect(recordLoaded.key2?.list).not.toBe(null);
304
305
  expect(recordLoaded.key2?.list).not.toBe(undefined);
305
306
  });
307
+
308
+ test("doesn't break on Map.Record key deletion when the key is referenced in the depth", async () => {
309
+ class JazzProfile extends CoMap {
310
+ firstName = co.string;
311
+ }
312
+
313
+ class JazzySnapStore extends CoMap.Record(co.ref(JazzProfile)) {}
314
+
315
+ const me = await Account.create({
316
+ creationProps: { name: "Tester McTesterson" },
317
+ crypto: Crypto,
318
+ });
319
+
320
+ const snapStore = JazzySnapStore.create(
321
+ {
322
+ profile1: JazzProfile.create({ firstName: "John" }, { owner: me }),
323
+ profile2: JazzProfile.create({ firstName: "John" }, { owner: me }),
324
+ },
325
+ { owner: me },
326
+ );
327
+
328
+ const spy = vi.fn();
329
+ const unsub = snapStore.subscribe({ profile1: {}, profile2: {} }, spy);
330
+
331
+ await waitFor(() => expect(spy).toHaveBeenCalled());
332
+
333
+ spy.mockClear();
334
+ delete snapStore.profile1;
335
+
336
+ expect(Object.keys(snapStore)).toEqual(["profile2"]);
337
+
338
+ unsub();
339
+
340
+ await expect(
341
+ snapStore.ensureLoaded({
342
+ profile1: {},
343
+ }),
344
+ ).rejects.toThrow("Failed to deeply load CoValue " + snapStore.id);
345
+ });
346
+
347
+ test("throw when calling ensureLoaded on a ref that's required but missing", async () => {
348
+ class JazzProfile extends CoMap {
349
+ firstName = co.string;
350
+ }
351
+
352
+ class JazzRoot extends CoMap {
353
+ profile = co.ref(JazzProfile);
354
+ }
355
+
356
+ const me = await Account.create({
357
+ creationProps: { name: "Tester McTesterson" },
358
+ crypto: Crypto,
359
+ });
360
+
361
+ const root = JazzRoot.create(
362
+ // @ts-expect-error missing required ref
363
+ {},
364
+ { owner: me },
365
+ );
366
+
367
+ await expect(
368
+ root.ensureLoaded({
369
+ profile: {},
370
+ }),
371
+ ).rejects.toThrow("Failed to deeply load CoValue " + root.id);
372
+ });
373
+
374
+ test("throw when calling ensureLoaded on a ref that is not defined in the schema", async () => {
375
+ class JazzRoot extends CoMap {}
376
+
377
+ const me = await Account.create({
378
+ creationProps: { name: "Tester McTesterson" },
379
+ crypto: Crypto,
380
+ });
381
+
382
+ const root = JazzRoot.create({}, { owner: me });
383
+
384
+ await expect(
385
+ root.ensureLoaded({
386
+ profile: {},
387
+ }),
388
+ ).rejects.toThrow("Failed to deeply load CoValue " + root.id);
389
+ });
@@ -150,6 +150,45 @@ describe("subscribeToCoValue", () => {
150
150
  );
151
151
  });
152
152
 
153
+ it("shouldn't fire updates after unsubscribing", async () => {
154
+ const { me, meOnSecondPeer } = await setupAccount();
155
+
156
+ const chatRoom = createChatRoom(me, "General");
157
+ const updateFn = vi.fn();
158
+
159
+ const { messages } = await chatRoom.ensureLoaded({ messages: [{}] });
160
+
161
+ messages.push(createMessage(me, "Hello"));
162
+
163
+ const unsubscribe = subscribeToCoValue(
164
+ ChatRoom,
165
+ chatRoom.id,
166
+ meOnSecondPeer,
167
+ {
168
+ messages: [{}],
169
+ },
170
+ updateFn,
171
+ );
172
+
173
+ await waitFor(() => {
174
+ expect(updateFn).toHaveBeenCalled();
175
+ });
176
+
177
+ unsubscribe();
178
+ chatRoom.name = "Lounge";
179
+ messages.push(createMessage(me, "Hello 2"));
180
+
181
+ await new Promise((resolve) => setTimeout(resolve, 100));
182
+
183
+ expect(updateFn).toHaveBeenCalledTimes(1);
184
+ expect(updateFn).toHaveBeenCalledWith(
185
+ expect.objectContaining({
186
+ id: chatRoom.id,
187
+ }),
188
+ expect.any(Function),
189
+ );
190
+ });
191
+
153
192
  it("should fire updates when a ref entity is updates", async () => {
154
193
  const { me, meOnSecondPeer } = await setupAccount();
155
194
 
package/src/types.ts CHANGED
@@ -24,6 +24,7 @@ export type JazzAuthContext<Acc extends Account> = {
24
24
  authenticate: AuthenticateAccountFunction;
25
25
  logOut: () => Promise<void>;
26
26
  done: () => void;
27
+ isAuthenticated?: boolean;
27
28
  };
28
29
 
29
30
  export type JazzGuestContext = {
@@ -32,6 +33,7 @@ export type JazzGuestContext = {
32
33
  authenticate: AuthenticateAccountFunction;
33
34
  logOut: () => void;
34
35
  done: () => void;
36
+ isAuthenticated?: boolean;
35
37
  };
36
38
 
37
39
  export type JazzContextType<Acc extends Account> =