@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.
- package/EDITING.md +131 -0
- package/NATIVE-SOURCES.md +222 -0
- package/README.md +18 -0
- package/dist/bb-entry-runtime.js +107 -87
- package/dist/client-entry-runtime.js +128 -36
- package/dist/editing-runtime.js +1845 -0
- package/dist/host-runtime.js +54 -29
- package/dist/index-runtime.js +21 -0
- package/dist/model-runtime.js +21 -0
- package/dist/native-runtime.js +476 -0
- package/dist/native-testing-runtime.js +11 -0
- package/dist/react-entry-runtime.js +253 -51
- package/dist/state-controller-runtime.js +96 -11
- package/dist/state-runtime.js +95 -11
- package/dist/testing-runtime.js +112 -92
- package/editing.d.ts +137 -0
- package/host.d.ts +1 -0
- package/native-testing.d.ts +15 -0
- package/native.d.ts +100 -0
- package/package.json +20 -3
- package/react.d.ts +16 -0
|
@@ -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
|
-
|
|
425
|
+
checkpointWriting--;
|
|
426
|
+
if (result.ok) {
|
|
410
427
|
checkpoint = result.value;
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
1526
|
-
return { generation: value.generation, identity:
|
|
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
|
-
|
|
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,
|