@agentshouse/mdruntime 0.1.0-c1ff51b87bc3d → 0.1.0-c20970f71a6a9

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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +68 -48
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +4 -7
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +19 -14
  7. package/dist/client.d.ts +4 -4
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +0 -1
  10. package/dist/collection.d.ts +0 -3
  11. package/dist/collection.d.ts.map +1 -1
  12. package/dist/collection.js +1 -17
  13. package/dist/compile.d.ts +1 -1
  14. package/dist/compile.d.ts.map +1 -1
  15. package/dist/compile.js +69 -45
  16. package/dist/connection.d.ts +3 -5
  17. package/dist/connection.d.ts.map +1 -1
  18. package/dist/connection.js +867 -851
  19. package/dist/conversation.d.ts +14 -14
  20. package/dist/conversation.d.ts.map +1 -1
  21. package/dist/conversation.js +153 -145
  22. package/dist/durability.d.ts +0 -1
  23. package/dist/durability.d.ts.map +1 -1
  24. package/dist/entry.d.ts.map +1 -1
  25. package/dist/entry.js +1 -0
  26. package/dist/host.d.ts +3 -6
  27. package/dist/host.d.ts.map +1 -1
  28. package/dist/host.js +8 -17
  29. package/dist/index.d.ts +5 -4
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/live.d.ts.map +1 -1
  33. package/dist/live.js +1 -2
  34. package/dist/media.d.ts +1 -1
  35. package/dist/media.d.ts.map +1 -1
  36. package/dist/media.js +19 -9
  37. package/dist/memory.d.ts +9 -5
  38. package/dist/memory.d.ts.map +1 -1
  39. package/dist/memory.js +134 -82
  40. package/dist/messages.d.ts +2 -2
  41. package/dist/messages.d.ts.map +1 -1
  42. package/dist/messages.js +15 -16
  43. package/dist/model.d.ts +0 -1
  44. package/dist/model.d.ts.map +1 -1
  45. package/dist/model.js +0 -9
  46. package/dist/outcome.d.ts +0 -3
  47. package/dist/outcome.d.ts.map +1 -1
  48. package/dist/phrase.d.ts +3 -0
  49. package/dist/phrase.d.ts.map +1 -0
  50. package/dist/phrase.js +8 -0
  51. package/dist/query.d.ts +5 -1
  52. package/dist/query.d.ts.map +1 -1
  53. package/dist/query.js +20 -1
  54. package/dist/reach.d.ts +3 -3
  55. package/dist/reach.d.ts.map +1 -1
  56. package/dist/reach.js +19 -29
  57. package/dist/react.d.ts +1 -0
  58. package/dist/react.d.ts.map +1 -1
  59. package/dist/readiness.d.ts +2 -15
  60. package/dist/readiness.d.ts.map +1 -1
  61. package/dist/readiness.js +4 -10
  62. package/dist/recovery.d.ts +1 -0
  63. package/dist/recovery.d.ts.map +1 -1
  64. package/dist/recovery.js +26 -0
  65. package/dist/refresh.d.ts +7 -5
  66. package/dist/refresh.d.ts.map +1 -1
  67. package/dist/refresh.js +46 -6
  68. package/dist/replica.d.ts +0 -1
  69. package/dist/replica.d.ts.map +1 -1
  70. package/dist/replica.js +3 -4
  71. package/dist/room-draft.d.ts +1 -1
  72. package/dist/room-draft.d.ts.map +1 -1
  73. package/dist/room-draft.js +61 -48
  74. package/dist/rows.d.ts +3 -2
  75. package/dist/rows.d.ts.map +1 -1
  76. package/dist/rows.js +34 -29
  77. package/dist/search.d.ts +12 -63
  78. package/dist/search.d.ts.map +1 -1
  79. package/dist/search.js +9 -52
  80. package/dist/shared.d.ts +0 -1
  81. package/dist/shared.d.ts.map +1 -1
  82. package/dist/shared.js +0 -10
  83. package/dist/status.d.ts +0 -4
  84. package/dist/status.d.ts.map +1 -1
  85. package/dist/store.d.ts +15 -19
  86. package/dist/store.d.ts.map +1 -1
  87. package/dist/store.js +40 -39
  88. package/dist/stored.d.ts +33 -6
  89. package/dist/stored.d.ts.map +1 -1
  90. package/dist/stored.js +97 -122
  91. package/dist/tables.d.ts +34 -33
  92. package/dist/tables.d.ts.map +1 -1
  93. package/dist/tables.js +18 -54
  94. package/dist/transfer.d.ts +0 -5
  95. package/dist/transfer.d.ts.map +1 -1
  96. package/dist/validation.d.ts +3 -4
  97. package/dist/validation.d.ts.map +1 -1
  98. package/dist/validation.js +35 -96
  99. package/dist/wire.d.ts +1 -4
  100. package/dist/wire.d.ts.map +1 -1
  101. package/dist/wire.js +1 -1
  102. package/package.json +2 -2
  103. package/dist/fulltext.d.ts +0 -27
  104. package/dist/fulltext.d.ts.map +0 -1
  105. package/dist/fulltext.js +0 -368
  106. package/dist/reads.d.ts +0 -11
  107. package/dist/reads.d.ts.map +0 -1
  108. package/dist/reads.js +0 -91
  109. package/dist/words.d.ts +0 -19
  110. package/dist/words.d.ts.map +0 -1
  111. package/dist/words.js +0 -156
@@ -1,8 +1,8 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
1
+ import { parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
2
  import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { bodied, identityOf, planOf, plain, queryKey, } from './query.js';
6
6
  import { createLiveQueries } from './live.js';
7
7
  import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
@@ -12,7 +12,7 @@ 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,10 +20,9 @@ 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';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
27
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';
@@ -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,37 +58,34 @@ 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
68
  const STREAMS_HELD = 4;
71
- const NOTHING_HELD = { parts: 0, bytes: 0 };
72
- const QUEUED_BYTES = 1 << 23;
73
- // 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.
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.
74
72
  const ARRIVAL_REPORT_MS = 250;
75
- const CLEAN_CHANGES = 2_000;
76
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.
77
74
  const LOCK_GRACE_MS = 120_000;
78
75
  function operationOf(input) {
79
76
  const operation = input?.operation_id;
80
77
  return typeof operation === 'string' ? operation : null;
81
78
  }
82
- function abandonedOn(signal) {
83
- return new Promise((resolve) => {
84
- if (signal.aborted)
85
- resolve(null);
86
- else
87
- 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));
88
86
  });
89
87
  }
90
- function roomLane(locator) {
91
- return `room ${locator}`;
92
- }
88
+ const TEXT_READS = 4;
93
89
  function pendingKey(locator, id) {
94
90
  return `${locator}\u0000${id}`;
95
91
  }
@@ -124,11 +120,19 @@ export function composeRuntime(options) {
124
120
  const liveModels = new FencedMap(isFenced);
125
121
  const invalidModels = new Set();
126
122
  const discovered = new Map();
127
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
123
+ let inFlight = new AbortController();
124
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
125
+ const reads = {
126
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
127
+ ...(refreshed === undefined
128
+ ? {}
129
+ : {
130
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
131
+ }),
132
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
133
+ };
128
134
  let roomsListed = false;
129
135
  const scopedRefusals = new Map();
130
- const sourceRefusals = new Map();
131
- const declarationRefusals = new Set();
132
136
  function modelOf(locator) {
133
137
  return models.get(locator) ?? NO_MODEL;
134
138
  }
@@ -151,10 +155,10 @@ export function composeRuntime(options) {
151
155
  let closed = false;
152
156
  let stopping = false;
153
157
  let withdrawStream;
154
- const announced = new Map();
155
158
  let underway = null;
156
159
  let streamed = null;
157
160
  let handed = false;
161
+ let naming = 0;
158
162
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
159
163
  const repairing = new Set();
160
164
  const calling = new Set();
@@ -165,12 +169,13 @@ export function composeRuntime(options) {
165
169
  let recorded = false;
166
170
  const stale = new Set();
167
171
  let generation = 0;
168
- let discovering = null;
169
- let opened = null;
170
- const owed = new Set();
171
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
172
175
  const lanes = new Map();
173
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
176
+ const roomLanes = new Map();
177
+ let roomWork = 0;
178
+ const roomReads = { free: ROOM_READS, waiting: [] };
174
179
  const working = new Set();
175
180
  let mutations = Promise.resolve();
176
181
  let gathering = null;
@@ -186,6 +191,8 @@ export function composeRuntime(options) {
186
191
  let withdrawConversations;
187
192
  let currencyQueued = null;
188
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
189
196
  const confirmed = new Set();
190
197
  let lockGrace = null;
191
198
  const lockListeners = new Set();
@@ -195,6 +202,8 @@ export function composeRuntime(options) {
195
202
  const serviceModeListeners = new Set();
196
203
  let member = null;
197
204
  const memberListeners = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
198
207
  const unpreparedDemand = new Set();
199
208
  const unpreparable = new Set();
200
209
  let preparing = false;
@@ -224,12 +233,10 @@ export function composeRuntime(options) {
224
233
  const streaming = new Set();
225
234
  const underived = new Map();
226
235
  const unindexed = new Map();
227
- const unchecked = new Map();
236
+ const startedOver = new Map();
228
237
  let arrivalReported = 0;
229
- let onScreen = options.shell.onScreen;
230
- const shownWaiters = [];
231
- let streams = { generation, free: STREAMS_HELD };
232
- let queuedBytes = 0;
238
+ let streamsFree = STREAMS_HELD;
239
+ let queuedText = 0;
233
240
  const installWaiters = [];
234
241
  const stageBudget = {
235
242
  bytes: STAGE_FLOOR * 4,
@@ -243,28 +250,14 @@ export function composeRuntime(options) {
243
250
  ceiling: DERIVE_BYTES,
244
251
  aim: DERIVE_AIM_MS,
245
252
  };
246
- let installWrites = Promise.resolve();
253
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
247
254
  const conversations = createConversationStore({
248
- agents: declaredAgents,
249
- documents: (declaration, after) => rowsQuery(planOf({
250
- rooms: [declaration.locator],
251
- types: [declaration.type],
252
- select: {
253
- fields: [
254
- declaration.identity,
255
- declaration.name,
256
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
257
- ].map((field) => `/${escapePointer(field)}`),
258
- },
259
- limit: MAXIMUM_WINDOW,
260
- ...(after === undefined ? {} : { after }),
261
- }, roomFacts())),
262
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
263
257
  durably,
264
258
  stream: () => options.adapter.conversations,
265
259
  publish: publishChangeSet,
266
260
  online: () => connectionOf() !== 'unreachable' && refusal === null,
267
- listed: () => roomsListed,
268
261
  full: () => full,
269
262
  closed: () => closed,
270
263
  unanswered: resendLater,
@@ -272,6 +265,8 @@ export function composeRuntime(options) {
272
265
  });
273
266
  const journal = createLog();
274
267
  const keptFiles = openedFiles(database, isFenced);
268
+ const holding = new Map();
269
+ const unholdable = new Map();
275
270
  let readyFromStore = () => { };
276
271
  const ready = new Promise((resolve) => {
277
272
  readyFromStore = resolve;
@@ -361,9 +356,9 @@ export function composeRuntime(options) {
361
356
  return value === undefined ? null : structuredClone(value);
362
357
  }
363
358
  async function writtenRoomDraft(room, path, revision, input, next) {
364
- const parts = readParts(input);
359
+ const parts = readParts(input, path);
365
360
  if (path === '' || normalize(path) !== path)
366
- throw runtimeError('room-draft-malformed');
361
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
367
362
  const local = await openLocal();
368
363
  const current = roomDrafts.get(room)?.get(path);
369
364
  if ((current?.revision ?? null) !== revision) {
@@ -412,7 +407,7 @@ export function composeRuntime(options) {
412
407
  const changed = new Map();
413
408
  for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
414
409
  for (const { batch, state } of entriesOf(room)) {
415
- if (state === 'accepted' || state === 'local')
410
+ if (state === 'local')
416
411
  continue;
417
412
  for (const { path } of batch.changes)
418
413
  changed.set(`${room}\u0000${path}`, { room, path });
@@ -433,16 +428,9 @@ export function composeRuntime(options) {
433
428
  returned: structuredClone(record.returned),
434
429
  });
435
430
  }
436
- for (const [room, refusal] of sourceRefusals)
437
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
438
431
  attentionRequired.sort((left, right) => {
439
432
  if (left.room !== right.room)
440
433
  return left.room < right.room ? -1 : 1;
441
- if ('refusal' in left || 'refusal' in right) {
442
- if ('refusal' in left && 'refusal' in right)
443
- return 0;
444
- return 'refusal' in left ? -1 : 1;
445
- }
446
434
  if (left.path !== right.path)
447
435
  return left.path < right.path ? -1 : 1;
448
436
  return 0;
@@ -597,9 +585,6 @@ export function composeRuntime(options) {
597
585
  if (localSaves)
598
586
  return Object.freeze({ batch, state: 'local' });
599
587
  const state = pendings.get(pendingKey(locator, batch.id));
600
- if (state?.accepted !== undefined) {
601
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
602
- }
603
588
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
604
589
  }
605
590
  function entriesOf(locator) {
@@ -673,11 +658,7 @@ export function composeRuntime(options) {
673
658
  })();
674
659
  }
675
660
  function roomsOfReach(reach) {
676
- return new Set([
677
- ...reach.rooms,
678
- ...reach.documents.map(roomOfKey),
679
- ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
680
- ]);
661
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
681
662
  }
682
663
  function reachedRooms(plan) {
683
664
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -716,7 +697,7 @@ export function composeRuntime(options) {
716
697
  coverage.push({
717
698
  room: locator,
718
699
  generation: replica.record.transfer,
719
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
700
+ current: !(catching.has(locator) || transfers.has(locator)),
720
701
  });
721
702
  read.push({ locator, key: replica.record.key });
722
703
  for (const batch of replica.queue)
@@ -743,8 +724,6 @@ export function composeRuntime(options) {
743
724
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
744
725
  }
745
726
  function roomState(locator) {
746
- if (sourceRefusals.has(locator))
747
- return 'refused';
748
727
  const listedRoom = discovered.get(locator);
749
728
  if (roomsListed &&
750
729
  (listedRoom === undefined ||
@@ -759,7 +738,7 @@ export function composeRuntime(options) {
759
738
  }
760
739
  function conversationCoverage(reach) {
761
740
  const declared = reach === null || reach.conversations;
762
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
741
+ if (!declared || options.adapter.conversations === undefined)
763
742
  return 'undeclared';
764
743
  if (!conversations.known() || !conversations.current())
765
744
  return 'acquiring';
@@ -774,79 +753,50 @@ export function composeRuntime(options) {
774
753
  ? 'connected'
775
754
  : 'connecting';
776
755
  }
777
- function refusedIn(locators) {
778
- return locators.flatMap((room) => {
779
- const held = sourceRefusals.get(room);
780
- if (held === undefined)
781
- return [];
782
- const { file, location } = held.fields;
783
- return [
784
- {
785
- room,
786
- name: discovered.get(room)?.name ?? room,
787
- file: typeof file === 'string' ? file : '',
788
- rule: held.code,
789
- location: typeof location === 'string' ? location : null,
790
- },
791
- ];
792
- });
756
+ function bytesOf(room, underway) {
757
+ const transfer = transfers.get(room);
758
+ if (transfer !== undefined) {
759
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
760
+ return {
761
+ done: transfer.done.bytes - Math.ceil(owed),
762
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
763
+ };
764
+ }
765
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
766
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
793
767
  }
794
768
  function installationOf(coverage) {
795
769
  const rooms = { done: 0, total: 0 };
796
- const parts = { done: 0, total: 0 };
797
770
  const bytes = { done: 0, total: 0 };
798
771
  let installing = 0;
799
772
  let downloading = false;
800
- let checking = 0;
801
773
  for (const { room, state } of coverage) {
802
774
  if (state === 'unadmitted')
803
775
  continue;
804
776
  rooms.total += 1;
805
777
  if (state === 'complete')
806
778
  rooms.done += 1;
807
- const transfer = transfers.get(room);
808
779
  const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
809
780
  if (underway)
810
781
  installing += 1;
811
782
  if (underway && (streaming.has(room) || !arrivals.has(room)))
812
783
  downloading = true;
813
- if (transfer !== undefined) {
814
- const check = unchecked.get(room);
815
- const unchecking = check === undefined ? 0 : check.bytes * (1 - Math.min(1, check.done / check.text));
816
- const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
817
- const arrived = arrivals.get(room);
818
- parts.done += transfer.done.parts;
819
- parts.total += Math.max(arrived?.parts ?? 0, transfer.done.parts);
820
- bytes.done += transfer.done.bytes - Math.ceil(owed + unchecking);
821
- checking += Math.ceil(owed + unchecking) - Math.ceil(owed);
822
- bytes.total += Math.max(arrived?.bytes ?? 0, transfer.done.bytes);
823
- continue;
824
- }
825
- const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
826
- const arrived = arrivals.get(room) ?? held;
827
- parts.done += held.parts;
828
- parts.total += arrived.parts;
829
- bytes.done += held.bytes;
830
- bytes.total += arrived.bytes;
784
+ const now = bytesOf(room, underway);
785
+ const earlier = underway ? startedOver.get(room) : undefined;
786
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
787
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
831
788
  }
832
789
  const step = installing === 0
833
790
  ? null
834
791
  : downloading
835
792
  ? 'downloading'
836
- : bytes.done + checking < bytes.total
793
+ : bytes.done < bytes.total
837
794
  ? 'writing'
838
795
  : 'replaying';
839
- return {
840
- step,
841
- paused: installing > 0 && !onScreen ? 'hidden' : null,
842
- rooms,
843
- parts,
844
- bytes,
845
- };
796
+ return { step, rooms, bytes };
846
797
  }
847
798
  function readinessFor(port, reach) {
848
799
  const required = requiredRooms(reach);
849
- const search = port.search === 'required';
850
800
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
851
801
  const preparing = coverage
852
802
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -859,14 +809,12 @@ export function composeRuntime(options) {
859
809
  listed: required.listed,
860
810
  rooms: coverage,
861
811
  conversations: conversationCoverage(reach),
862
- search: search ? 'required' : 'unused',
863
812
  },
864
813
  persistence,
865
814
  connection: connectionOf(),
866
815
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
867
816
  space: full ? 'full' : port.space,
868
817
  installation: installationOf(coverage),
869
- refused: refusedIn(required.rooms),
870
818
  lost: storageLoss !== null,
871
819
  established: targets[port.target]?.established === true ||
872
820
  (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
@@ -954,8 +902,12 @@ export function composeRuntime(options) {
954
902
  unpreparable.clear();
955
903
  journal.record('space freed');
956
904
  queueReadiness();
957
- if (!closed && authorization !== undefined)
958
- void sync('unstreamed');
905
+ if (closed || authorization === undefined)
906
+ return;
907
+ if (options.adapter.roomStream === undefined)
908
+ void sync('unstreamed', null);
909
+ else
910
+ void resynchronize();
959
911
  }
960
912
  function seedRoomHeld(room, version) {
961
913
  return indexed(room) && reachedBy(room, version);
@@ -995,7 +947,24 @@ export function composeRuntime(options) {
995
947
  function sameCover(left, right) {
996
948
  return JSON.stringify(left) === JSON.stringify(right);
997
949
  }
950
+ async function unheldRows(local, plan, rows, rooms) {
951
+ if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
952
+ return [];
953
+ if (!('documents' in rows) || !textOnline())
954
+ return [];
955
+ const named = new Map();
956
+ for (const { room, path, textHeld } of rows.documents) {
957
+ if (textHeld === false)
958
+ named.set(room, [...(named.get(room) ?? []), path]);
959
+ }
960
+ const unheld = [];
961
+ for (const { locator, key } of rooms) {
962
+ unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
963
+ }
964
+ return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
965
+ }
998
966
  async function coveredRows(plan, cover) {
967
+ const asked = new Set();
999
968
  for (;;) {
1000
969
  const before = cover();
1001
970
  if ('pending' in before)
@@ -1006,8 +975,14 @@ export function composeRuntime(options) {
1006
975
  const current = () => local.changes(keys) === changes;
1007
976
  const rows = await windowRows(local, plan, before.rooms, current);
1008
977
  const after = cover();
1009
- if (rows !== null && sameCover(before, after))
978
+ if (rows === null || !sameCover(before, after))
979
+ continue;
980
+ const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
981
+ if (unheld.length === 0)
1010
982
  return { rows, cover: before, current };
983
+ for (const one of unheld)
984
+ asked.add(textKey(one));
985
+ await holdTexts(unheld);
1011
986
  }
1012
987
  }
1013
988
  async function windowRows(local, plan, rooms, unchanged) {
@@ -1103,6 +1078,8 @@ export function composeRuntime(options) {
1103
1078
  }
1104
1079
  async function refuse(reason, provider = null) {
1105
1080
  discoveryPaused = true;
1081
+ inFlight.abort();
1082
+ inFlight = new AbortController();
1106
1083
  try {
1107
1084
  if (reason === 'revoked-authorization') {
1108
1085
  await exclusive(async () => {
@@ -1187,10 +1164,10 @@ export function composeRuntime(options) {
1187
1164
  providerOwnedPaths: room.providerOwnedPaths,
1188
1165
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1189
1166
  synchronizationAvailable,
1190
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1167
+ refusal: scopedRefusals.get(room.locator) ?? null,
1191
1168
  });
1192
1169
  }
1193
- async function applyDiscovery(found, started, covered) {
1170
+ async function applyDiscovery(found, started, left) {
1194
1171
  const lockedBefore = lockedRooms();
1195
1172
  for (const room of found) {
1196
1173
  const fence = fences.get(room.locator);
@@ -1204,18 +1181,11 @@ export function composeRuntime(options) {
1204
1181
  const listedLocators = new Set(listed.map((room) => room.locator));
1205
1182
  for (const locator of listedLocators)
1206
1183
  scopedRefusals.delete(locator);
1207
- if (covered === undefined)
1184
+ if (left === undefined)
1208
1185
  discovered.clear();
1209
- else
1210
- for (const locator of covered)
1211
- discovered.delete(locator);
1212
1186
  for (const room of listed)
1213
1187
  discovered.set(room.locator, room);
1214
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1215
- if (options.adapter.exhaustiveDiscovery === true)
1216
- await loseRooms(unlisted);
1217
- else
1218
- unlistedKept(unlisted);
1188
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1219
1189
  for (const room of listed) {
1220
1190
  const held = replicas.get(room.locator)?.record;
1221
1191
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1246,15 +1216,6 @@ export function composeRuntime(options) {
1246
1216
  ]),
1247
1217
  ].filter((locator) => !listed.has(locator));
1248
1218
  }
1249
- function unlistedKept(unlisted) {
1250
- for (const locator of unlisted) {
1251
- const held = replicas.get(locator)?.record;
1252
- if (held !== undefined)
1253
- list(held, false);
1254
- else
1255
- rooms.delete(locator);
1256
- }
1257
- }
1258
1219
  function fence(locators) {
1259
1220
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1260
1221
  const removed = fresh.map((locator) => ({
@@ -1268,21 +1229,21 @@ export function composeRuntime(options) {
1268
1229
  for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1269
1230
  pendings.delete(pendingKey(locator, batch.id));
1270
1231
  replicas.delete(locator);
1232
+ highest.delete(locator);
1233
+ beyondHorizon.delete(locator);
1271
1234
  queuesWithoutReplica.delete(locator);
1272
1235
  rooms.delete(locator);
1273
1236
  roomDrafts.delete(locator);
1274
1237
  attachedFiles.delete(locator);
1275
1238
  untransferred(locator);
1239
+ startedOver.delete(locator);
1276
1240
  catching.delete(locator);
1277
1241
  discovered.delete(locator);
1278
- announced.delete(locator);
1279
1242
  unreadable.delete(locator);
1280
1243
  stale.delete(locator);
1281
1244
  repairable.delete(locator);
1282
1245
  unpreparable.delete(locator);
1283
1246
  scopedRefusals.delete(locator);
1284
- sourceRefusals.delete(locator);
1285
- declarationRefusals.delete(locator);
1286
1247
  models.delete(locator);
1287
1248
  liveModels.delete(locator);
1288
1249
  invalidModels.delete(locator);
@@ -1373,6 +1334,7 @@ export function composeRuntime(options) {
1373
1334
  transfers.clear();
1374
1335
  arrivals.clear();
1375
1336
  streaming.clear();
1337
+ startedOver.clear();
1376
1338
  limits = held.providerLimits;
1377
1339
  capabilities = held.capabilities;
1378
1340
  member = held.member;
@@ -1382,7 +1344,7 @@ export function composeRuntime(options) {
1382
1344
  presentations.clear();
1383
1345
  for (const record of held.presentation)
1384
1346
  presentations.set(record.app, record.value);
1385
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1347
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1386
1348
  pendings.clear();
1387
1349
  for (const queue of queues) {
1388
1350
  for (const { batch, state } of queue.pending) {
@@ -1534,6 +1496,16 @@ export function composeRuntime(options) {
1534
1496
  }
1535
1497
  const REWRITE_LINKS = ['field', 'body'];
1536
1498
  const CHECKED_LINKS = ['field', 'section'];
1499
+ const BODY_LINKS = ['body'];
1500
+ async function textsNeeded(local, record, named) {
1501
+ const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
1502
+ const unheld = await unheldIn(local, record.locator, record.key, paths);
1503
+ if (unheld.length === 0)
1504
+ return null;
1505
+ await holdTexts(unheld);
1506
+ const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
1507
+ return first?.path ?? null;
1508
+ }
1537
1509
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1538
1510
  const local = await openLocal();
1539
1511
  const needed = new Set([...touchedBy(queue), ...extra]);
@@ -1634,8 +1606,6 @@ export function composeRuntime(options) {
1634
1606
  function unpairedIn(locator, batch) {
1635
1607
  if (!queueIn(locator).some((held) => held.id === batch.id))
1636
1608
  return false;
1637
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1638
- return false;
1639
1609
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1640
1610
  }
1641
1611
  async function together(local, settlements) {
@@ -1804,28 +1774,34 @@ export function composeRuntime(options) {
1804
1774
  models.set(locator, state);
1805
1775
  return { collection: state };
1806
1776
  }
1807
- function publish(local, room, record, landing, live) {
1777
+ function publish(local, room, reached, landing, live) {
1808
1778
  const landed = exclusive(async () => {
1809
1779
  if (!live()) {
1810
1780
  caughtUpTo(room.locator);
1811
1781
  return true;
1812
1782
  }
1813
1783
  const prior = replicas.get(room.locator);
1814
- if (landing.kind === 'refreshed' &&
1815
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1816
- await local.unrefresh(record.key);
1784
+ if (!stands(room.locator, landing.from)) {
1785
+ if (landing.kind === 'refreshed') {
1786
+ await local.unrefresh(reached.key);
1787
+ return false;
1788
+ }
1789
+ caughtUpTo(room.locator);
1790
+ await abandoned(local, room.locator);
1817
1791
  return false;
1818
1792
  }
1793
+ const record = landing.kind === 'refreshed' && prior !== undefined
1794
+ ? {
1795
+ ...prior.record,
1796
+ roomVersion: reached.roomVersion,
1797
+ ...(landing.remodelled ? { model: reached.model } : {}),
1798
+ }
1799
+ : reached;
1819
1800
  adoptModels(room.locator, record);
1820
1801
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1821
- const roomVersion = record.roomVersion;
1822
- const accepted = queue.filter((batch) => {
1823
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1824
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1825
- });
1826
1802
  let set;
1827
1803
  try {
1828
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1804
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1829
1805
  ? {
1830
1806
  room: true,
1831
1807
  view: 'refreshed',
@@ -1847,24 +1823,26 @@ export function composeRuntime(options) {
1847
1823
  return true;
1848
1824
  }
1849
1825
  if (landing.kind === 'bootstrapped') {
1826
+ highest.delete(room.locator);
1827
+ beyondHorizon.delete(room.locator);
1850
1828
  untransferred(room.locator);
1829
+ startedOver.delete(room.locator);
1851
1830
  catching.delete(room.locator);
1852
- sweep();
1831
+ if (prior !== undefined && prior.record.key !== record.key)
1832
+ sweep();
1833
+ name(room.locator);
1853
1834
  }
1854
- for (const batch of accepted)
1855
- pendings.delete(pendingKey(room.locator, batch.id));
1856
1835
  stale.delete(room.locator);
1857
1836
  unpreparable.delete(room.locator);
1858
1837
  if (unreadable.delete(room.locator))
1859
1838
  scopedRefusals.delete(room.locator);
1860
- sourceRefusals.delete(room.locator);
1861
- declarationRefusals.delete(room.locator);
1862
1839
  list(room, true);
1863
1840
  publishChangeSet(set);
1841
+ if (landing.kind === 'bootstrapped')
1842
+ void heldAgain(room.locator);
1864
1843
  await unpair(local);
1865
1844
  moveTo({ ...status, ...attention() });
1866
- options.adapter.roomStream?.published(room.locator, roomVersion);
1867
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1845
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1868
1846
  submitLater(room.locator);
1869
1847
  }
1870
1848
  return true;
@@ -1876,7 +1854,7 @@ export function composeRuntime(options) {
1876
1854
  const replica = replicas.get(locator);
1877
1855
  const room = rooms.get(locator);
1878
1856
  if (replica === undefined || room === undefined) {
1879
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1857
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1880
1858
  }
1881
1859
  return { local, replica, room };
1882
1860
  }
@@ -1938,6 +1916,19 @@ export function composeRuntime(options) {
1938
1916
  const referring = removed.length === 0
1939
1917
  ? []
1940
1918
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1919
+ const unheld = await textsNeeded(local, record, [
1920
+ ...paths,
1921
+ ...referring,
1922
+ ...(moved.length === 0
1923
+ ? []
1924
+ : await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
1925
+ ...(removed.length === 0
1926
+ ? []
1927
+ : await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
1928
+ ]);
1929
+ if (unheld !== null) {
1930
+ return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
1931
+ }
1941
1932
  const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1942
1933
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1943
1934
  const composition = composed(record);
@@ -1968,18 +1959,6 @@ export function composeRuntime(options) {
1968
1959
  return undefined;
1969
1960
  return entry.binary !== null ? null : entry.content;
1970
1961
  }
1971
- function walkOf(local, key, view) {
1972
- return async (after) => {
1973
- const page = await local.walk(key, view, after, WALK_BYTES);
1974
- return {
1975
- entries: page.entries.map(({ path, content, binary }) => ({
1976
- path,
1977
- content: binary !== null ? null : content,
1978
- })),
1979
- next: page.next,
1980
- };
1981
- };
1982
- }
1983
1962
  function lookupOf(local, key, view) {
1984
1963
  return async (paths) => {
1985
1964
  if (paths.length === 0)
@@ -1996,37 +1975,6 @@ export function composeRuntime(options) {
1996
1975
  const read = parseFrontmatter(entry.content ?? '');
1997
1976
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1998
1977
  }
1999
- function overriddenWalk(walk, overrides) {
2000
- const sorted = [...overrides.keys()].sort();
2001
- const beyond = (path) => {
2002
- if (path === null)
2003
- return 0;
2004
- let low = 0;
2005
- let high = sorted.length;
2006
- while (low < high) {
2007
- const middle = (low + high) >>> 1;
2008
- if (sorted[middle] <= path)
2009
- low = middle + 1;
2010
- else
2011
- high = middle;
2012
- }
2013
- return low;
2014
- };
2015
- return async (after) => {
2016
- const page = await walk(after);
2017
- const end = page.next;
2018
- const added = sorted
2019
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
2020
- .flatMap((path) => {
2021
- const entry = overrides.get(path);
2022
- return entry === null || entry === undefined
2023
- ? []
2024
- : [{ path, content: entry.binary !== null ? null : entry.content }];
2025
- });
2026
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
2027
- return { entries, next: end };
2028
- };
2029
- }
2030
1978
  function overriddenLookup(lookup, overrides) {
2031
1979
  return async (paths) => {
2032
1980
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2044,9 +1992,8 @@ export function composeRuntime(options) {
2044
1992
  }
2045
1993
  function validatorOf(record, confirmedLayer) {
2046
1994
  return async (resulting, touched) => {
2047
- const composition = compositionFor(record);
2048
1995
  const local = await openLocal();
2049
- const changed = [];
1996
+ const changes = [];
2050
1997
  const overrides = new Map();
2051
1998
  const pace = paced();
2052
1999
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2057,19 +2004,13 @@ export function composeRuntime(options) {
2057
2004
  continue;
2058
2005
  overrides.set(path, now);
2059
2006
  if (textOf(was) !== textOf(now))
2060
- changed.push({ path, was: textOf(was), now: textOf(now) });
2007
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2061
2008
  }
2062
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2063
- const clean = available(composition) &&
2064
- changed.length <= CLEAN_CHANGES &&
2065
- (await changedCleanly(changed, composed(record), composition, {
2066
- lookup,
2067
- texts: textsOf(local, record.key, 'confirmed'),
2068
- referrers: referrersOf(local, record.key, 'confirmed'),
2069
- }));
2070
- if (clean)
2071
- return { verdict: 'accept' };
2072
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
2009
+ return validatedNeighbourhood(changes, compositionFor(record), {
2010
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
2011
+ texts: textsOf(local, record.key, 'confirmed'),
2012
+ referrers: referrersOf(local, record.key, 'confirmed'),
2013
+ });
2073
2014
  };
2074
2015
  }
2075
2016
  function textsOf(local, key, view) {
@@ -2146,15 +2087,19 @@ export function composeRuntime(options) {
2146
2087
  }
2147
2088
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2148
2089
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2090
+ if ('refusal' in sourceSaving)
2091
+ return { outcome: 'refused', ...sourceSaving.refusal };
2149
2092
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2150
2093
  if (changed.length > 0)
2151
2094
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2152
2095
  const target = replicas.get(destination);
2153
2096
  const listedRoom = rooms.get(destination);
2154
2097
  if (target === undefined || listedRoom === undefined) {
2155
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2098
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2156
2099
  }
2157
2100
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2101
+ if ('refusal' in arriving)
2102
+ return { outcome: 'refused', ...arriving.refusal };
2158
2103
  const built = await buildMove({
2159
2104
  source: await sideOf(locator, replica, room, sourceSaving),
2160
2105
  destination: await sideOf(destination, target, listedRoom, arriving),
@@ -2215,10 +2160,10 @@ export function composeRuntime(options) {
2215
2160
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2216
2161
  const { answer, touched } = committedBoth;
2217
2162
  if (answer.fenced !== null) {
2218
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2163
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2219
2164
  }
2220
2165
  if (fences.has(locator) || fences.has(destination)) {
2221
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2166
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2222
2167
  }
2223
2168
  pendings.set(pendingKey(locator, id), {
2224
2169
  drafts: saved,
@@ -2271,6 +2216,8 @@ export function composeRuntime(options) {
2271
2216
  ...saved.map(({ path }) => path),
2272
2217
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2273
2218
  ]);
2219
+ if ('refusal' in saving)
2220
+ return { outcome: 'refused', ...saving.refusal };
2274
2221
  const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2275
2222
  if (changed.length > 0)
2276
2223
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -2389,10 +2336,10 @@ export function composeRuntime(options) {
2389
2336
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2390
2337
  const { answer, touched } = done;
2391
2338
  if (answer.fenced !== null) {
2392
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2339
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2393
2340
  }
2394
2341
  if (fences.has(locator))
2395
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2342
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2396
2343
  for (const origin of withdrawn.keys())
2397
2344
  pendings.delete(pendingKey(locator, origin));
2398
2345
  for (const [id, drafts] of drafted) {
@@ -2441,7 +2388,7 @@ export function composeRuntime(options) {
2441
2388
  throw new Error('connection refused');
2442
2389
  await local.keepUpload(locator, identifier, upload);
2443
2390
  if (fences.has(locator))
2444
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2391
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2445
2392
  const key = pendingKey(locator, identifier);
2446
2393
  pendings.set(key, { ...pendings.get(key), upload });
2447
2394
  if (!continuing())
@@ -2578,6 +2525,83 @@ export function composeRuntime(options) {
2578
2525
  return UNCERTAIN;
2579
2526
  }
2580
2527
  }
2528
+ async function heldAgain(locator) {
2529
+ const replica = replicas.get(locator);
2530
+ if (replica === undefined || !textOnline())
2531
+ return;
2532
+ try {
2533
+ const wanted = await (await openLocal()).wanted(locator, replica.record.key);
2534
+ await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
2535
+ }
2536
+ catch {
2537
+ journal.record(`hold text ${locator} unwritten`);
2538
+ }
2539
+ }
2540
+ function textOnline() {
2541
+ return !closed && refusal === null && connectionOf() === 'connected';
2542
+ }
2543
+ function textKey({ locator, path }) {
2544
+ return documentKey(locator, path);
2545
+ }
2546
+ async function readText({ locator, path, revision }) {
2547
+ const replica = replicas.get(locator);
2548
+ if (replica === undefined || !textOnline())
2549
+ return;
2550
+ try {
2551
+ await ensureAuthorization();
2552
+ }
2553
+ catch {
2554
+ return;
2555
+ }
2556
+ if (closed || authorization === undefined || refusal !== null)
2557
+ return;
2558
+ const since = fencings;
2559
+ journal.record(`hold text ${locator} ${path}`);
2560
+ const read = await reads
2561
+ .bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
2562
+ .catch(() => null);
2563
+ journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
2564
+ if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
2565
+ return;
2566
+ const content = read.documents[path];
2567
+ const now = replicas.get(locator);
2568
+ if (content === undefined || read.revisions[path] !== revision || now === undefined) {
2569
+ unholdable.set(textKey({ locator, path, revision }), revision);
2570
+ return;
2571
+ }
2572
+ const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
2573
+ if (held.length === 0)
2574
+ unholdable.set(textKey({ locator, path, revision }), revision);
2575
+ else
2576
+ publishChangeSet(changeSet(locator, { documents: held }));
2577
+ }
2578
+ function holdText(unheld) {
2579
+ const key = textKey(unheld);
2580
+ const running = holding.get(key);
2581
+ if (running !== undefined)
2582
+ return running;
2583
+ const started = readText(unheld).finally(() => holding.delete(key));
2584
+ holding.set(key, started);
2585
+ return started;
2586
+ }
2587
+ async function holdTexts(wanted) {
2588
+ const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
2589
+ await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
2590
+ for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
2591
+ await holdText(next);
2592
+ }
2593
+ }));
2594
+ }
2595
+ async function unheldIn(local, locator, key, paths) {
2596
+ if (paths.length === 0)
2597
+ return [];
2598
+ return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
2599
+ entry.binary === null &&
2600
+ entry.outline !== null &&
2601
+ entry.revision !== null
2602
+ ? [{ locator, path: entry.path, revision: entry.revision }]
2603
+ : []);
2604
+ }
2581
2605
  function fetchBinary(request, name) {
2582
2606
  return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2583
2607
  }
@@ -2713,14 +2737,16 @@ export function composeRuntime(options) {
2713
2737
  }
2714
2738
  async function cleanUp() {
2715
2739
  const freed = await database.ask({ kind: 'opened.clean' });
2716
- return freed + ((await options.shell.cleanUp?.()) ?? 0);
2717
- }
2718
- function declaredAgents() {
2719
- for (const capability of capabilities) {
2720
- if (capability.conversationAgents !== undefined)
2721
- return capability.conversationAgents;
2740
+ const local = await openLocal();
2741
+ let texts = 0;
2742
+ for (const [locator, replica] of replicas) {
2743
+ const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
2744
+ texts += released.bytes;
2745
+ if (released.paths.length > 0) {
2746
+ publishChangeSet(changeSet(locator, { documents: released.paths }));
2747
+ }
2722
2748
  }
2723
- return undefined;
2749
+ return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
2724
2750
  }
2725
2751
  function declaredAction(name) {
2726
2752
  for (const capability of capabilities) {
@@ -2773,6 +2799,24 @@ export function composeRuntime(options) {
2773
2799
  return UNCERTAIN;
2774
2800
  }
2775
2801
  }
2802
+ async function readConversationHistory(request) {
2803
+ await ready;
2804
+ const stream = options.adapter.conversations;
2805
+ if (closed || refusal !== null || stream?.history === undefined)
2806
+ return UNCERTAIN;
2807
+ journal.record('transfer conversation history');
2808
+ try {
2809
+ const result = await stream.history(request);
2810
+ if (result.kind === 'revoked-authorization')
2811
+ await refuse('revoked-authorization');
2812
+ journal.record(`outcome conversation history ${result.kind}`);
2813
+ return calledResult(result);
2814
+ }
2815
+ catch {
2816
+ journal.record('outcome conversation history transport-uncertainty');
2817
+ return UNCERTAIN;
2818
+ }
2819
+ }
2776
2820
  function unsettled(result) {
2777
2821
  return result.kind !== 'action' && result.retryable;
2778
2822
  }
@@ -2834,6 +2878,33 @@ export function composeRuntime(options) {
2834
2878
  const held = replicas.get(locator);
2835
2879
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2836
2880
  }
2881
+ function shownAt(locator) {
2882
+ const held = replicas.get(locator)?.record.roomVersion;
2883
+ const raised = highest.get(locator);
2884
+ if (held === undefined || raised === undefined)
2885
+ return held ?? raised;
2886
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2887
+ }
2888
+ function showing(locator, roomVersion) {
2889
+ const at = shownAt(locator);
2890
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2891
+ highest.set(locator, roomVersion);
2892
+ }
2893
+ }
2894
+ function older(locator, roomVersion) {
2895
+ if (beyondHorizon.has(locator))
2896
+ return false;
2897
+ const at = shownAt(locator);
2898
+ return (at !== undefined &&
2899
+ options.adapter.reached(locator, at, roomVersion) &&
2900
+ !options.adapter.reached(locator, roomVersion, at));
2901
+ }
2902
+ function replaced(locators) {
2903
+ for (const locator of locators) {
2904
+ highest.delete(locator);
2905
+ beyondHorizon.add(locator);
2906
+ }
2907
+ }
2837
2908
  async function writtenAfter(locator, roomVersion, path) {
2838
2909
  const held = replicas.get(locator);
2839
2910
  if (held === undefined)
@@ -2844,122 +2915,27 @@ export function composeRuntime(options) {
2844
2915
  options.adapter.reached(locator, written, roomVersion) &&
2845
2916
  !options.adapter.reached(locator, roomVersion, written));
2846
2917
  }
2847
- function overriddenTexts(texts, overrides) {
2848
- return async (paths) => {
2849
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2850
- for (const path of paths) {
2851
- const entry = overrides.get(path);
2852
- if (entry === undefined)
2853
- continue;
2854
- if (entry === null)
2855
- known.delete(path);
2856
- else
2857
- known.set(path, entry.binary !== null ? null : entry.content);
2858
- }
2859
- return known;
2860
- };
2861
- }
2862
- async function unreadableRoom(texts, walk, paths) {
2863
- if (paths !== null) {
2864
- const text = {};
2865
- for (const [path, content] of await texts(paths))
2866
- if (content !== null)
2867
- text[path] = content;
2868
- return stepped(unreadableIn(text, Object.keys(text)));
2869
- }
2870
- for (let after = null;;) {
2871
- const page = await walk(after);
2872
- const text = {};
2873
- for (const { path, content } of page.entries)
2874
- if (content !== null)
2875
- text[path] = content;
2876
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2877
- if (refused !== null)
2878
- return refused;
2879
- if (page.next === null)
2880
- return null;
2881
- after = page.next;
2882
- }
2883
- }
2884
- async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
2885
- const composition = composed(record);
2886
- const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2887
- const lookup = overrides === null
2888
- ? lookupOf(local, record.key, view)
2889
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2890
- const texts = overrides === null
2891
- ? textsOf(local, record.key, view)
2892
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2893
- const walking = overrides === null
2894
- ? walkOf(local, record.key, view)
2895
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2896
- const walk = checked === null
2897
- ? walking
2898
- : async (after) => {
2899
- const page = await walking(after);
2900
- checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
2901
- return page;
2902
- };
2903
- let changes = null;
2904
- let reshaped = false;
2905
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2906
- const [was, now] = await Promise.all([
2907
- local.entries(record.key, 'confirmed', previous.changed),
2908
- overrides === null
2909
- ? local.entries(record.key, view, previous.changed)
2910
- : Promise.resolve([]),
2911
- ]);
2912
- const before = new Map(was.map((entry) => [entry.path, entry]));
2913
- const after = overrides === null
2914
- ? new Map(now.map((entry) => [entry.path, entry]))
2915
- : overrides;
2916
- changes = previous.changed.flatMap((path) => {
2917
- const one = before.get(path) ?? null;
2918
- const other = after.get(path) ?? null;
2919
- reshaped ||= textDiffers(one, other);
2920
- const held = textOf(one);
2921
- const next = textOf(other);
2922
- return held === next ? [] : [{ path, was: held, now: next }];
2923
- });
2924
- }
2925
- const named = changes === null ? null : changes.map(({ path }) => path);
2926
- const textChanged = changes === null || changes.length > 0 || reshaped;
2927
- const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2928
- if (modeled && (textChanged || modelsChanged)) {
2929
- const clean = previous !== null &&
2930
- changes !== null &&
2931
- (await changedCleanly(changes, composed(previous.record), composition, {
2932
- lookup,
2933
- texts,
2934
- referrers: referrersOf(local, record.key, 'confirmed'),
2935
- }));
2936
- if (clean)
2937
- return null;
2938
- const verdict = await validatedCollection(walk, lookup, composition);
2939
- if (verdict.verdict === 'accept')
2940
- return null;
2941
- if (verdict.rule !== 'model-unavailable')
2942
- return { kind: 'refused', refusal: verdict };
2943
- return unreadableRoom(texts, walk, named);
2944
- }
2945
- if (textChanged)
2946
- return unreadableRoom(texts, walk, named);
2947
- return null;
2918
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2919
+ if (paths.length === 0 || !older(locator, roomVersion))
2920
+ return new Set();
2921
+ const entries = await local.entries(key, 'confirmed', paths);
2922
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2923
+ options.adapter.reached(locator, written, roomVersion) &&
2924
+ !options.adapter.reached(locator, roomVersion, written)
2925
+ ? [path]
2926
+ : []));
2948
2927
  }
2949
- function refreshingOf(local, key, composition, liveness, replaying = null) {
2950
- const write = replaying === null ? exclusive : installWrite;
2928
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2929
+ const write = replaying ? finishingWrite : exclusive;
2951
2930
  return {
2952
- async turn() {
2953
- if (replaying === null)
2954
- return;
2955
- installing(replaying);
2956
- await screened();
2931
+ turn() {
2932
+ if (replaying)
2933
+ installing(locator);
2957
2934
  },
2958
2935
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2936
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2959
2937
  async stage(page) {
2960
2938
  for (let written = 0;;) {
2961
- if (replaying !== null)
2962
- await screened();
2963
2939
  const end = batchEnd(page.entries, written);
2964
2940
  const last = end === page.entries.length;
2965
2941
  const entries = page.entries.slice(written, end);
@@ -3004,32 +2980,71 @@ export function composeRuntime(options) {
3004
2980
  return done;
3005
2981
  }
3006
2982
  function installWrite(work) {
3007
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
3008
- installWrites = next.then(() => undefined, () => undefined);
3009
- return next;
2983
+ return installTurn(installTurns.starting, work);
2984
+ }
2985
+ function finishingWrite(work) {
2986
+ return installTurn(installTurns.finishing, work);
2987
+ }
2988
+ function installTurn(queue, work) {
2989
+ return new Promise((resolve, reject) => {
2990
+ queue.push(() => void exclusive(work)
2991
+ .then(resolve, reject)
2992
+ .finally(() => {
2993
+ installTurns.busy = false;
2994
+ nextInstallTurn();
2995
+ }));
2996
+ nextInstallTurn();
2997
+ });
3010
2998
  }
3011
- function screened() {
3012
- if (onScreen || closed)
3013
- return Promise.resolve();
3014
- queueReadiness();
3015
- return new Promise((resolve) => shownWaiters.push(resolve));
2999
+ function nextInstallTurn() {
3000
+ if (installTurns.busy || installTurns.ahead > 0)
3001
+ return;
3002
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3003
+ if (turn === undefined)
3004
+ return;
3005
+ installTurns.busy = true;
3006
+ turn();
3007
+ }
3008
+ async function ahead(work) {
3009
+ let taken = false;
3010
+ const hold = {
3011
+ take() {
3012
+ if (taken)
3013
+ return;
3014
+ taken = true;
3015
+ installTurns.ahead += 1;
3016
+ },
3017
+ release() {
3018
+ if (!taken)
3019
+ return;
3020
+ taken = false;
3021
+ installTurns.ahead -= 1;
3022
+ resumeInstall();
3023
+ },
3024
+ };
3025
+ hold.take();
3026
+ try {
3027
+ return await work(hold);
3028
+ }
3029
+ finally {
3030
+ hold.release();
3031
+ }
3032
+ }
3033
+ function resumeInstall() {
3034
+ if (installTurns.ahead > 0)
3035
+ return;
3036
+ void yielded().then(nextInstallTurn);
3016
3037
  }
3017
3038
  async function heldStream() {
3018
- for (;;) {
3019
- if (streams.generation !== generation)
3020
- streams = { generation, free: STREAMS_HELD };
3021
- const held = streams;
3022
- if (held.free > 0) {
3023
- held.free -= 1;
3024
- return once(() => {
3025
- held.free += 1;
3026
- });
3027
- }
3039
+ while (streamsFree === 0)
3028
3040
  await new Promise((admit) => installWaiters.push(admit));
3029
- }
3041
+ streamsFree -= 1;
3042
+ return once(() => {
3043
+ streamsFree += 1;
3044
+ });
3030
3045
  }
3031
3046
  async function unqueued() {
3032
- while (queuedBytes >= QUEUED_BYTES) {
3047
+ while (queuedText >= QUEUED_TEXT) {
3033
3048
  await new Promise((admit) => installWaiters.push(admit));
3034
3049
  }
3035
3050
  }
@@ -3054,14 +3069,19 @@ export function composeRuntime(options) {
3054
3069
  streaming.delete(locator);
3055
3070
  underived.delete(locator);
3056
3071
  unindexed.delete(locator);
3057
- unchecked.delete(locator);
3072
+ }
3073
+ function setBack(locator) {
3074
+ const now = bytesOf(locator, true);
3075
+ const earlier = startedOver.get(locator);
3076
+ startedOver.set(locator, {
3077
+ done: Math.max(now.done, earlier?.done ?? 0),
3078
+ total: Math.max(now.total, earlier?.total ?? 0),
3079
+ });
3058
3080
  }
3059
3081
  function owedIndexes(locator, bytes, text) {
3060
3082
  if (bytes <= 0 || text <= 0)
3061
3083
  return;
3062
- unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
3063
- const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
3064
- unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
3084
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
3065
3085
  }
3066
3086
  function built(locator, { done, text }) {
3067
3087
  const owed = unindexed.get(locator) ?? [];
@@ -3087,41 +3107,34 @@ export function composeRuntime(options) {
3087
3107
  if (installs.delete(locator))
3088
3108
  queueReadiness();
3089
3109
  }
3090
- function screen(value) {
3091
- if (value === onScreen)
3092
- return;
3093
- onScreen = value;
3094
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3095
- queueReadiness();
3096
- if (!value)
3097
- return;
3098
- for (const resolve of shownWaiters.splice(0))
3099
- resolve();
3100
- }
3101
- async function pullRefresh(authorized, held, liveness) {
3110
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3102
3111
  if (!liveness())
3103
3112
  return unconfirmed;
3104
- const turn = generation;
3105
3113
  const { record } = held;
3106
- const base = held.confirmed;
3114
+ const from = standingOf(record.locator);
3107
3115
  const local = await openLocal();
3108
3116
  try {
3109
3117
  await exclusive(() => local.unrefresh(record.key));
3110
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
3118
+ const result = await advancement(async (authorization, request) => {
3119
+ hold.release();
3120
+ try {
3121
+ return await refresh(authorization, request);
3122
+ }
3123
+ finally {
3124
+ hold.take();
3125
+ }
3126
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3111
3127
  if (result.kind !== 'advanced') {
3112
3128
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
3113
3129
  return result;
3114
3130
  }
3115
- const refused = await validatedRoom(local, result.record, 'refreshed', {
3116
- record,
3117
- changed: [...result.changed],
3118
- });
3119
- if (refused !== null) {
3120
- journal.record(`transfer refresh ${record.locator} refused`);
3121
- return refused;
3122
- }
3123
3131
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3124
- return { kind: 'advanced', record: result.record, base };
3132
+ return {
3133
+ kind: 'advanced',
3134
+ record: result.record,
3135
+ from,
3136
+ remodelled: result.record.model !== record.model,
3137
+ };
3125
3138
  }
3126
3139
  catch (error) {
3127
3140
  if (refusedForSpace(error))
@@ -3129,9 +3142,6 @@ export function composeRuntime(options) {
3129
3142
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
3130
3143
  throw error;
3131
3144
  }
3132
- finally {
3133
- reads.finished(record.locator, turn);
3134
- }
3135
3145
  }
3136
3146
  const heldContents = new Map();
3137
3147
  function heldContentOf(local, key) {
@@ -3149,14 +3159,20 @@ export function composeRuntime(options) {
3149
3159
  heldContents.set(key, made);
3150
3160
  return made;
3151
3161
  }
3162
+ async function abandoned(local, locator) {
3163
+ await local.abandon(locator);
3164
+ setBack(locator);
3165
+ untransferred(locator);
3166
+ liveQueries.recover();
3167
+ sweep();
3168
+ }
3152
3169
  function staging(local, liveness) {
3153
3170
  const derived = async (locator, key, composition) => {
3154
3171
  underived.delete(locator);
3155
3172
  for (;;) {
3156
- await screened();
3157
3173
  if (!liveness())
3158
3174
  return false;
3159
- const done = await installWrite(async () => liveness()
3175
+ const done = await finishingWrite(async () => liveness()
3160
3176
  ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3161
3177
  : null);
3162
3178
  if (done === null)
@@ -3170,19 +3186,13 @@ export function composeRuntime(options) {
3170
3186
  held: (locator) => transfers.get(locator) ?? null,
3171
3187
  async turn(locator, step) {
3172
3188
  installing(locator);
3173
- for (;;) {
3174
- await screened();
3175
- const release = step === 'downloading' ? await heldStream() : () => { };
3176
- if (onScreen || closed)
3177
- return release;
3178
- release();
3179
- }
3189
+ return step === 'downloading' ? heldStream() : () => { };
3180
3190
  },
3181
3191
  unqueued,
3182
- queued(bytes) {
3183
- queuedBytes += bytes;
3192
+ queued(text) {
3193
+ queuedText += text;
3184
3194
  return once(() => {
3185
- queuedBytes -= bytes;
3195
+ queuedText -= text;
3186
3196
  });
3187
3197
  },
3188
3198
  arriving(locator) {
@@ -3191,7 +3201,7 @@ export function composeRuntime(options) {
3191
3201
  at(bytes) {
3192
3202
  if (ended)
3193
3203
  return;
3194
- arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
3204
+ arrivals.set(locator, bytes);
3195
3205
  streaming.add(locator);
3196
3206
  const now = performance.now();
3197
3207
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
@@ -3199,14 +3209,6 @@ export function composeRuntime(options) {
3199
3209
  arrivalReported = now;
3200
3210
  queueReadiness();
3201
3211
  },
3202
- part() {
3203
- const arrived = arrivals.get(locator);
3204
- arrivals.set(locator, {
3205
- parts: (arrived?.parts ?? 0) + 1,
3206
- bytes: arrived?.bytes ?? 0,
3207
- });
3208
- queueReadiness();
3209
- },
3210
3212
  end() {
3211
3213
  if (ended)
3212
3214
  return;
@@ -3216,14 +3218,10 @@ export function composeRuntime(options) {
3216
3218
  },
3217
3219
  };
3218
3220
  },
3219
- announced(locator) {
3220
- const later = announced.get(locator);
3221
- return later === undefined ? [] : [later];
3222
- },
3221
+ streamed: options.adapter.roomStream !== undefined,
3223
3222
  async stage(record, page, composition) {
3224
3223
  const { locator } = record;
3225
3224
  installing(locator);
3226
- await screened();
3227
3225
  if (!liveness())
3228
3226
  return null;
3229
3227
  const key = await installWrite(async () => {
@@ -3252,19 +3250,16 @@ export function composeRuntime(options) {
3252
3250
  derive: derived,
3253
3251
  async refreshing(locator, key, composition) {
3254
3252
  await exclusive(() => local.unrefresh(key));
3255
- return refreshingOf(local, key, composition, liveness, locator);
3253
+ return refreshingOf(local, locator, key, composition, liveness, true);
3256
3254
  },
3257
3255
  abandon: (locator) => exclusive(async () => {
3258
- if (!liveness())
3259
- return;
3260
- await local.abandon(locator);
3261
- untransferred(locator);
3262
- liveQueries.recover();
3263
- sweep();
3256
+ if (liveness())
3257
+ await abandoned(local, locator);
3264
3258
  }),
3265
3259
  setAside: async (locator) => {
3266
3260
  if (!liveness())
3267
3261
  return;
3262
+ setBack(locator);
3268
3263
  untransferred(locator);
3269
3264
  liveQueries.recover();
3270
3265
  },
@@ -3273,19 +3268,6 @@ export function composeRuntime(options) {
3273
3268
  sweep();
3274
3269
  }),
3275
3270
  content: (key) => heldContentOf(local, key),
3276
- async validate(record, view) {
3277
- const { locator } = record;
3278
- const refused = await validatedRoom(local, record, view, null, null, (text) => {
3279
- const check = unchecked.get(locator);
3280
- if (check === undefined)
3281
- return;
3282
- unchecked.set(locator, { ...check, done: check.done + text });
3283
- queueReadiness();
3284
- });
3285
- unchecked.delete(locator);
3286
- queueReadiness();
3287
- return refused;
3288
- },
3289
3271
  };
3290
3272
  }
3291
3273
  function reusable(room, local) {
@@ -3297,7 +3279,6 @@ export function composeRuntime(options) {
3297
3279
  async function pullBootstrap(authorized, room, local, liveness) {
3298
3280
  if (!liveness())
3299
3281
  return unconfirmed;
3300
- const turn = generation;
3301
3282
  if (replicas.has(room.locator)) {
3302
3283
  catching.add(room.locator);
3303
3284
  liveQueries.recover();
@@ -3306,6 +3287,13 @@ export function composeRuntime(options) {
3306
3287
  const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3307
3288
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3308
3289
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3290
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3291
+ caughtUpTo(room.locator);
3292
+ installed(room.locator);
3293
+ await abandoned(local, room.locator);
3294
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3295
+ return { kind: 'transport-uncertainty' };
3296
+ }
3309
3297
  if (result.kind !== 'staged') {
3310
3298
  caughtUpTo(room.locator);
3311
3299
  installed(room.locator);
@@ -3320,9 +3308,6 @@ export function composeRuntime(options) {
3320
3308
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3321
3309
  throw error;
3322
3310
  }
3323
- finally {
3324
- reads.finished(room.locator, turn);
3325
- }
3326
3311
  }
3327
3312
  function caughtUpTo(locator) {
3328
3313
  if (catching.delete(locator))
@@ -3330,72 +3315,83 @@ export function composeRuntime(options) {
3330
3315
  }
3331
3316
  function behind(room) {
3332
3317
  const held = replicas.get(room.locator)?.record.roomVersion;
3333
- return (held !== undefined &&
3334
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3318
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3335
3319
  }
3336
- async function advance(room, local, authorized, live) {
3337
- const carried = reads.carried(room.locator);
3338
- const withheld = await advanceOnce(room, local, authorized, live);
3339
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3340
- return withheld;
3341
- }
3342
- return advanceOnce(room, local, authorized, live);
3320
+ function standingOf(locator) {
3321
+ const held = replicas.get(locator);
3322
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3323
+ }
3324
+ function stands(locator, from) {
3325
+ const now = standingOf(locator);
3326
+ return now.key === from.key && now.confirmed === from.confirmed;
3343
3327
  }
3344
- async function advanceOnce(room, local, authorized, running) {
3328
+ async function advance(room, local, authorized, running) {
3345
3329
  const live = () => running() && !full;
3346
3330
  if (!live())
3347
3331
  return unconfirmed;
3348
3332
  const held = replicas.get(room.locator);
3349
- if (held !== undefined) {
3350
- const refreshed = await pullRefresh(authorized, held, live);
3351
- if (!live())
3352
- return unconfirmed;
3353
- if (refreshed.kind === 'still-current') {
3354
- stale.delete(room.locator);
3355
- if (sourceRefusals.has(room.locator))
3356
- return null;
3357
- if (unreadable.delete(room.locator)) {
3358
- scopedRefusals.delete(room.locator);
3359
- list(room, true);
3360
- emitRooms();
3361
- }
3362
- return null;
3363
- }
3364
- if (refreshed.kind === 'advanced') {
3365
- const landing = { kind: 'refreshed', base: refreshed.base };
3366
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3367
- const now = replicas.get(room.locator)?.record.roomVersion;
3368
- const reached = now !== undefined &&
3369
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3370
- return reached ? null : advance(room, local, authorized, live);
3371
- }
3372
- await retainModel(local, room, modelOf(room.locator));
3373
- return null;
3374
- }
3375
- if (refreshed.kind !== 'beyond-horizon')
3333
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3334
+ stale.delete(room.locator);
3335
+ name(room.locator);
3336
+ return null;
3337
+ }
3338
+ const refresh = reads.refresh;
3339
+ if (held !== undefined && refresh !== undefined) {
3340
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3341
+ if (refreshed === 'behind')
3342
+ return advance(room, local, authorized, live);
3343
+ if (refreshed !== 'beyond-horizon')
3376
3344
  return refreshed;
3377
- if (opened !== null) {
3378
- owed.add(room.locator);
3379
- liveQueries.recover();
3380
- return unconfirmed;
3381
- }
3382
- if (reloadsDue.get(room.locator) === generation)
3345
+ if (reloadsDue.has(room.locator))
3383
3346
  return unconfirmed;
3347
+ replaced([room.locator]);
3384
3348
  }
3349
+ const from = standingOf(room.locator);
3385
3350
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3386
- if (bootstrapped.kind === 'staged') {
3387
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3351
+ if (bootstrapped.kind !== 'staged')
3352
+ return live() ? bootstrapped : unconfirmed;
3353
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3354
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3388
3355
  return live() ? null : unconfirmed;
3389
3356
  }
3390
- return live() ? bootstrapped : unconfirmed;
3357
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3358
+ ? null
3359
+ : advance(room, local, authorized, live);
3360
+ }
3361
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3362
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3363
+ if (!live())
3364
+ return unconfirmed;
3365
+ if (refreshed.kind === 'still-current') {
3366
+ stale.delete(room.locator);
3367
+ if (unreadable.delete(room.locator)) {
3368
+ scopedRefusals.delete(room.locator);
3369
+ list(room, true);
3370
+ emitRooms();
3371
+ }
3372
+ return null;
3373
+ }
3374
+ if (refreshed.kind === 'advanced') {
3375
+ const landing = {
3376
+ kind: 'refreshed',
3377
+ from: refreshed.from,
3378
+ remodelled: refreshed.remodelled,
3379
+ };
3380
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3381
+ const now = replicas.get(room.locator)?.record.roomVersion;
3382
+ const reached = now !== undefined &&
3383
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3384
+ return reached ? null : 'behind';
3385
+ }
3386
+ await retainModel(local, room, modelOf(room.locator));
3387
+ return null;
3388
+ }
3389
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3391
3390
  }
3392
3391
  async function withhold(withheld, room, local, progress) {
3393
- if (withheld.kind === 'unconfirmed')
3392
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3394
3393
  return;
3395
- if (withheld.kind === 'refused')
3396
- stale.delete(room.locator);
3397
- else
3398
- stale.add(room.locator);
3394
+ stale.add(room.locator);
3399
3395
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3400
3396
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3401
3397
  repairable.set(room.locator, reset);
@@ -3417,30 +3413,10 @@ export function composeRuntime(options) {
3417
3413
  list(room, false);
3418
3414
  emitRooms();
3419
3415
  }
3420
- else if (withheld.kind === 'refused') {
3421
- refusedSource(room, withheld);
3422
- }
3423
3416
  else if (withheld.kind === 'revoked-authorization') {
3424
3417
  await refuse('revoked-authorization');
3425
3418
  }
3426
3419
  }
3427
- function refusedSource(room, { refusal }) {
3428
- sourceRefusals.set(room.locator, {
3429
- code: refusal.rule,
3430
- retryable: false,
3431
- fields: {
3432
- file: refusal.file,
3433
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3434
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3435
- },
3436
- });
3437
- declarationRefusals.delete(room.locator);
3438
- unreadable.add(room.locator);
3439
- list(room, false);
3440
- emitRooms();
3441
- moveTo({ ...status, ...attention() });
3442
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3443
- }
3444
3420
  function leaving(batch, result) {
3445
3421
  if (result.kind === 'expired')
3446
3422
  return { batch, returned: returnedBy('expired') };
@@ -3462,8 +3438,8 @@ export function composeRuntime(options) {
3462
3438
  function settling(acceptance) {
3463
3439
  return acceptance !== null && 'apply' in acceptance;
3464
3440
  }
3465
- function refusalOf(acceptance) {
3466
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3441
+ function failureOf(acceptance) {
3442
+ return settling(acceptance) ? null : acceptance;
3467
3443
  }
3468
3444
  async function acceptance(room, local, submitted, accepted) {
3469
3445
  if (!continuing())
@@ -3476,7 +3452,8 @@ export function composeRuntime(options) {
3476
3452
  logAccepted(room.locator, submitted, accepted);
3477
3453
  let record = current.record;
3478
3454
  let changes;
3479
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3455
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3456
+ !older(room.locator, accepted.roomVersion)) {
3480
3457
  const paths = [...returnedPaths(submitted, accepted)];
3481
3458
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3482
3459
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3486,34 +3463,6 @@ export function composeRuntime(options) {
3486
3463
  ? accepted.roomVersion
3487
3464
  : held,
3488
3465
  };
3489
- const overrides = new Map([
3490
- ...back.set.map((entry) => [entry.path, entry]),
3491
- ...back.removed.map((path) => [path, null]),
3492
- ]);
3493
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3494
- if (refused !== null) {
3495
- const key = pendingKey(room.locator, submitted.id);
3496
- return {
3497
- locator: room.locator,
3498
- left: [],
3499
- commit: {
3500
- rooms: [],
3501
- writes: [
3502
- {
3503
- locator: room.locator,
3504
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3505
- },
3506
- ],
3507
- },
3508
- refused,
3509
- apply() {
3510
- const state = pendings.get(key);
3511
- if (state !== undefined && !fences.has(room.locator))
3512
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3513
- return changeSet(room.locator, { queue: true });
3514
- },
3515
- };
3516
- }
3517
3466
  changes = back;
3518
3467
  }
3519
3468
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3521,10 +3470,9 @@ export function composeRuntime(options) {
3521
3470
  ...settled,
3522
3471
  apply(answer) {
3523
3472
  const set = settled.apply(answer);
3473
+ showing(room.locator, accepted.roomVersion);
3524
3474
  pendings.delete(pendingKey(room.locator, submitted.id));
3525
3475
  unreadable.delete(room.locator);
3526
- sourceRefusals.delete(room.locator);
3527
- declarationRefusals.delete(room.locator);
3528
3476
  list(room, true);
3529
3477
  return set;
3530
3478
  },
@@ -3542,13 +3490,9 @@ export function composeRuntime(options) {
3542
3490
  const failed = await exclusive(async () => {
3543
3491
  const adopting = await acceptance(room, local, submitted, accepted);
3544
3492
  await adoptedTogether(local, [adopting]);
3545
- return refusalOf(adopting);
3493
+ return failureOf(adopting);
3546
3494
  });
3547
- if (failed === null)
3548
- return true;
3549
- if (failed.kind === 'refused' && continuing())
3550
- refusedSource(room, failed);
3551
- return false;
3495
+ return failed === null;
3552
3496
  }
3553
3497
  async function adoptedMove(room, partner, local, submitted, accepted) {
3554
3498
  const other = discovered.get(partner);
@@ -3562,18 +3506,10 @@ export function composeRuntime(options) {
3562
3506
  ...accepted.destination,
3563
3507
  });
3564
3508
  await adoptedTogether(local, [left, arrived]);
3565
- const refusedArrival = refusalOf(arrived);
3566
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3567
- refusedSource(other, refusedArrival);
3568
- }
3569
- return refusalOf(left);
3509
+ return failureOf(left);
3570
3510
  });
3571
3511
  submitLater(partner);
3572
- if (failed === null)
3573
- return true;
3574
- if (failed.kind === 'refused' && continuing())
3575
- refusedSource(room, failed);
3576
- return false;
3512
+ return failed === null;
3577
3513
  }
3578
3514
  function queueIn(locator) {
3579
3515
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3696,8 +3632,6 @@ export function composeRuntime(options) {
3696
3632
  if (replica === undefined || head === undefined)
3697
3633
  return 'done';
3698
3634
  const state = pendings.get(pendingKey(room.locator, head.id));
3699
- if (state?.accepted !== undefined)
3700
- return 'done';
3701
3635
  const partner = partnerOf(head, room.locator);
3702
3636
  if (partner !== null && !leavingIn(head, room.locator)) {
3703
3637
  submitLater(partner);
@@ -3811,12 +3745,12 @@ export function composeRuntime(options) {
3811
3745
  }
3812
3746
  if (result.kind === 'expired') {
3813
3747
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3814
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3748
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3815
3749
  if (other !== undefined && there !== null) {
3816
3750
  await withhold(there, other, local, progress);
3817
3751
  return 'done';
3818
3752
  }
3819
- const withheld = await advance(room, local, authorized, continuing);
3753
+ const withheld = await advanced(room, local, authorized);
3820
3754
  if (withheld !== null) {
3821
3755
  await withhold(withheld, room, local, progress);
3822
3756
  return 'done';
@@ -3838,8 +3772,8 @@ export function composeRuntime(options) {
3838
3772
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3839
3773
  const left = leaving(head, result);
3840
3774
  const refreshed = async () => ({
3841
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3842
- here: await advance(room, local, authorized, continuing),
3775
+ there: other === undefined ? null : await advanced(other, local, authorized),
3776
+ here: await advanced(room, local, authorized),
3843
3777
  });
3844
3778
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3845
3779
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3943,25 +3877,22 @@ export function composeRuntime(options) {
3943
3877
  if (signal.aborted)
3944
3878
  return;
3945
3879
  const progress = { complete: true, reachable: true };
3946
- await roomTask(room.locator, async (live) => {
3947
- await Promise.race([
3948
- reload(room, local, progress, () => live() && !signal.aborted),
3949
- abandonedOn(signal),
3950
- ]);
3951
- });
3880
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3952
3881
  if (signal.aborted)
3953
3882
  return;
3954
3883
  await rest(progress);
3955
3884
  }
3956
3885
  }
3957
3886
  function awaitingAnswer(locator) {
3887
+ answering.get(locator)?.release();
3958
3888
  let release = () => { };
3959
- const awaited = new Promise((resolve) => {
3889
+ const answered = new Promise((resolve) => {
3960
3890
  release = resolve;
3961
3891
  });
3962
- answering.set(locator, { generation, answered: awaited });
3892
+ const awaited = { answered, release };
3893
+ answering.set(locator, awaited);
3963
3894
  return () => {
3964
- if (answering.get(locator)?.answered === awaited)
3895
+ if (answering.get(locator) === awaited)
3965
3896
  answering.delete(locator);
3966
3897
  release();
3967
3898
  };
@@ -3973,6 +3904,30 @@ export function composeRuntime(options) {
3973
3904
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3974
3905
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3975
3906
  }
3907
+ async function keepNaming(local, room) {
3908
+ const held = replicas.get(room.locator)?.record;
3909
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3910
+ return;
3911
+ await exclusive(async () => {
3912
+ if (!continuing())
3913
+ return;
3914
+ const replica = replicas.get(room.locator);
3915
+ if (replica === undefined)
3916
+ return;
3917
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3918
+ await local.commit({
3919
+ rooms: [
3920
+ {
3921
+ locator: room.locator,
3922
+ key: record.key,
3923
+ record: storedRecord(record),
3924
+ composed: composedOf(composed(record)),
3925
+ },
3926
+ ],
3927
+ });
3928
+ replicas.set(room.locator, { ...replica, record });
3929
+ });
3930
+ }
3976
3931
  async function retainModel(local, room, next) {
3977
3932
  const key = room.locator;
3978
3933
  const previous = modelOf(room.locator);
@@ -3988,26 +3943,15 @@ export function composeRuntime(options) {
3988
3943
  const held = replicas.get(room.locator);
3989
3944
  let changed = null;
3990
3945
  if (held !== undefined) {
3991
- const declared = {
3946
+ const record = {
3992
3947
  ...held.record,
3948
+ name: room.name,
3949
+ handle: room.handle,
3993
3950
  operations: room.operations,
3994
3951
  replica: room.replica,
3995
3952
  typedSelection: room.typedSelection ?? null,
3996
3953
  model: next,
3997
3954
  };
3998
- let read = 'staged';
3999
- if (!sameMembership(declared, held.record)) {
4000
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
4001
- read =
4002
- verdict.verdict !== 'refuse'
4003
- ? 'staged'
4004
- : verdict.rule === 'model-unavailable'
4005
- ? 'deferred'
4006
- : { kind: 'refused', refusal: verdict };
4007
- }
4008
- const record = read === 'staged'
4009
- ? declared
4010
- : { ...declared, typedSelection: held.record.typedSelection };
4011
3955
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
4012
3956
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
4013
3957
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -4029,16 +3973,6 @@ export function composeRuntime(options) {
4029
3973
  documents: answer.changed[room.locator] ?? [],
4030
3974
  room: true,
4031
3975
  });
4032
- if (read === 'staged' || read === 'deferred') {
4033
- if (declarationRefusals.delete(room.locator)) {
4034
- sourceRefusals.delete(room.locator);
4035
- unreadable.delete(room.locator);
4036
- }
4037
- }
4038
- else {
4039
- refusedSource(room, read);
4040
- declarationRefusals.add(room.locator);
4041
- }
4042
3976
  }
4043
3977
  heldModel(room, next);
4044
3978
  if (changed === null)
@@ -4083,6 +4017,17 @@ export function composeRuntime(options) {
4083
4017
  emitRooms();
4084
4018
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
4085
4019
  }
4020
+ function name(locator) {
4021
+ const stream = options.adapter.roomStream;
4022
+ const record = replicas.get(locator)?.record;
4023
+ const room = discovered.get(locator);
4024
+ if (stream === undefined || record === undefined || room === undefined)
4025
+ return;
4026
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
4027
+ return;
4028
+ naming += 1;
4029
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
4030
+ }
4086
4031
  function heldRooms(listed) {
4087
4032
  return listed.flatMap((room) => {
4088
4033
  const record = replicas.get(room.locator)?.record;
@@ -4100,14 +4045,19 @@ export function composeRuntime(options) {
4100
4045
  return outcome?.status === 'fulfilled' && outcome.value;
4101
4046
  });
4102
4047
  if (attempt === 'opened') {
4048
+ naming += 1;
4103
4049
  options.adapter.roomStream?.hold(heldRooms(ready));
4104
4050
  handed = true;
4105
4051
  }
4106
- discovering?.release();
4107
- const loaded = await Promise.allSettled(ready
4108
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
4109
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
4110
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
4052
+ for (const room of ready) {
4053
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4054
+ if (attempt === 'authorized')
4055
+ name(room.locator);
4056
+ continue;
4057
+ }
4058
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
4059
+ }
4060
+ const failed = prepared.find((room) => room.status === 'rejected');
4111
4061
  if (failed !== undefined)
4112
4062
  throw failed.reason;
4113
4063
  }
@@ -4126,12 +4076,16 @@ export function composeRuntime(options) {
4126
4076
  }
4127
4077
  if (prior !== undefined)
4128
4078
  await retainModel(local, room, modelOf(room.locator));
4079
+ if (prior !== undefined)
4080
+ await keepNaming(local, room);
4129
4081
  if (discoveryPaused || authorization === undefined)
4130
4082
  return false;
4131
4083
  return room.replica.admission === 'admitted';
4132
4084
  }
4133
4085
  async function caughtUp(room, local, progress, live) {
4134
- if (authorization === undefined || !live() || !behind(room))
4086
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
4087
+ return;
4088
+ if (!live() || !behind(room))
4135
4089
  return;
4136
4090
  const withheld = await advance(room, local, authorization, live);
4137
4091
  if (withheld !== null)
@@ -4144,6 +4098,8 @@ export function composeRuntime(options) {
4144
4098
  async function loadRoom(room, local, progress, live) {
4145
4099
  if (!live() || authorization === undefined)
4146
4100
  return;
4101
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
4102
+ return;
4147
4103
  if (replicas.has(room.locator) && revoked(room.locator)) {
4148
4104
  await reload(room, local, progress, live);
4149
4105
  return;
@@ -4161,20 +4117,53 @@ export function composeRuntime(options) {
4161
4117
  reloadsDue.delete(room.locator);
4162
4118
  if (!live() || authorization === undefined)
4163
4119
  return;
4120
+ const from = standingOf(room.locator);
4164
4121
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
4165
4122
  if (bootstrapped.kind !== 'staged') {
4166
4123
  if (live())
4167
4124
  await withhold(bootstrapped, room, local, progress);
4168
4125
  return;
4169
4126
  }
4170
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
4127
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4171
4128
  await caughtUp(room, local, progress, live);
4172
4129
  }
4173
4130
  finally {
4174
4131
  caughtUpTo(room.locator);
4175
4132
  }
4176
4133
  }
4177
- async function discoverRooms(local, progress, attempt, live, scope) {
4134
+ async function asked(authorized) {
4135
+ try {
4136
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
4137
+ }
4138
+ catch {
4139
+ return { kind: 'transport-uncertainty' };
4140
+ }
4141
+ }
4142
+ function malformed(listed) {
4143
+ for (const room of listed) {
4144
+ if (room.operations === undefined ||
4145
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4146
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
4147
+ room.replica === undefined ||
4148
+ typeof room.replica.selection !== 'string' ||
4149
+ room.replica.selection.length === 0 ||
4150
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4151
+ !['locked', 'open'].includes(room.replica.offline)) {
4152
+ return `invalid model or replica declaration ${room.locator}`;
4153
+ }
4154
+ if (!declaredSelection(room))
4155
+ return `invalid typed selection declaration ${room.locator}`;
4156
+ }
4157
+ return null;
4158
+ }
4159
+ async function signIn(local, signedIn) {
4160
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
4161
+ return;
4162
+ await local.fact('member', signedIn);
4163
+ member = signedIn;
4164
+ emitMember();
4165
+ }
4166
+ async function discoverRooms(local, progress, attempt, live, found) {
4178
4167
  if (closed || authorization === undefined) {
4179
4168
  notifyReady();
4180
4169
  return;
@@ -4182,13 +4171,7 @@ export function composeRuntime(options) {
4182
4171
  moveTo(transferring(status));
4183
4172
  discoveries += 1;
4184
4173
  const started = discoveries;
4185
- let result;
4186
- try {
4187
- result = await options.adapter.discover(authorization, scope);
4188
- }
4189
- catch {
4190
- result = { kind: 'transport-uncertainty' };
4191
- }
4174
+ const result = found ?? (await asked(authorization));
4192
4175
  if (result.kind === 'discovery')
4193
4176
  roomsNamed = true;
4194
4177
  if (!live()) {
@@ -4198,7 +4181,7 @@ export function composeRuntime(options) {
4198
4181
  }
4199
4182
  journal.record(`connection discover ${result.kind}`);
4200
4183
  if (result.kind === 'transport-uncertainty') {
4201
- await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4184
+ await cachedModels(local);
4202
4185
  progress.reachable = false;
4203
4186
  progress.complete = false;
4204
4187
  notifyReady();
@@ -4219,26 +4202,12 @@ export function composeRuntime(options) {
4219
4202
  notifyReady();
4220
4203
  return;
4221
4204
  }
4222
- for (const room of result.rooms) {
4223
- if (room.operations === undefined ||
4224
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4225
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
4226
- room.replica === undefined ||
4227
- typeof room.replica.selection !== 'string' ||
4228
- room.replica.selection.length === 0 ||
4229
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4230
- !['locked', 'open'].includes(room.replica.offline)) {
4231
- journal.record(`invalid model or replica declaration ${room.locator}`);
4232
- await refuse('missing-required-guarantee');
4233
- notifyReady();
4234
- return;
4235
- }
4236
- if (!declaredSelection(room)) {
4237
- journal.record(`invalid typed selection declaration ${room.locator}`);
4238
- await refuse('missing-required-guarantee');
4239
- notifyReady();
4240
- return;
4241
- }
4205
+ const invalid = malformed(result.rooms);
4206
+ if (invalid !== null) {
4207
+ journal.record(invalid);
4208
+ await refuse('missing-required-guarantee');
4209
+ notifyReady();
4210
+ return;
4242
4211
  }
4243
4212
  if (refusal !== null) {
4244
4213
  refusal = null;
@@ -4253,15 +4222,9 @@ export function composeRuntime(options) {
4253
4222
  limits = result.providerLimits;
4254
4223
  await local.fact('providerLimits', limits);
4255
4224
  await local.fact('capabilities', capabilities);
4256
- conversations.declared();
4257
- }
4258
- const signedIn = result.member ?? null;
4259
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4260
- await local.fact('member', signedIn);
4261
- member = signedIn;
4262
- emitMember();
4263
4225
  }
4264
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4226
+ await signIn(local, result.member ?? null);
4227
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4265
4228
  }
4266
4229
  async function ensureAuthorization() {
4267
4230
  if (authorization !== undefined)
@@ -4276,23 +4239,45 @@ export function composeRuntime(options) {
4276
4239
  }
4277
4240
  }
4278
4241
  async function admittedRead(work) {
4279
- if (roomReads.generation !== generation) {
4280
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4242
+ while (roomReads.free === 0) {
4243
+ await new Promise((admit) => roomReads.waiting.push(admit));
4281
4244
  }
4282
- const reads = roomReads;
4283
- while (reads.free === 0)
4284
- await new Promise((admit) => reads.waiting.push(admit));
4285
- reads.free -= 1;
4245
+ roomReads.free -= 1;
4286
4246
  try {
4287
4247
  await work();
4288
4248
  }
4289
4249
  finally {
4290
- reads.free += 1;
4291
- reads.waiting.shift()?.();
4250
+ roomReads.free += 1;
4251
+ roomReads.waiting.shift()?.();
4292
4252
  }
4293
4253
  }
4254
+ function inRoom(locator, work) {
4255
+ roomWork += 1;
4256
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4257
+ try {
4258
+ return await work(continuing);
4259
+ }
4260
+ finally {
4261
+ roomWork -= 1;
4262
+ }
4263
+ });
4264
+ const chain = ran.then(() => undefined, () => undefined);
4265
+ roomLanes.set(locator, chain);
4266
+ working.add(chain);
4267
+ void chain.finally(() => {
4268
+ working.delete(chain);
4269
+ if (roomLanes.get(locator) === chain)
4270
+ roomLanes.delete(locator);
4271
+ });
4272
+ return ran;
4273
+ }
4294
4274
  function roomTask(locator, work) {
4295
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4275
+ if (stopping)
4276
+ return Promise.resolve();
4277
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4278
+ }
4279
+ function advanced(room, local, authorized) {
4280
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4296
4281
  }
4297
4282
  function task(lane, work) {
4298
4283
  if (stopping)
@@ -4308,23 +4293,19 @@ export function composeRuntime(options) {
4308
4293
  void run.finally(() => working.delete(run));
4309
4294
  return run;
4310
4295
  }
4311
- function sync(attempt) {
4296
+ function sync(attempt, found) {
4312
4297
  return task(OPENING_LANE, async (live) => {
4313
4298
  if (!live()) {
4314
4299
  readyFromStore();
4315
4300
  return;
4316
4301
  }
4317
4302
  const progress = { complete: true, reachable: true };
4318
- const gate = discover();
4319
- if (attempt === 'opened')
4320
- opened = generation;
4321
- underway = attempt === 'opened' ? { generation, progress } : null;
4303
+ underway = attempt === 'opened' ? progress : null;
4322
4304
  if (attempt === 'opened')
4323
4305
  streamed = progress;
4324
4306
  complete = false;
4325
4307
  recorded = false;
4326
4308
  moveTo(transferring(status));
4327
- handed = false;
4328
4309
  try {
4329
4310
  const local = await openLocal();
4330
4311
  if (!live())
@@ -4332,7 +4313,7 @@ export function composeRuntime(options) {
4332
4313
  await ensureAuthorization();
4333
4314
  if (!live())
4334
4315
  return;
4335
- await discoverRooms(local, progress, attempt, live);
4316
+ await discoverRooms(local, progress, attempt, live, found);
4336
4317
  }
4337
4318
  catch {
4338
4319
  progress.complete = false;
@@ -4340,7 +4321,6 @@ export function composeRuntime(options) {
4340
4321
  progress.reachable = false;
4341
4322
  }
4342
4323
  finally {
4343
- gate.release();
4344
4324
  notifyReady();
4345
4325
  }
4346
4326
  if (!live())
@@ -4352,35 +4332,59 @@ export function composeRuntime(options) {
4352
4332
  if (handed || closed || progress.reachable)
4353
4333
  return;
4354
4334
  underway = null;
4355
- opened = null;
4356
- journal.record('connection reopen');
4357
4335
  await rest(progress, live);
4358
- options.adapter.roomStream?.reopen();
4359
4336
  });
4360
4337
  }
4361
- function rediscover(scope) {
4338
+ function resynchronize() {
4362
4339
  return task(OPENING_LANE, async (live) => {
4363
- if (!live() || !socketOpen)
4340
+ if (!live() || !announced)
4364
4341
  return;
4365
4342
  const progress = { complete: true, reachable: true };
4366
- const gate = discover();
4367
4343
  moveTo(transferring(status));
4368
4344
  try {
4369
4345
  const local = await openLocal();
4370
4346
  if (!live())
4371
4347
  return;
4372
- await ensureAuthorization();
4348
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4349
+ }
4350
+ catch {
4351
+ progress.complete = false;
4352
+ }
4353
+ if (live())
4354
+ await finishOpening(progress, live);
4355
+ });
4356
+ }
4357
+ function countAuthorityChange() {
4358
+ authorityChanges += 1;
4359
+ for (const listener of authorityChangeListeners)
4360
+ listener(authorityChanges);
4361
+ }
4362
+ function changeAuthority(change) {
4363
+ return task(OPENING_LANE, async (live) => {
4364
+ if (!live() || !announced)
4365
+ return;
4366
+ countAuthorityChange();
4367
+ const progress = { complete: true, reachable: true };
4368
+ moveTo(transferring(status));
4369
+ try {
4370
+ const local = await openLocal();
4373
4371
  if (!live())
4374
4372
  return;
4375
- await discoverRooms(local, progress, 'authorized', live, scope);
4373
+ const invalid = malformed(change.rooms);
4374
+ if (invalid !== null) {
4375
+ journal.record(invalid);
4376
+ await refuse('missing-required-guarantee');
4377
+ return;
4378
+ }
4379
+ if (change.member !== undefined)
4380
+ await signIn(local, change.member);
4381
+ discoveries += 1;
4382
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4383
+ await synchronize(listed, local, progress, 'authorized', live);
4376
4384
  }
4377
4385
  catch {
4378
4386
  progress.complete = false;
4379
4387
  }
4380
- finally {
4381
- gate.release();
4382
- notifyReady();
4383
- }
4384
4388
  if (!live())
4385
4389
  return;
4386
4390
  if (!progress.complete)
@@ -4388,15 +4392,17 @@ export function composeRuntime(options) {
4388
4392
  await rest(progress, live);
4389
4393
  });
4390
4394
  }
4391
- function closedSocket(revoked, lost) {
4392
- notifyDiscovered();
4393
- const opened = socketOpen;
4395
+ function closedSocket(revoked, lost, authorityChanged) {
4396
+ notifyReady();
4397
+ const opened = announced;
4394
4398
  socketOpen = false;
4399
+ announced = false;
4400
+ missedChange ||= authorityChanged;
4401
+ handed = false;
4402
+ naming = 0;
4395
4403
  connectionLost();
4396
- if (lost) {
4397
- reads.lost();
4404
+ if (lost)
4398
4405
  conversations.lost();
4399
- }
4400
4406
  if (revoked) {
4401
4407
  void refuse('revoked-authorization');
4402
4408
  return;
@@ -4407,18 +4413,32 @@ export function composeRuntime(options) {
4407
4413
  moveTo({
4408
4414
  ...status,
4409
4415
  state: status.state === 'synced' ? 'syncing' : status.state,
4410
- providerUnreachable: status.providerUnreachable || !opened,
4416
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4417
+ });
4418
+ void closing(opened);
4419
+ }
4420
+ function closing(opened) {
4421
+ return task(OPENING_LANE, async (live) => {
4422
+ if (!live())
4423
+ return;
4424
+ const local = await openLocal();
4425
+ if (!opened && !socketOpen)
4426
+ await cachedModels(local);
4427
+ await rest(null, live);
4411
4428
  });
4412
4429
  }
4413
- function applyOpening(expired) {
4414
- const progress = underway?.generation === generation ? underway.progress : null;
4430
+ function cachedModels(local) {
4431
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4432
+ }
4433
+ function caughtUpRooms(expired) {
4434
+ const progress = underway;
4435
+ underway = null;
4436
+ naming = Math.max(0, naming - 1);
4415
4437
  const cycle = progress ?? { complete: true, reachable: true };
4416
- if (opened === generation)
4417
- opened = null;
4438
+ replaced(expired);
4418
4439
  const reloading = progress === null ? [] : owedReloads(expired);
4419
4440
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4420
4441
  return task(OPENING_LANE, async (live) => {
4421
- underway = null;
4422
4442
  if (!live())
4423
4443
  return;
4424
4444
  if (progress === null) {
@@ -4426,20 +4446,14 @@ export function composeRuntime(options) {
4426
4446
  await rest(cycle, live);
4427
4447
  return;
4428
4448
  }
4429
- await roomsCarried([...expired, ...reloading]);
4430
- if (!live())
4431
- return;
4432
4449
  try {
4433
4450
  const local = await openLocal();
4434
- const reloaded = await replacing;
4451
+ await replacing;
4435
4452
  if (progress.reachable) {
4436
4453
  await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4437
4454
  !expired.includes(room.locator) &&
4438
4455
  !reloading.includes(room.locator)), 'current', local);
4439
4456
  }
4440
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4441
- if (failed !== undefined)
4442
- throw failed.reason;
4443
4457
  }
4444
4458
  catch {
4445
4459
  progress.complete = false;
@@ -4451,11 +4465,8 @@ export function composeRuntime(options) {
4451
4465
  }
4452
4466
  function owedReloads(expired) {
4453
4467
  const reloading = [...discovered.values()]
4454
- .filter((room) => replicas.has(room.locator) &&
4455
- !expired.includes(room.locator) &&
4456
- (owed.has(room.locator) || revoked(room.locator)))
4468
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4457
4469
  .map((room) => room.locator);
4458
- owed.clear();
4459
4470
  for (const locator of reloading)
4460
4471
  catching.add(locator);
4461
4472
  liveQueries.recover();
@@ -4464,16 +4475,17 @@ export function composeRuntime(options) {
4464
4475
  async function reloadNow(replaced, progress) {
4465
4476
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4466
4477
  for (const room of reloaded)
4467
- reloadsDue.set(room.locator, generation);
4478
+ reloadsDue.add(room.locator);
4468
4479
  try {
4469
4480
  const local = await openLocal();
4470
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4481
+ for (const room of reloaded) {
4482
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4483
+ }
4471
4484
  }
4472
4485
  catch {
4473
4486
  for (const room of reloaded)
4474
4487
  reloadsDue.delete(room.locator);
4475
4488
  progress.reachable = false;
4476
- return [];
4477
4489
  }
4478
4490
  }
4479
4491
  async function settleModels(held, status, local) {
@@ -4484,7 +4496,7 @@ export function composeRuntime(options) {
4484
4496
  const settled = await exclusive(async () => {
4485
4497
  if (!continuing())
4486
4498
  return [];
4487
- const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4499
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4488
4500
  for (const room of unchanged) {
4489
4501
  const next = { ...modelOf(room.locator), status };
4490
4502
  const replica = replicas.get(room.locator);
@@ -4544,14 +4556,10 @@ export function composeRuntime(options) {
4544
4556
  try {
4545
4557
  const local = await openLocal();
4546
4558
  await roomTask(locator, async (live) => {
4547
- const current = () => live() && !signal.aborted;
4548
- if (!current() || authorization === undefined)
4559
+ if (!live() || authorization === undefined)
4549
4560
  return;
4550
- const withheld = await Promise.race([
4551
- advance(room, local, authorization, current),
4552
- abandonedOn(signal),
4553
- ]);
4554
- if (withheld !== null && current())
4561
+ const withheld = await advance(room, local, authorization, live);
4562
+ if (withheld !== null && live())
4555
4563
  await withhold(withheld, room, local, progress);
4556
4564
  });
4557
4565
  }
@@ -4606,14 +4614,11 @@ export function composeRuntime(options) {
4606
4614
  progress.complete = false;
4607
4615
  return;
4608
4616
  }
4609
- reads.applied();
4610
4617
  submitAll();
4611
4618
  for (const locator of stale)
4612
4619
  if (repairable.has(locator))
4613
4620
  repairLater(locator, {});
4614
4621
  await conversations.reconcile();
4615
- if (status.providerUnreachable)
4616
- conversations.reconnected();
4617
4622
  const reading = { complete: true, reachable: true };
4618
4623
  await conversations.catchUp(reading);
4619
4624
  if (progress.reachable && reading.reachable)
@@ -4639,7 +4644,7 @@ export function composeRuntime(options) {
4639
4644
  if (!live())
4640
4645
  return;
4641
4646
  let lastCompleted = status.lastCompleted;
4642
- let synced = complete && stale.size === 0 && !pending();
4647
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4643
4648
  if (synced && !recorded) {
4644
4649
  const completedAt = new Date().toISOString();
4645
4650
  try {
@@ -4654,74 +4659,78 @@ export function composeRuntime(options) {
4654
4659
  }
4655
4660
  }
4656
4661
  moveTo({
4657
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4662
+ state: pending()
4663
+ ? 'pending'
4664
+ : synced && complete && roomWork === 0 && naming === 0
4665
+ ? 'synced'
4666
+ : 'syncing',
4658
4667
  lastCompleted,
4659
- providerUnreachable: !progress.reachable,
4668
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4660
4669
  ...attention(),
4661
4670
  });
4662
4671
  }
4663
- function discover() {
4664
- let release = () => { };
4665
- const done = new Promise((resolve) => {
4666
- release = resolve;
4667
- });
4668
- discovering = { generation, done, release };
4669
- return discovering;
4670
- }
4671
- function applyRoom(locator, roomVersion) {
4672
- announced.set(locator, roomVersion);
4673
- return task(roomLane(locator), async (live) => {
4674
- if (discovering?.generation === generation)
4675
- await discovering.done;
4676
- const saving = answering.get(locator);
4677
- if (saving?.generation === generation)
4678
- await saving.answered;
4679
- if (!live() || authorization === undefined)
4680
- return;
4681
- const room = discovered.get(locator);
4682
- const held = replicas.get(locator);
4683
- if (room === undefined || room.replica.admission !== 'admitted')
4684
- return;
4685
- if (held !== undefined &&
4686
- (held.record.replica.selection !== room.replica.selection ||
4687
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4688
- return;
4689
- }
4690
- const during = underway?.generation === generation ? underway.progress : null;
4691
- const progress = during ?? { complete: true, reachable: true };
4692
- if (during === null)
4693
- moveTo(transferring(status));
4672
+ function applyRoom(change) {
4673
+ const { locator } = change;
4674
+ if (stopping)
4675
+ return Promise.resolve();
4676
+ return inRoom(locator, async (live) => {
4677
+ await answering.get(locator)?.answered;
4678
+ const progress = underway ?? { complete: true, reachable: true };
4694
4679
  try {
4695
4680
  const local = await openLocal();
4696
- await admittedRead(async () => {
4697
- if (!live() || authorization === undefined)
4698
- return;
4699
- const withheld = await advance(room, local, authorization, live);
4700
- if (withheld !== null)
4701
- await withhold(withheld, room, local, progress);
4702
- });
4681
+ for (;;) {
4682
+ if (!live())
4683
+ return null;
4684
+ const room = discovered.get(locator);
4685
+ const held = replicas.get(locator);
4686
+ if (room === undefined ||
4687
+ held === undefined ||
4688
+ room.replica.admission !== 'admitted' ||
4689
+ held.record.replica.selection !== room.replica.selection) {
4690
+ return null;
4691
+ }
4692
+ const at = held.record.roomVersion;
4693
+ if (options.adapter.reached(locator, at, change.roomVersion))
4694
+ return progress;
4695
+ if (!options.adapter.reached(locator, at, change.from)) {
4696
+ name(locator);
4697
+ return progress;
4698
+ }
4699
+ if (underway === null)
4700
+ moveTo(transferring(status));
4701
+ const writing = () => live() && !full;
4702
+ let outlined = false;
4703
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
4704
+ const page = await change.read();
4705
+ const paths = Object.keys(page.outlines ?? {});
4706
+ outlined = paths.length > 0;
4707
+ await local.remember(locator, held.record.key, paths);
4708
+ return page;
4709
+ }));
4710
+ if (refreshed.kind !== 'advanced')
4711
+ return progress;
4712
+ const landing = {
4713
+ kind: 'refreshed',
4714
+ from: refreshed.from,
4715
+ remodelled: refreshed.remodelled,
4716
+ };
4717
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4718
+ stale.delete(locator);
4719
+ await retainModel(local, room, modelOf(locator));
4720
+ if (outlined)
4721
+ void heldAgain(locator);
4722
+ return progress;
4723
+ }
4724
+ }
4703
4725
  }
4704
4726
  catch {
4727
+ if (!stale.has(locator))
4728
+ name(locator);
4705
4729
  stale.add(locator);
4730
+ return progress;
4706
4731
  }
4707
- finally {
4708
- if (during === null)
4709
- await rest(progress, live);
4710
- }
4711
- });
4732
+ }).then((progress) => rest(progress), () => undefined);
4712
4733
  }
4713
- function roomsCarried(replacing) {
4714
- const replaced = new Set(replacing.map(roomLane));
4715
- return Promise.all([...lanes]
4716
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4717
- .map(([, held]) => held.chain));
4718
- }
4719
- const receipts = {
4720
- heads: async () => (await openLocal()).receivedHeads(),
4721
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4722
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4723
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4724
- };
4725
4734
  function observeStream() {
4726
4735
  const stream = options.adapter.roomStream;
4727
4736
  if (stream === undefined || withdrawStream !== undefined)
@@ -4731,31 +4740,35 @@ export function composeRuntime(options) {
4731
4740
  return;
4732
4741
  if (event.kind === 'opened') {
4733
4742
  socketOpen = true;
4743
+ announced = false;
4744
+ handed = false;
4745
+ naming = 0;
4734
4746
  connectionLive();
4735
4747
  generation += 1;
4736
- for (const admit of installWaiters.splice(0))
4737
- admit();
4738
- reads.opened();
4739
4748
  conversations.reopened();
4740
4749
  recovery.resume();
4741
- void sync('opened');
4750
+ }
4751
+ else if (event.kind === 'discovered') {
4752
+ if (!socketOpen)
4753
+ return;
4754
+ announced = true;
4755
+ if (missedChange) {
4756
+ missedChange = false;
4757
+ countAuthorityChange();
4758
+ }
4759
+ void sync('opened', event.discovery);
4760
+ for (const locator of replicas.keys())
4761
+ void heldAgain(locator);
4742
4762
  }
4743
4763
  else if (event.kind === 'closed')
4744
- closedSocket(event.revoked, event.lost);
4745
- else if (event.kind === 'opening')
4746
- void applyOpening(event.expired);
4764
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4765
+ else if (event.kind === 'caught-up')
4766
+ void caughtUpRooms(event.expired);
4747
4767
  else if (event.kind === 'authority-changed')
4748
- void rediscover(event.scope);
4768
+ void changeAuthority(event);
4749
4769
  else
4750
- void applyRoom(event.locator, event.roomVersion);
4751
- }, receipts, heldRevisions);
4752
- }
4753
- async function heldRevisions(locator, paths) {
4754
- const held = replicas.get(locator);
4755
- if (held === undefined || paths.length === 0)
4756
- return new Map();
4757
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4758
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4770
+ void applyRoom(event);
4771
+ });
4759
4772
  }
4760
4773
  function writesHeld() {
4761
4774
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4828,8 +4841,6 @@ export function composeRuntime(options) {
4828
4841
  notifyDiscovered();
4829
4842
  if (probing !== null)
4830
4843
  clearInterval(probing);
4831
- for (const resolve of shownWaiters.splice(0))
4832
- resolve();
4833
4844
  connectionLive();
4834
4845
  lockListeners.clear();
4835
4846
  readinessWatches.clear();
@@ -4847,6 +4858,7 @@ export function composeRuntime(options) {
4847
4858
  statusListeners.clear();
4848
4859
  serviceModeListeners.clear();
4849
4860
  memberListeners.clear();
4861
+ authorityChangeListeners.clear();
4850
4862
  liveQueries.close();
4851
4863
  queueSubscriptions.clear();
4852
4864
  roomDraftListeners.clear();
@@ -4854,6 +4866,7 @@ export function composeRuntime(options) {
4854
4866
  }
4855
4867
  async function stop() {
4856
4868
  stopping = true;
4869
+ inFlight.abort();
4857
4870
  await recovery.retire();
4858
4871
  await Promise.all(working);
4859
4872
  await mutations;
@@ -4872,7 +4885,7 @@ export function composeRuntime(options) {
4872
4885
  readyFromStore();
4873
4886
  });
4874
4887
  if (options.adapter.roomStream === undefined)
4875
- void sync('unstreamed');
4888
+ void sync('unstreamed', null);
4876
4889
  function planned(input, reach) {
4877
4890
  const plan = planOf(input, roomFacts(reach), planReach(reach));
4878
4891
  const named = plan.paths !== null &&
@@ -4914,58 +4927,56 @@ export function composeRuntime(options) {
4914
4927
  stop();
4915
4928
  };
4916
4929
  }
4917
- async function searched(input, reach, locked) {
4930
+ async function searched(input, reach) {
4918
4931
  const asked = searchPlanOf(input);
4919
- const filter = {
4920
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4921
- locked,
4922
- };
4923
- const unlocking = (filter.rooms ?? [])
4924
- .filter((room) => locked.includes(room))
4925
- .map((room) => ({ room, awaits: 'unlock' }));
4926
- for (;;) {
4927
- const cover = covered(filter);
4928
- awaitPreparation(cover);
4929
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4930
- const complete = pending.length === 0 && !('pending' in cover);
4931
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4932
- const answering = complete
4933
- ? filter
4934
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4935
- const answered = covered(answering);
4936
- if ('pending' in answered)
4937
- continue;
4938
- const listed = conversations.searchable();
4939
- const named = reach !== null && !reach.conversations
4940
- ? []
4941
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4942
- const hits = await (await openLocal()).search({
4943
- text: asked.text,
4944
- limit: capped(asked.limit),
4945
- after: asked.after,
4946
- total: asked.total,
4947
- documents: {
4948
- plan: answering,
4949
- rooms: answered.rooms,
4950
- beneath: asked.collection
4951
- ? answered.rooms.map(({ locator, key }) => ({
4952
- key,
4953
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4954
- }))
4955
- : [],
4956
- },
4957
- conversations: named,
4958
- });
4959
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4960
- continue;
4961
- const coverage = {
4962
- rooms: answered.coverage.rooms,
4963
- conversations: named,
4964
- overlay: answered.coverage.overlay,
4932
+ await ready;
4933
+ const search = options.adapter.search;
4934
+ if (search === undefined) {
4935
+ return {
4936
+ kind: 'undeclared',
4937
+ code: 'action-undeclared',
4938
+ fields: { name: 'search' },
4939
+ retryable: false,
4940
+ };
4941
+ }
4942
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4943
+ return OFFLINE;
4944
+ const reached = reach === null ? null : new Set(reach.rooms);
4945
+ const named = asked.rooms === null
4946
+ ? reached === null
4947
+ ? null
4948
+ : [...reached]
4949
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4950
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4951
+ if (rooms !== null && rooms.length === 0)
4952
+ return { kind: 'found', documents: [] };
4953
+ try {
4954
+ await ensureAuthorization();
4955
+ }
4956
+ catch {
4957
+ return UNCERTAIN;
4958
+ }
4959
+ if (closed || authorization === undefined || refusal !== null)
4960
+ return UNCERTAIN;
4961
+ journal.record('transfer search');
4962
+ try {
4963
+ const result = await search(authorization, { text: asked.text, rooms });
4964
+ if (result.kind === 'revoked-authorization')
4965
+ await refuse('revoked-authorization');
4966
+ journal.record(`outcome search ${result.kind}`);
4967
+ if (result.kind === 'rejected')
4968
+ return result;
4969
+ if (result.kind !== 'found')
4970
+ return failure(result);
4971
+ const kept = rooms === null ? null : new Set(rooms);
4972
+ return {
4973
+ kind: 'found',
4974
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4965
4975
  };
4966
- return complete
4967
- ? { ...hits, coverage }
4968
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4976
+ }
4977
+ catch {
4978
+ journal.record('outcome search transport-uncertainty');
4979
+ return UNCERTAIN;
4969
4980
  }
4970
4981
  }
4971
4982
  const watch = (topic, input, deliver) => {
@@ -5017,7 +5028,7 @@ export function composeRuntime(options) {
5017
5028
  };
5018
5029
  }
5019
5030
  if (topic === 'readiness') {
5020
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
5031
+ return watchReadiness({ target: '', code: 'stored', space: null }, null, deliver);
5021
5032
  }
5022
5033
  if (topic === 'rooms') {
5023
5034
  unlistedRooms.add(deliver);
@@ -5062,6 +5073,13 @@ export function composeRuntime(options) {
5062
5073
  memberListeners.delete(deliver);
5063
5074
  };
5064
5075
  }
5076
+ if (topic === 'authority-changes') {
5077
+ authorityChangeListeners.add(deliver);
5078
+ deliver(authorityChanges);
5079
+ return () => {
5080
+ authorityChangeListeners.delete(deliver);
5081
+ };
5082
+ }
5065
5083
  if (topic === 'queue') {
5066
5084
  const queue = { locator: input[0], notify: deliver };
5067
5085
  queueSubscriptions.add(queue);
@@ -5090,7 +5108,7 @@ export function composeRuntime(options) {
5090
5108
  });
5091
5109
  },
5092
5110
  query: ((input) => rowsQuery(planned(input, null))),
5093
- search: (input) => searched(input, null, []),
5111
+ search: (input) => searched(input, null),
5094
5112
  types(locator) {
5095
5113
  return {
5096
5114
  collection: modelTypes(modelOf(locator)),
@@ -5215,7 +5233,7 @@ export function composeRuntime(options) {
5215
5233
  authorization = undefined;
5216
5234
  discoveryPaused = false;
5217
5235
  if (options.adapter.roomStream === undefined)
5218
- await sync('unstreamed');
5236
+ await sync('unstreamed', null);
5219
5237
  else
5220
5238
  await task(OPENING_LANE, ensureAuthorization);
5221
5239
  },
@@ -5238,7 +5256,6 @@ export function composeRuntime(options) {
5238
5256
  md,
5239
5257
  seed,
5240
5258
  stop,
5241
- lose: loseRooms,
5242
5259
  watch,
5243
5260
  persisted(answer) {
5244
5261
  if (answer === persistence)
@@ -5247,7 +5264,6 @@ export function composeRuntime(options) {
5247
5264
  journal.record(`persistence ${answer}`);
5248
5265
  queueReadiness();
5249
5266
  },
5250
- shown: screen,
5251
5267
  within: (reach, port) => {
5252
5268
  if (full)
5253
5269
  void probed();