@agentshouse/mdruntime 0.1.0-c5d81e5d5cb58 → 0.1.0-c5fe3b2cddda7

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 (79) hide show
  1. package/dist/adapter.d.ts +15 -28
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/bootstrap.d.ts +7 -14
  4. package/dist/bootstrap.d.ts.map +1 -1
  5. package/dist/bootstrap.js +82 -107
  6. package/dist/collection.d.ts +0 -3
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +1 -17
  9. package/dist/compile.d.ts.map +1 -1
  10. package/dist/compile.js +16 -8
  11. package/dist/connection.d.ts +0 -1
  12. package/dist/connection.d.ts.map +1 -1
  13. package/dist/connection.js +455 -688
  14. package/dist/conversation.d.ts +2 -3
  15. package/dist/conversation.d.ts.map +1 -1
  16. package/dist/conversation.js +56 -10
  17. package/dist/durability.d.ts +0 -1
  18. package/dist/durability.d.ts.map +1 -1
  19. package/dist/entry.d.ts +1 -0
  20. package/dist/entry.d.ts.map +1 -1
  21. package/dist/entry.js +1 -1
  22. package/dist/fulltext.d.ts.map +1 -1
  23. package/dist/fulltext.js +7 -5
  24. package/dist/host.d.ts +0 -1
  25. package/dist/host.d.ts.map +1 -1
  26. package/dist/host.js +6 -4
  27. package/dist/index.d.ts +4 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/dist/media.d.ts +1 -1
  31. package/dist/media.d.ts.map +1 -1
  32. package/dist/media.js +19 -9
  33. package/dist/memory.d.ts +2 -1
  34. package/dist/memory.d.ts.map +1 -1
  35. package/dist/memory.js +50 -20
  36. package/dist/model.d.ts +0 -1
  37. package/dist/model.d.ts.map +1 -1
  38. package/dist/model.js +0 -9
  39. package/dist/outcome.d.ts +0 -3
  40. package/dist/outcome.d.ts.map +1 -1
  41. package/dist/reach.js +1 -1
  42. package/dist/readiness.d.ts +3 -15
  43. package/dist/readiness.d.ts.map +1 -1
  44. package/dist/readiness.js +4 -10
  45. package/dist/recovery.d.ts +1 -0
  46. package/dist/recovery.d.ts.map +1 -1
  47. package/dist/recovery.js +26 -0
  48. package/dist/refresh.d.ts +5 -4
  49. package/dist/refresh.d.ts.map +1 -1
  50. package/dist/refresh.js +4 -4
  51. package/dist/replica.d.ts +1 -2
  52. package/dist/replica.d.ts.map +1 -1
  53. package/dist/replica.js +0 -4
  54. package/dist/room-draft.d.ts +17 -2
  55. package/dist/room-draft.d.ts.map +1 -1
  56. package/dist/room-draft.js +40 -6
  57. package/dist/shared.d.ts +0 -1
  58. package/dist/shared.d.ts.map +1 -1
  59. package/dist/shared.js +0 -10
  60. package/dist/status.d.ts +0 -4
  61. package/dist/status.d.ts.map +1 -1
  62. package/dist/store.d.ts +6 -29
  63. package/dist/store.d.ts.map +1 -1
  64. package/dist/store.js +7 -63
  65. package/dist/stored.d.ts +1 -25
  66. package/dist/stored.d.ts.map +1 -1
  67. package/dist/stored.js +51 -115
  68. package/dist/tables.d.ts +3 -40
  69. package/dist/tables.d.ts.map +1 -1
  70. package/dist/tables.js +3 -52
  71. package/dist/transfer.d.ts +0 -5
  72. package/dist/transfer.d.ts.map +1 -1
  73. package/dist/validation.d.ts +3 -4
  74. package/dist/validation.d.ts.map +1 -1
  75. package/dist/validation.js +35 -96
  76. package/package.json +2 -2
  77. package/dist/reads.d.ts +0 -11
  78. package/dist/reads.d.ts.map +0 -1
  79. package/dist/reads.js +0 -100
@@ -8,11 +8,11 @@ import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
9
9
  import { searchPlanOf } from './search.js';
10
10
  import { capped, MAXIMUM_WINDOW } from './windows.js';
11
- import { bootstrapRoom, NOTHING_DONE } from './bootstrap.js';
11
+ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, composedOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
@@ -20,11 +20,10 @@ import { attachedFile } from './file.js';
20
20
  import { openedFiles } from './opened.js';
21
21
  import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
22
22
  import { createRecovery } from './recovery.js';
23
- import { doorReads } from './reads.js';
24
23
  import { advancement } from './refresh.js';
25
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
26
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
27
- import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
28
27
  import { same, saving, STARTING, transferring, } from './status.js';
29
28
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
30
29
  import { readinessOf, } from './readiness.js';
@@ -34,7 +33,7 @@ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
33
  import { identityTime, roomDraftRevision } from './identity.js';
35
34
  import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
38
37
  import { unconfirmed } from './transfer.js';
39
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
39
  function within(root, path) {
@@ -59,36 +58,33 @@ const DERIVE_BYTES = 1 << 23;
59
58
  const DERIVE_FLOOR = 1 << 18;
60
59
  // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
61
60
  const DERIVE_AIM_MS = 250;
62
- // A build past its floor of text stops after a second and a half of work, so a Room whose text costs ten times the usual still reports progress within the 2 s bound.
61
+ // A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
63
62
  const DERIVE_LONGEST_MS = 1_500;
64
- const WALK_BYTES = 1 << 20;
65
63
  const QUERY_BYTES = 1 << 20;
66
64
  const STAGE_BYTES = 1 << 20;
67
65
  const STAGE_FLOOR = 1 << 16;
68
66
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
69
67
  const STAGE_AIM_MS = 50;
70
- const PARTS_HELD = 4;
71
- const QUEUED_BYTES = 1 << 23;
72
- // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
68
+ const STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
73
72
  const ARRIVAL_REPORT_MS = 250;
74
- const CLEAN_CHANGES = 2_000;
75
73
  // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
76
74
  const LOCK_GRACE_MS = 120_000;
77
75
  function operationOf(input) {
78
76
  const operation = input?.operation_id;
79
77
  return typeof operation === 'string' ? operation : null;
80
78
  }
81
- function abandonedOn(signal) {
82
- return new Promise((resolve) => {
83
- if (signal.aborted)
84
- resolve(null);
85
- else
86
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
87
86
  });
88
87
  }
89
- function roomLane(locator) {
90
- return `room ${locator}`;
91
- }
92
88
  function pendingKey(locator, id) {
93
89
  return `${locator}\u0000${id}`;
94
90
  }
@@ -123,11 +119,19 @@ export function composeRuntime(options) {
123
119
  const liveModels = new FencedMap(isFenced);
124
120
  const invalidModels = new Set();
125
121
  const discovered = new Map();
126
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
122
+ let inFlight = new AbortController();
123
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
124
+ const reads = {
125
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
126
+ ...(refreshed === undefined
127
+ ? {}
128
+ : {
129
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
130
+ }),
131
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
132
+ };
127
133
  let roomsListed = false;
128
134
  const scopedRefusals = new Map();
129
- const sourceRefusals = new Map();
130
- const declarationRefusals = new Set();
131
135
  function modelOf(locator) {
132
136
  return models.get(locator) ?? NO_MODEL;
133
137
  }
@@ -150,10 +154,10 @@ export function composeRuntime(options) {
150
154
  let closed = false;
151
155
  let stopping = false;
152
156
  let withdrawStream;
153
- const announced = new Map();
154
157
  let underway = null;
155
158
  let streamed = null;
156
159
  let handed = false;
160
+ let naming = 0;
157
161
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
158
162
  const repairing = new Set();
159
163
  const calling = new Set();
@@ -165,11 +169,11 @@ export function composeRuntime(options) {
165
169
  const stale = new Set();
166
170
  let generation = 0;
167
171
  let discovering = null;
168
- let opened = null;
169
- const owed = new Set();
170
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
171
173
  const lanes = new Map();
172
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
174
+ const roomLanes = new Map();
175
+ let roomWork = 0;
176
+ const roomReads = { free: ROOM_READS, waiting: [] };
173
177
  const working = new Set();
174
178
  let mutations = Promise.resolve();
175
179
  let gathering = null;
@@ -218,17 +222,16 @@ export function composeRuntime(options) {
218
222
  let readinessQueued = false;
219
223
  const catching = new Set();
220
224
  const transfers = new FencedMap(isFenced);
221
- const installs = new Map();
222
- const keptParts = new Map();
225
+ const installs = new Set();
223
226
  const arrivals = new Map();
227
+ const streaming = new Set();
224
228
  const underived = new Map();
225
229
  const unindexed = new Map();
230
+ const startedOver = new Map();
226
231
  let arrivalReported = 0;
227
- let onScreen = options.shell.onScreen;
228
- const shownWaiters = [];
229
- let partsFree = PARTS_HELD;
230
- let queuedBytes = 0;
231
- const partWaiters = [];
232
+ let streamsFree = STREAMS_HELD;
233
+ let queuedText = 0;
234
+ const installWaiters = [];
232
235
  const stageBudget = {
233
236
  bytes: STAGE_FLOOR * 4,
234
237
  floor: STAGE_FLOOR,
@@ -241,7 +244,7 @@ export function composeRuntime(options) {
241
244
  ceiling: DERIVE_BYTES,
242
245
  aim: DERIVE_AIM_MS,
243
246
  };
244
- let installWrites = Promise.resolve();
247
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
245
248
  const conversations = createConversationStore({
246
249
  agents: declaredAgents,
247
250
  documents: (declaration, after) => rowsQuery(planOf({
@@ -369,7 +372,9 @@ export function composeRuntime(options) {
369
372
  }
370
373
  const file = parts.file === undefined
371
374
  ? undefined
372
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
375
+ : await heldFile(parts.file.blob, parts.file.base === undefined
376
+ ? binaryBase(await visibleOf(room, [path]), path)
377
+ : parts.file.base);
373
378
  const record = {
374
379
  locator: room,
375
380
  path,
@@ -408,7 +413,7 @@ export function composeRuntime(options) {
408
413
  const changed = new Map();
409
414
  for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
410
415
  for (const { batch, state } of entriesOf(room)) {
411
- if (state === 'accepted' || state === 'local')
416
+ if (state === 'local')
412
417
  continue;
413
418
  for (const { path } of batch.changes)
414
419
  changed.set(`${room}\u0000${path}`, { room, path });
@@ -429,16 +434,9 @@ export function composeRuntime(options) {
429
434
  returned: structuredClone(record.returned),
430
435
  });
431
436
  }
432
- for (const [room, refusal] of sourceRefusals)
433
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
434
437
  attentionRequired.sort((left, right) => {
435
438
  if (left.room !== right.room)
436
439
  return left.room < right.room ? -1 : 1;
437
- if ('refusal' in left || 'refusal' in right) {
438
- if ('refusal' in left && 'refusal' in right)
439
- return 0;
440
- return 'refusal' in left ? -1 : 1;
441
- }
442
440
  if (left.path !== right.path)
443
441
  return left.path < right.path ? -1 : 1;
444
442
  return 0;
@@ -593,9 +591,6 @@ export function composeRuntime(options) {
593
591
  if (localSaves)
594
592
  return Object.freeze({ batch, state: 'local' });
595
593
  const state = pendings.get(pendingKey(locator, batch.id));
596
- if (state?.accepted !== undefined) {
597
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
598
- }
599
594
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
600
595
  }
601
596
  function entriesOf(locator) {
@@ -712,7 +707,7 @@ export function composeRuntime(options) {
712
707
  coverage.push({
713
708
  room: locator,
714
709
  generation: replica.record.transfer,
715
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
710
+ current: !(catching.has(locator) || transfers.has(locator)),
716
711
  });
717
712
  read.push({ locator, key: replica.record.key });
718
713
  for (const batch of replica.queue)
@@ -739,8 +734,6 @@ export function composeRuntime(options) {
739
734
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
740
735
  }
741
736
  function roomState(locator) {
742
- if (sourceRefusals.has(locator))
743
- return 'refused';
744
737
  const listedRoom = discovered.get(locator);
745
738
  if (roomsListed &&
746
739
  (listedRoom === undefined ||
@@ -770,80 +763,47 @@ export function composeRuntime(options) {
770
763
  ? 'connected'
771
764
  : 'connecting';
772
765
  }
773
- function refusedIn(locators) {
774
- return locators.flatMap((room) => {
775
- const held = sourceRefusals.get(room);
776
- if (held === undefined)
777
- return [];
778
- const { file, location } = held.fields;
779
- return [
780
- {
781
- room,
782
- name: discovered.get(room)?.name ?? room,
783
- file: typeof file === 'string' ? file : '',
784
- rule: held.code,
785
- location: typeof location === 'string' ? location : null,
786
- },
787
- ];
788
- });
766
+ function bytesOf(room, underway) {
767
+ const transfer = transfers.get(room);
768
+ if (transfer !== undefined) {
769
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
770
+ return {
771
+ done: transfer.done.bytes - Math.ceil(owed),
772
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
773
+ };
774
+ }
775
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
776
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
789
777
  }
790
778
  function installationOf(coverage) {
791
779
  const rooms = { done: 0, total: 0 };
792
- const parts = { done: 0, total: 0 };
793
780
  const bytes = { done: 0, total: 0 };
794
- const received = { done: 0, total: 0 };
795
781
  let installing = 0;
796
- let unlisted = false;
797
- let listed = false;
798
- let waiting = false;
782
+ let downloading = false;
799
783
  for (const { room, state } of coverage) {
800
784
  if (state === 'unadmitted')
801
785
  continue;
802
786
  rooms.total += 1;
803
787
  if (state === 'complete')
804
788
  rooms.done += 1;
805
- const transfer = transfers.get(room);
806
- const replica = replicas.get(room)?.record.inventory ?? null;
807
- const inventory = transfer?.inventory ?? replica ?? installs.get(room) ?? null;
808
789
  const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
809
790
  if (underway)
810
791
  installing += 1;
811
- if (underway && inventory === null)
812
- unlisted = true;
813
- if (inventory !== null) {
814
- listed = true;
815
- const done = transfer?.done ?? (replica === null ? NOTHING_DONE : inventory);
816
- const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
817
- parts.total += inventory.parts;
818
- bytes.total += inventory.bytes;
819
- parts.done += done.parts;
820
- bytes.done += done.bytes - Math.ceil(owed);
821
- received.total += inventory.bytes;
822
- received.done += done.bytes;
823
- for (const held of [keptParts.get(room), arrivals.get(room)]) {
824
- for (const arrived of held?.values() ?? [])
825
- received.done += arrived;
826
- }
827
- }
828
- waiting ||= underway;
792
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
793
+ downloading = true;
794
+ const now = bytesOf(room, underway);
795
+ const earlier = underway ? startedOver.get(room) : undefined;
796
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
797
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
829
798
  }
830
799
  const step = installing === 0
831
800
  ? null
832
- : !listed
833
- ? 'listing'
834
- : unlisted || received.done < received.total
835
- ? 'downloading'
836
- : bytes.done < bytes.total
837
- ? 'writing'
838
- : 'replaying';
839
- return {
840
- step,
841
- paused: waiting && !onScreen ? 'hidden' : null,
842
- rooms,
843
- parts,
844
- bytes,
845
- received,
846
- };
801
+ : downloading
802
+ ? 'downloading'
803
+ : bytes.done < bytes.total
804
+ ? 'writing'
805
+ : 'replaying';
806
+ return { step, rooms, bytes };
847
807
  }
848
808
  function readinessFor(port, reach) {
849
809
  const required = requiredRooms(reach);
@@ -867,7 +827,6 @@ export function composeRuntime(options) {
867
827
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
868
828
  space: full ? 'full' : port.space,
869
829
  installation: installationOf(coverage),
870
- refused: refusedIn(required.rooms),
871
830
  lost: storageLoss !== null,
872
831
  established: targets[port.target]?.established === true ||
873
832
  (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
@@ -1104,6 +1063,8 @@ export function composeRuntime(options) {
1104
1063
  }
1105
1064
  async function refuse(reason, provider = null) {
1106
1065
  discoveryPaused = true;
1066
+ inFlight.abort();
1067
+ inFlight = new AbortController();
1107
1068
  try {
1108
1069
  if (reason === 'revoked-authorization') {
1109
1070
  await exclusive(async () => {
@@ -1188,7 +1149,7 @@ export function composeRuntime(options) {
1188
1149
  providerOwnedPaths: room.providerOwnedPaths,
1189
1150
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1190
1151
  synchronizationAvailable,
1191
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1152
+ refusal: scopedRefusals.get(room.locator) ?? null,
1192
1153
  });
1193
1154
  }
1194
1155
  async function applyDiscovery(found, started, covered) {
@@ -1274,16 +1235,14 @@ export function composeRuntime(options) {
1274
1235
  roomDrafts.delete(locator);
1275
1236
  attachedFiles.delete(locator);
1276
1237
  untransferred(locator);
1238
+ startedOver.delete(locator);
1277
1239
  catching.delete(locator);
1278
1240
  discovered.delete(locator);
1279
- announced.delete(locator);
1280
1241
  unreadable.delete(locator);
1281
1242
  stale.delete(locator);
1282
1243
  repairable.delete(locator);
1283
1244
  unpreparable.delete(locator);
1284
1245
  scopedRefusals.delete(locator);
1285
- sourceRefusals.delete(locator);
1286
- declarationRefusals.delete(locator);
1287
1246
  models.delete(locator);
1288
1247
  liveModels.delete(locator);
1289
1248
  invalidModels.delete(locator);
@@ -1372,16 +1331,9 @@ export function composeRuntime(options) {
1372
1331
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1373
1332
  targets = held.targets;
1374
1333
  transfers.clear();
1375
- for (const [locator, record] of held.transfers) {
1376
- transfers.set(locator, {
1377
- ...record,
1378
- models: { collection: retyped(record.models.collection) },
1379
- });
1380
- }
1381
1334
  arrivals.clear();
1382
- keptParts.clear();
1383
- for (const [locator, parts] of held.kept)
1384
- keptParts.set(locator, new Map(parts));
1335
+ streaming.clear();
1336
+ startedOver.clear();
1385
1337
  limits = held.providerLimits;
1386
1338
  capabilities = held.capabilities;
1387
1339
  member = held.member;
@@ -1391,7 +1343,7 @@ export function composeRuntime(options) {
1391
1343
  presentations.clear();
1392
1344
  for (const record of held.presentation)
1393
1345
  presentations.set(record.app, record.value);
1394
- conversations.hydrate(heldConversations, chatDrafts);
1346
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1395
1347
  pendings.clear();
1396
1348
  for (const queue of queues) {
1397
1349
  for (const { batch, state } of queue.pending) {
@@ -1434,8 +1386,14 @@ export function composeRuntime(options) {
1434
1386
  });
1435
1387
  }
1436
1388
  });
1437
- for (const key of held.orphans)
1438
- await opened.release(key);
1389
+ try {
1390
+ for (const key of held.orphans)
1391
+ await opened.release(key);
1392
+ }
1393
+ catch (error) {
1394
+ if (!refusedForSpace(error))
1395
+ throw error;
1396
+ }
1439
1397
  if (held.erasing || held.orphans.length > 0)
1440
1398
  queueMicrotask(sweep);
1441
1399
  return {
@@ -1637,8 +1595,6 @@ export function composeRuntime(options) {
1637
1595
  function unpairedIn(locator, batch) {
1638
1596
  if (!queueIn(locator).some((held) => held.id === batch.id))
1639
1597
  return false;
1640
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1641
- return false;
1642
1598
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1643
1599
  }
1644
1600
  async function together(local, settlements) {
@@ -1807,28 +1763,34 @@ export function composeRuntime(options) {
1807
1763
  models.set(locator, state);
1808
1764
  return { collection: state };
1809
1765
  }
1810
- function publish(local, room, record, landing, live) {
1766
+ function publish(local, room, reached, landing, live) {
1811
1767
  const landed = exclusive(async () => {
1812
1768
  if (!live()) {
1813
1769
  caughtUpTo(room.locator);
1814
1770
  return true;
1815
1771
  }
1816
1772
  const prior = replicas.get(room.locator);
1817
- if (landing.kind === 'refreshed' &&
1818
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1819
- await local.unrefresh(record.key);
1773
+ if (!stands(room.locator, landing.from)) {
1774
+ if (landing.kind === 'refreshed') {
1775
+ await local.unrefresh(reached.key);
1776
+ return false;
1777
+ }
1778
+ caughtUpTo(room.locator);
1779
+ await abandoned(local, room.locator);
1820
1780
  return false;
1821
1781
  }
1782
+ const record = landing.kind === 'refreshed' && prior !== undefined
1783
+ ? {
1784
+ ...prior.record,
1785
+ roomVersion: reached.roomVersion,
1786
+ ...(landing.remodelled ? { model: reached.model } : {}),
1787
+ }
1788
+ : reached;
1822
1789
  adoptModels(room.locator, record);
1823
1790
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1824
- const roomVersion = record.roomVersion;
1825
- const accepted = queue.filter((batch) => {
1826
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1827
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1828
- });
1829
1791
  let set;
1830
1792
  try {
1831
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1793
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1832
1794
  ? {
1833
1795
  room: true,
1834
1796
  view: 'refreshed',
@@ -1851,23 +1813,21 @@ export function composeRuntime(options) {
1851
1813
  }
1852
1814
  if (landing.kind === 'bootstrapped') {
1853
1815
  untransferred(room.locator);
1816
+ startedOver.delete(room.locator);
1854
1817
  catching.delete(room.locator);
1855
- sweep();
1818
+ if (prior !== undefined && prior.record.key !== record.key)
1819
+ sweep();
1820
+ name(room.locator);
1856
1821
  }
1857
- for (const batch of accepted)
1858
- pendings.delete(pendingKey(room.locator, batch.id));
1859
1822
  stale.delete(room.locator);
1860
1823
  unpreparable.delete(room.locator);
1861
1824
  if (unreadable.delete(room.locator))
1862
1825
  scopedRefusals.delete(room.locator);
1863
- sourceRefusals.delete(room.locator);
1864
- declarationRefusals.delete(room.locator);
1865
1826
  list(room, true);
1866
1827
  publishChangeSet(set);
1867
1828
  await unpair(local);
1868
1829
  moveTo({ ...status, ...attention() });
1869
- options.adapter.roomStream?.published(room.locator, roomVersion);
1870
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1830
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1871
1831
  submitLater(room.locator);
1872
1832
  }
1873
1833
  return true;
@@ -1897,12 +1857,7 @@ export function composeRuntime(options) {
1897
1857
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1898
1858
  }
1899
1859
  async function saveParts(locator, named, writtenBy) {
1900
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1901
- if (read.some(({ path }, at) => path === '' ||
1902
- normalize(path) !== path ||
1903
- read.findIndex((one) => one.path === path) !== at)) {
1904
- throw runtimeError('room-draft-malformed');
1905
- }
1860
+ const read = readSavedParts(named);
1906
1861
  const target = await savedRoom(locator);
1907
1862
  const saved = [];
1908
1863
  const files = [];
@@ -1911,7 +1866,7 @@ export function composeRuntime(options) {
1911
1866
  for (const { path, parts } of read) {
1912
1867
  const file = parts.file === undefined
1913
1868
  ? undefined
1914
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1869
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1915
1870
  if (parts.file !== undefined && file !== undefined) {
1916
1871
  files.push({ key: file.localId, blob: parts.file.blob });
1917
1872
  }
@@ -1976,18 +1931,6 @@ export function composeRuntime(options) {
1976
1931
  return undefined;
1977
1932
  return entry.binary !== null ? null : entry.content;
1978
1933
  }
1979
- function walkOf(local, key, view) {
1980
- return async (after) => {
1981
- const page = await local.walk(key, view, after, WALK_BYTES);
1982
- return {
1983
- entries: page.entries.map(({ path, content, binary }) => ({
1984
- path,
1985
- content: binary !== null ? null : content,
1986
- })),
1987
- next: page.next,
1988
- };
1989
- };
1990
- }
1991
1934
  function lookupOf(local, key, view) {
1992
1935
  return async (paths) => {
1993
1936
  if (paths.length === 0)
@@ -2004,37 +1947,6 @@ export function composeRuntime(options) {
2004
1947
  const read = parseFrontmatter(entry.content ?? '');
2005
1948
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
2006
1949
  }
2007
- function overriddenWalk(walk, overrides) {
2008
- const sorted = [...overrides.keys()].sort();
2009
- const beyond = (path) => {
2010
- if (path === null)
2011
- return 0;
2012
- let low = 0;
2013
- let high = sorted.length;
2014
- while (low < high) {
2015
- const middle = (low + high) >>> 1;
2016
- if (sorted[middle] <= path)
2017
- low = middle + 1;
2018
- else
2019
- high = middle;
2020
- }
2021
- return low;
2022
- };
2023
- return async (after) => {
2024
- const page = await walk(after);
2025
- const end = page.next;
2026
- const added = sorted
2027
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
2028
- .flatMap((path) => {
2029
- const entry = overrides.get(path);
2030
- return entry === null || entry === undefined
2031
- ? []
2032
- : [{ path, content: entry.binary !== null ? null : entry.content }];
2033
- });
2034
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
2035
- return { entries, next: end };
2036
- };
2037
- }
2038
1950
  function overriddenLookup(lookup, overrides) {
2039
1951
  return async (paths) => {
2040
1952
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2052,9 +1964,8 @@ export function composeRuntime(options) {
2052
1964
  }
2053
1965
  function validatorOf(record, confirmedLayer) {
2054
1966
  return async (resulting, touched) => {
2055
- const composition = compositionFor(record);
2056
1967
  const local = await openLocal();
2057
- const changed = [];
1968
+ const changes = [];
2058
1969
  const overrides = new Map();
2059
1970
  const pace = paced();
2060
1971
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2065,19 +1976,13 @@ export function composeRuntime(options) {
2065
1976
  continue;
2066
1977
  overrides.set(path, now);
2067
1978
  if (textOf(was) !== textOf(now))
2068
- changed.push({ path, was: textOf(was), now: textOf(now) });
1979
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2069
1980
  }
2070
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2071
- const clean = available(composition) &&
2072
- changed.length <= CLEAN_CHANGES &&
2073
- (await changedCleanly(changed, composed(record), composition, {
2074
- lookup,
2075
- texts: textsOf(local, record.key, 'confirmed'),
2076
- referrers: referrersOf(local, record.key, 'confirmed'),
2077
- }));
2078
- if (clean)
2079
- return { verdict: 'accept' };
2080
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1981
+ return validatedNeighbourhood(changes, compositionFor(record), {
1982
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1983
+ texts: textsOf(local, record.key, 'confirmed'),
1984
+ referrers: referrersOf(local, record.key, 'confirmed'),
1985
+ });
2081
1986
  };
2082
1987
  }
2083
1988
  function textsOf(local, key, view) {
@@ -2154,7 +2059,7 @@ export function composeRuntime(options) {
2154
2059
  }
2155
2060
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2156
2061
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2157
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2062
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2158
2063
  if (changed.length > 0)
2159
2064
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2160
2065
  const target = replicas.get(destination);
@@ -2279,7 +2184,7 @@ export function composeRuntime(options) {
2279
2184
  ...saved.map(({ path }) => path),
2280
2185
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2281
2186
  ]);
2282
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2187
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2283
2188
  if (changed.length > 0)
2284
2189
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2285
2190
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2852,115 +2757,16 @@ export function composeRuntime(options) {
2852
2757
  options.adapter.reached(locator, written, roomVersion) &&
2853
2758
  !options.adapter.reached(locator, roomVersion, written));
2854
2759
  }
2855
- function overriddenTexts(texts, overrides) {
2856
- return async (paths) => {
2857
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2858
- for (const path of paths) {
2859
- const entry = overrides.get(path);
2860
- if (entry === undefined)
2861
- continue;
2862
- if (entry === null)
2863
- known.delete(path);
2864
- else
2865
- known.set(path, entry.binary !== null ? null : entry.content);
2866
- }
2867
- return known;
2868
- };
2869
- }
2870
- async function unreadableRoom(texts, walk, paths) {
2871
- if (paths !== null) {
2872
- const text = {};
2873
- for (const [path, content] of await texts(paths))
2874
- if (content !== null)
2875
- text[path] = content;
2876
- return stepped(unreadableIn(text, Object.keys(text)));
2877
- }
2878
- for (let after = null;;) {
2879
- const page = await walk(after);
2880
- const text = {};
2881
- for (const { path, content } of page.entries)
2882
- if (content !== null)
2883
- text[path] = content;
2884
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2885
- if (refused !== null)
2886
- return refused;
2887
- if (page.next === null)
2888
- return null;
2889
- after = page.next;
2890
- }
2891
- }
2892
- async function validatedRoom(local, record, view, previous, overrides = null) {
2893
- const composition = composed(record);
2894
- const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2895
- const lookup = overrides === null
2896
- ? lookupOf(local, record.key, view)
2897
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2898
- const texts = overrides === null
2899
- ? textsOf(local, record.key, view)
2900
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2901
- const walk = overrides === null
2902
- ? walkOf(local, record.key, view)
2903
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2904
- let changes = null;
2905
- let reshaped = false;
2906
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2907
- const [was, now] = await Promise.all([
2908
- local.entries(record.key, 'confirmed', previous.changed),
2909
- overrides === null
2910
- ? local.entries(record.key, view, previous.changed)
2911
- : Promise.resolve([]),
2912
- ]);
2913
- const before = new Map(was.map((entry) => [entry.path, entry]));
2914
- const after = overrides === null
2915
- ? new Map(now.map((entry) => [entry.path, entry]))
2916
- : overrides;
2917
- changes = previous.changed.flatMap((path) => {
2918
- const one = before.get(path) ?? null;
2919
- const other = after.get(path) ?? null;
2920
- reshaped ||= textDiffers(one, other);
2921
- const held = textOf(one);
2922
- const next = textOf(other);
2923
- return held === next ? [] : [{ path, was: held, now: next }];
2924
- });
2925
- }
2926
- const named = changes === null ? null : changes.map(({ path }) => path);
2927
- const textChanged = changes === null || changes.length > 0 || reshaped;
2928
- const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2929
- if (modeled && (textChanged || modelsChanged)) {
2930
- const clean = previous !== null &&
2931
- changes !== null &&
2932
- (await changedCleanly(changes, composed(previous.record), composition, {
2933
- lookup,
2934
- texts,
2935
- referrers: referrersOf(local, record.key, 'confirmed'),
2936
- }));
2937
- if (clean)
2938
- return null;
2939
- const verdict = await validatedCollection(walk, lookup, composition);
2940
- if (verdict.verdict === 'accept')
2941
- return null;
2942
- if (verdict.rule !== 'model-unavailable')
2943
- return { kind: 'refused', refusal: verdict };
2944
- return unreadableRoom(texts, walk, named);
2945
- }
2946
- if (textChanged)
2947
- return unreadableRoom(texts, walk, named);
2948
- return null;
2949
- }
2950
2760
  function refreshingOf(local, key, composition, liveness, replaying = null) {
2951
- const write = replaying === null ? exclusive : installWrite;
2761
+ const write = replaying === null ? exclusive : finishingWrite;
2952
2762
  return {
2953
- async turn() {
2954
- if (replaying === null)
2955
- return;
2956
- installing(replaying);
2957
- await screened();
2763
+ turn() {
2764
+ if (replaying !== null)
2765
+ installing(replaying);
2958
2766
  },
2959
2767
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2960
2768
  async stage(page) {
2961
2769
  for (let written = 0;;) {
2962
- if (replaying !== null)
2963
- await screened();
2964
2770
  const end = batchEnd(page.entries, written);
2965
2771
  const last = end === page.entries.length;
2966
2772
  const entries = page.entries.slice(written, end);
@@ -3005,25 +2811,74 @@ export function composeRuntime(options) {
3005
2811
  return done;
3006
2812
  }
3007
2813
  function installWrite(work) {
3008
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
3009
- installWrites = next.then(() => undefined, () => undefined);
3010
- return next;
3011
- }
3012
- function screened() {
3013
- if (onScreen || closed)
3014
- return Promise.resolve();
3015
- queueReadiness();
3016
- return new Promise((resolve) => shownWaiters.push(resolve));
2814
+ return installTurn(installTurns.starting, work);
2815
+ }
2816
+ function finishingWrite(work) {
2817
+ return installTurn(installTurns.finishing, work);
2818
+ }
2819
+ function installTurn(queue, work) {
2820
+ return new Promise((resolve, reject) => {
2821
+ queue.push(() => void exclusive(work)
2822
+ .then(resolve, reject)
2823
+ .finally(() => {
2824
+ installTurns.busy = false;
2825
+ nextInstallTurn();
2826
+ }));
2827
+ nextInstallTurn();
2828
+ });
3017
2829
  }
3018
- async function heldPart() {
3019
- while (partsFree === 0 || queuedBytes >= QUEUED_BYTES) {
3020
- await new Promise((admit) => partWaiters.push(admit));
2830
+ function nextInstallTurn() {
2831
+ if (installTurns.busy || installTurns.ahead > 0)
2832
+ return;
2833
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2834
+ if (turn === undefined)
2835
+ return;
2836
+ installTurns.busy = true;
2837
+ turn();
2838
+ }
2839
+ async function ahead(work) {
2840
+ let taken = false;
2841
+ const hold = {
2842
+ take() {
2843
+ if (taken)
2844
+ return;
2845
+ taken = true;
2846
+ installTurns.ahead += 1;
2847
+ },
2848
+ release() {
2849
+ if (!taken)
2850
+ return;
2851
+ taken = false;
2852
+ installTurns.ahead -= 1;
2853
+ resumeInstall();
2854
+ },
2855
+ };
2856
+ hold.take();
2857
+ try {
2858
+ return await work(hold);
3021
2859
  }
3022
- partsFree -= 1;
2860
+ finally {
2861
+ hold.release();
2862
+ }
2863
+ }
2864
+ function resumeInstall() {
2865
+ if (installTurns.ahead > 0)
2866
+ return;
2867
+ void yielded().then(nextInstallTurn);
2868
+ }
2869
+ async function heldStream() {
2870
+ while (streamsFree === 0)
2871
+ await new Promise((admit) => installWaiters.push(admit));
2872
+ streamsFree -= 1;
3023
2873
  return once(() => {
3024
- partsFree += 1;
2874
+ streamsFree += 1;
3025
2875
  });
3026
2876
  }
2877
+ async function unqueued() {
2878
+ while (queuedText >= QUEUED_TEXT) {
2879
+ await new Promise((admit) => installWaiters.push(admit));
2880
+ }
2881
+ }
3027
2882
  function once(release) {
3028
2883
  let released = false;
3029
2884
  return () => {
@@ -3031,20 +2886,29 @@ export function composeRuntime(options) {
3031
2886
  return;
3032
2887
  released = true;
3033
2888
  release();
3034
- for (const admit of partWaiters.splice(0))
2889
+ for (const admit of installWaiters.splice(0))
3035
2890
  admit();
3036
2891
  };
3037
2892
  }
3038
- function installing(locator, inventory = null) {
3039
- installs.set(locator, inventory ?? installs.get(locator) ?? null);
2893
+ function installing(locator) {
2894
+ installs.add(locator);
3040
2895
  queueReadiness();
3041
2896
  }
3042
2897
  function untransferred(locator) {
3043
2898
  transfers.delete(locator);
3044
- keptParts.delete(locator);
2899
+ arrivals.delete(locator);
2900
+ streaming.delete(locator);
3045
2901
  underived.delete(locator);
3046
2902
  unindexed.delete(locator);
3047
2903
  }
2904
+ function setBack(locator) {
2905
+ const now = bytesOf(locator, true);
2906
+ const earlier = startedOver.get(locator);
2907
+ startedOver.set(locator, {
2908
+ done: Math.max(now.done, earlier?.done ?? 0),
2909
+ total: Math.max(now.total, earlier?.total ?? 0),
2910
+ });
2911
+ }
3048
2912
  function owedIndexes(locator, bytes, text) {
3049
2913
  if (bytes <= 0 || text <= 0)
3050
2914
  return;
@@ -3074,41 +2938,34 @@ export function composeRuntime(options) {
3074
2938
  if (installs.delete(locator))
3075
2939
  queueReadiness();
3076
2940
  }
3077
- function screen(value) {
3078
- if (value === onScreen)
3079
- return;
3080
- onScreen = value;
3081
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3082
- queueReadiness();
3083
- if (!value)
3084
- return;
3085
- for (const resolve of shownWaiters.splice(0))
3086
- resolve();
3087
- }
3088
- async function pullRefresh(authorized, held, liveness) {
2941
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3089
2942
  if (!liveness())
3090
2943
  return unconfirmed;
3091
- const turn = generation;
3092
2944
  const { record } = held;
3093
- const base = held.confirmed;
2945
+ const from = standingOf(record.locator);
3094
2946
  const local = await openLocal();
3095
2947
  try {
3096
2948
  await exclusive(() => local.unrefresh(record.key));
3097
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2949
+ const result = await advancement(async (authorization, request) => {
2950
+ hold.release();
2951
+ try {
2952
+ return await refresh(authorization, request);
2953
+ }
2954
+ finally {
2955
+ hold.take();
2956
+ }
2957
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
3098
2958
  if (result.kind !== 'advanced') {
3099
2959
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
3100
2960
  return result;
3101
2961
  }
3102
- const refused = await validatedRoom(local, result.record, 'refreshed', {
3103
- record,
3104
- changed: [...result.changed],
3105
- });
3106
- if (refused !== null) {
3107
- journal.record(`transfer refresh ${record.locator} refused`);
3108
- return refused;
3109
- }
3110
2962
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3111
- return { kind: 'advanced', record: result.record, base };
2963
+ return {
2964
+ kind: 'advanced',
2965
+ record: result.record,
2966
+ from,
2967
+ remodelled: result.record.model !== record.model,
2968
+ };
3112
2969
  }
3113
2970
  catch (error) {
3114
2971
  if (refusedForSpace(error))
@@ -3116,9 +2973,6 @@ export function composeRuntime(options) {
3116
2973
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
3117
2974
  throw error;
3118
2975
  }
3119
- finally {
3120
- reads.finished(record.locator, turn);
3121
- }
3122
2976
  }
3123
2977
  const heldContents = new Map();
3124
2978
  function heldContentOf(local, key) {
@@ -3136,24 +2990,20 @@ export function composeRuntime(options) {
3136
2990
  heldContents.set(key, made);
3137
2991
  return made;
3138
2992
  }
3139
- async function keptTogether({ local, parts }) {
3140
- const live = parts.filter((one) => one.live() && transfers.has(one.locator));
3141
- await local.keepParts(live.map(({ locator, token, part }) => ({ locator, token, part })));
3142
- for (const { locator, token, part } of live) {
3143
- const held = keptParts.get(locator) ?? new Map();
3144
- keptParts.set(locator, held.set(token, part.byteLength));
3145
- arrivals.get(locator)?.delete(token);
3146
- }
3147
- queueReadiness();
2993
+ async function abandoned(local, locator) {
2994
+ await local.abandon(locator);
2995
+ setBack(locator);
2996
+ untransferred(locator);
2997
+ liveQueries.recover();
2998
+ sweep();
3148
2999
  }
3149
3000
  function staging(local, liveness) {
3150
3001
  const derived = async (locator, key, composition) => {
3151
3002
  underived.delete(locator);
3152
3003
  for (;;) {
3153
- await screened();
3154
3004
  if (!liveness())
3155
3005
  return false;
3156
- const done = await installWrite(async () => liveness()
3006
+ const done = await finishingWrite(async () => liveness()
3157
3007
  ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3158
3008
  : null);
3159
3009
  if (done === null)
@@ -3167,28 +3017,23 @@ export function composeRuntime(options) {
3167
3017
  held: (locator) => transfers.get(locator) ?? null,
3168
3018
  async turn(locator, step) {
3169
3019
  installing(locator);
3170
- for (;;) {
3171
- await screened();
3172
- const release = step === 'downloading' ? await heldPart() : () => { };
3173
- if (onScreen || closed)
3174
- return release;
3175
- release();
3176
- }
3020
+ return step === 'downloading' ? heldStream() : () => { };
3177
3021
  },
3178
- queued(bytes) {
3179
- queuedBytes += bytes;
3022
+ unqueued,
3023
+ queued(text) {
3024
+ queuedText += text;
3180
3025
  return once(() => {
3181
- queuedBytes -= bytes;
3026
+ queuedText -= text;
3182
3027
  });
3183
3028
  },
3184
3029
  arriving(locator) {
3185
3030
  let ended = false;
3186
3031
  return {
3187
- at(token, bytes) {
3188
- if (ended || keptParts.get(locator)?.has(token) === true)
3032
+ at(bytes) {
3033
+ if (ended)
3189
3034
  return;
3190
- const held = arrivals.get(locator) ?? new Map();
3191
- arrivals.set(locator, held.set(token, bytes));
3035
+ arrivals.set(locator, bytes);
3036
+ streaming.add(locator);
3192
3037
  const now = performance.now();
3193
3038
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
3194
3039
  return;
@@ -3196,30 +3041,24 @@ export function composeRuntime(options) {
3196
3041
  queueReadiness();
3197
3042
  },
3198
3043
  end() {
3044
+ if (ended)
3045
+ return;
3199
3046
  ended = true;
3200
- if (arrivals.delete(locator))
3047
+ if (streaming.delete(locator))
3201
3048
  queueReadiness();
3202
3049
  },
3203
3050
  };
3204
3051
  },
3205
- received: (locator, token) => local.transferPart(locator, token),
3206
- keep: (locator, token, received) => gathered('kept parts', { local, parts: [{ locator, token, part: received, live: liveness }] }, (earlier, later) => earlier.local === later.local
3207
- ? { local: earlier.local, parts: [...earlier.parts, ...later.parts] }
3208
- : null, keptTogether),
3209
- announced(locator) {
3210
- const later = announced.get(locator);
3211
- return later === undefined ? [] : [later];
3212
- },
3213
- async stage(record, page, composition, written) {
3052
+ streamed: options.adapter.roomStream !== undefined,
3053
+ async stage(record, page, composition) {
3214
3054
  const { locator } = record;
3215
- installing(locator, record.inventory);
3216
- await screened();
3055
+ installing(locator);
3217
3056
  if (!liveness())
3218
3057
  return null;
3219
3058
  const key = await installWrite(async () => {
3220
3059
  if (!liveness())
3221
3060
  return null;
3222
- return local.stage(record, composition, page, written ?? undefined);
3061
+ return local.stage(record, composition, page);
3223
3062
  });
3224
3063
  if (key === null || !liveness())
3225
3064
  return null;
@@ -3227,10 +3066,6 @@ export function composeRuntime(options) {
3227
3066
  const prior = transfers.get(locator)?.done.bytes ?? 0;
3228
3067
  const held = { ...record, key };
3229
3068
  transfers.set(locator, held);
3230
- if (written !== null) {
3231
- keptParts.get(locator)?.delete(written);
3232
- arrivals.get(locator)?.delete(written);
3233
- }
3234
3069
  if (arrived)
3235
3070
  liveQueries.recover();
3236
3071
  installing(locator);
@@ -3249,26 +3084,21 @@ export function composeRuntime(options) {
3249
3084
  return refreshingOf(local, key, composition, liveness, locator);
3250
3085
  },
3251
3086
  abandon: (locator) => exclusive(async () => {
3252
- if (!liveness())
3253
- return;
3254
- await local.abandon(locator);
3255
- untransferred(locator);
3256
- liveQueries.recover();
3257
- sweep();
3087
+ if (liveness())
3088
+ await abandoned(local, locator);
3258
3089
  }),
3259
- setAside: (locator) => exclusive(async () => {
3090
+ setAside: async (locator) => {
3260
3091
  if (!liveness())
3261
3092
  return;
3262
- await local.abandon(locator, true);
3093
+ setBack(locator);
3263
3094
  untransferred(locator);
3264
3095
  liveQueries.recover();
3265
- }),
3096
+ },
3266
3097
  release: (key) => exclusive(async () => {
3267
3098
  await local.release(key);
3268
3099
  sweep();
3269
3100
  }),
3270
3101
  content: (key) => heldContentOf(local, key),
3271
- validate: (record, view) => validatedRoom(local, record, view, null),
3272
3102
  };
3273
3103
  }
3274
3104
  function reusable(room, local) {
@@ -3280,7 +3110,6 @@ export function composeRuntime(options) {
3280
3110
  async function pullBootstrap(authorized, room, local, liveness) {
3281
3111
  if (!liveness())
3282
3112
  return unconfirmed;
3283
- const turn = generation;
3284
3113
  if (replicas.has(room.locator)) {
3285
3114
  catching.add(room.locator);
3286
3115
  liveQueries.recover();
@@ -3303,9 +3132,6 @@ export function composeRuntime(options) {
3303
3132
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3304
3133
  throw error;
3305
3134
  }
3306
- finally {
3307
- reads.finished(room.locator, turn);
3308
- }
3309
3135
  }
3310
3136
  function caughtUpTo(locator) {
3311
3137
  if (catching.delete(locator))
@@ -3313,72 +3139,82 @@ export function composeRuntime(options) {
3313
3139
  }
3314
3140
  function behind(room) {
3315
3141
  const held = replicas.get(room.locator)?.record.roomVersion;
3316
- return (held !== undefined &&
3317
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3142
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3318
3143
  }
3319
- async function advance(room, local, authorized, live) {
3320
- const carried = reads.carried(room.locator);
3321
- const withheld = await advanceOnce(room, local, authorized, live);
3322
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3323
- return withheld;
3324
- }
3325
- return advanceOnce(room, local, authorized, live);
3144
+ function standingOf(locator) {
3145
+ const held = replicas.get(locator);
3146
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3147
+ }
3148
+ function stands(locator, from) {
3149
+ const now = standingOf(locator);
3150
+ return now.key === from.key && now.confirmed === from.confirmed;
3326
3151
  }
3327
- async function advanceOnce(room, local, authorized, running) {
3152
+ async function advance(room, local, authorized, running) {
3328
3153
  const live = () => running() && !full;
3329
3154
  if (!live())
3330
3155
  return unconfirmed;
3331
3156
  const held = replicas.get(room.locator);
3332
- if (held !== undefined) {
3333
- const refreshed = await pullRefresh(authorized, held, live);
3334
- if (!live())
3335
- return unconfirmed;
3336
- if (refreshed.kind === 'still-current') {
3337
- stale.delete(room.locator);
3338
- if (sourceRefusals.has(room.locator))
3339
- return null;
3340
- if (unreadable.delete(room.locator)) {
3341
- scopedRefusals.delete(room.locator);
3342
- list(room, true);
3343
- emitRooms();
3344
- }
3345
- return null;
3346
- }
3347
- if (refreshed.kind === 'advanced') {
3348
- const landing = { kind: 'refreshed', base: refreshed.base };
3349
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3350
- const now = replicas.get(room.locator)?.record.roomVersion;
3351
- const reached = now !== undefined &&
3352
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3353
- return reached ? null : advance(room, local, authorized, live);
3354
- }
3355
- await retainModel(local, room, modelOf(room.locator));
3356
- return null;
3357
- }
3358
- if (refreshed.kind !== 'beyond-horizon')
3157
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3158
+ stale.delete(room.locator);
3159
+ name(room.locator);
3160
+ return null;
3161
+ }
3162
+ const refresh = reads.refresh;
3163
+ if (held !== undefined && refresh !== undefined) {
3164
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3165
+ if (refreshed === 'behind')
3166
+ return advance(room, local, authorized, live);
3167
+ if (refreshed !== 'beyond-horizon')
3359
3168
  return refreshed;
3360
- if (opened !== null) {
3361
- owed.add(room.locator);
3362
- liveQueries.recover();
3363
- return unconfirmed;
3364
- }
3365
- if (reloadsDue.get(room.locator) === generation)
3169
+ if (reloadsDue.has(room.locator))
3366
3170
  return unconfirmed;
3367
3171
  }
3172
+ const from = standingOf(room.locator);
3368
3173
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3369
- if (bootstrapped.kind === 'staged') {
3370
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3174
+ if (bootstrapped.kind !== 'staged')
3175
+ return live() ? bootstrapped : unconfirmed;
3176
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3177
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3371
3178
  return live() ? null : unconfirmed;
3372
3179
  }
3373
- return live() ? bootstrapped : unconfirmed;
3180
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3181
+ ? null
3182
+ : advance(room, local, authorized, live);
3183
+ }
3184
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3185
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3186
+ if (!live())
3187
+ return unconfirmed;
3188
+ if (refreshed.kind === 'still-current') {
3189
+ stale.delete(room.locator);
3190
+ if (unreadable.delete(room.locator)) {
3191
+ scopedRefusals.delete(room.locator);
3192
+ list(room, true);
3193
+ emitRooms();
3194
+ }
3195
+ return null;
3196
+ }
3197
+ if (refreshed.kind === 'advanced') {
3198
+ const landing = {
3199
+ kind: 'refreshed',
3200
+ from: refreshed.from,
3201
+ remodelled: refreshed.remodelled,
3202
+ };
3203
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3204
+ const now = replicas.get(room.locator)?.record.roomVersion;
3205
+ const reached = now !== undefined &&
3206
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3207
+ return reached ? null : 'behind';
3208
+ }
3209
+ await retainModel(local, room, modelOf(room.locator));
3210
+ return null;
3211
+ }
3212
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3374
3213
  }
3375
3214
  async function withhold(withheld, room, local, progress) {
3376
3215
  if (withheld.kind === 'unconfirmed')
3377
3216
  return;
3378
- if (withheld.kind === 'refused')
3379
- stale.delete(room.locator);
3380
- else
3381
- stale.add(room.locator);
3217
+ stale.add(room.locator);
3382
3218
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3383
3219
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3384
3220
  repairable.set(room.locator, reset);
@@ -3400,30 +3236,10 @@ export function composeRuntime(options) {
3400
3236
  list(room, false);
3401
3237
  emitRooms();
3402
3238
  }
3403
- else if (withheld.kind === 'refused') {
3404
- refusedSource(room, withheld);
3405
- }
3406
3239
  else if (withheld.kind === 'revoked-authorization') {
3407
3240
  await refuse('revoked-authorization');
3408
3241
  }
3409
3242
  }
3410
- function refusedSource(room, { refusal }) {
3411
- sourceRefusals.set(room.locator, {
3412
- code: refusal.rule,
3413
- retryable: false,
3414
- fields: {
3415
- file: refusal.file,
3416
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3417
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3418
- },
3419
- });
3420
- declarationRefusals.delete(room.locator);
3421
- unreadable.add(room.locator);
3422
- list(room, false);
3423
- emitRooms();
3424
- moveTo({ ...status, ...attention() });
3425
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3426
- }
3427
3243
  function leaving(batch, result) {
3428
3244
  if (result.kind === 'expired')
3429
3245
  return { batch, returned: returnedBy('expired') };
@@ -3445,8 +3261,8 @@ export function composeRuntime(options) {
3445
3261
  function settling(acceptance) {
3446
3262
  return acceptance !== null && 'apply' in acceptance;
3447
3263
  }
3448
- function refusalOf(acceptance) {
3449
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3264
+ function failureOf(acceptance) {
3265
+ return settling(acceptance) ? null : acceptance;
3450
3266
  }
3451
3267
  async function acceptance(room, local, submitted, accepted) {
3452
3268
  if (!continuing())
@@ -3469,34 +3285,6 @@ export function composeRuntime(options) {
3469
3285
  ? accepted.roomVersion
3470
3286
  : held,
3471
3287
  };
3472
- const overrides = new Map([
3473
- ...back.set.map((entry) => [entry.path, entry]),
3474
- ...back.removed.map((path) => [path, null]),
3475
- ]);
3476
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3477
- if (refused !== null) {
3478
- const key = pendingKey(room.locator, submitted.id);
3479
- return {
3480
- locator: room.locator,
3481
- left: [],
3482
- commit: {
3483
- rooms: [],
3484
- writes: [
3485
- {
3486
- locator: room.locator,
3487
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3488
- },
3489
- ],
3490
- },
3491
- refused,
3492
- apply() {
3493
- const state = pendings.get(key);
3494
- if (state !== undefined && !fences.has(room.locator))
3495
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3496
- return changeSet(room.locator, { queue: true });
3497
- },
3498
- };
3499
- }
3500
3288
  changes = back;
3501
3289
  }
3502
3290
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3506,8 +3294,6 @@ export function composeRuntime(options) {
3506
3294
  const set = settled.apply(answer);
3507
3295
  pendings.delete(pendingKey(room.locator, submitted.id));
3508
3296
  unreadable.delete(room.locator);
3509
- sourceRefusals.delete(room.locator);
3510
- declarationRefusals.delete(room.locator);
3511
3297
  list(room, true);
3512
3298
  return set;
3513
3299
  },
@@ -3525,13 +3311,9 @@ export function composeRuntime(options) {
3525
3311
  const failed = await exclusive(async () => {
3526
3312
  const adopting = await acceptance(room, local, submitted, accepted);
3527
3313
  await adoptedTogether(local, [adopting]);
3528
- return refusalOf(adopting);
3314
+ return failureOf(adopting);
3529
3315
  });
3530
- if (failed === null)
3531
- return true;
3532
- if (failed.kind === 'refused' && continuing())
3533
- refusedSource(room, failed);
3534
- return false;
3316
+ return failed === null;
3535
3317
  }
3536
3318
  async function adoptedMove(room, partner, local, submitted, accepted) {
3537
3319
  const other = discovered.get(partner);
@@ -3545,18 +3327,10 @@ export function composeRuntime(options) {
3545
3327
  ...accepted.destination,
3546
3328
  });
3547
3329
  await adoptedTogether(local, [left, arrived]);
3548
- const refusedArrival = refusalOf(arrived);
3549
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3550
- refusedSource(other, refusedArrival);
3551
- }
3552
- return refusalOf(left);
3330
+ return failureOf(left);
3553
3331
  });
3554
3332
  submitLater(partner);
3555
- if (failed === null)
3556
- return true;
3557
- if (failed.kind === 'refused' && continuing())
3558
- refusedSource(room, failed);
3559
- return false;
3333
+ return failed === null;
3560
3334
  }
3561
3335
  function queueIn(locator) {
3562
3336
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3679,8 +3453,6 @@ export function composeRuntime(options) {
3679
3453
  if (replica === undefined || head === undefined)
3680
3454
  return 'done';
3681
3455
  const state = pendings.get(pendingKey(room.locator, head.id));
3682
- if (state?.accepted !== undefined)
3683
- return 'done';
3684
3456
  const partner = partnerOf(head, room.locator);
3685
3457
  if (partner !== null && !leavingIn(head, room.locator)) {
3686
3458
  submitLater(partner);
@@ -3794,12 +3566,12 @@ export function composeRuntime(options) {
3794
3566
  }
3795
3567
  if (result.kind === 'expired') {
3796
3568
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3797
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3569
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3798
3570
  if (other !== undefined && there !== null) {
3799
3571
  await withhold(there, other, local, progress);
3800
3572
  return 'done';
3801
3573
  }
3802
- const withheld = await advance(room, local, authorized, continuing);
3574
+ const withheld = await advanced(room, local, authorized);
3803
3575
  if (withheld !== null) {
3804
3576
  await withhold(withheld, room, local, progress);
3805
3577
  return 'done';
@@ -3821,8 +3593,8 @@ export function composeRuntime(options) {
3821
3593
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3822
3594
  const left = leaving(head, result);
3823
3595
  const refreshed = async () => ({
3824
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3825
- here: await advance(room, local, authorized, continuing),
3596
+ there: other === undefined ? null : await advanced(other, local, authorized),
3597
+ here: await advanced(room, local, authorized),
3826
3598
  });
3827
3599
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3828
3600
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3926,25 +3698,22 @@ export function composeRuntime(options) {
3926
3698
  if (signal.aborted)
3927
3699
  return;
3928
3700
  const progress = { complete: true, reachable: true };
3929
- await roomTask(room.locator, async (live) => {
3930
- await Promise.race([
3931
- reload(room, local, progress, () => live() && !signal.aborted),
3932
- abandonedOn(signal),
3933
- ]);
3934
- });
3701
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3935
3702
  if (signal.aborted)
3936
3703
  return;
3937
3704
  await rest(progress);
3938
3705
  }
3939
3706
  }
3940
3707
  function awaitingAnswer(locator) {
3708
+ answering.get(locator)?.release();
3941
3709
  let release = () => { };
3942
- const awaited = new Promise((resolve) => {
3710
+ const answered = new Promise((resolve) => {
3943
3711
  release = resolve;
3944
3712
  });
3945
- answering.set(locator, { generation, answered: awaited });
3713
+ const awaited = { answered, release };
3714
+ answering.set(locator, awaited);
3946
3715
  return () => {
3947
- if (answering.get(locator)?.answered === awaited)
3716
+ if (answering.get(locator) === awaited)
3948
3717
  answering.delete(locator);
3949
3718
  release();
3950
3719
  };
@@ -3971,26 +3740,13 @@ export function composeRuntime(options) {
3971
3740
  const held = replicas.get(room.locator);
3972
3741
  let changed = null;
3973
3742
  if (held !== undefined) {
3974
- const declared = {
3743
+ const record = {
3975
3744
  ...held.record,
3976
3745
  operations: room.operations,
3977
3746
  replica: room.replica,
3978
3747
  typedSelection: room.typedSelection ?? null,
3979
3748
  model: next,
3980
3749
  };
3981
- let read = 'staged';
3982
- if (!sameMembership(declared, held.record)) {
3983
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3984
- read =
3985
- verdict.verdict !== 'refuse'
3986
- ? 'staged'
3987
- : verdict.rule === 'model-unavailable'
3988
- ? 'deferred'
3989
- : { kind: 'refused', refusal: verdict };
3990
- }
3991
- const record = read === 'staged'
3992
- ? declared
3993
- : { ...declared, typedSelection: held.record.typedSelection };
3994
3750
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3995
3751
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3996
3752
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -4012,16 +3768,6 @@ export function composeRuntime(options) {
4012
3768
  documents: answer.changed[room.locator] ?? [],
4013
3769
  room: true,
4014
3770
  });
4015
- if (read === 'staged' || read === 'deferred') {
4016
- if (declarationRefusals.delete(room.locator)) {
4017
- sourceRefusals.delete(room.locator);
4018
- unreadable.delete(room.locator);
4019
- }
4020
- }
4021
- else {
4022
- refusedSource(room, read);
4023
- declarationRefusals.add(room.locator);
4024
- }
4025
3771
  }
4026
3772
  heldModel(room, next);
4027
3773
  if (changed === null)
@@ -4066,6 +3812,17 @@ export function composeRuntime(options) {
4066
3812
  emitRooms();
4067
3813
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
4068
3814
  }
3815
+ function name(locator) {
3816
+ const stream = options.adapter.roomStream;
3817
+ const record = replicas.get(locator)?.record;
3818
+ const room = discovered.get(locator);
3819
+ if (stream === undefined || record === undefined || room === undefined)
3820
+ return;
3821
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3822
+ return;
3823
+ naming += 1;
3824
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3825
+ }
4069
3826
  function heldRooms(listed) {
4070
3827
  return listed.flatMap((room) => {
4071
3828
  const record = replicas.get(room.locator)?.record;
@@ -4083,14 +3840,20 @@ export function composeRuntime(options) {
4083
3840
  return outcome?.status === 'fulfilled' && outcome.value;
4084
3841
  });
4085
3842
  if (attempt === 'opened') {
3843
+ naming += 1;
4086
3844
  options.adapter.roomStream?.hold(heldRooms(ready));
4087
3845
  handed = true;
4088
3846
  }
4089
3847
  discovering?.release();
4090
- const loaded = await Promise.allSettled(ready
4091
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
4092
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
4093
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3848
+ for (const room of ready) {
3849
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3850
+ if (attempt === 'authorized')
3851
+ name(room.locator);
3852
+ continue;
3853
+ }
3854
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3855
+ }
3856
+ const failed = prepared.find((room) => room.status === 'rejected');
4094
3857
  if (failed !== undefined)
4095
3858
  throw failed.reason;
4096
3859
  }
@@ -4114,7 +3877,9 @@ export function composeRuntime(options) {
4114
3877
  return room.replica.admission === 'admitted';
4115
3878
  }
4116
3879
  async function caughtUp(room, local, progress, live) {
4117
- if (authorization === undefined || !live() || !behind(room))
3880
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3881
+ return;
3882
+ if (!live() || !behind(room))
4118
3883
  return;
4119
3884
  const withheld = await advance(room, local, authorization, live);
4120
3885
  if (withheld !== null)
@@ -4127,6 +3892,8 @@ export function composeRuntime(options) {
4127
3892
  async function loadRoom(room, local, progress, live) {
4128
3893
  if (!live() || authorization === undefined)
4129
3894
  return;
3895
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3896
+ return;
4130
3897
  if (replicas.has(room.locator) && revoked(room.locator)) {
4131
3898
  await reload(room, local, progress, live);
4132
3899
  return;
@@ -4144,13 +3911,14 @@ export function composeRuntime(options) {
4144
3911
  reloadsDue.delete(room.locator);
4145
3912
  if (!live() || authorization === undefined)
4146
3913
  return;
3914
+ const from = standingOf(room.locator);
4147
3915
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
4148
3916
  if (bootstrapped.kind !== 'staged') {
4149
3917
  if (live())
4150
3918
  await withhold(bootstrapped, room, local, progress);
4151
3919
  return;
4152
3920
  }
4153
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3921
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4154
3922
  await caughtUp(room, local, progress, live);
4155
3923
  }
4156
3924
  finally {
@@ -4259,23 +4027,45 @@ export function composeRuntime(options) {
4259
4027
  }
4260
4028
  }
4261
4029
  async function admittedRead(work) {
4262
- if (roomReads.generation !== generation) {
4263
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4030
+ while (roomReads.free === 0) {
4031
+ await new Promise((admit) => roomReads.waiting.push(admit));
4264
4032
  }
4265
- const reads = roomReads;
4266
- while (reads.free === 0)
4267
- await new Promise((admit) => reads.waiting.push(admit));
4268
- reads.free -= 1;
4033
+ roomReads.free -= 1;
4269
4034
  try {
4270
4035
  await work();
4271
4036
  }
4272
4037
  finally {
4273
- reads.free += 1;
4274
- reads.waiting.shift()?.();
4038
+ roomReads.free += 1;
4039
+ roomReads.waiting.shift()?.();
4275
4040
  }
4276
4041
  }
4042
+ function inRoom(locator, work) {
4043
+ roomWork += 1;
4044
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4045
+ try {
4046
+ return await work(continuing);
4047
+ }
4048
+ finally {
4049
+ roomWork -= 1;
4050
+ }
4051
+ });
4052
+ const chain = ran.then(() => undefined, () => undefined);
4053
+ roomLanes.set(locator, chain);
4054
+ working.add(chain);
4055
+ void chain.finally(() => {
4056
+ working.delete(chain);
4057
+ if (roomLanes.get(locator) === chain)
4058
+ roomLanes.delete(locator);
4059
+ });
4060
+ return ran;
4061
+ }
4277
4062
  function roomTask(locator, work) {
4278
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4063
+ if (stopping)
4064
+ return Promise.resolve();
4065
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4066
+ }
4067
+ function advanced(room, local, authorized) {
4068
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4279
4069
  }
4280
4070
  function task(lane, work) {
4281
4071
  if (stopping)
@@ -4299,15 +4089,12 @@ export function composeRuntime(options) {
4299
4089
  }
4300
4090
  const progress = { complete: true, reachable: true };
4301
4091
  const gate = discover();
4302
- if (attempt === 'opened')
4303
- opened = generation;
4304
- underway = attempt === 'opened' ? { generation, progress } : null;
4092
+ underway = attempt === 'opened' ? progress : null;
4305
4093
  if (attempt === 'opened')
4306
4094
  streamed = progress;
4307
4095
  complete = false;
4308
4096
  recorded = false;
4309
4097
  moveTo(transferring(status));
4310
- handed = false;
4311
4098
  try {
4312
4099
  const local = await openLocal();
4313
4100
  if (!live())
@@ -4335,7 +4122,6 @@ export function composeRuntime(options) {
4335
4122
  if (handed || closed || progress.reachable)
4336
4123
  return;
4337
4124
  underway = null;
4338
- opened = null;
4339
4125
  journal.record('connection reopen');
4340
4126
  await rest(progress, live);
4341
4127
  options.adapter.roomStream?.reopen();
@@ -4375,11 +4161,11 @@ export function composeRuntime(options) {
4375
4161
  notifyDiscovered();
4376
4162
  const opened = socketOpen;
4377
4163
  socketOpen = false;
4164
+ handed = false;
4165
+ naming = 0;
4378
4166
  connectionLost();
4379
- if (lost) {
4380
- reads.lost();
4167
+ if (lost)
4381
4168
  conversations.lost();
4382
- }
4383
4169
  if (revoked) {
4384
4170
  void refuse('revoked-authorization');
4385
4171
  return;
@@ -4394,14 +4180,13 @@ export function composeRuntime(options) {
4394
4180
  });
4395
4181
  }
4396
4182
  function applyOpening(expired) {
4397
- const progress = underway?.generation === generation ? underway.progress : null;
4183
+ const progress = underway;
4184
+ underway = null;
4185
+ naming = Math.max(0, naming - 1);
4398
4186
  const cycle = progress ?? { complete: true, reachable: true };
4399
- if (opened === generation)
4400
- opened = null;
4401
4187
  const reloading = progress === null ? [] : owedReloads(expired);
4402
4188
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4403
4189
  return task(OPENING_LANE, async (live) => {
4404
- underway = null;
4405
4190
  if (!live())
4406
4191
  return;
4407
4192
  if (progress === null) {
@@ -4409,20 +4194,14 @@ export function composeRuntime(options) {
4409
4194
  await rest(cycle, live);
4410
4195
  return;
4411
4196
  }
4412
- await roomsCarried([...expired, ...reloading]);
4413
- if (!live())
4414
- return;
4415
4197
  try {
4416
4198
  const local = await openLocal();
4417
- const reloaded = await replacing;
4199
+ await replacing;
4418
4200
  if (progress.reachable) {
4419
4201
  await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4420
4202
  !expired.includes(room.locator) &&
4421
4203
  !reloading.includes(room.locator)), 'current', local);
4422
4204
  }
4423
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4424
- if (failed !== undefined)
4425
- throw failed.reason;
4426
4205
  }
4427
4206
  catch {
4428
4207
  progress.complete = false;
@@ -4434,11 +4213,8 @@ export function composeRuntime(options) {
4434
4213
  }
4435
4214
  function owedReloads(expired) {
4436
4215
  const reloading = [...discovered.values()]
4437
- .filter((room) => replicas.has(room.locator) &&
4438
- !expired.includes(room.locator) &&
4439
- (owed.has(room.locator) || revoked(room.locator)))
4216
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4440
4217
  .map((room) => room.locator);
4441
- owed.clear();
4442
4218
  for (const locator of reloading)
4443
4219
  catching.add(locator);
4444
4220
  liveQueries.recover();
@@ -4447,16 +4223,17 @@ export function composeRuntime(options) {
4447
4223
  async function reloadNow(replaced, progress) {
4448
4224
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4449
4225
  for (const room of reloaded)
4450
- reloadsDue.set(room.locator, generation);
4226
+ reloadsDue.add(room.locator);
4451
4227
  try {
4452
4228
  const local = await openLocal();
4453
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4229
+ for (const room of reloaded) {
4230
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4231
+ }
4454
4232
  }
4455
4233
  catch {
4456
4234
  for (const room of reloaded)
4457
4235
  reloadsDue.delete(room.locator);
4458
4236
  progress.reachable = false;
4459
- return [];
4460
4237
  }
4461
4238
  }
4462
4239
  async function settleModels(held, status, local) {
@@ -4467,7 +4244,7 @@ export function composeRuntime(options) {
4467
4244
  const settled = await exclusive(async () => {
4468
4245
  if (!continuing())
4469
4246
  return [];
4470
- const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4247
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4471
4248
  for (const room of unchanged) {
4472
4249
  const next = { ...modelOf(room.locator), status };
4473
4250
  const replica = replicas.get(room.locator);
@@ -4527,14 +4304,10 @@ export function composeRuntime(options) {
4527
4304
  try {
4528
4305
  const local = await openLocal();
4529
4306
  await roomTask(locator, async (live) => {
4530
- const current = () => live() && !signal.aborted;
4531
- if (!current() || authorization === undefined)
4307
+ if (!live() || authorization === undefined)
4532
4308
  return;
4533
- const withheld = await Promise.race([
4534
- advance(room, local, authorization, current),
4535
- abandonedOn(signal),
4536
- ]);
4537
- if (withheld !== null && current())
4309
+ const withheld = await advance(room, local, authorization, live);
4310
+ if (withheld !== null && live())
4538
4311
  await withhold(withheld, room, local, progress);
4539
4312
  });
4540
4313
  }
@@ -4589,14 +4362,11 @@ export function composeRuntime(options) {
4589
4362
  progress.complete = false;
4590
4363
  return;
4591
4364
  }
4592
- reads.applied();
4593
4365
  submitAll();
4594
4366
  for (const locator of stale)
4595
4367
  if (repairable.has(locator))
4596
4368
  repairLater(locator, {});
4597
4369
  await conversations.reconcile();
4598
- if (status.providerUnreachable)
4599
- conversations.reconnected();
4600
4370
  const reading = { complete: true, reachable: true };
4601
4371
  await conversations.catchUp(reading);
4602
4372
  if (progress.reachable && reading.reachable)
@@ -4622,7 +4392,7 @@ export function composeRuntime(options) {
4622
4392
  if (!live())
4623
4393
  return;
4624
4394
  let lastCompleted = status.lastCompleted;
4625
- let synced = complete && stale.size === 0 && !pending();
4395
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4626
4396
  if (synced && !recorded) {
4627
4397
  const completedAt = new Date().toISOString();
4628
4398
  try {
@@ -4637,74 +4407,79 @@ export function composeRuntime(options) {
4637
4407
  }
4638
4408
  }
4639
4409
  moveTo({
4640
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4410
+ state: pending()
4411
+ ? 'pending'
4412
+ : synced && complete && roomWork === 0 && naming === 0
4413
+ ? 'synced'
4414
+ : 'syncing',
4641
4415
  lastCompleted,
4642
- providerUnreachable: !progress.reachable,
4416
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4643
4417
  ...attention(),
4644
4418
  });
4645
4419
  }
4646
4420
  function discover() {
4421
+ discovering?.release();
4647
4422
  let release = () => { };
4648
4423
  const done = new Promise((resolve) => {
4649
4424
  release = resolve;
4650
4425
  });
4651
- discovering = { generation, done, release };
4426
+ discovering = { done, release };
4652
4427
  return discovering;
4653
4428
  }
4654
- function applyRoom(locator, roomVersion) {
4655
- announced.set(locator, roomVersion);
4656
- return task(roomLane(locator), async (live) => {
4657
- if (discovering?.generation === generation)
4658
- await discovering.done;
4659
- const saving = answering.get(locator);
4660
- if (saving?.generation === generation)
4661
- await saving.answered;
4662
- if (!live() || authorization === undefined)
4663
- return;
4664
- const room = discovered.get(locator);
4665
- const held = replicas.get(locator);
4666
- if (room === undefined || room.replica.admission !== 'admitted')
4667
- return;
4668
- if (held !== undefined &&
4669
- (held.record.replica.selection !== room.replica.selection ||
4670
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4671
- return;
4672
- }
4673
- const during = underway?.generation === generation ? underway.progress : null;
4674
- const progress = during ?? { complete: true, reachable: true };
4675
- if (during === null)
4676
- moveTo(transferring(status));
4429
+ function applyRoom(change) {
4430
+ const { locator } = change;
4431
+ if (stopping)
4432
+ return Promise.resolve();
4433
+ return inRoom(locator, async (live) => {
4434
+ await discovering?.done;
4435
+ await answering.get(locator)?.answered;
4436
+ const progress = underway ?? { complete: true, reachable: true };
4677
4437
  try {
4678
4438
  const local = await openLocal();
4679
- await admittedRead(async () => {
4680
- if (!live() || authorization === undefined)
4681
- return;
4682
- const withheld = await advance(room, local, authorization, live);
4683
- if (withheld !== null)
4684
- await withhold(withheld, room, local, progress);
4685
- });
4439
+ for (;;) {
4440
+ if (!live())
4441
+ return null;
4442
+ const room = discovered.get(locator);
4443
+ const held = replicas.get(locator);
4444
+ if (room === undefined ||
4445
+ held === undefined ||
4446
+ room.replica.admission !== 'admitted' ||
4447
+ held.record.replica.selection !== room.replica.selection) {
4448
+ return null;
4449
+ }
4450
+ const at = held.record.roomVersion;
4451
+ if (options.adapter.reached(locator, at, change.roomVersion))
4452
+ return progress;
4453
+ if (!options.adapter.reached(locator, at, change.from)) {
4454
+ name(locator);
4455
+ return progress;
4456
+ }
4457
+ if (underway === null)
4458
+ moveTo(transferring(status));
4459
+ const writing = () => live() && !full;
4460
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4461
+ if (refreshed.kind !== 'advanced')
4462
+ return progress;
4463
+ const landing = {
4464
+ kind: 'refreshed',
4465
+ from: refreshed.from,
4466
+ remodelled: refreshed.remodelled,
4467
+ };
4468
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4469
+ stale.delete(locator);
4470
+ await retainModel(local, room, modelOf(locator));
4471
+ return progress;
4472
+ }
4473
+ }
4686
4474
  }
4687
4475
  catch {
4476
+ if (!stale.has(locator))
4477
+ name(locator);
4688
4478
  stale.add(locator);
4479
+ return progress;
4689
4480
  }
4690
- finally {
4691
- if (during === null)
4692
- await rest(progress, live);
4693
- }
4694
- });
4481
+ }).then((progress) => rest(progress), () => undefined);
4695
4482
  }
4696
- function roomsCarried(replacing) {
4697
- const replaced = new Set(replacing.map(roomLane));
4698
- return Promise.all([...lanes]
4699
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4700
- .map(([, held]) => held.chain));
4701
- }
4702
- const receipts = {
4703
- heads: async () => (await openLocal()).receivedHeads(),
4704
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4705
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4706
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4707
- };
4708
4483
  function observeStream() {
4709
4484
  const stream = options.adapter.roomStream;
4710
4485
  if (stream === undefined || withdrawStream !== undefined)
@@ -4714,9 +4489,10 @@ export function composeRuntime(options) {
4714
4489
  return;
4715
4490
  if (event.kind === 'opened') {
4716
4491
  socketOpen = true;
4492
+ handed = false;
4493
+ naming = 0;
4717
4494
  connectionLive();
4718
4495
  generation += 1;
4719
- reads.opened();
4720
4496
  conversations.reopened();
4721
4497
  recovery.resume();
4722
4498
  void sync('opened');
@@ -4728,15 +4504,8 @@ export function composeRuntime(options) {
4728
4504
  else if (event.kind === 'authority-changed')
4729
4505
  void rediscover(event.scope);
4730
4506
  else
4731
- void applyRoom(event.locator, event.roomVersion);
4732
- }, receipts, heldRevisions);
4733
- }
4734
- async function heldRevisions(locator, paths) {
4735
- const held = replicas.get(locator);
4736
- if (held === undefined || paths.length === 0)
4737
- return new Map();
4738
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4739
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4507
+ void applyRoom(event);
4508
+ });
4740
4509
  }
4741
4510
  function writesHeld() {
4742
4511
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4809,8 +4578,6 @@ export function composeRuntime(options) {
4809
4578
  notifyDiscovered();
4810
4579
  if (probing !== null)
4811
4580
  clearInterval(probing);
4812
- for (const resolve of shownWaiters.splice(0))
4813
- resolve();
4814
4581
  connectionLive();
4815
4582
  lockListeners.clear();
4816
4583
  readinessWatches.clear();
@@ -4835,6 +4602,7 @@ export function composeRuntime(options) {
4835
4602
  }
4836
4603
  async function stop() {
4837
4604
  stopping = true;
4605
+ inFlight.abort();
4838
4606
  await recovery.retire();
4839
4607
  await Promise.all(working);
4840
4608
  await mutations;
@@ -5228,7 +4996,6 @@ export function composeRuntime(options) {
5228
4996
  journal.record(`persistence ${answer}`);
5229
4997
  queueReadiness();
5230
4998
  },
5231
- shown: screen,
5232
4999
  within: (reach, port) => {
5233
5000
  if (full)
5234
5001
  void probed();