jazz-tools 0.18.27 → 0.18.28

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 (40) hide show
  1. package/.turbo/turbo-build.log +57 -57
  2. package/CHANGELOG.md +13 -0
  3. package/dist/{chunk-ZIAN4UY5.js → chunk-YOL3XDDW.js} +158 -120
  4. package/dist/chunk-YOL3XDDW.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/react-core/hooks.d.ts +4 -0
  7. package/dist/react-core/hooks.d.ts.map +1 -1
  8. package/dist/react-core/index.js +5 -0
  9. package/dist/react-core/index.js.map +1 -1
  10. package/dist/testing.js +1 -1
  11. package/dist/tools/coValues/coList.d.ts +11 -3
  12. package/dist/tools/coValues/coList.d.ts.map +1 -1
  13. package/dist/tools/coValues/coMap.d.ts +21 -5
  14. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  15. package/dist/tools/coValues/group.d.ts +2 -2
  16. package/dist/tools/coValues/group.d.ts.map +1 -1
  17. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  18. package/dist/tools/coValues/interfaces.d.ts +9 -0
  19. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  20. package/dist/tools/exports.d.ts +1 -1
  21. package/dist/tools/exports.d.ts.map +1 -1
  22. package/dist/tools/tests/coList.unique.test.d.ts +2 -0
  23. package/dist/tools/tests/coList.unique.test.d.ts.map +1 -0
  24. package/dist/tools/tests/coMap.unique.test.d.ts +2 -0
  25. package/dist/tools/tests/coMap.unique.test.d.ts.map +1 -0
  26. package/package.json +4 -4
  27. package/src/react-core/hooks.ts +8 -0
  28. package/src/react-core/tests/useAccount.test.ts +61 -1
  29. package/src/react-core/tests/usePassPhraseAuth.test.ts +74 -2
  30. package/src/tools/coValues/coList.ts +38 -35
  31. package/src/tools/coValues/coMap.ts +38 -38
  32. package/src/tools/coValues/group.ts +5 -1
  33. package/src/tools/coValues/inbox.ts +4 -3
  34. package/src/tools/coValues/interfaces.ts +88 -0
  35. package/src/tools/exports.ts +1 -0
  36. package/src/tools/tests/coList.test.ts +0 -190
  37. package/src/tools/tests/coList.unique.test.ts +244 -0
  38. package/src/tools/tests/coMap.test.ts +0 -433
  39. package/src/tools/tests/coMap.unique.test.ts +474 -0
  40. package/dist/chunk-ZIAN4UY5.js.map +0 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=coList.unique.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coList.unique.test.d.ts","sourceRoot":"","sources":["../../../src/tools/tests/coList.unique.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=coMap.unique.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coMap.unique.test.d.ts","sourceRoot":"","sources":["../../../src/tools/tests/coMap.unique.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -187,7 +187,7 @@
187
187
  },
188
188
  "type": "module",
189
189
  "license": "MIT",
190
- "version": "0.18.27",
190
+ "version": "0.18.28",
191
191
  "dependencies": {
192
192
  "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
193
193
  "@scure/base": "1.2.1",
@@ -204,9 +204,9 @@
204
204
  "prosemirror-transform": "^1.9.0",
205
205
  "use-sync-external-store": "^1.5.0",
206
206
  "zod": "4.1.11",
207
- "cojson": "0.18.27",
208
- "cojson-storage-indexeddb": "0.18.27",
209
- "cojson-transport-ws": "0.18.27"
207
+ "cojson": "0.18.28",
208
+ "cojson-storage-indexeddb": "0.18.28",
209
+ "cojson-transport-ws": "0.18.28"
210
210
  },
211
211
  "devDependencies": {
212
212
  "@scure/bip39": "^1.3.0",
@@ -792,6 +792,14 @@ export function useAccountWithSelector<
792
792
  );
793
793
  }
794
794
 
795
+ /**
796
+ * Returns a function for logging out of the current account.
797
+ */
798
+ export function useLogOut(): () => void {
799
+ const contextManager = useJazzContextManager();
800
+ return contextManager.logOut;
801
+ }
802
+
795
803
  export function experimental_useInboxSender<
796
804
  I extends CoValue,
797
805
  O extends CoValue | undefined,
@@ -64,7 +64,67 @@ describe("useAccount", () => {
64
64
  expect(result.current?.me?.root?.value).toBe("123");
65
65
  });
66
66
 
67
- it("should be in sync with useIsAuthenticated when logOut is called", async () => {
67
+ it("should be in sync with useIsAuthenticated when logOut (from useAccount.logOut) is called", async () => {
68
+ const account = await createJazzTestAccount({});
69
+
70
+ const accounts: string[] = [];
71
+ const updates: { isAuthenticated: boolean; accountIndex: number }[] = [];
72
+
73
+ const { result } = renderHook(
74
+ () => {
75
+ const isAuthenticated = useIsAuthenticated();
76
+ const account = useAccount();
77
+
78
+ if (account.me) {
79
+ if (!accounts.includes(account.me.$jazz.id)) {
80
+ accounts.push(account.me.$jazz.id);
81
+ }
82
+
83
+ updates.push({
84
+ isAuthenticated,
85
+ accountIndex: accounts.indexOf(account.me.$jazz.id),
86
+ });
87
+ }
88
+
89
+ return { isAuthenticated, account };
90
+ },
91
+ {
92
+ account,
93
+ isAuthenticated: true,
94
+ },
95
+ );
96
+
97
+ expect(result.current?.isAuthenticated).toBe(true);
98
+ expect(result.current?.account?.me).toBeDefined();
99
+
100
+ const id = result.current?.account?.me?.$jazz.id;
101
+
102
+ await act(async () => {
103
+ await result.current?.account?.logOut();
104
+ });
105
+
106
+ expect(result.current?.isAuthenticated).toBe(false);
107
+ expect(result.current?.account?.me?.$jazz.id).not.toBe(id);
108
+
109
+ expect(updates).toMatchInlineSnapshot(`
110
+ [
111
+ {
112
+ "accountIndex": 0,
113
+ "isAuthenticated": true,
114
+ },
115
+ {
116
+ "accountIndex": 0,
117
+ "isAuthenticated": false,
118
+ },
119
+ {
120
+ "accountIndex": 1,
121
+ "isAuthenticated": false,
122
+ },
123
+ ]
124
+ `);
125
+ });
126
+
127
+ it("should be in sync with useIsAuthenticated when logOut (from useLogOut) is called", async () => {
68
128
  const account = await createJazzTestAccount({});
69
129
 
70
130
  const accounts: string[] = [];
@@ -4,7 +4,7 @@ import { mnemonicToEntropy } from "@scure/bip39";
4
4
  import { AuthSecretStorage, KvStoreContext } from "jazz-tools";
5
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
6
  import { usePassphraseAuth } from "../auth/PassphraseAuth";
7
- import { useAccount } from "../hooks";
7
+ import { useAccount, useLogOut } from "../hooks";
8
8
  import {
9
9
  createJazzTestAccount,
10
10
  createJazzTestGuest,
@@ -107,7 +107,7 @@ describe("usePassphraseAuth", () => {
107
107
  expect(await result.current.signUp()).toBe(passphrase);
108
108
  });
109
109
 
110
- it("should be able to logout after sign up", async () => {
110
+ it("should be able to logout after sign up using useAccount.logOut", async () => {
111
111
  const account = await createJazzTestAccount({});
112
112
 
113
113
  const accounts: string[] = [];
@@ -177,4 +177,76 @@ describe("usePassphraseAuth", () => {
177
177
  ]
178
178
  `);
179
179
  });
180
+
181
+ it("should be able to logout after sign up using useLogout", async () => {
182
+ const account = await createJazzTestAccount({});
183
+
184
+ const accounts: string[] = [];
185
+ const updates: { state: string; accountIndex: number }[] = [];
186
+
187
+ const { result } = renderHook(
188
+ () => {
189
+ const passphraseAuth = usePassphraseAuth({ wordlist: testWordlist });
190
+ const account = useAccount();
191
+ const logOut = useLogOut();
192
+
193
+ if (account.me) {
194
+ if (!accounts.includes(account.me.$jazz.id)) {
195
+ accounts.push(account.me.$jazz.id);
196
+ }
197
+
198
+ updates.push({
199
+ state: passphraseAuth.state,
200
+ accountIndex: accounts.indexOf(account.me.$jazz.id),
201
+ });
202
+ }
203
+
204
+ return { passphraseAuth, account, logOut };
205
+ },
206
+ {
207
+ account,
208
+ isAuthenticated: false,
209
+ },
210
+ );
211
+
212
+ expect(result.current?.passphraseAuth.state).toBe("anonymous");
213
+ expect(result.current?.account?.me).toBeDefined();
214
+
215
+ const id = result.current?.account?.me?.$jazz.id;
216
+
217
+ await act(async () => {
218
+ await result.current?.passphraseAuth.signUp();
219
+ });
220
+
221
+ expect(result.current?.passphraseAuth.state).toBe("signedIn");
222
+ expect(result.current?.account?.me?.$jazz.id).toBe(id);
223
+
224
+ await act(async () => {
225
+ await result.current?.logOut();
226
+ });
227
+
228
+ expect(result.current?.passphraseAuth.state).toBe("anonymous");
229
+ expect(result.current?.account?.me?.$jazz.id).not.toBe(id);
230
+
231
+ expect(updates).toMatchInlineSnapshot(`
232
+ [
233
+ {
234
+ "accountIndex": 0,
235
+ "state": "anonymous",
236
+ },
237
+ {
238
+ "accountIndex": 0,
239
+ "state": "signedIn",
240
+ },
241
+ {
242
+ "accountIndex": 0,
243
+ "state": "anonymous",
244
+ },
245
+ {
246
+ "accountIndex": 1,
247
+ "state": "anonymous",
248
+ },
249
+ ]
250
+ `);
251
+ });
180
252
  });
@@ -21,6 +21,8 @@ import {
21
21
  SubscribeRestArgs,
22
22
  TypeSym,
23
23
  BranchDefinition,
24
+ getIdFromHeader,
25
+ unstable_loadUnique,
24
26
  } from "../internal.js";
25
27
  import {
26
28
  AnonymousJazzAgent,
@@ -320,19 +322,17 @@ export class CoList<out Item = any>
320
322
  ownerID: ID<Account> | ID<Group>,
321
323
  as?: Account | Group | AnonymousJazzAgent,
322
324
  ) {
323
- return CoList._findUnique(unique, ownerID, as);
325
+ const header = CoList._getUniqueHeader(unique, ownerID);
326
+
327
+ return getIdFromHeader(header, as);
324
328
  }
325
329
 
326
330
  /** @internal */
327
- static _findUnique<L extends CoList>(
328
- this: CoValueClass<L>,
331
+ static _getUniqueHeader(
329
332
  unique: CoValueUniqueness["uniqueness"],
330
333
  ownerID: ID<Account> | ID<Group>,
331
- as?: Account | Group | AnonymousJazzAgent,
332
334
  ) {
333
- as ||= activeAccountContext.get();
334
-
335
- const header = {
335
+ return {
336
336
  type: "colist" as const,
337
337
  ruleset: {
338
338
  type: "ownedByGroup" as const,
@@ -341,9 +341,6 @@ export class CoList<out Item = any>
341
341
  meta: null,
342
342
  uniqueness: unique,
343
343
  };
344
- const crypto =
345
- as[TypeSym] === "Anonymous" ? as.node.crypto : as.$jazz.localNode.crypto;
346
- return cojsonInternals.idforHeader(header, crypto) as ID<L>;
347
344
  }
348
345
 
349
346
  /**
@@ -378,29 +375,24 @@ export class CoList<out Item = any>
378
375
  resolve?: RefsToResolveStrict<L, R>;
379
376
  },
380
377
  ): Promise<Resolved<L, R> | null> {
381
- const listId = CoList._findUnique(
378
+ const header = CoList._getUniqueHeader(
382
379
  options.unique,
383
380
  options.owner.$jazz.id,
384
- options.owner.$jazz.loadedAs,
385
381
  );
386
- let list: Resolved<L, R> | null = await loadCoValueWithoutMe(this, listId, {
387
- ...options,
388
- loadAs: options.owner.$jazz.loadedAs,
389
- skipRetry: true,
390
- });
391
- if (!list) {
392
- list = (this as any).create(options.value, {
393
- owner: options.owner,
394
- unique: options.unique,
395
- }) as Resolved<L, R>;
396
- } else {
397
- (list as L).$jazz.applyDiff(options.value);
398
- }
399
382
 
400
- return await loadCoValueWithoutMe(this, listId, {
401
- ...options,
402
- loadAs: options.owner.$jazz.loadedAs,
403
- skipRetry: true,
383
+ return unstable_loadUnique(this, {
384
+ header,
385
+ owner: options.owner,
386
+ resolve: options.resolve,
387
+ onCreateWhenMissing: () => {
388
+ (this as any).create(options.value, {
389
+ owner: options.owner,
390
+ unique: options.unique,
391
+ });
392
+ },
393
+ onUpdateWhenFound(value) {
394
+ value.$jazz.applyDiff(options.value);
395
+ },
404
396
  });
405
397
  }
406
398
 
@@ -411,7 +403,10 @@ export class CoList<out Item = any>
411
403
  * @param options Additional options for loading the CoList.
412
404
  * @returns The loaded CoList, or null if unavailable.
413
405
  */
414
- static loadUnique<L extends CoList, const R extends RefsToResolve<L> = true>(
406
+ static async loadUnique<
407
+ L extends CoList,
408
+ const R extends RefsToResolve<L> = true,
409
+ >(
415
410
  this: CoValueClass<L>,
416
411
  unique: CoValueUniqueness["uniqueness"],
417
412
  ownerID: ID<Account> | ID<Group>,
@@ -420,11 +415,19 @@ export class CoList<out Item = any>
420
415
  loadAs?: Account | AnonymousJazzAgent;
421
416
  },
422
417
  ): Promise<Resolved<L, R> | null> {
423
- return loadCoValueWithoutMe(
424
- this,
425
- CoList._findUnique(unique, ownerID, options?.loadAs),
426
- { ...options, skipRetry: true },
427
- );
418
+ const header = CoList._getUniqueHeader(unique, ownerID);
419
+
420
+ const owner = await Group.load(ownerID, {
421
+ loadAs: options?.loadAs,
422
+ });
423
+
424
+ if (!owner) return owner;
425
+
426
+ return unstable_loadUnique(this, {
427
+ header,
428
+ owner,
429
+ resolve: options?.resolve,
430
+ });
428
431
  }
429
432
 
430
433
  // Override mutation methods defined on Array, as CoLists aren't meant to be mutated directly
@@ -28,6 +28,8 @@ import {
28
28
  SubscribeRestArgs,
29
29
  TypeSym,
30
30
  BranchDefinition,
31
+ getIdFromHeader,
32
+ unstable_loadUnique,
31
33
  } from "../internal.js";
32
34
  import {
33
35
  Account,
@@ -434,19 +436,17 @@ export class CoMap extends CoValueBase implements CoValue {
434
436
  ownerID: ID<Account> | ID<Group>,
435
437
  as?: Account | Group | AnonymousJazzAgent,
436
438
  ) {
437
- return CoMap._findUnique(unique, ownerID, as);
439
+ const header = CoMap._getUniqueHeader(unique, ownerID);
440
+
441
+ return getIdFromHeader(header, as);
438
442
  }
439
443
 
440
444
  /** @internal */
441
- static _findUnique<M extends CoMap>(
442
- this: CoValueClass<M>,
445
+ static _getUniqueHeader(
443
446
  unique: CoValueUniqueness["uniqueness"],
444
447
  ownerID: ID<Account> | ID<Group>,
445
- as?: Account | Group | AnonymousJazzAgent,
446
448
  ) {
447
- as ||= activeAccountContext.get();
448
-
449
- const header = {
449
+ return {
450
450
  type: "comap" as const,
451
451
  ruleset: {
452
452
  type: "ownedByGroup" as const,
@@ -455,9 +455,6 @@ export class CoMap extends CoValueBase implements CoValue {
455
455
  meta: null,
456
456
  uniqueness: unique,
457
457
  };
458
- const crypto =
459
- as[TypeSym] === "Anonymous" ? as.node.crypto : as.$jazz.localNode.crypto;
460
- return cojsonInternals.idforHeader(header, crypto) as ID<M>;
461
458
  }
462
459
 
463
460
  /**
@@ -497,32 +494,24 @@ export class CoMap extends CoValueBase implements CoValue {
497
494
  resolve?: RefsToResolveStrict<M, R>;
498
495
  },
499
496
  ): Promise<Resolved<M, R> | null> {
500
- const mapId = CoMap._findUnique(
497
+ const header = CoMap._getUniqueHeader(
501
498
  options.unique,
502
499
  options.owner.$jazz.id,
503
- options.owner.$jazz.loadedAs,
504
500
  );
505
- let map: Resolved<M, R> | null = await loadCoValueWithoutMe(this, mapId, {
506
- ...options,
507
- loadAs: options.owner.$jazz.loadedAs,
508
- skipRetry: true,
509
- });
510
- if (!map) {
511
- const instance = new this();
512
- map = CoMap._createCoMap(instance, options.value, {
513
- owner: options.owner,
514
- unique: options.unique,
515
- }) as Resolved<M, R>;
516
- } else {
517
- (map as M).$jazz.applyDiff(
518
- options.value as unknown as Partial<CoMapInit<M>>,
519
- );
520
- }
521
501
 
522
- return await loadCoValueWithoutMe(this, mapId, {
523
- ...options,
524
- loadAs: options.owner.$jazz.loadedAs,
525
- skipRetry: true,
502
+ return unstable_loadUnique(this, {
503
+ header,
504
+ owner: options.owner,
505
+ resolve: options.resolve,
506
+ onCreateWhenMissing: () => {
507
+ (this as any).create(options.value, {
508
+ owner: options.owner,
509
+ unique: options.unique,
510
+ });
511
+ },
512
+ onUpdateWhenFound(value) {
513
+ value.$jazz.applyDiff(options.value);
514
+ },
526
515
  });
527
516
  }
528
517
 
@@ -535,7 +524,10 @@ export class CoMap extends CoValueBase implements CoValue {
535
524
  *
536
525
  * @deprecated Use `co.map(...).loadUnique` instead.
537
526
  */
538
- static loadUnique<M extends CoMap, const R extends RefsToResolve<M> = true>(
527
+ static async loadUnique<
528
+ M extends CoMap,
529
+ const R extends RefsToResolve<M> = true,
530
+ >(
539
531
  this: CoValueClass<M>,
540
532
  unique: CoValueUniqueness["uniqueness"],
541
533
  ownerID: ID<Account> | ID<Group>,
@@ -544,11 +536,19 @@ export class CoMap extends CoValueBase implements CoValue {
544
536
  loadAs?: Account | AnonymousJazzAgent;
545
537
  },
546
538
  ): Promise<Resolved<M, R> | null> {
547
- return loadCoValueWithoutMe(
548
- this,
549
- CoMap._findUnique(unique, ownerID, options?.loadAs),
550
- { ...options, skipRetry: true },
551
- );
539
+ const header = CoMap._getUniqueHeader(unique, ownerID);
540
+
541
+ const owner = await Group.load(ownerID, {
542
+ loadAs: options?.loadAs,
543
+ });
544
+
545
+ if (!owner) return owner;
546
+
547
+ return unstable_loadUnique(this, {
548
+ header,
549
+ owner,
550
+ resolve: options?.resolve,
551
+ });
552
552
  }
553
553
  }
554
554
 
@@ -9,6 +9,7 @@ import {
9
9
  type Role,
10
10
  } from "cojson";
11
11
  import {
12
+ AnonymousJazzAgent,
12
13
  BranchDefinition,
13
14
  CoValue,
14
15
  CoValueClass,
@@ -272,7 +273,10 @@ export class Group extends CoValueBase implements CoValue {
272
273
  static load<G extends Group, const R extends RefsToResolve<G>>(
273
274
  this: CoValueClass<G>,
274
275
  id: ID<G>,
275
- options?: { resolve?: RefsToResolveStrict<G, R>; loadAs?: Account },
276
+ options?: {
277
+ resolve?: RefsToResolveStrict<G, R>;
278
+ loadAs?: Account | AnonymousJazzAgent;
279
+ },
276
280
  ): Promise<Resolved<G, R> | null> {
277
281
  return loadCoValueWithoutMe(this, id, options);
278
282
  }
@@ -293,12 +293,12 @@ export class Inbox {
293
293
 
294
294
  const handleNewMessages = () => {
295
295
  for (const tx of messagesFeed.getNewItems()) {
296
- const accountID = getAccountIDfromSessionID(tx.txID.sessionID);
296
+ const accountID = getAccountIDfromSessionID(tx.currentTxID.sessionID);
297
297
 
298
298
  if (!accountID) {
299
299
  console.warn(
300
300
  "Received message from unknown account",
301
- tx.txID.sessionID,
301
+ tx.currentTxID.sessionID,
302
302
  );
303
303
  continue;
304
304
  }
@@ -309,7 +309,8 @@ export class Inbox {
309
309
  continue;
310
310
  }
311
311
 
312
- const txKey = `${tx.txID.sessionID}/${tx.txID.txIndex}` as const;
312
+ const txKey =
313
+ `${tx.currentTxID.sessionID}/${tx.currentTxID.txIndex}` as const;
313
314
 
314
315
  if (processed.has(txKey)) {
315
316
  continue;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ cojsonInternals,
2
3
  type CoValueUniqueness,
3
4
  type CojsonInternalTypes,
4
5
  type RawCoValue,
@@ -25,6 +26,7 @@ import {
25
26
  inspect,
26
27
  } from "../internal.js";
27
28
  import type { BranchDefinition } from "../subscribe/types.js";
29
+ import { CoValueHeader } from "cojson/dist/coValueCore/verifiedState.js";
28
30
 
29
31
  /** @category Abstract interfaces */
30
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -446,6 +448,92 @@ export function parseGroupCreateOptions(
446
448
  : { owner: options.owner ?? activeAccountContext.get() };
447
449
  }
448
450
 
451
+ export function getIdFromHeader(
452
+ header: CoValueHeader,
453
+ loadAs?: Account | AnonymousJazzAgent | Group,
454
+ ) {
455
+ loadAs ||= activeAccountContext.get();
456
+
457
+ const node =
458
+ loadAs[TypeSym] === "Anonymous" ? loadAs.node : loadAs.$jazz.localNode;
459
+
460
+ return cojsonInternals.idforHeader(header, node.crypto);
461
+ }
462
+
463
+ export async function unstable_loadUnique<
464
+ V extends CoValue,
465
+ R extends RefsToResolve<V>,
466
+ >(
467
+ cls: CoValueClass<V>,
468
+ options: {
469
+ header: CoValueHeader;
470
+ onCreateWhenMissing?: () => void;
471
+ onUpdateWhenFound?: (value: Resolved<V, R>) => void;
472
+ owner: Account | Group;
473
+ resolve?: RefsToResolveStrict<V, R>;
474
+ },
475
+ ): Promise<Resolved<V, R> | null> {
476
+ const loadAs = options.owner.$jazz.loadedAs;
477
+
478
+ const node =
479
+ loadAs[TypeSym] === "Anonymous" ? loadAs.node : loadAs.$jazz.localNode;
480
+
481
+ const id = cojsonInternals.idforHeader(options.header, node.crypto);
482
+
483
+ // We first try to load the unique value without using resolve and without
484
+ // retrying failures
485
+ // This way when we want to upsert we are sure that, if the load failed
486
+ // it failed because the unique value was missing
487
+ let result = await loadCoValueWithoutMe(cls, id, {
488
+ skipRetry: true,
489
+ loadAs,
490
+ });
491
+
492
+ if (options.onCreateWhenMissing) {
493
+ // if load returns unavailable, we check the state in localNode
494
+ // to ward against race conditions that would happen when
495
+ // running the same upsert unique concurrently
496
+ if (!result && node.getCoValue(id).hasVerifiedContent()) {
497
+ result = await loadCoValueWithoutMe(cls, id, {
498
+ loadAs,
499
+ });
500
+ }
501
+
502
+ if (!result) {
503
+ options.onCreateWhenMissing();
504
+
505
+ return loadCoValueWithoutMe(cls, id, {
506
+ loadAs,
507
+ resolve: options.resolve,
508
+ });
509
+ }
510
+ }
511
+
512
+ if (!result) return result;
513
+
514
+ if (options.onUpdateWhenFound) {
515
+ // we deeply load the value, retrying any failures
516
+ const loaded = await loadCoValueWithoutMe(cls, id, {
517
+ loadAs,
518
+ resolve: options.resolve,
519
+ });
520
+
521
+ if (loaded) {
522
+ // we don't return the update result because
523
+ // we want to run another load to backfill any possible partially loaded
524
+ // values that have been set in the update
525
+ options.onUpdateWhenFound(loaded);
526
+ } else {
527
+ return loaded;
528
+ }
529
+ }
530
+
531
+ return loadCoValueWithoutMe(cls, id, {
532
+ loadAs,
533
+ resolve: options.resolve,
534
+ });
535
+ }
536
+
449
537
  /**
450
538
  * Deeply export a CoValue to a content piece.
451
539
  *
@@ -36,6 +36,7 @@ export type {
36
36
  AccountClass,
37
37
  AccountCreationProps,
38
38
  BaseProfileShape,
39
+ unstable_loadUnique,
39
40
  } from "./internal.js";
40
41
 
41
42
  export {