@phosphorco/bb-identity 0.3.0 → 0.4.0

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.
@@ -302,6 +302,9 @@ function sameVersion(a, b) {
302
302
  return a.status === b.status && version(a).epoch === version(b).epoch && version(a).sequence === version(b).sequence;
303
303
  }
304
304
  function createIdentityState(options) {
305
+ return createStateController(options);
306
+ }
307
+ function createStateController(options, editing = false) {
305
308
  const address = frozen({ ...options.address });
306
309
  const expected = frozen({ ...options.expected });
307
310
  const definition = options.definition;
@@ -335,6 +338,12 @@ function createIdentityState(options) {
335
338
  let closing = false;
336
339
  let checkpoint = null;
337
340
  let recoverySource = null;
341
+ let checkpointError = null;
342
+ let checkpointWriting = 0;
343
+ let lastRejection = null;
344
+ let rejectedGeneration = -1;
345
+ let ownerFenced = false;
346
+ let loadFailed = false;
338
347
  let automaticSuspensions = 0;
339
348
  let automaticEpoch = 0;
340
349
  let persistQueue = Promise.resolve();
@@ -350,7 +359,7 @@ function createIdentityState(options) {
350
359
  timer?.();
351
360
  timer = null;
352
361
  };
353
- const draft = () => base === null || desired === null ? null : frozen({ formatVersion: 1, key: { address, actor: expected.actor, ownerSession: options.ownerSession }, schemaVersion: definition.schemaVersion, acknowledged: base, desired: desired.value, localGeneration, inFlight: pending?.mutation ?? null });
362
+ const draft = () => base === null || desired === null ? null : frozen({ formatVersion: 1, key: { address, actor: expected.actor, ownerSession: options.ownerSession }, schemaVersion: definition.schemaVersion, acknowledged: base, desired: desired.value, localGeneration, inFlight: pending?.mutation ?? null, ...editing ? { rejection: lastRejection, rejectedGeneration } : {} });
354
363
  function emit() {
355
364
  if (lifecycle === "detached" || lifecycle === "disposed")
356
365
  snapshot = { status: "detached", address, draft: draft() };
@@ -374,6 +383,8 @@ function createIdentityState(options) {
374
383
  function sessionCheck() {
375
384
  if (!active())
376
385
  return err("disposed", "This state controller is no longer active.");
386
+ if (editing && ownerFenced)
387
+ return err("stale-context", "This editing owner was retired.");
377
388
  const result = options.currentSession();
378
389
  if (!result.ok) {
379
390
  block("identity-unavailable", result.error.message);
@@ -385,6 +396,8 @@ function createIdentityState(options) {
385
396
  return { ok: false, error: session.error };
386
397
  }
387
398
  if (session.instanceId !== address.instanceId || session.actor.identity.key !== expected.actor || session.stamp !== expected.session) {
399
+ if (editing)
400
+ ownerFenced = true;
388
401
  block("owner-changed", "The identity session changed.");
389
402
  return err("stale-context", "The identity session changed.", "after-refresh");
390
403
  }
@@ -405,11 +418,23 @@ function createIdentityState(options) {
405
418
  const capturedEpoch = epoch;
406
419
  const serial = ++persistSerial;
407
420
  const work = persistQueue.then(async () => {
421
+ checkpointWriting++;
422
+ if (editing)
423
+ emit();
408
424
  const result = await safe(() => drafts.write(captured, checkpoint?.revision ?? null));
409
- if (result.ok)
425
+ checkpointWriting--;
426
+ if (result.ok) {
410
427
  checkpoint = result.value;
411
- else if (active(capturedEpoch) && serial === persistSerial)
412
- block("storage-error", result.error.message);
428
+ if (editing)
429
+ checkpointError = null;
430
+ } else if (active(capturedEpoch) && serial === persistSerial) {
431
+ if (editing)
432
+ checkpointError = result.error;
433
+ else
434
+ block("storage-error", result.error.message);
435
+ }
436
+ if (editing)
437
+ emit();
413
438
  return result;
414
439
  });
415
440
  persistQueue = work.then(() => {
@@ -419,7 +444,7 @@ function createIdentityState(options) {
419
444
  }
420
445
  function schedule() {
421
446
  clearTimer();
422
- if (!active() || automaticSuspensions || closing || blocked || pending || !needsSave())
447
+ if (editing || !active() || automaticSuspensions || closing || blocked || pending || !needsSave())
423
448
  return;
424
449
  timer = scheduler.schedule(debounce, () => {
425
450
  timer = null;
@@ -488,7 +513,7 @@ function createIdentityState(options) {
488
513
  return ok(undefined);
489
514
  if (base && dirty()) {
490
515
  if (sameVersion(base, read)) {
491
- if (!blocked?.conflict)
516
+ if (!blocked?.conflict && (!editing || blocked?.reason !== "rejected"))
492
517
  blocked = null;
493
518
  emit();
494
519
  schedule();
@@ -498,7 +523,8 @@ function createIdentityState(options) {
498
523
  }
499
524
  base = read;
500
525
  desired = { value: valueClone(codec, currentValue(read)) };
501
- blocked = null;
526
+ if (!editing || blocked?.reason !== "rejected")
527
+ blocked = null;
502
528
  emit();
503
529
  schedule();
504
530
  return ok(undefined);
@@ -524,14 +550,17 @@ function createIdentityState(options) {
524
550
  if (!live.ok)
525
551
  return live;
526
552
  if (!loaded.ok) {
553
+ loadFailed = true;
527
554
  block("storage-error", loaded.error.message);
528
555
  return loaded;
529
556
  }
530
557
  const parsed = readClone(loaded.value);
531
558
  if (!parsed.ok) {
559
+ loadFailed = true;
532
560
  block("storage-error", parsed.error.message);
533
561
  return parsed;
534
562
  }
563
+ loadFailed = false;
535
564
  return applyRemote(parsed.value, cause);
536
565
  })();
537
566
  activeLoad = work;
@@ -545,6 +574,10 @@ function createIdentityState(options) {
545
574
  return work;
546
575
  }
547
576
  function outcomeCheck(outcome, operationId) {
577
+ if (editing && outcome.status === "rejected")
578
+ return outcome.operationId === operationId ? ok(frozen(outcome)) : err("incompatible", "Rejected receipt has another operation.");
579
+ if (outcome.status === "rejected")
580
+ return err("incompatible", "Rejection is not a legacy state outcome.");
548
581
  const parsed = codecs.save.decode(structuredClone(codecs.save.encode(outcome)));
549
582
  if (!parsed.ok)
550
583
  return parsed;
@@ -564,6 +597,17 @@ function createIdentityState(options) {
564
597
  pending = null;
565
598
  loadId++;
566
599
  blocked = null;
600
+ if (outcome.status === "rejected") {
601
+ lastRejection = outcome.evidence;
602
+ rejectedGeneration = captured.generation;
603
+ block("rejected", "The submitted edit was definitively rejected.");
604
+ const saved2 = await persist();
605
+ return saved2.ok ? err("invalid-input", "The submitted edit was definitively rejected.") : saved2;
606
+ }
607
+ if (editing && (outcome.status === "saved" || outcome.status === "unchanged")) {
608
+ lastRejection = null;
609
+ rejectedGeneration = -1;
610
+ }
567
611
  if (outcome.status === "saved" || outcome.status === "unchanged") {
568
612
  base = { status: "present", envelope: outcome.envelope };
569
613
  acknowledgedGeneration = Math.max(acknowledgedGeneration, captured.generation);
@@ -595,6 +639,8 @@ function createIdentityState(options) {
595
639
  return reload("reconnect");
596
640
  schedule();
597
641
  }
642
+ if (editing && snapshot.status === "blocked" && snapshot.reason === "conflict")
643
+ return err("conflict", snapshot.message);
598
644
  return ok(undefined);
599
645
  }
600
646
  function transmit(captured, automatic = false) {
@@ -663,7 +709,9 @@ function createIdentityState(options) {
663
709
  const valid = sessionCheck();
664
710
  if (!valid.ok)
665
711
  return Promise.resolve(valid);
666
- if (blocked)
712
+ if (editing && lastRejection && localGeneration <= rejectedGeneration)
713
+ return Promise.resolve(err("invalid-input", "Correct the rejected draft before submitting a new operation."));
714
+ if (blocked && !(editing && blocked.reason === "rejected" && localGeneration > rejectedGeneration))
667
715
  return Promise.resolve(err("conflict", blocked.message));
668
716
  if (activeSave)
669
717
  return activeSave;
@@ -672,6 +720,8 @@ function createIdentityState(options) {
672
720
  if (!base || !desired || !needsSave())
673
721
  return Promise.resolve(ok(undefined));
674
722
  const mutation = frozen({ kind: base.status === "empty" ? "initialize" : "replace", address, expectedVersion: { ...version(base) }, expected, ownerSession: options.ownerSession, localGeneration, operationId: newOperationId(), schemaVersion: definition.schemaVersion, value: valueClone(codec, desired.value) });
723
+ if (editing && blocked?.reason === "rejected")
724
+ blocked = null;
675
725
  pending = { mutation, base, generation: localGeneration };
676
726
  return transmit(pending, automatic);
677
727
  }
@@ -696,7 +746,7 @@ function createIdentityState(options) {
696
746
  const valid = sessionCheck();
697
747
  if (!valid.ok)
698
748
  return valid;
699
- const result = await safe(() => options.transport.reconcile({ address, expected, operationId: captured.mutation.operationId }, { signal: abort.signal }));
749
+ const result = await safe(() => options.transport.reconcile({ address, expected, operationId: captured.mutation.operationId, mutation: captured.mutation }, { signal: abort.signal }));
700
750
  if (!active(capturedEpoch) || pending !== captured)
701
751
  return err("cancelled", "State reconciliation was superseded.");
702
752
  const live = sessionCheck();
@@ -708,7 +758,7 @@ function createIdentityState(options) {
708
758
  }
709
759
  if (result.value.status === "final")
710
760
  return applyOutcome(captured, result.value.outcome);
711
- if (result.value.status === "absent-final" && allowReplay) {
761
+ if (result.value.status === "absent-final" && allowReplay && !editing) {
712
762
  if (captured.mutation.expected.actor !== expected.actor || captured.mutation.expected.session !== expected.session) {
713
763
  block("draft-recovery", "The original operation cannot be re-authored under a new session.");
714
764
  return err("stale-context", "The original operation requires its original session.");
@@ -724,6 +774,13 @@ function createIdentityState(options) {
724
774
  const valid = sessionCheck();
725
775
  if (!valid.ok)
726
776
  return valid;
777
+ if (editing) {
778
+ if (activeSave)
779
+ return activeSave;
780
+ if (pending)
781
+ return reconcile(false);
782
+ return sendNew();
783
+ }
727
784
  const goal = localGeneration;
728
785
  let retries = 0;
729
786
  if (activeSave) {
@@ -777,6 +834,27 @@ function createIdentityState(options) {
777
834
  }
778
835
  return {
779
836
  getSnapshot: () => snapshot,
837
+ inspect() {
838
+ const retained = draft();
839
+ const checkpointMatches = checkpoint !== null && retained !== null && checkpoint.draft.localGeneration === retained.localGeneration && JSON.stringify(checkpoint.draft.inFlight ? codecs.mutation.encode(checkpoint.draft.inFlight) : null) === JSON.stringify(retained.inFlight ? codecs.mutation.encode(retained.inFlight) : null) && JSON.stringify(checkpoint.draft.rejection ?? null) === JSON.stringify(retained.rejection ?? null) && JSON.stringify(codecs.read.encode(checkpoint.draft.acknowledged)) === JSON.stringify(codecs.read.encode(retained.acknowledged));
840
+ return {
841
+ draft: retained,
842
+ base,
843
+ desired: desired?.value,
844
+ generation: localGeneration,
845
+ pending: pending?.mutation ?? null,
846
+ saving: networkActive,
847
+ dirty: dirty(),
848
+ saveEligible: needsSave() && (!lastRejection || localGeneration > rejectedGeneration),
849
+ editable: active() && !closing && !ownerFenced && !loadFailed && base !== null && desired !== null && (!blocked || ["conflict", "indeterminate", "rejected", "draft-recovery"].includes(blocked.reason)),
850
+ checkpoint: {
851
+ status: !drafts ? "disabled" : checkpointWriting ? "writing" : checkpointError ? "failed" : checkpointMatches ? "backed-up" : "local-only",
852
+ generation: checkpoint?.draft.localGeneration ?? null,
853
+ revision: checkpoint?.revision ?? null,
854
+ error: checkpointError
855
+ }
856
+ };
857
+ },
780
858
  subscribe(listener) {
781
859
  if (lifecycle === "disposed")
782
860
  return () => {};
@@ -812,7 +890,7 @@ function createIdentityState(options) {
812
890
  const valid = sessionCheck();
813
891
  if (!valid.ok)
814
892
  return valid;
815
- if (closing || blocked || snapshot.status !== "ready")
893
+ if (closing || (editing ? ownerFenced || loadFailed || !base || !desired || !!blocked && !["conflict", "indeterminate", "rejected", "draft-recovery"].includes(blocked.reason) : blocked || snapshot.status !== "ready"))
816
894
  return err("unavailable", "State is not editable.");
817
895
  try {
818
896
  desired = { value: valueClone(codec, value) };
@@ -820,6 +898,8 @@ function createIdentityState(options) {
820
898
  return err("invalid-input", error instanceof Error ? error.message : "Invalid state.");
821
899
  }
822
900
  localGeneration++;
901
+ if (editing && blocked?.conflict)
902
+ blocked = { ...blocked, conflict: frozen({ ...blocked.conflict, token: newConflictToken(), local: desired.value }) };
823
903
  emit();
824
904
  schedule();
825
905
  return ok(undefined);
@@ -888,6 +968,10 @@ function createIdentityState(options) {
888
968
  return err("invalid-input", "Draft operation does not match its owner or generation.");
889
969
  mutation = frozen(result.value);
890
970
  }
971
+ if (editing) {
972
+ lastRejection = source.rejection ?? null;
973
+ rejectedGeneration = source.rejectedGeneration ?? -1;
974
+ }
891
975
  clearTimer();
892
976
  base = parsed.value;
893
977
  desired = { value: valueClone(codec, source.desired) };
@@ -1092,6 +1176,35 @@ function createStateTransport(options) {
1092
1176
  // client-state-binding-runtime.ts
1093
1177
  var waiting = Object.freeze({ status: "waiting-for-identity" });
1094
1178
  function bindIdentityState(options, retirementOwner) {
1179
+ return bindOwnedIdentityController({
1180
+ ...options,
1181
+ hasPending: (state) => state.status === "ready" ? state.dirty || state.saving : (state.status === "blocked" || state.status === "detached") && state.draft !== null,
1182
+ create: (input) => options.create ? options.create(input) : createIdentityState({
1183
+ address: {
1184
+ instanceId: input.session.instanceId,
1185
+ pluginId: options.resource.pluginId,
1186
+ collection: options.resource.definition.collection,
1187
+ recordId: options.recordId,
1188
+ owner: input.subject.key
1189
+ },
1190
+ expected: { actor: input.session.actor.identity.key, session: input.session.stamp },
1191
+ ownerSession: input.ownerSession,
1192
+ definition: options.resource.definition,
1193
+ transport: createStateTransport({ connection: options.client.connection, resource: options.resource }),
1194
+ ...options.drafts ? { drafts: options.drafts } : {},
1195
+ onConflict: options.onConflict,
1196
+ initializeEmpty: options.initializeEmpty && (options.editPolicy === "collaborators" || input.subject.key === input.session.actor.identity.key),
1197
+ ...options.scheduler ? { scheduler: options.scheduler } : {},
1198
+ currentSession: () => options.client.currentSession()
1199
+ }),
1200
+ preservation: (controller, session) => options.resource && options.drafts ? { storage: options.drafts, scope: {
1201
+ address: controller.getSnapshot().address,
1202
+ actor: session.actor.identity.key,
1203
+ schemaVersion: options.resource.definition.schemaVersion
1204
+ } } : null
1205
+ }, retirementOwner);
1206
+ }
1207
+ function bindOwnedIdentityController(options, retirementOwner) {
1095
1208
  let disposed = false;
1096
1209
  let viewDisposed = false;
1097
1210
  let current = null;
@@ -1107,7 +1220,7 @@ function bindIdentityState(options, retirementOwner) {
1107
1220
  } catch {}
1108
1221
  }
1109
1222
  };
1110
- const hasPending = (state) => state.status === "ready" ? state.dirty || state.saving : (state.status === "blocked" || state.status === "detached") && state.draft !== null;
1223
+ const hasPending = options.hasPending;
1111
1224
  const live = () => {
1112
1225
  if (disposed || viewDisposed)
1113
1226
  return null;
@@ -1201,34 +1314,13 @@ function bindIdentityState(options, retirementOwner) {
1201
1314
  const owner = idCodec("owner-session").decode(`owner-${newOperationId()}`);
1202
1315
  if (!owner.ok)
1203
1316
  throw new Error(owner.error.message);
1204
- const controller = options.create ? options.create({ session: next.session, subject: next.subject, ownerSession: owner.value }) : createIdentityState({
1205
- address: {
1206
- instanceId: next.session.instanceId,
1207
- pluginId: options.resource.pluginId,
1208
- collection: options.resource.definition.collection,
1209
- recordId: options.recordId,
1210
- owner: next.subject.key
1211
- },
1212
- expected: { actor: next.session.actor.identity.key, session: next.session.stamp },
1213
- ownerSession: owner.value,
1214
- definition: options.resource.definition,
1215
- transport: createStateTransport({ connection: options.client.connection, resource: options.resource }),
1216
- ...options.drafts ? { drafts: options.drafts } : {},
1217
- onConflict: options.onConflict,
1218
- initializeEmpty: next.writable && options.initializeEmpty,
1219
- ...options.scheduler ? { scheduler: options.scheduler } : {},
1220
- currentSession: () => options.client.currentSession()
1221
- });
1317
+ const controller = options.create({ session: next.session, subject: next.subject, ownerSession: owner.value });
1222
1318
  if (live()?.key !== next.key) {
1223
1319
  controller.dispose();
1224
1320
  observeAgain = true;
1225
1321
  continue;
1226
1322
  }
1227
- const preservation = options.resource && options.drafts ? { storage: options.drafts, scope: {
1228
- address: controller.getSnapshot().address,
1229
- actor: next.session.actor.identity.key,
1230
- schemaVersion: options.resource.definition.schemaVersion
1231
- } } : null;
1323
+ const preservation = options.preservation?.(controller, next.session) ?? null;
1232
1324
  const entry = { key: next.key, recoveryKey: JSON.stringify([next.session.instanceId, next.session.actor.identity.key, next.subject.key]), owner: owner.value, controller, stop: () => {}, writable: next.writable, reconnect: null, preservation };
1233
1325
  current = entry;
1234
1326
  const update = () => {
@@ -1320,7 +1412,7 @@ function bindIdentityState(options, retirementOwner) {
1320
1412
  if (!valid(entry) || transition.signal.aborted)
1321
1413
  return err("stale-context", "View transition expired.");
1322
1414
  pause = token;
1323
- resume = entry.controller.suspendAutomaticDispatch();
1415
+ resume = entry.controller.suspendAutomaticDispatch?.() ?? (() => {});
1324
1416
  if (transition.pending === "block" && hasPending(entry.controller.getSnapshot()))
1325
1417
  return err("conflict", "New edits arrived before the view transition.");
1326
1418
  const result = transition.pending === "flush" ? await entry.controller.flush() : transition.pending === "preserve" ? await entry.controller.checkpoint() : ok(undefined);
@@ -1441,6 +1533,67 @@ function bindIdentityState(options, retirementOwner) {
1441
1533
  };
1442
1534
  }
1443
1535
 
1536
+ // editing-codecs-runtime.ts
1537
+ var EDITING_LIMITS = Object.freeze({ bytes: 4 * 1024 * 1024, diagnostics: 64, message: 4096, code: 128, path: 32 });
1538
+ function canonicalEditingJson(value) {
1539
+ if (value === null || typeof value !== "object")
1540
+ return JSON.stringify(value);
1541
+ if (Array.isArray(value))
1542
+ return `[${value.map(canonicalEditingJson).join(",")}]`;
1543
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalEditingJson(value[key])}`).join(",")}}`;
1544
+ }
1545
+
1546
+ // editing-runtime.ts
1547
+ function frozen2(value) {
1548
+ if (value && typeof value === "object") {
1549
+ for (const child of Object.values(value))
1550
+ frozen2(child);
1551
+ Object.freeze(value);
1552
+ }
1553
+ return value;
1554
+ }
1555
+ function clone(codec, value) {
1556
+ const result = codec.decode(structuredClone(codec.encode(value)));
1557
+ if (!result.ok)
1558
+ throw new TypeError(result.error.message);
1559
+ return frozen2(result.value);
1560
+ }
1561
+ function bindIdentityEditingState(options, retirementOwner) {
1562
+ if (!options.resourceKey || options.resourceKey.length > 1024)
1563
+ throw new TypeError("Editing binding requires a bounded semantic resource key.");
1564
+ const target = clone(options.targetCodec, options.target);
1565
+ const targetKey = canonicalEditingJson(options.targetCodec.encode(target));
1566
+ let currentController = null;
1567
+ return bindOwnedIdentityController({
1568
+ client: options.client,
1569
+ view: options.view,
1570
+ target: "actor",
1571
+ editPolicy: "actor-only",
1572
+ ...options.onUnpersistedDraft ? { onUnpersistedDraft: options.onUnpersistedDraft } : {},
1573
+ hasPending: (state) => state.dirty || state.submitted !== null || state.checkpoint.status === "failed" || state.publication.rejection !== null,
1574
+ create(input) {
1575
+ const controller = options.create(input);
1576
+ const state = controller.getSnapshot();
1577
+ if (canonicalEditingJson(options.targetCodec.encode(state.scope.target)) !== targetKey || state.scope.expected.actor !== input.session.actor.identity.key || state.scope.expected.session !== input.session.stamp || state.scope.draftAddress.owner !== input.session.actor.identity.key || state.ownerSession !== input.ownerSession) {
1578
+ controller.dispose();
1579
+ throw new TypeError("Editing binding factory returned another target or owner.");
1580
+ }
1581
+ currentController = controller;
1582
+ return controller;
1583
+ },
1584
+ preservation(controller, session) {
1585
+ const actual = currentController;
1586
+ const state = controller.getSnapshot();
1587
+ return actual?.draftStorage ? { storage: actual.draftStorage, scope: {
1588
+ address: state.scope.draftAddress,
1589
+ actor: session.actor.identity.key,
1590
+ schemaVersion: state.scope.schemaVersion,
1591
+ targetKey
1592
+ } } : null;
1593
+ }
1594
+ }, retirementOwner);
1595
+ }
1596
+
1444
1597
  // client-connection-runtime.ts
1445
1598
  var errorCodes = new Set([
1446
1599
  "unavailable",
@@ -1522,8 +1675,8 @@ function decodeEvent(value) {
1522
1675
  return null;
1523
1676
  }
1524
1677
  function cloneHealth(value) {
1525
- const clone = (link) => link.status === "unavailable" ? { status: "unavailable", error: { ...link.error } } : { status: link.status };
1526
- return { generation: value.generation, identity: clone(value.identity), state: clone(value.state) };
1678
+ const clone2 = (link) => link.status === "unavailable" ? { status: "unavailable", error: { ...link.error } } : { status: link.status };
1679
+ return { generation: value.generation, identity: clone2(value.identity), state: clone2(value.state) };
1527
1680
  }
1528
1681
  function cancelled(options) {
1529
1682
  return options?.signal?.aborted ? err("cancelled", "Identity request was cancelled.") : null;
@@ -2730,7 +2883,7 @@ function createIdentityView(options) {
2730
2883
  }
2731
2884
 
2732
2885
  // draft-retirement-runtime.ts
2733
- var scopeKey = ({ address: a, actor, schemaVersion }) => JSON.stringify([a.instanceId, a.pluginId, a.collection, a.recordId, a.owner, actor, schemaVersion]);
2886
+ var scopeKey = ({ address: a, actor, schemaVersion, targetKey }) => JSON.stringify([a.instanceId, a.pluginId, a.collection, a.recordId, a.owner, actor, schemaVersion, ...targetKey === undefined ? [] : [targetKey]]);
2734
2887
  function createDraftRetirementOwner() {
2735
2888
  let partitions = new WeakMap;
2736
2889
  const lifetime = new AbortController;
@@ -2812,22 +2965,22 @@ function avatarInitials(label) {
2812
2965
  function selectableProfile(profile) {
2813
2966
  return profile.identity.kind === "person" || profile.identity.kind === "default-user";
2814
2967
  }
2815
- function createFailureSink() {
2968
+ var legacyFailureKey = (draft) => JSON.stringify([
2969
+ draft.key.address.instanceId,
2970
+ draft.key.address.pluginId,
2971
+ draft.key.address.collection,
2972
+ draft.key.address.recordId,
2973
+ draft.key.address.owner,
2974
+ draft.key.actor,
2975
+ draft.key.ownerSession,
2976
+ draft.schemaVersion
2977
+ ]);
2978
+ function createFailureSink(keyFor) {
2816
2979
  let disposed = false;
2817
2980
  const listeners = new Set;
2818
2981
  const entries = new Map;
2819
2982
  let current = Object.freeze([]);
2820
2983
  let nextGeneration = 0;
2821
- const keyFor = (draft) => JSON.stringify([
2822
- draft.key.address.instanceId,
2823
- draft.key.address.pluginId,
2824
- draft.key.address.collection,
2825
- draft.key.address.recordId,
2826
- draft.key.address.owner,
2827
- draft.key.actor,
2828
- draft.key.ownerSession,
2829
- draft.schemaVersion
2830
- ]);
2831
2984
  const emit = () => {
2832
2985
  for (const listener of [...listeners])
2833
2986
  try {
@@ -2876,17 +3029,22 @@ function createFailureSink() {
2876
3029
  }
2877
3030
  function createProviderRoot() {
2878
3031
  const retirements = createDraftRetirementOwner();
2879
- const failures = createFailureSink();
3032
+ const failures = createFailureSink(legacyFailureKey);
3033
+ const editingFailures = createFailureSink((draft) => JSON.stringify([
3034
+ legacyFailureKey(draft.draft)
3035
+ ]));
2880
3036
  let disposed = false;
2881
3037
  return {
2882
3038
  retirements,
2883
3039
  failures,
3040
+ editingFailures,
2884
3041
  dispose() {
2885
3042
  if (disposed)
2886
3043
  return;
2887
3044
  disposed = true;
2888
3045
  retirements.dispose();
2889
3046
  failures.dispose();
3047
+ editingFailures.dispose();
2890
3048
  }
2891
3049
  };
2892
3050
  }
@@ -3014,6 +3172,38 @@ function createIdentityReactRuntime(native) {
3014
3172
  useIdentityStateSnapshot(binding);
3015
3173
  return binding;
3016
3174
  };
3175
+ const useIdentityEditingSnapshot = (binding) => React.useSyncExternalStore(React.useCallback((listener) => binding?.subscribe(listener) ?? (() => {}), [binding]), React.useCallback(() => binding?.getSnapshot() ?? waitingForBinding, [binding]), React.useCallback(() => binding?.getSnapshot() ?? waitingForBinding, [binding]));
3176
+ const useBoundIdentityEditing = (options) => {
3177
+ const root = requireContext(React.useContext(ViewContext), "useIdentityEditingBinding");
3178
+ const callbacks = React.useRef({ create: options.create, failure: options.onUnpersistedDraft });
3179
+ React.useLayoutEffect(() => {
3180
+ callbacks.current = { create: options.create, failure: options.onUnpersistedDraft };
3181
+ }, [options.create, options.onUnpersistedDraft]);
3182
+ const targetKey = canonicalEditingJson(options.targetCodec.encode(options.target));
3183
+ const key = React.useMemo(() => ({ root, targetKey, resourceKey: options.resourceKey }), [root, targetKey, options.resourceKey]);
3184
+ const binding = useCommittedResource(key, () => bindIdentityEditingState({
3185
+ ...options,
3186
+ client: root.client,
3187
+ view: root.view,
3188
+ create: (input) => callbacks.current.create(input),
3189
+ onUnpersistedDraft(draft, error) {
3190
+ root.root.editingFailures.report(draft, error);
3191
+ try {
3192
+ callbacks.current.failure?.(draft, error);
3193
+ } catch {}
3194
+ }
3195
+ }, root.root.retirements), (value) => value.dispose());
3196
+ useIdentityEditingSnapshot(binding);
3197
+ return binding;
3198
+ };
3199
+ const useEditingPreservationFailures = () => {
3200
+ const root = requireContext(React.useContext(ProviderContext), "useIdentityEditingPreservationFailures");
3201
+ return React.useSyncExternalStore(React.useCallback((listener) => root.root.editingFailures.subscribe(listener), [root.root]), React.useCallback(() => root.root.editingFailures.snapshot(), [root.root]), React.useCallback(() => root.root.editingFailures.snapshot(), [root.root]));
3202
+ };
3203
+ const useAcknowledgeEditingPreservationFailure = () => {
3204
+ const root = requireContext(React.useContext(ProviderContext), "useAcknowledgeIdentityEditingPreservationFailure");
3205
+ return React.useCallback((generation) => root.root.editingFailures.acknowledge(generation), [root.root]);
3206
+ };
3017
3207
  const useIdentityViewSnapshot = () => {
3018
3208
  const root = requireContext(React.useContext(ViewContext), "useIdentityViewSnapshot");
3019
3209
  return React.useSyncExternalStore(React.useCallback((listener) => root.view.subscribe(listener), [root.view]), React.useCallback(() => root.view.getSnapshot(), [root.view]), React.useCallback(() => root.view.getSnapshot(), [root.view]));
@@ -3274,6 +3464,10 @@ function createIdentityReactRuntime(native) {
3274
3464
  useNativeIdentityClient,
3275
3465
  useBoundIdentityState,
3276
3466
  useIdentityStateSnapshot,
3467
+ useBoundIdentityEditing,
3468
+ useIdentityEditingSnapshot,
3469
+ useEditingPreservationFailures,
3470
+ useAcknowledgeEditingPreservationFailure,
3277
3471
  useIdentityViewSnapshot,
3278
3472
  useIdentityViewActions,
3279
3473
  usePreservationFailures,
@@ -3305,6 +3499,10 @@ var useIdentityViewSnapshot = () => BbIdentityReact.useIdentityViewSnapshot();
3305
3499
  var useIdentityViewActions = () => BbIdentityReact.useIdentityViewActions();
3306
3500
  var useIdentityPreservationFailures = () => BbIdentityReact.usePreservationFailures();
3307
3501
  var useAcknowledgeIdentityPreservationFailure = () => BbIdentityReact.useAcknowledgePreservationFailure();
3502
+ var useIdentityEditingBinding = BbIdentityReact.useBoundIdentityEditing;
3503
+ var useIdentityEditingSnapshot = BbIdentityReact.useIdentityEditingSnapshot;
3504
+ var useIdentityEditingPreservationFailures = BbIdentityReact.useEditingPreservationFailures;
3505
+ var useAcknowledgeIdentityEditingPreservationFailure = BbIdentityReact.useAcknowledgeEditingPreservationFailure;
3308
3506
  export {
3309
3507
  useIdentityViewSnapshot,
3310
3508
  useIdentityViewActions,
@@ -3312,10 +3510,14 @@ export {
3312
3510
  useIdentityStateBinding,
3313
3511
  useIdentitySession,
3314
3512
  useIdentityPreservationFailures,
3513
+ useIdentityEditingSnapshot,
3514
+ useIdentityEditingPreservationFailures,
3515
+ useIdentityEditingBinding,
3315
3516
  useBorrowedIdentityClient,
3316
3517
  useBbIdentityConnection,
3317
3518
  useBbIdentityClient,
3318
3519
  useAcknowledgeIdentityPreservationFailure,
3520
+ useAcknowledgeIdentityEditingPreservationFailure,
3319
3521
  IdentityViewStatus,
3320
3522
  IdentityViewPicker,
3321
3523
  IdentityLabel,