@agentshouse/mdruntime 0.1.0-c08b314dc5105 → 0.1.0-c09bf842921ff

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 (137) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +72 -56
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/attached-model.d.ts +3 -10
  5. package/dist/attached-model.d.ts.map +1 -1
  6. package/dist/attached-model.js +29 -47
  7. package/dist/bootstrap.d.ts +9 -16
  8. package/dist/bootstrap.d.ts.map +1 -1
  9. package/dist/bootstrap.js +85 -109
  10. package/dist/chain.d.ts +2 -1
  11. package/dist/chain.d.ts.map +1 -1
  12. package/dist/client.d.ts +9 -5
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +2 -1
  15. package/dist/collection.d.ts +2 -4
  16. package/dist/collection.d.ts.map +1 -1
  17. package/dist/collection.js +1 -17
  18. package/dist/compile.d.ts +1 -6
  19. package/dist/compile.d.ts.map +1 -1
  20. package/dist/compile.js +91 -270
  21. package/dist/connection.d.ts +9 -9
  22. package/dist/connection.d.ts.map +1 -1
  23. package/dist/connection.js +965 -1050
  24. package/dist/context.d.ts +2 -0
  25. package/dist/context.d.ts.map +1 -0
  26. package/dist/context.js +7 -0
  27. package/dist/conversation.d.ts +16 -13
  28. package/dist/conversation.d.ts.map +1 -1
  29. package/dist/conversation.js +229 -155
  30. package/dist/durability.d.ts +6 -1
  31. package/dist/durability.d.ts.map +1 -1
  32. package/dist/entry.d.ts +3 -2
  33. package/dist/entry.d.ts.map +1 -1
  34. package/dist/entry.js +17 -8
  35. package/dist/host.d.ts +3 -7
  36. package/dist/host.d.ts.map +1 -1
  37. package/dist/host.js +22 -27
  38. package/dist/identity.d.ts +0 -1
  39. package/dist/identity.d.ts.map +1 -1
  40. package/dist/identity.js +0 -3
  41. package/dist/index.d.ts +11 -7
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -1
  44. package/dist/job.d.ts +14 -0
  45. package/dist/job.d.ts.map +1 -0
  46. package/dist/job.js +28 -0
  47. package/dist/live.d.ts +2 -1
  48. package/dist/live.d.ts.map +1 -1
  49. package/dist/live.js +10 -7
  50. package/dist/media.d.ts +1 -1
  51. package/dist/media.d.ts.map +1 -1
  52. package/dist/media.js +27 -14
  53. package/dist/memory.d.ts +11 -5
  54. package/dist/memory.d.ts.map +1 -1
  55. package/dist/memory.js +166 -83
  56. package/dist/messages.d.ts +2 -2
  57. package/dist/messages.d.ts.map +1 -1
  58. package/dist/messages.js +15 -16
  59. package/dist/model.d.ts +2 -8
  60. package/dist/model.d.ts.map +1 -1
  61. package/dist/model.js +0 -12
  62. package/dist/observation.d.ts +2 -2
  63. package/dist/observation.d.ts.map +1 -1
  64. package/dist/observation.js +7 -2
  65. package/dist/outcome.d.ts +0 -3
  66. package/dist/outcome.d.ts.map +1 -1
  67. package/dist/paths.d.ts +1 -0
  68. package/dist/paths.d.ts.map +1 -1
  69. package/dist/paths.js +4 -0
  70. package/dist/query.d.ts +4 -209
  71. package/dist/query.d.ts.map +1 -1
  72. package/dist/query.js +3 -505
  73. package/dist/reach.d.ts +9 -8
  74. package/dist/reach.d.ts.map +1 -1
  75. package/dist/reach.js +37 -48
  76. package/dist/react.d.ts +3 -1
  77. package/dist/react.d.ts.map +1 -1
  78. package/dist/readiness.d.ts +3 -17
  79. package/dist/readiness.d.ts.map +1 -1
  80. package/dist/readiness.js +4 -10
  81. package/dist/recovery.d.ts +1 -0
  82. package/dist/recovery.d.ts.map +1 -1
  83. package/dist/recovery.js +26 -0
  84. package/dist/refresh.d.ts +7 -5
  85. package/dist/refresh.d.ts.map +1 -1
  86. package/dist/refresh.js +29 -5
  87. package/dist/replica.d.ts +9 -7
  88. package/dist/replica.d.ts.map +1 -1
  89. package/dist/replica.js +27 -13
  90. package/dist/room-draft.d.ts +21 -5
  91. package/dist/room-draft.d.ts.map +1 -1
  92. package/dist/room-draft.js +98 -51
  93. package/dist/rows.d.ts +14 -21
  94. package/dist/rows.d.ts.map +1 -1
  95. package/dist/rows.js +48 -184
  96. package/dist/save.d.ts +2 -0
  97. package/dist/save.d.ts.map +1 -1
  98. package/dist/save.js +12 -6
  99. package/dist/search.d.ts +12 -63
  100. package/dist/search.d.ts.map +1 -1
  101. package/dist/search.js +9 -52
  102. package/dist/shared.d.ts +0 -1
  103. package/dist/shared.d.ts.map +1 -1
  104. package/dist/shared.js +0 -10
  105. package/dist/status.d.ts +4 -3
  106. package/dist/status.d.ts.map +1 -1
  107. package/dist/status.js +3 -1
  108. package/dist/store.d.ts +15 -36
  109. package/dist/store.d.ts.map +1 -1
  110. package/dist/store.js +26 -81
  111. package/dist/stored.d.ts +7 -30
  112. package/dist/stored.d.ts.map +1 -1
  113. package/dist/stored.js +142 -189
  114. package/dist/tables.d.ts +19 -46
  115. package/dist/tables.d.ts.map +1 -1
  116. package/dist/tables.js +29 -66
  117. package/dist/transfer.d.ts +0 -5
  118. package/dist/transfer.d.ts.map +1 -1
  119. package/dist/validation.d.ts +3 -4
  120. package/dist/validation.d.ts.map +1 -1
  121. package/dist/validation.js +42 -100
  122. package/dist/windows.d.ts +1 -12
  123. package/dist/windows.d.ts.map +1 -1
  124. package/dist/windows.js +2 -75
  125. package/dist/wire.d.ts +9 -4
  126. package/dist/wire.d.ts.map +1 -1
  127. package/dist/wire.js +3 -1
  128. package/package.json +2 -2
  129. package/dist/fulltext.d.ts +0 -27
  130. package/dist/fulltext.d.ts.map +0 -1
  131. package/dist/fulltext.js +0 -368
  132. package/dist/reads.d.ts +0 -11
  133. package/dist/reads.d.ts.map +0 -1
  134. package/dist/reads.js +0 -100
  135. package/dist/words.d.ts +0 -19
  136. package/dist/words.d.ts.map +0 -1
  137. package/dist/words.js +0 -150
@@ -1,18 +1,18 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
2
- import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
1
+ import { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
2
+ import { attachedModel, builtInModel, roomTypesOf, schemaTypes } 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 { planWithin, } 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';
9
9
  import { searchPlanOf } from './search.js';
10
10
  import { capped, MAXIMUM_WINDOW } from './windows.js';
11
- import { bootstrapRoom, NOTHING_DONE } from './bootstrap.js';
11
+ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, composedOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
@@ -20,21 +20,20 @@ import { attachedFile } from './file.js';
20
20
  import { openedFiles } from './opened.js';
21
21
  import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
22
22
  import { createRecovery } from './recovery.js';
23
- import { doorReads } from './reads.js';
24
23
  import { advancement } from './refresh.js';
25
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
26
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
27
- import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
+ import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
28
27
  import { same, saving, STARTING, transferring, } from './status.js';
29
28
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
30
29
  import { readinessOf, } from './readiness.js';
31
30
  import { FencedMap } from './fence.js';
32
31
  import { sameBinary } from './overlay.js';
33
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
- import { identityTime, roomDraftRevision } from './identity.js';
35
- import { beneath, normalize } from './paths.js';
33
+ import { roomDraftRevision } from './identity.js';
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,34 +58,33 @@ const DERIVE_BYTES = 1 << 23;
59
58
  const DERIVE_FLOOR = 1 << 18;
60
59
  // An index build aims at this much database work, so a Save, a query or the next part's progress waits behind at most one short build.
61
60
  const DERIVE_AIM_MS = 250;
62
- const WALK_BYTES = 1 << 20;
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.
62
+ const DERIVE_LONGEST_MS = 1_500;
63
63
  const QUERY_BYTES = 1 << 20;
64
64
  const STAGE_BYTES = 1 << 20;
65
65
  const STAGE_FLOOR = 1 << 16;
66
66
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
67
67
  const STAGE_AIM_MS = 50;
68
- const PARTS_HELD = 4;
69
- const QUEUED_BYTES = 1 << 23;
70
- // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
68
+ const STREAMS_HELD = 4;
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
71
72
  const ARRIVAL_REPORT_MS = 250;
72
- const CLEAN_CHANGES = 2_000;
73
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.
74
74
  const LOCK_GRACE_MS = 120_000;
75
75
  function operationOf(input) {
76
76
  const operation = input?.operation_id;
77
77
  return typeof operation === 'string' ? operation : null;
78
78
  }
79
- function abandonedOn(signal) {
80
- return new Promise((resolve) => {
81
- if (signal.aborted)
82
- resolve(null);
83
- else
84
- 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));
85
86
  });
86
87
  }
87
- function roomLane(locator) {
88
- return `room ${locator}`;
89
- }
90
88
  function pendingKey(locator, id) {
91
89
  return `${locator}\u0000${id}`;
92
90
  }
@@ -106,6 +104,7 @@ export function composeRuntime(options) {
106
104
  const usableWhenHeld = options.usableWhenHeld ?? [];
107
105
  const queuesWithoutReplica = new FencedMap(isFenced);
108
106
  const pendings = new Map();
107
+ const landedAhead = new Map();
109
108
  const seeds = new Map();
110
109
  const seedWatchers = new Map();
111
110
  const roomDrafts = new FencedMap(isFenced);
@@ -117,15 +116,24 @@ export function composeRuntime(options) {
117
116
  const counted = new Map();
118
117
  const unreadable = new Set();
119
118
  const builtIn = builtInModel(roomTypes);
119
+ const typedPaths = options.adapter.typedPaths ?? [];
120
120
  const models = new FencedMap(isFenced);
121
121
  const liveModels = new FencedMap(isFenced);
122
122
  const invalidModels = new Set();
123
123
  const discovered = new Map();
124
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
124
+ let inFlight = new AbortController();
125
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
126
+ const reads = {
127
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
128
+ ...(refreshed === undefined
129
+ ? {}
130
+ : {
131
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
132
+ }),
133
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
134
+ };
125
135
  let roomsListed = false;
126
136
  const scopedRefusals = new Map();
127
- const sourceRefusals = new Map();
128
- const declarationRefusals = new Set();
129
137
  function modelOf(locator) {
130
138
  return models.get(locator) ?? NO_MODEL;
131
139
  }
@@ -135,7 +143,7 @@ export function composeRuntime(options) {
135
143
  : modelOf(locator);
136
144
  }
137
145
  function composed(record) {
138
- return compositionOf(record, builtIn);
146
+ return compositionOf(record, builtIn, typedPaths);
139
147
  }
140
148
  function compositionFor(record) {
141
149
  return composed({ ...record, model: savedModel(record.locator) });
@@ -148,30 +156,31 @@ export function composeRuntime(options) {
148
156
  let closed = false;
149
157
  let stopping = false;
150
158
  let withdrawStream;
151
- const announced = new Map();
152
159
  let underway = null;
153
160
  let streamed = null;
154
161
  let handed = false;
162
+ let naming = 0;
155
163
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
156
164
  const repairing = new Set();
157
165
  const calling = new Set();
158
166
  const repairable = new Map();
159
- let outcomeRetention = null;
160
167
  const answering = new Map();
161
168
  let complete = false;
162
169
  let recorded = false;
163
170
  const stale = new Set();
164
171
  let generation = 0;
165
- let discovering = null;
166
- let opened = null;
167
- const owed = new Set();
168
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
169
175
  const lanes = new Map();
170
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
176
+ const roomLanes = new Map();
177
+ let roomWork = 0;
178
+ const roomReads = { free: ROOM_READS, waiting: [] };
171
179
  const working = new Set();
172
180
  let mutations = Promise.resolve();
173
181
  let gathering = null;
174
182
  const roomListeners = new Set();
183
+ const unlistedRooms = new Set();
175
184
  const refusalListeners = new Set();
176
185
  const storageLossListeners = new Set();
177
186
  const statusListeners = new Set();
@@ -182,6 +191,8 @@ export function composeRuntime(options) {
182
191
  let withdrawConversations;
183
192
  let currencyQueued = null;
184
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
185
196
  const confirmed = new Set();
186
197
  let lockGrace = null;
187
198
  const lockListeners = new Set();
@@ -191,6 +202,8 @@ export function composeRuntime(options) {
191
202
  const serviceModeListeners = new Set();
192
203
  let member = null;
193
204
  const memberListeners = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
194
207
  const unpreparedDemand = new Set();
195
208
  const unpreparable = new Set();
196
209
  let preparing = false;
@@ -215,16 +228,16 @@ export function composeRuntime(options) {
215
228
  let readinessQueued = false;
216
229
  const catching = new Set();
217
230
  const transfers = new FencedMap(isFenced);
218
- const installs = new Map();
219
- const keptParts = new Map();
231
+ const installs = new Set();
220
232
  const arrivals = new Map();
233
+ const streaming = new Set();
221
234
  const underived = new Map();
235
+ const unindexed = new Map();
236
+ const startedOver = new Map();
222
237
  let arrivalReported = 0;
223
- let onScreen = options.shell.onScreen;
224
- const shownWaiters = [];
225
- let partsFree = PARTS_HELD;
226
- let queuedBytes = 0;
227
- const partWaiters = [];
238
+ let streamsFree = STREAMS_HELD;
239
+ let queuedText = 0;
240
+ const installWaiters = [];
228
241
  const stageBudget = {
229
242
  bytes: STAGE_FLOOR * 4,
230
243
  floor: STAGE_FLOOR,
@@ -237,23 +250,10 @@ export function composeRuntime(options) {
237
250
  ceiling: DERIVE_BYTES,
238
251
  aim: DERIVE_AIM_MS,
239
252
  };
240
- let installWrites = Promise.resolve();
253
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
241
254
  const conversations = createConversationStore({
242
- agents: declaredAgents,
243
- documents: (declaration, after) => rowsQuery(planOf({
244
- rooms: [declaration.locator],
245
- types: [declaration.type],
246
- select: {
247
- fields: [
248
- declaration.identity,
249
- declaration.name,
250
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
251
- ].map((field) => `/${escapePointer(field)}`),
252
- },
253
- limit: MAXIMUM_WINDOW,
254
- ...(after === undefined ? {} : { after }),
255
- }, roomFacts())),
256
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
257
257
  durably,
258
258
  stream: () => options.adapter.conversations,
259
259
  publish: publishChangeSet,
@@ -261,14 +261,22 @@ export function composeRuntime(options) {
261
261
  full: () => full,
262
262
  closed: () => closed,
263
263
  unanswered: resendLater,
264
- expired: (operation) => pastHorizon(operation),
265
264
  });
266
265
  const journal = createLog();
267
266
  const keptFiles = openedFiles(database, isFenced);
268
- let notifyReady = () => { };
267
+ let readyFromStore = () => { };
269
268
  const ready = new Promise((resolve) => {
270
- notifyReady = resolve;
269
+ readyFromStore = resolve;
270
+ });
271
+ let notifyDiscovered = () => { };
272
+ const firstDiscovery = new Promise((resolve) => {
273
+ notifyDiscovered = resolve;
271
274
  });
275
+ let roomsNamed = false;
276
+ function notifyReady() {
277
+ notifyDiscovered();
278
+ readyFromStore();
279
+ }
272
280
  function rememberRoomDraft(record) {
273
281
  const room = roomDrafts.get(record.locator) ?? new Map();
274
282
  room.set(record.path, record);
@@ -345,9 +353,9 @@ export function composeRuntime(options) {
345
353
  return value === undefined ? null : structuredClone(value);
346
354
  }
347
355
  async function writtenRoomDraft(room, path, revision, input, next) {
348
- const parts = readParts(input);
356
+ const parts = readParts(input, path);
349
357
  if (path === '' || normalize(path) !== path)
350
- throw runtimeError('room-draft-malformed');
358
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
351
359
  const local = await openLocal();
352
360
  const current = roomDrafts.get(room)?.get(path);
353
361
  if ((current?.revision ?? null) !== revision) {
@@ -355,7 +363,9 @@ export function composeRuntime(options) {
355
363
  }
356
364
  const file = parts.file === undefined
357
365
  ? undefined
358
- : await heldFile(parts.file.blob, binaryBase(await visibleOf(room, [path]), path));
366
+ : await heldFile(parts.file.blob, parts.file.base === undefined
367
+ ? binaryBase(await visibleOf(room, [path]), path)
368
+ : parts.file.base);
359
369
  const record = {
360
370
  locator: room,
361
371
  path,
@@ -390,6 +400,20 @@ export function composeRuntime(options) {
390
400
  (queuesWithoutReplica.size > 0 ||
391
401
  [...replicas.values()].some((replica) => replica.queue.length > 0)));
392
402
  }
403
+ function unsent() {
404
+ const changed = new Map();
405
+ for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
406
+ for (const { batch, state } of entriesOf(room)) {
407
+ if (state === 'local')
408
+ continue;
409
+ for (const { path } of batch.changes)
410
+ changed.set(`${room}\u0000${path}`, { room, path });
411
+ }
412
+ }
413
+ return [...changed]
414
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
415
+ .map(([, change]) => change);
416
+ }
393
417
  function attention() {
394
418
  const attentionRequired = [];
395
419
  for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
@@ -401,16 +425,9 @@ export function composeRuntime(options) {
401
425
  returned: structuredClone(record.returned),
402
426
  });
403
427
  }
404
- for (const [room, refusal] of sourceRefusals)
405
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
406
428
  attentionRequired.sort((left, right) => {
407
429
  if (left.room !== right.room)
408
430
  return left.room < right.room ? -1 : 1;
409
- if ('refusal' in left || 'refusal' in right) {
410
- if ('refusal' in left && 'refusal' in right)
411
- return 0;
412
- return 'refusal' in left ? -1 : 1;
413
- }
414
431
  if (left.path !== right.path)
415
432
  return left.path < right.path ? -1 : 1;
416
433
  return 0;
@@ -422,7 +439,7 @@ export function composeRuntime(options) {
422
439
  if (reset === null || submission.reset < reset)
423
440
  reset = submission.reset;
424
441
  }
425
- return { attentionRequired, reset };
442
+ return { attentionRequired, unsent: unsent(), reset };
426
443
  }
427
444
  function documentsIn(locator) {
428
445
  return replicas.get(locator)?.count ?? 0;
@@ -511,11 +528,26 @@ export function composeRuntime(options) {
511
528
  const value = listed();
512
529
  for (const listener of roomListeners)
513
530
  listener(value);
531
+ emitKnownRooms();
532
+ }
533
+ function emitKnownRooms() {
534
+ conversations.learned();
535
+ if (unlistedRooms.size === 0)
536
+ return;
537
+ if (!roomsListed && rooms.size === 0 && refusal === null && !status.providerUnreachable)
538
+ return;
539
+ const value = listed();
540
+ for (const listener of [...unlistedRooms]) {
541
+ unlistedRooms.delete(listener);
542
+ roomListeners.add(listener);
543
+ listener(value);
544
+ }
514
545
  }
515
546
  function emitRefusal() {
516
547
  queueReadiness();
517
548
  for (const listener of refusalListeners)
518
549
  listener(refusal);
550
+ emitKnownRooms();
519
551
  }
520
552
  function emitStorageLoss() {
521
553
  queueReadiness();
@@ -536,6 +568,7 @@ export function composeRuntime(options) {
536
568
  if (closed)
537
569
  return;
538
570
  queueReadiness();
571
+ emitKnownRooms();
539
572
  const next = status.state === 'synced' && !conversations.current()
540
573
  ? { ...status, state: 'syncing' }
541
574
  : status;
@@ -549,9 +582,6 @@ export function composeRuntime(options) {
549
582
  if (localSaves)
550
583
  return Object.freeze({ batch, state: 'local' });
551
584
  const state = pendings.get(pendingKey(locator, batch.id));
552
- if (state?.accepted !== undefined) {
553
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
554
- }
555
585
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
556
586
  }
557
587
  function entriesOf(locator) {
@@ -581,12 +611,12 @@ export function composeRuntime(options) {
581
611
  const key = held.record.key;
582
612
  const done = await exclusive(async () => {
583
613
  const local = await openLocal();
584
- return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes));
614
+ return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
585
615
  });
586
616
  const now = replicas.get(locator);
587
617
  if (now?.record.key !== key)
588
618
  continue;
589
- if (!done)
619
+ if (!done.done)
590
620
  continue;
591
621
  replicas.set(locator, { ...now, indexed: true });
592
622
  publishChangeSet(changeSet(locator, { documents: null }));
@@ -625,11 +655,7 @@ export function composeRuntime(options) {
625
655
  })();
626
656
  }
627
657
  function roomsOfReach(reach) {
628
- return new Set([
629
- ...reach.rooms,
630
- ...reach.documents.map(roomOfKey),
631
- ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
632
- ]);
658
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
633
659
  }
634
660
  function reachedRooms(plan) {
635
661
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -668,7 +694,7 @@ export function composeRuntime(options) {
668
694
  coverage.push({
669
695
  room: locator,
670
696
  generation: replica.record.transfer,
671
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
697
+ current: !(catching.has(locator) || transfers.has(locator)),
672
698
  });
673
699
  read.push({ locator, key: replica.record.key });
674
700
  for (const batch of replica.queue)
@@ -695,8 +721,6 @@ export function composeRuntime(options) {
695
721
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
696
722
  }
697
723
  function roomState(locator) {
698
- if (sourceRefusals.has(locator))
699
- return 'refused';
700
724
  const listedRoom = discovered.get(locator);
701
725
  if (roomsListed &&
702
726
  (listedRoom === undefined ||
@@ -711,7 +735,7 @@ export function composeRuntime(options) {
711
735
  }
712
736
  function conversationCoverage(reach) {
713
737
  const declared = reach === null || reach.conversations;
714
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
738
+ if (!declared || options.adapter.conversations === undefined)
715
739
  return 'undeclared';
716
740
  if (!conversations.known() || !conversations.current())
717
741
  return 'acquiring';
@@ -726,83 +750,50 @@ export function composeRuntime(options) {
726
750
  ? 'connected'
727
751
  : 'connecting';
728
752
  }
729
- function refusedIn(locators) {
730
- return locators.flatMap((room) => {
731
- const held = sourceRefusals.get(room);
732
- if (held === undefined)
733
- return [];
734
- const { file, location } = held.fields;
735
- return [
736
- {
737
- room,
738
- name: discovered.get(room)?.name ?? room,
739
- file: typeof file === 'string' ? file : '',
740
- rule: held.code,
741
- location: typeof location === 'string' ? location : null,
742
- },
743
- ];
744
- });
753
+ function bytesOf(room, underway) {
754
+ const transfer = transfers.get(room);
755
+ if (transfer !== undefined) {
756
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
757
+ return {
758
+ done: transfer.done.bytes - Math.ceil(owed),
759
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
760
+ };
761
+ }
762
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
763
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
745
764
  }
746
765
  function installationOf(coverage) {
747
766
  const rooms = { done: 0, total: 0 };
748
- const parts = { done: 0, total: 0 };
749
767
  const bytes = { done: 0, total: 0 };
750
- const received = { done: 0, total: 0 };
751
768
  let installing = 0;
752
- let unlisted = false;
753
- let listed = false;
754
- let waiting = false;
769
+ let downloading = false;
755
770
  for (const { room, state } of coverage) {
756
771
  if (state === 'unadmitted')
757
772
  continue;
758
773
  rooms.total += 1;
759
774
  if (state === 'complete')
760
775
  rooms.done += 1;
761
- const transfer = transfers.get(room);
762
- const replica = replicas.get(room)?.record.inventory ?? null;
763
- const inventory = transfer?.inventory ?? replica ?? installs.get(room) ?? null;
764
776
  const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
765
777
  if (underway)
766
778
  installing += 1;
767
- if (underway && inventory === null)
768
- unlisted = true;
769
- if (inventory !== null) {
770
- listed = true;
771
- const done = transfer?.done ?? (replica === null ? NOTHING_DONE : inventory);
772
- parts.total += inventory.parts;
773
- bytes.total += inventory.bytes;
774
- parts.done += done.parts;
775
- bytes.done += done.bytes;
776
- received.total += inventory.bytes;
777
- received.done += done.bytes;
778
- for (const held of [keptParts.get(room), arrivals.get(room)]) {
779
- for (const arrived of held?.values() ?? [])
780
- received.done += arrived;
781
- }
782
- }
783
- waiting ||= underway;
779
+ if (underway && (streaming.has(room) || !arrivals.has(room)))
780
+ downloading = true;
781
+ const now = bytesOf(room, underway);
782
+ const earlier = underway ? startedOver.get(room) : undefined;
783
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
784
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
784
785
  }
785
786
  const step = installing === 0
786
787
  ? null
787
- : !listed
788
- ? 'listing'
789
- : unlisted || received.done < received.total
790
- ? 'downloading'
791
- : bytes.done < bytes.total
792
- ? 'writing'
793
- : 'replaying';
794
- return {
795
- step,
796
- paused: waiting && !onScreen ? 'hidden' : null,
797
- rooms,
798
- parts,
799
- bytes,
800
- received,
801
- };
788
+ : downloading
789
+ ? 'downloading'
790
+ : bytes.done < bytes.total
791
+ ? 'writing'
792
+ : 'replaying';
793
+ return { step, rooms, bytes };
802
794
  }
803
795
  function readinessFor(port, reach) {
804
796
  const required = requiredRooms(reach);
805
- const search = port.search === 'required';
806
797
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
807
798
  const preparing = coverage
808
799
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -815,14 +806,12 @@ export function composeRuntime(options) {
815
806
  listed: required.listed,
816
807
  rooms: coverage,
817
808
  conversations: conversationCoverage(reach),
818
- search: search ? 'required' : 'unused',
819
809
  },
820
810
  persistence,
821
811
  connection: connectionOf(),
822
812
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
823
- space: full ? 'full' : port.space,
813
+ space: full ? 'full' : null,
824
814
  installation: installationOf(coverage),
825
- refused: refusedIn(required.rooms),
826
815
  lost: storageLoss !== null,
827
816
  established: targets[port.target]?.established === true ||
828
817
  (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
@@ -910,8 +899,12 @@ export function composeRuntime(options) {
910
899
  unpreparable.clear();
911
900
  journal.record('space freed');
912
901
  queueReadiness();
913
- if (!closed && authorization !== undefined)
914
- void sync('unstreamed');
902
+ if (closed || authorization === undefined)
903
+ return;
904
+ if (options.adapter.roomStream === undefined)
905
+ void sync('unstreamed', null);
906
+ else
907
+ void resynchronize();
915
908
  }
916
909
  function seedRoomHeld(room, version) {
917
910
  return indexed(room) && reachedBy(room, version);
@@ -1059,6 +1052,8 @@ export function composeRuntime(options) {
1059
1052
  }
1060
1053
  async function refuse(reason, provider = null) {
1061
1054
  discoveryPaused = true;
1055
+ inFlight.abort();
1056
+ inFlight = new AbortController();
1062
1057
  try {
1063
1058
  if (reason === 'revoked-authorization') {
1064
1059
  await exclusive(async () => {
@@ -1143,10 +1138,10 @@ export function composeRuntime(options) {
1143
1138
  providerOwnedPaths: room.providerOwnedPaths,
1144
1139
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1145
1140
  synchronizationAvailable,
1146
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1141
+ refusal: scopedRefusals.get(room.locator) ?? null,
1147
1142
  });
1148
1143
  }
1149
- async function applyDiscovery(found, started, covered) {
1144
+ async function applyDiscovery(found, started, left) {
1150
1145
  const lockedBefore = lockedRooms();
1151
1146
  for (const room of found) {
1152
1147
  const fence = fences.get(room.locator);
@@ -1160,18 +1155,11 @@ export function composeRuntime(options) {
1160
1155
  const listedLocators = new Set(listed.map((room) => room.locator));
1161
1156
  for (const locator of listedLocators)
1162
1157
  scopedRefusals.delete(locator);
1163
- if (covered === undefined)
1158
+ if (left === undefined)
1164
1159
  discovered.clear();
1165
- else
1166
- for (const locator of covered)
1167
- discovered.delete(locator);
1168
1160
  for (const room of listed)
1169
1161
  discovered.set(room.locator, room);
1170
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1171
- if (options.adapter.exhaustiveDiscovery === true)
1172
- await loseRooms(unlisted);
1173
- else
1174
- unlistedKept(unlisted);
1162
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1175
1163
  for (const room of listed) {
1176
1164
  const held = replicas.get(room.locator)?.record;
1177
1165
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1202,15 +1190,6 @@ export function composeRuntime(options) {
1202
1190
  ]),
1203
1191
  ].filter((locator) => !listed.has(locator));
1204
1192
  }
1205
- function unlistedKept(unlisted) {
1206
- for (const locator of unlisted) {
1207
- const held = replicas.get(locator)?.record;
1208
- if (held !== undefined)
1209
- list(held, false);
1210
- else
1211
- rooms.delete(locator);
1212
- }
1213
- }
1214
1193
  function fence(locators) {
1215
1194
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1216
1195
  const removed = fresh.map((locator) => ({
@@ -1221,24 +1200,26 @@ export function composeRuntime(options) {
1221
1200
  fences.set(locator, { at: discoveries, erased: false });
1222
1201
  lastFenced.set(locator, ++fencings);
1223
1202
  confirmed.delete(locator);
1224
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1203
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1225
1204
  pendings.delete(pendingKey(locator, batch.id));
1205
+ landedAhead.delete(pendingKey(locator, batch.id));
1206
+ }
1226
1207
  replicas.delete(locator);
1208
+ highest.delete(locator);
1209
+ beyondHorizon.delete(locator);
1227
1210
  queuesWithoutReplica.delete(locator);
1228
1211
  rooms.delete(locator);
1229
1212
  roomDrafts.delete(locator);
1230
1213
  attachedFiles.delete(locator);
1231
1214
  untransferred(locator);
1215
+ startedOver.delete(locator);
1232
1216
  catching.delete(locator);
1233
1217
  discovered.delete(locator);
1234
- announced.delete(locator);
1235
1218
  unreadable.delete(locator);
1236
1219
  stale.delete(locator);
1237
1220
  repairable.delete(locator);
1238
1221
  unpreparable.delete(locator);
1239
1222
  scopedRefusals.delete(locator);
1240
- sourceRefusals.delete(locator);
1241
- declarationRefusals.delete(locator);
1242
1223
  models.delete(locator);
1243
1224
  liveModels.delete(locator);
1244
1225
  invalidModels.delete(locator);
@@ -1292,8 +1273,7 @@ export function composeRuntime(options) {
1292
1273
  function retyped(persisted) {
1293
1274
  if (persisted.status === 'unavailable')
1294
1275
  return persisted;
1295
- const { state } = attachedModel({ kind: 'model', snapshot: persisted.snapshot }, NO_MODEL, roomTypes);
1296
- return state.status === 'unavailable' ? state : { ...persisted, types: state.types };
1276
+ return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
1297
1277
  }
1298
1278
  function restored(key, held) {
1299
1279
  const persisted = retyped(held);
@@ -1327,16 +1307,9 @@ export function composeRuntime(options) {
1327
1307
  const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1328
1308
  targets = held.targets;
1329
1309
  transfers.clear();
1330
- for (const [locator, record] of held.transfers) {
1331
- transfers.set(locator, {
1332
- ...record,
1333
- models: { collection: retyped(record.models.collection) },
1334
- });
1335
- }
1336
1310
  arrivals.clear();
1337
- keptParts.clear();
1338
- for (const [locator, parts] of held.kept)
1339
- keptParts.set(locator, new Map(parts));
1311
+ streaming.clear();
1312
+ startedOver.clear();
1340
1313
  limits = held.providerLimits;
1341
1314
  capabilities = held.capabilities;
1342
1315
  member = held.member;
@@ -1346,8 +1319,9 @@ export function composeRuntime(options) {
1346
1319
  presentations.clear();
1347
1320
  for (const record of held.presentation)
1348
1321
  presentations.set(record.app, record.value);
1349
- conversations.hydrate(heldConversations, chatDrafts);
1322
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1350
1323
  pendings.clear();
1324
+ landedAhead.clear();
1351
1325
  for (const queue of queues) {
1352
1326
  for (const { batch, state } of queue.pending) {
1353
1327
  pendings.set(pendingKey(queue.locator, batch.id), state);
@@ -1375,7 +1349,7 @@ export function composeRuntime(options) {
1375
1349
  });
1376
1350
  const existing = rooms.get(record.locator);
1377
1351
  if (existing === undefined)
1378
- list(record, false);
1352
+ list(record, record.roomVersion !== null);
1379
1353
  else {
1380
1354
  rooms.set(record.locator, {
1381
1355
  ...existing,
@@ -1389,8 +1363,14 @@ export function composeRuntime(options) {
1389
1363
  });
1390
1364
  }
1391
1365
  });
1392
- for (const key of held.orphans)
1393
- await opened.release(key);
1366
+ try {
1367
+ for (const key of held.orphans)
1368
+ await opened.release(key);
1369
+ }
1370
+ catch (error) {
1371
+ if (!refusedForSpace(error))
1372
+ throw error;
1373
+ }
1394
1374
  if (held.erasing || held.orphans.length > 0)
1395
1375
  queueMicrotask(sweep);
1396
1376
  return {
@@ -1490,14 +1470,12 @@ export function composeRuntime(options) {
1490
1470
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1491
1471
  };
1492
1472
  }
1493
- const REWRITE_LINKS = ['field', 'body'];
1494
- const CHECKED_LINKS = ['field', 'section'];
1495
1473
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1496
1474
  const local = await openLocal();
1497
1475
  const needed = new Set([...touchedBy(queue), ...extra]);
1498
1476
  const movedPaths = [...new Set([...movedBy(queue, record.locator), ...moved])];
1499
1477
  if (movedPaths.length > 0) {
1500
- for (const { path } of await local.referrers(record.key, view, movedPaths, REWRITE_LINKS)) {
1478
+ for (const { path } of await local.referrers(record.key, view, movedPaths)) {
1501
1479
  needed.add(path);
1502
1480
  }
1503
1481
  }
@@ -1592,8 +1570,6 @@ export function composeRuntime(options) {
1592
1570
  function unpairedIn(locator, batch) {
1593
1571
  if (!queueIn(locator).some((held) => held.id === batch.id))
1594
1572
  return false;
1595
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1596
- return false;
1597
1573
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1598
1574
  }
1599
1575
  async function together(local, settlements) {
@@ -1636,10 +1612,16 @@ export function composeRuntime(options) {
1636
1612
  ? read
1637
1613
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1638
1614
  const composition = composed(record);
1639
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1640
- const founded = kept.filter((batch) => !queue.includes(batch));
1615
+ const shown = queue.filter((batch) => {
1616
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1617
+ return (held !== undefined &&
1618
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1619
+ });
1620
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1621
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1622
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1641
1623
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1642
- const unqueued = [];
1624
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1643
1625
  for (const batch of withdrawn) {
1644
1626
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1645
1627
  }
@@ -1704,8 +1686,10 @@ export function composeRuntime(options) {
1704
1686
  commit,
1705
1687
  apply: (answer) => {
1706
1688
  for (const batch of previousQueue) {
1707
- if (!staying.has(batch.id))
1708
- pendings.delete(pendingKey(locator, batch.id));
1689
+ if (staying.has(batch.id))
1690
+ continue;
1691
+ pendings.delete(pendingKey(locator, batch.id));
1692
+ landedAhead.delete(pendingKey(locator, batch.id));
1709
1693
  }
1710
1694
  for (const draft of written.values())
1711
1695
  rememberRoomDraft(draft);
@@ -1754,36 +1738,38 @@ export function composeRuntime(options) {
1754
1738
  const answer = answers?.collection;
1755
1739
  if (answer === undefined)
1756
1740
  return held;
1757
- const { state, refused } = attachedModel(answer, held.collection, roomTypes);
1758
- if (refused !== null) {
1759
- const where = refused.location === undefined ? '' : ` ${refused.location}`;
1760
- journal.record(`model invalid ${locator} collection ${refused.identity} ${refused.rule}${where}`);
1761
- }
1741
+ const state = attachedModel(answer, roomTypes);
1762
1742
  models.set(locator, state);
1763
1743
  return { collection: state };
1764
1744
  }
1765
- function publish(local, room, record, landing, live) {
1745
+ function publish(local, room, reached, landing, live) {
1766
1746
  const landed = exclusive(async () => {
1767
1747
  if (!live()) {
1768
1748
  caughtUpTo(room.locator);
1769
1749
  return true;
1770
1750
  }
1771
1751
  const prior = replicas.get(room.locator);
1772
- if (landing.kind === 'refreshed' &&
1773
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1774
- await local.unrefresh(record.key);
1752
+ if (!stands(room.locator, landing.from)) {
1753
+ if (landing.kind === 'refreshed') {
1754
+ await local.unrefresh(reached.key);
1755
+ return false;
1756
+ }
1757
+ caughtUpTo(room.locator);
1758
+ await abandoned(local, room.locator);
1775
1759
  return false;
1776
1760
  }
1761
+ const record = landing.kind === 'refreshed' && prior !== undefined
1762
+ ? {
1763
+ ...prior.record,
1764
+ roomVersion: reached.roomVersion,
1765
+ ...(landing.remodelled ? { model: reached.model } : {}),
1766
+ }
1767
+ : reached;
1777
1768
  adoptModels(room.locator, record);
1778
1769
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1779
- const roomVersion = record.roomVersion;
1780
- const accepted = queue.filter((batch) => {
1781
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1782
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1783
- });
1784
1770
  let set;
1785
1771
  try {
1786
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1772
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1787
1773
  ? {
1788
1774
  room: true,
1789
1775
  view: 'refreshed',
@@ -1805,24 +1791,24 @@ export function composeRuntime(options) {
1805
1791
  return true;
1806
1792
  }
1807
1793
  if (landing.kind === 'bootstrapped') {
1794
+ highest.delete(room.locator);
1795
+ beyondHorizon.delete(room.locator);
1808
1796
  untransferred(room.locator);
1797
+ startedOver.delete(room.locator);
1809
1798
  catching.delete(room.locator);
1810
- sweep();
1799
+ if (prior !== undefined && prior.record.key !== record.key)
1800
+ sweep();
1801
+ name(room.locator);
1811
1802
  }
1812
- for (const batch of accepted)
1813
- pendings.delete(pendingKey(room.locator, batch.id));
1814
1803
  stale.delete(room.locator);
1815
1804
  unpreparable.delete(room.locator);
1816
1805
  if (unreadable.delete(room.locator))
1817
1806
  scopedRefusals.delete(room.locator);
1818
- sourceRefusals.delete(room.locator);
1819
- declarationRefusals.delete(room.locator);
1820
1807
  list(room, true);
1821
1808
  publishChangeSet(set);
1822
1809
  await unpair(local);
1823
1810
  moveTo({ ...status, ...attention() });
1824
- options.adapter.roomStream?.published(room.locator, roomVersion);
1825
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1811
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1826
1812
  submitLater(room.locator);
1827
1813
  }
1828
1814
  return true;
@@ -1834,7 +1820,7 @@ export function composeRuntime(options) {
1834
1820
  const replica = replicas.get(locator);
1835
1821
  const room = rooms.get(locator);
1836
1822
  if (replica === undefined || room === undefined) {
1837
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1823
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1838
1824
  }
1839
1825
  return { local, replica, room };
1840
1826
  }
@@ -1852,12 +1838,7 @@ export function composeRuntime(options) {
1852
1838
  return committed(locator, target, saved, new Set(saved.map(({ path }) => path)), writtenBy);
1853
1839
  }
1854
1840
  async function saveParts(locator, named, writtenBy) {
1855
- const read = named.map(({ path, parts }) => ({ path, parts: readParts(parts) }));
1856
- if (read.some(({ path }, at) => path === '' ||
1857
- normalize(path) !== path ||
1858
- read.findIndex((one) => one.path === path) !== at)) {
1859
- throw runtimeError('room-draft-malformed');
1860
- }
1841
+ const read = readSavedParts(named);
1861
1842
  const target = await savedRoom(locator);
1862
1843
  const saved = [];
1863
1844
  const files = [];
@@ -1866,7 +1847,7 @@ export function composeRuntime(options) {
1866
1847
  for (const { path, parts } of read) {
1867
1848
  const file = parts.file === undefined
1868
1849
  ? undefined
1869
- : await heldFile(parts.file.blob, binaryBase(visible, path));
1850
+ : await heldFile(parts.file.blob, parts.file.base === undefined ? binaryBase(visible, path) : parts.file.base);
1870
1851
  if (parts.file !== undefined && file !== undefined) {
1871
1852
  files.push({ key: file.localId, blob: parts.file.blob });
1872
1853
  }
@@ -1900,14 +1881,15 @@ export function composeRuntime(options) {
1900
1881
  const removed = changes.flatMap((change) => (change.kind === 'remove' ? [change.path] : []));
1901
1882
  const referring = removed.length === 0
1902
1883
  ? []
1903
- : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1904
- const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...referring], moved);
1884
+ : (await local.referrers(record.key, 'confirmed', removed)).map(({ path }) => path);
1885
+ const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1905
1886
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1906
1887
  const composition = composed(record);
1907
1888
  return {
1908
1889
  record,
1909
1890
  confirmed: confirmedLayer,
1910
1891
  visible,
1892
+ folders: new Set(await local.folders(record.key, 'visible', paths)),
1911
1893
  *visibleOver(queue) {
1912
1894
  return (yield* replaying(confirmedLayer, queue, record.locator, composition)).visible;
1913
1895
  },
@@ -1930,18 +1912,6 @@ export function composeRuntime(options) {
1930
1912
  return undefined;
1931
1913
  return entry.binary !== null ? null : entry.content;
1932
1914
  }
1933
- function walkOf(local, key, view) {
1934
- return async (after) => {
1935
- const page = await local.walk(key, view, after, WALK_BYTES);
1936
- return {
1937
- entries: page.entries.map(({ path, content, binary }) => ({
1938
- path,
1939
- content: binary !== null ? null : content,
1940
- })),
1941
- next: page.next,
1942
- };
1943
- };
1944
- }
1945
1915
  function lookupOf(local, key, view) {
1946
1916
  return async (paths) => {
1947
1917
  if (paths.length === 0)
@@ -1958,37 +1928,6 @@ export function composeRuntime(options) {
1958
1928
  const read = parseFrontmatter(entry.content ?? '');
1959
1929
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1960
1930
  }
1961
- function overriddenWalk(walk, overrides) {
1962
- const sorted = [...overrides.keys()].sort();
1963
- const beyond = (path) => {
1964
- if (path === null)
1965
- return 0;
1966
- let low = 0;
1967
- let high = sorted.length;
1968
- while (low < high) {
1969
- const middle = (low + high) >>> 1;
1970
- if (sorted[middle] <= path)
1971
- low = middle + 1;
1972
- else
1973
- high = middle;
1974
- }
1975
- return low;
1976
- };
1977
- return async (after) => {
1978
- const page = await walk(after);
1979
- const end = page.next;
1980
- const added = sorted
1981
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
1982
- .flatMap((path) => {
1983
- const entry = overrides.get(path);
1984
- return entry === null || entry === undefined
1985
- ? []
1986
- : [{ path, content: entry.binary !== null ? null : entry.content }];
1987
- });
1988
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1989
- return { entries, next: end };
1990
- };
1991
- }
1992
1931
  function overriddenLookup(lookup, overrides) {
1993
1932
  return async (paths) => {
1994
1933
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2006,9 +1945,8 @@ export function composeRuntime(options) {
2006
1945
  }
2007
1946
  function validatorOf(record, confirmedLayer) {
2008
1947
  return async (resulting, touched) => {
2009
- const composition = compositionFor(record);
2010
1948
  const local = await openLocal();
2011
- const changed = [];
1949
+ const changes = [];
2012
1950
  const overrides = new Map();
2013
1951
  const pace = paced();
2014
1952
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2019,19 +1957,13 @@ export function composeRuntime(options) {
2019
1957
  continue;
2020
1958
  overrides.set(path, now);
2021
1959
  if (textOf(was) !== textOf(now))
2022
- changed.push({ path, was: textOf(was), now: textOf(now) });
1960
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2023
1961
  }
2024
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2025
- const clean = available(composition) &&
2026
- changed.length <= CLEAN_CHANGES &&
2027
- (await changedCleanly(changed, composed(record), composition, {
2028
- lookup,
2029
- texts: textsOf(local, record.key, 'confirmed'),
2030
- referrers: referrersOf(local, record.key, 'confirmed'),
2031
- }));
2032
- if (clean)
2033
- return { verdict: 'accept' };
2034
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1962
+ return validatedNeighbourhood(changes, compositionFor(record), {
1963
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1964
+ texts: textsOf(local, record.key, 'confirmed'),
1965
+ referrers: referrersOf(local, record.key, 'confirmed'),
1966
+ });
2035
1967
  };
2036
1968
  }
2037
1969
  function textsOf(local, key, view) {
@@ -2042,7 +1974,7 @@ export function composeRuntime(options) {
2042
1974
  }
2043
1975
  function referrersOf(local, key, view) {
2044
1976
  return async (targets) => [
2045
- ...new Set((await local.referrers(key, view, targets, CHECKED_LINKS)).map(({ path }) => path)),
1977
+ ...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
2046
1978
  ];
2047
1979
  }
2048
1980
  async function sideOf(locator, replica, room, saving) {
@@ -2055,6 +1987,7 @@ export function composeRuntime(options) {
2055
1987
  operations: room.operations,
2056
1988
  providerOwnedPaths: room.providerOwnedPaths,
2057
1989
  visible: saving.visible,
1990
+ folders: saving.folders,
2058
1991
  queued: queuedDocuments(replica.queue),
2059
1992
  unmodeled: await unmodeledIn(record),
2060
1993
  validate: validatorOf(record, saving.confirmed),
@@ -2107,13 +2040,13 @@ export function composeRuntime(options) {
2107
2040
  }
2108
2041
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2109
2042
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2110
- const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, sourceSaving.visible)));
2043
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2111
2044
  if (changed.length > 0)
2112
2045
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2113
2046
  const target = replicas.get(destination);
2114
2047
  const listedRoom = rooms.get(destination);
2115
2048
  if (target === undefined || listedRoom === undefined) {
2116
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2049
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2117
2050
  }
2118
2051
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2119
2052
  const built = await buildMove({
@@ -2176,10 +2109,10 @@ export function composeRuntime(options) {
2176
2109
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2177
2110
  const { answer, touched } = committedBoth;
2178
2111
  if (answer.fenced !== null) {
2179
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2112
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2180
2113
  }
2181
2114
  if (fences.has(locator) || fences.has(destination)) {
2182
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2115
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2183
2116
  }
2184
2117
  pendings.set(pendingKey(locator, id), {
2185
2118
  drafts: saved,
@@ -2232,7 +2165,7 @@ export function composeRuntime(options) {
2232
2165
  ...saved.map(({ path }) => path),
2233
2166
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2234
2167
  ]);
2235
- const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, underneath(held, path, saving.visible)));
2168
+ const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2236
2169
  if (changed.length > 0)
2237
2170
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2238
2171
  const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
@@ -2260,6 +2193,7 @@ export function composeRuntime(options) {
2260
2193
  providerLimits: limits,
2261
2194
  handle: room.handle,
2262
2195
  visible: saving.visible,
2196
+ folders: saving.folders,
2263
2197
  changes,
2264
2198
  bases: parts.flatMap(({ bases }) => bases),
2265
2199
  queued: queuedDocuments(replica.queue),
@@ -2349,10 +2283,10 @@ export function composeRuntime(options) {
2349
2283
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2350
2284
  const { answer, touched } = done;
2351
2285
  if (answer.fenced !== null) {
2352
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2286
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2353
2287
  }
2354
2288
  if (fences.has(locator))
2355
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2289
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2356
2290
  for (const origin of withdrawn.keys())
2357
2291
  pendings.delete(pendingKey(locator, origin));
2358
2292
  for (const [id, drafts] of drafted) {
@@ -2401,7 +2335,7 @@ export function composeRuntime(options) {
2401
2335
  throw new Error('connection refused');
2402
2336
  await local.keepUpload(locator, identifier, upload);
2403
2337
  if (fences.has(locator))
2404
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2338
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2405
2339
  const key = pendingKey(locator, identifier);
2406
2340
  pendings.set(key, { ...pendings.get(key), upload });
2407
2341
  if (!continuing())
@@ -2410,6 +2344,9 @@ export function composeRuntime(options) {
2410
2344
  async function readHistory(locator, path, query = {}) {
2411
2345
  if (closed || refusal !== null)
2412
2346
  return UNCERTAIN;
2347
+ await firstDiscovery;
2348
+ if (!roomsNamed)
2349
+ return UNCERTAIN;
2413
2350
  try {
2414
2351
  await ensureAuthorization();
2415
2352
  }
@@ -2502,6 +2439,9 @@ export function composeRuntime(options) {
2502
2439
  async function transferBinary(request, transfer, answer) {
2503
2440
  if (closed || refusal !== null)
2504
2441
  return UNCERTAIN;
2442
+ await firstDiscovery;
2443
+ if (!roomsNamed)
2444
+ return UNCERTAIN;
2505
2445
  try {
2506
2446
  await ensureAuthorization();
2507
2447
  }
@@ -2648,7 +2588,8 @@ export function composeRuntime(options) {
2648
2588
  }
2649
2589
  async function attachBinary(locator, file) {
2650
2590
  await ready;
2651
- if (closed || connectionOf() === 'unreachable' || refusal !== null)
2591
+ await firstDiscovery;
2592
+ if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2652
2593
  return OFFLINE;
2653
2594
  const request = await attachedFile(locator, file);
2654
2595
  return transferBinary({ locator, path: file.name }, async (held) => {
@@ -2668,13 +2609,6 @@ export function composeRuntime(options) {
2668
2609
  const freed = await database.ask({ kind: 'opened.clean' });
2669
2610
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2670
2611
  }
2671
- function declaredAgents() {
2672
- for (const capability of capabilities) {
2673
- if (capability.conversationAgents !== undefined)
2674
- return capability.conversationAgents;
2675
- }
2676
- return undefined;
2677
- }
2678
2612
  function declaredAction(name) {
2679
2613
  for (const capability of capabilities) {
2680
2614
  for (const action of capability.actions ?? []) {
@@ -2726,6 +2660,24 @@ export function composeRuntime(options) {
2726
2660
  return UNCERTAIN;
2727
2661
  }
2728
2662
  }
2663
+ async function readConversationHistory(request) {
2664
+ await ready;
2665
+ const stream = options.adapter.conversations;
2666
+ if (closed || refusal !== null || stream?.history === undefined)
2667
+ return UNCERTAIN;
2668
+ journal.record('transfer conversation history');
2669
+ try {
2670
+ const result = await stream.history(request);
2671
+ if (result.kind === 'revoked-authorization')
2672
+ await refuse('revoked-authorization');
2673
+ journal.record(`outcome conversation history ${result.kind}`);
2674
+ return calledResult(result);
2675
+ }
2676
+ catch {
2677
+ journal.record('outcome conversation history transport-uncertainty');
2678
+ return UNCERTAIN;
2679
+ }
2680
+ }
2729
2681
  function unsettled(result) {
2730
2682
  return result.kind !== 'action' && result.retryable;
2731
2683
  }
@@ -2764,10 +2716,6 @@ export function composeRuntime(options) {
2764
2716
  continue;
2765
2717
  if (closed || signal.aborted)
2766
2718
  return 'done';
2767
- if (pastHorizon(held.operation)) {
2768
- await local.dropCall(held.operation);
2769
- continue;
2770
- }
2771
2719
  calling.add(held.operation);
2772
2720
  try {
2773
2721
  if (unsettled(await invokeAction(held.name, held.input)))
@@ -2787,6 +2735,33 @@ export function composeRuntime(options) {
2787
2735
  const held = replicas.get(locator);
2788
2736
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2789
2737
  }
2738
+ function shownAt(locator) {
2739
+ const held = replicas.get(locator)?.record.roomVersion;
2740
+ const raised = highest.get(locator);
2741
+ if (held === undefined || raised === undefined)
2742
+ return held ?? raised;
2743
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2744
+ }
2745
+ function showing(locator, roomVersion) {
2746
+ const at = shownAt(locator);
2747
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2748
+ highest.set(locator, roomVersion);
2749
+ }
2750
+ }
2751
+ function older(locator, roomVersion) {
2752
+ if (beyondHorizon.has(locator))
2753
+ return false;
2754
+ const at = shownAt(locator);
2755
+ return (at !== undefined &&
2756
+ options.adapter.reached(locator, at, roomVersion) &&
2757
+ !options.adapter.reached(locator, roomVersion, at));
2758
+ }
2759
+ function replaced(locators) {
2760
+ for (const locator of locators) {
2761
+ highest.delete(locator);
2762
+ beyondHorizon.add(locator);
2763
+ }
2764
+ }
2790
2765
  async function writtenAfter(locator, roomVersion, path) {
2791
2766
  const held = replicas.get(locator);
2792
2767
  if (held === undefined)
@@ -2797,115 +2772,27 @@ export function composeRuntime(options) {
2797
2772
  options.adapter.reached(locator, written, roomVersion) &&
2798
2773
  !options.adapter.reached(locator, roomVersion, written));
2799
2774
  }
2800
- function overriddenTexts(texts, overrides) {
2801
- return async (paths) => {
2802
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2803
- for (const path of paths) {
2804
- const entry = overrides.get(path);
2805
- if (entry === undefined)
2806
- continue;
2807
- if (entry === null)
2808
- known.delete(path);
2809
- else
2810
- known.set(path, entry.binary !== null ? null : entry.content);
2811
- }
2812
- return known;
2813
- };
2814
- }
2815
- async function unreadableRoom(texts, walk, paths) {
2816
- if (paths !== null) {
2817
- const text = {};
2818
- for (const [path, content] of await texts(paths))
2819
- if (content !== null)
2820
- text[path] = content;
2821
- return stepped(unreadableIn(text, Object.keys(text)));
2822
- }
2823
- for (let after = null;;) {
2824
- const page = await walk(after);
2825
- const text = {};
2826
- for (const { path, content } of page.entries)
2827
- if (content !== null)
2828
- text[path] = content;
2829
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2830
- if (refused !== null)
2831
- return refused;
2832
- if (page.next === null)
2833
- return null;
2834
- after = page.next;
2835
- }
2836
- }
2837
- async function validatedRoom(local, record, view, previous, overrides = null) {
2838
- const composition = composed(record);
2839
- const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2840
- const lookup = overrides === null
2841
- ? lookupOf(local, record.key, view)
2842
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2843
- const texts = overrides === null
2844
- ? textsOf(local, record.key, view)
2845
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2846
- const walk = overrides === null
2847
- ? walkOf(local, record.key, view)
2848
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2849
- let changes = null;
2850
- let reshaped = false;
2851
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2852
- const [was, now] = await Promise.all([
2853
- local.entries(record.key, 'confirmed', previous.changed),
2854
- overrides === null
2855
- ? local.entries(record.key, view, previous.changed)
2856
- : Promise.resolve([]),
2857
- ]);
2858
- const before = new Map(was.map((entry) => [entry.path, entry]));
2859
- const after = overrides === null
2860
- ? new Map(now.map((entry) => [entry.path, entry]))
2861
- : overrides;
2862
- changes = previous.changed.flatMap((path) => {
2863
- const one = before.get(path) ?? null;
2864
- const other = after.get(path) ?? null;
2865
- reshaped ||= textDiffers(one, other);
2866
- const held = textOf(one);
2867
- const next = textOf(other);
2868
- return held === next ? [] : [{ path, was: held, now: next }];
2869
- });
2870
- }
2871
- const named = changes === null ? null : changes.map(({ path }) => path);
2872
- const textChanged = changes === null || changes.length > 0 || reshaped;
2873
- const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2874
- if (modeled && (textChanged || modelsChanged)) {
2875
- const clean = previous !== null &&
2876
- changes !== null &&
2877
- (await changedCleanly(changes, composed(previous.record), composition, {
2878
- lookup,
2879
- texts,
2880
- referrers: referrersOf(local, record.key, 'confirmed'),
2881
- }));
2882
- if (clean)
2883
- return null;
2884
- const verdict = await validatedCollection(walk, lookup, composition);
2885
- if (verdict.verdict === 'accept')
2886
- return null;
2887
- if (verdict.rule !== 'model-unavailable')
2888
- return { kind: 'refused', refusal: verdict };
2889
- return unreadableRoom(texts, walk, named);
2890
- }
2891
- if (textChanged)
2892
- return unreadableRoom(texts, walk, named);
2893
- return null;
2775
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2776
+ if (paths.length === 0 || !older(locator, roomVersion))
2777
+ return new Set();
2778
+ const entries = await local.entries(key, 'confirmed', paths);
2779
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2780
+ options.adapter.reached(locator, written, roomVersion) &&
2781
+ !options.adapter.reached(locator, roomVersion, written)
2782
+ ? [path]
2783
+ : []));
2894
2784
  }
2895
- function refreshingOf(local, key, composition, liveness, replaying = null) {
2896
- const write = replaying === null ? exclusive : installWrite;
2785
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2786
+ const write = replaying ? finishingWrite : exclusive;
2897
2787
  return {
2898
- async turn() {
2899
- if (replaying === null)
2900
- return;
2901
- installing(replaying);
2902
- await screened();
2788
+ turn() {
2789
+ if (replaying)
2790
+ installing(locator);
2903
2791
  },
2904
2792
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2793
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2905
2794
  async stage(page) {
2906
2795
  for (let written = 0;;) {
2907
- if (replaying !== null)
2908
- await screened();
2909
2796
  const end = batchEnd(page.entries, written);
2910
2797
  const last = end === page.entries.length;
2911
2798
  const entries = page.entries.slice(written, end);
@@ -2950,25 +2837,74 @@ export function composeRuntime(options) {
2950
2837
  return done;
2951
2838
  }
2952
2839
  function installWrite(work) {
2953
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
2954
- installWrites = next.then(() => undefined, () => undefined);
2955
- return next;
2956
- }
2957
- function screened() {
2958
- if (onScreen || closed)
2959
- return Promise.resolve();
2960
- queueReadiness();
2961
- return new Promise((resolve) => shownWaiters.push(resolve));
2840
+ return installTurn(installTurns.starting, work);
2841
+ }
2842
+ function finishingWrite(work) {
2843
+ return installTurn(installTurns.finishing, work);
2844
+ }
2845
+ function installTurn(queue, work) {
2846
+ return new Promise((resolve, reject) => {
2847
+ queue.push(() => void exclusive(work)
2848
+ .then(resolve, reject)
2849
+ .finally(() => {
2850
+ installTurns.busy = false;
2851
+ nextInstallTurn();
2852
+ }));
2853
+ nextInstallTurn();
2854
+ });
2962
2855
  }
2963
- async function heldPart() {
2964
- while (partsFree === 0 || queuedBytes >= QUEUED_BYTES) {
2965
- await new Promise((admit) => partWaiters.push(admit));
2856
+ function nextInstallTurn() {
2857
+ if (installTurns.busy || installTurns.ahead > 0)
2858
+ return;
2859
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
2860
+ if (turn === undefined)
2861
+ return;
2862
+ installTurns.busy = true;
2863
+ turn();
2864
+ }
2865
+ async function ahead(work) {
2866
+ let taken = false;
2867
+ const hold = {
2868
+ take() {
2869
+ if (taken)
2870
+ return;
2871
+ taken = true;
2872
+ installTurns.ahead += 1;
2873
+ },
2874
+ release() {
2875
+ if (!taken)
2876
+ return;
2877
+ taken = false;
2878
+ installTurns.ahead -= 1;
2879
+ resumeInstall();
2880
+ },
2881
+ };
2882
+ hold.take();
2883
+ try {
2884
+ return await work(hold);
2966
2885
  }
2967
- partsFree -= 1;
2886
+ finally {
2887
+ hold.release();
2888
+ }
2889
+ }
2890
+ function resumeInstall() {
2891
+ if (installTurns.ahead > 0)
2892
+ return;
2893
+ void yielded().then(nextInstallTurn);
2894
+ }
2895
+ async function heldStream() {
2896
+ while (streamsFree === 0)
2897
+ await new Promise((admit) => installWaiters.push(admit));
2898
+ streamsFree -= 1;
2968
2899
  return once(() => {
2969
- partsFree += 1;
2900
+ streamsFree += 1;
2970
2901
  });
2971
2902
  }
2903
+ async function unqueued() {
2904
+ while (queuedText >= QUEUED_TEXT) {
2905
+ await new Promise((admit) => installWaiters.push(admit));
2906
+ }
2907
+ }
2972
2908
  function once(release) {
2973
2909
  let released = false;
2974
2910
  return () => {
@@ -2976,58 +2912,86 @@ export function composeRuntime(options) {
2976
2912
  return;
2977
2913
  released = true;
2978
2914
  release();
2979
- for (const admit of partWaiters.splice(0))
2915
+ for (const admit of installWaiters.splice(0))
2980
2916
  admit();
2981
2917
  };
2982
2918
  }
2983
- function installing(locator, inventory = null) {
2984
- installs.set(locator, inventory ?? installs.get(locator) ?? null);
2919
+ function installing(locator) {
2920
+ installs.add(locator);
2985
2921
  queueReadiness();
2986
2922
  }
2987
2923
  function untransferred(locator) {
2988
2924
  transfers.delete(locator);
2989
- keptParts.delete(locator);
2925
+ arrivals.delete(locator);
2926
+ streaming.delete(locator);
2990
2927
  underived.delete(locator);
2928
+ unindexed.delete(locator);
2929
+ }
2930
+ function setBack(locator) {
2931
+ const now = bytesOf(locator, true);
2932
+ const earlier = startedOver.get(locator);
2933
+ startedOver.set(locator, {
2934
+ done: Math.max(now.done, earlier?.done ?? 0),
2935
+ total: Math.max(now.total, earlier?.total ?? 0),
2936
+ });
2937
+ }
2938
+ function owedIndexes(locator, bytes, text) {
2939
+ if (bytes <= 0 || text <= 0)
2940
+ return;
2941
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
2942
+ }
2943
+ function built(locator, { done, text }) {
2944
+ const owed = unindexed.get(locator) ?? [];
2945
+ let left = text;
2946
+ while (!done && left > 0 && owed.length > 0) {
2947
+ const [first] = owed;
2948
+ if (left >= first.text) {
2949
+ left -= first.text;
2950
+ owed.shift();
2951
+ continue;
2952
+ }
2953
+ owed[0] = {
2954
+ bytes: (first.bytes * (first.text - left)) / first.text,
2955
+ text: first.text - left,
2956
+ };
2957
+ left = 0;
2958
+ }
2959
+ if (done || owed.length === 0)
2960
+ unindexed.delete(locator);
2961
+ queueReadiness();
2991
2962
  }
2992
2963
  function installed(locator) {
2993
2964
  if (installs.delete(locator))
2994
2965
  queueReadiness();
2995
2966
  }
2996
- function screen(value) {
2997
- if (value === onScreen)
2998
- return;
2999
- onScreen = value;
3000
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3001
- queueReadiness();
3002
- if (!value)
3003
- return;
3004
- for (const resolve of shownWaiters.splice(0))
3005
- resolve();
3006
- }
3007
- async function pullRefresh(authorized, held, liveness) {
2967
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3008
2968
  if (!liveness())
3009
2969
  return unconfirmed;
3010
- const turn = generation;
3011
2970
  const { record } = held;
3012
- const base = held.confirmed;
2971
+ const from = standingOf(record.locator);
3013
2972
  const local = await openLocal();
3014
2973
  try {
3015
2974
  await exclusive(() => local.unrefresh(record.key));
3016
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2975
+ const result = await advancement(async (authorization, request) => {
2976
+ hold.release();
2977
+ try {
2978
+ return await refresh(authorization, request);
2979
+ }
2980
+ finally {
2981
+ hold.take();
2982
+ }
2983
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3017
2984
  if (result.kind !== 'advanced') {
3018
2985
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
3019
2986
  return result;
3020
2987
  }
3021
- const refused = await validatedRoom(local, result.record, 'refreshed', {
3022
- record,
3023
- changed: [...result.changed],
3024
- });
3025
- if (refused !== null) {
3026
- journal.record(`transfer refresh ${record.locator} refused`);
3027
- return refused;
3028
- }
3029
2988
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3030
- return { kind: 'advanced', record: result.record, base };
2989
+ return {
2990
+ kind: 'advanced',
2991
+ record: result.record,
2992
+ from,
2993
+ remodelled: result.record.model !== record.model,
2994
+ };
3031
2995
  }
3032
2996
  catch (error) {
3033
2997
  if (refusedForSpace(error))
@@ -3035,9 +2999,6 @@ export function composeRuntime(options) {
3035
2999
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
3036
3000
  throw error;
3037
3001
  }
3038
- finally {
3039
- reads.finished(record.locator, turn);
3040
- }
3041
3002
  }
3042
3003
  const heldContents = new Map();
3043
3004
  function heldContentOf(local, key) {
@@ -3055,29 +3016,26 @@ export function composeRuntime(options) {
3055
3016
  heldContents.set(key, made);
3056
3017
  return made;
3057
3018
  }
3058
- async function keptTogether({ local, parts }) {
3059
- const live = parts.filter((one) => one.live() && transfers.has(one.locator));
3060
- await local.keepParts(live.map(({ locator, token, part }) => ({ locator, token, part })));
3061
- for (const { locator, token, part } of live) {
3062
- const held = keptParts.get(locator) ?? new Map();
3063
- keptParts.set(locator, held.set(token, part.byteLength));
3064
- arrivals.get(locator)?.delete(token);
3065
- }
3066
- queueReadiness();
3019
+ async function abandoned(local, locator) {
3020
+ await local.abandon(locator);
3021
+ setBack(locator);
3022
+ untransferred(locator);
3023
+ liveQueries.recover();
3024
+ sweep();
3067
3025
  }
3068
3026
  function staging(local, liveness) {
3069
3027
  const derived = async (locator, key, composition) => {
3070
3028
  underived.delete(locator);
3071
3029
  for (;;) {
3072
- await screened();
3073
3030
  if (!liveness())
3074
3031
  return false;
3075
- const done = await installWrite(async () => liveness()
3076
- ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes))
3032
+ const done = await finishingWrite(async () => liveness()
3033
+ ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3077
3034
  : null);
3078
3035
  if (done === null)
3079
3036
  return false;
3080
- if (done)
3037
+ built(locator, done);
3038
+ if (done.done)
3081
3039
  return true;
3082
3040
  }
3083
3041
  };
@@ -3085,28 +3043,23 @@ export function composeRuntime(options) {
3085
3043
  held: (locator) => transfers.get(locator) ?? null,
3086
3044
  async turn(locator, step) {
3087
3045
  installing(locator);
3088
- for (;;) {
3089
- await screened();
3090
- const release = step === 'downloading' ? await heldPart() : () => { };
3091
- if (onScreen || closed)
3092
- return release;
3093
- release();
3094
- }
3046
+ return step === 'downloading' ? heldStream() : () => { };
3095
3047
  },
3096
- queued(bytes) {
3097
- queuedBytes += bytes;
3048
+ unqueued,
3049
+ queued(text) {
3050
+ queuedText += text;
3098
3051
  return once(() => {
3099
- queuedBytes -= bytes;
3052
+ queuedText -= text;
3100
3053
  });
3101
3054
  },
3102
3055
  arriving(locator) {
3103
3056
  let ended = false;
3104
3057
  return {
3105
- at(token, bytes) {
3106
- if (ended || keptParts.get(locator)?.has(token) === true)
3058
+ at(bytes) {
3059
+ if (ended)
3107
3060
  return;
3108
- const held = arrivals.get(locator) ?? new Map();
3109
- arrivals.set(locator, held.set(token, bytes));
3061
+ arrivals.set(locator, bytes);
3062
+ streaming.add(locator);
3110
3063
  const now = performance.now();
3111
3064
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
3112
3065
  return;
@@ -3114,45 +3067,38 @@ export function composeRuntime(options) {
3114
3067
  queueReadiness();
3115
3068
  },
3116
3069
  end() {
3070
+ if (ended)
3071
+ return;
3117
3072
  ended = true;
3118
- if (arrivals.delete(locator))
3073
+ if (streaming.delete(locator))
3119
3074
  queueReadiness();
3120
3075
  },
3121
3076
  };
3122
3077
  },
3123
- received: (locator, token) => local.transferPart(locator, token),
3124
- keep: (locator, token, received) => gathered('kept parts', { local, parts: [{ locator, token, part: received, live: liveness }] }, (earlier, later) => earlier.local === later.local
3125
- ? { local: earlier.local, parts: [...earlier.parts, ...later.parts] }
3126
- : null, keptTogether),
3127
- announced(locator) {
3128
- const later = announced.get(locator);
3129
- return later === undefined ? [] : [later];
3130
- },
3131
- async stage(record, page, composition, written) {
3078
+ streamed: options.adapter.roomStream !== undefined,
3079
+ async stage(record, page, composition) {
3132
3080
  const { locator } = record;
3133
- installing(locator, record.inventory);
3134
- await screened();
3081
+ installing(locator);
3135
3082
  if (!liveness())
3136
3083
  return null;
3137
3084
  const key = await installWrite(async () => {
3138
3085
  if (!liveness())
3139
3086
  return null;
3140
- return local.stage(record, composition, page, written ?? undefined);
3087
+ return local.stage(record, composition, page);
3141
3088
  });
3142
3089
  if (key === null || !liveness())
3143
3090
  return null;
3144
3091
  const arrived = !transfers.has(locator);
3092
+ const prior = transfers.get(locator)?.done.bytes ?? 0;
3145
3093
  const held = { ...record, key };
3146
3094
  transfers.set(locator, held);
3147
- if (written !== null) {
3148
- keptParts.get(locator)?.delete(written);
3149
- arrivals.get(locator)?.delete(written);
3150
- }
3151
3095
  if (arrived)
3152
3096
  liveQueries.recover();
3153
3097
  installing(locator);
3154
3098
  liveQueries.recover();
3155
- const owed = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), underived.get(locator) ?? 0);
3099
+ const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3100
+ owedIndexes(locator, record.done.bytes - prior, text);
3101
+ const owed = text + (underived.get(locator) ?? 0);
3156
3102
  underived.set(locator, owed);
3157
3103
  if (owed < deriveBudget.bytes)
3158
3104
  return held;
@@ -3161,29 +3107,24 @@ export function composeRuntime(options) {
3161
3107
  derive: derived,
3162
3108
  async refreshing(locator, key, composition) {
3163
3109
  await exclusive(() => local.unrefresh(key));
3164
- return refreshingOf(local, key, composition, liveness, locator);
3110
+ return refreshingOf(local, locator, key, composition, liveness, true);
3165
3111
  },
3166
3112
  abandon: (locator) => exclusive(async () => {
3167
- if (!liveness())
3168
- return;
3169
- await local.abandon(locator);
3170
- untransferred(locator);
3171
- liveQueries.recover();
3172
- sweep();
3113
+ if (liveness())
3114
+ await abandoned(local, locator);
3173
3115
  }),
3174
- setAside: (locator) => exclusive(async () => {
3116
+ setAside: async (locator) => {
3175
3117
  if (!liveness())
3176
3118
  return;
3177
- await local.abandon(locator, true);
3119
+ setBack(locator);
3178
3120
  untransferred(locator);
3179
3121
  liveQueries.recover();
3180
- }),
3122
+ },
3181
3123
  release: (key) => exclusive(async () => {
3182
3124
  await local.release(key);
3183
3125
  sweep();
3184
3126
  }),
3185
3127
  content: (key) => heldContentOf(local, key),
3186
- validate: (record, view) => validatedRoom(local, record, view, null),
3187
3128
  };
3188
3129
  }
3189
3130
  function reusable(room, local) {
@@ -3195,15 +3136,21 @@ export function composeRuntime(options) {
3195
3136
  async function pullBootstrap(authorized, room, local, liveness) {
3196
3137
  if (!liveness())
3197
3138
  return unconfirmed;
3198
- const turn = generation;
3199
3139
  if (replicas.has(room.locator)) {
3200
3140
  catching.add(room.locator);
3201
3141
  liveQueries.recover();
3202
3142
  }
3203
3143
  try {
3204
- 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));
3144
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3205
3145
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3206
3146
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3147
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3148
+ caughtUpTo(room.locator);
3149
+ installed(room.locator);
3150
+ await abandoned(local, room.locator);
3151
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3152
+ return { kind: 'transport-uncertainty' };
3153
+ }
3207
3154
  if (result.kind !== 'staged') {
3208
3155
  caughtUpTo(room.locator);
3209
3156
  installed(room.locator);
@@ -3218,9 +3165,6 @@ export function composeRuntime(options) {
3218
3165
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3219
3166
  throw error;
3220
3167
  }
3221
- finally {
3222
- reads.finished(room.locator, turn);
3223
- }
3224
3168
  }
3225
3169
  function caughtUpTo(locator) {
3226
3170
  if (catching.delete(locator))
@@ -3228,72 +3172,83 @@ export function composeRuntime(options) {
3228
3172
  }
3229
3173
  function behind(room) {
3230
3174
  const held = replicas.get(room.locator)?.record.roomVersion;
3231
- return (held !== undefined &&
3232
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3175
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3233
3176
  }
3234
- async function advance(room, local, authorized, live) {
3235
- const carried = reads.carried(room.locator);
3236
- const withheld = await advanceOnce(room, local, authorized, live);
3237
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3238
- return withheld;
3239
- }
3240
- return advanceOnce(room, local, authorized, live);
3177
+ function standingOf(locator) {
3178
+ const held = replicas.get(locator);
3179
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3180
+ }
3181
+ function stands(locator, from) {
3182
+ const now = standingOf(locator);
3183
+ return now.key === from.key && now.confirmed === from.confirmed;
3241
3184
  }
3242
- async function advanceOnce(room, local, authorized, running) {
3185
+ async function advance(room, local, authorized, running) {
3243
3186
  const live = () => running() && !full;
3244
3187
  if (!live())
3245
3188
  return unconfirmed;
3246
3189
  const held = replicas.get(room.locator);
3247
- if (held !== undefined) {
3248
- const refreshed = await pullRefresh(authorized, held, live);
3249
- if (!live())
3250
- return unconfirmed;
3251
- if (refreshed.kind === 'still-current') {
3252
- stale.delete(room.locator);
3253
- if (sourceRefusals.has(room.locator))
3254
- return null;
3255
- if (unreadable.delete(room.locator)) {
3256
- scopedRefusals.delete(room.locator);
3257
- list(room, true);
3258
- emitRooms();
3259
- }
3260
- return null;
3261
- }
3262
- if (refreshed.kind === 'advanced') {
3263
- const landing = { kind: 'refreshed', base: refreshed.base };
3264
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3265
- const now = replicas.get(room.locator)?.record.roomVersion;
3266
- const reached = now !== undefined &&
3267
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3268
- return reached ? null : advance(room, local, authorized, live);
3269
- }
3270
- await retainModel(local, room, modelOf(room.locator));
3271
- return null;
3272
- }
3273
- if (refreshed.kind !== 'beyond-horizon')
3190
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3191
+ stale.delete(room.locator);
3192
+ name(room.locator);
3193
+ return null;
3194
+ }
3195
+ const refresh = reads.refresh;
3196
+ if (held !== undefined && refresh !== undefined) {
3197
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3198
+ if (refreshed === 'behind')
3199
+ return advance(room, local, authorized, live);
3200
+ if (refreshed !== 'beyond-horizon')
3274
3201
  return refreshed;
3275
- if (opened !== null) {
3276
- owed.add(room.locator);
3277
- liveQueries.recover();
3278
- return unconfirmed;
3279
- }
3280
- if (reloadsDue.get(room.locator) === generation)
3202
+ if (reloadsDue.has(room.locator))
3281
3203
  return unconfirmed;
3204
+ replaced([room.locator]);
3282
3205
  }
3206
+ const from = standingOf(room.locator);
3283
3207
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3284
- if (bootstrapped.kind === 'staged') {
3285
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3208
+ if (bootstrapped.kind !== 'staged')
3209
+ return live() ? bootstrapped : unconfirmed;
3210
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3211
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3286
3212
  return live() ? null : unconfirmed;
3287
3213
  }
3288
- return live() ? bootstrapped : unconfirmed;
3214
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3215
+ ? null
3216
+ : advance(room, local, authorized, live);
3217
+ }
3218
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3219
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3220
+ if (!live())
3221
+ return unconfirmed;
3222
+ if (refreshed.kind === 'still-current') {
3223
+ stale.delete(room.locator);
3224
+ if (unreadable.delete(room.locator)) {
3225
+ scopedRefusals.delete(room.locator);
3226
+ list(room, true);
3227
+ emitRooms();
3228
+ }
3229
+ return null;
3230
+ }
3231
+ if (refreshed.kind === 'advanced') {
3232
+ const landing = {
3233
+ kind: 'refreshed',
3234
+ from: refreshed.from,
3235
+ remodelled: refreshed.remodelled,
3236
+ };
3237
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3238
+ const now = replicas.get(room.locator)?.record.roomVersion;
3239
+ const reached = now !== undefined &&
3240
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3241
+ return reached ? null : 'behind';
3242
+ }
3243
+ await retainModel(local, room, modelOf(room.locator));
3244
+ return null;
3245
+ }
3246
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3289
3247
  }
3290
3248
  async function withhold(withheld, room, local, progress) {
3291
- if (withheld.kind === 'unconfirmed')
3249
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3292
3250
  return;
3293
- if (withheld.kind === 'refused')
3294
- stale.delete(room.locator);
3295
- else
3296
- stale.add(room.locator);
3251
+ stale.add(room.locator);
3297
3252
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3298
3253
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3299
3254
  repairable.set(room.locator, reset);
@@ -3315,33 +3270,11 @@ export function composeRuntime(options) {
3315
3270
  list(room, false);
3316
3271
  emitRooms();
3317
3272
  }
3318
- else if (withheld.kind === 'refused') {
3319
- refusedSource(room, withheld);
3320
- }
3321
3273
  else if (withheld.kind === 'revoked-authorization') {
3322
3274
  await refuse('revoked-authorization');
3323
3275
  }
3324
3276
  }
3325
- function refusedSource(room, { refusal }) {
3326
- sourceRefusals.set(room.locator, {
3327
- code: refusal.rule,
3328
- retryable: false,
3329
- fields: {
3330
- file: refusal.file,
3331
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3332
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3333
- },
3334
- });
3335
- declarationRefusals.delete(room.locator);
3336
- unreadable.add(room.locator);
3337
- list(room, false);
3338
- emitRooms();
3339
- moveTo({ ...status, ...attention() });
3340
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3341
- }
3342
3277
  function leaving(batch, result) {
3343
- if (result.kind === 'expired')
3344
- return { batch, returned: returnedBy('expired') };
3345
3278
  return {
3346
3279
  batch,
3347
3280
  returned: {
@@ -3360,8 +3293,8 @@ export function composeRuntime(options) {
3360
3293
  function settling(acceptance) {
3361
3294
  return acceptance !== null && 'apply' in acceptance;
3362
3295
  }
3363
- function refusalOf(acceptance) {
3364
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3296
+ function failureOf(acceptance) {
3297
+ return settling(acceptance) ? null : acceptance;
3365
3298
  }
3366
3299
  async function acceptance(room, local, submitted, accepted) {
3367
3300
  if (!continuing())
@@ -3374,44 +3307,17 @@ export function composeRuntime(options) {
3374
3307
  logAccepted(room.locator, submitted, accepted);
3375
3308
  let record = current.record;
3376
3309
  let changes;
3377
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3310
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3311
+ !older(room.locator, accepted.roomVersion)) {
3378
3312
  const paths = [...returnedPaths(submitted, accepted)];
3379
3313
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3380
- const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
3314
+ const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
3381
3315
  record = {
3382
3316
  ...record,
3383
3317
  roomVersion: options.adapter.follows(room.locator, held, accepted.roomVersion)
3384
3318
  ? accepted.roomVersion
3385
3319
  : held,
3386
3320
  };
3387
- const overrides = new Map([
3388
- ...back.set.map((entry) => [entry.path, entry]),
3389
- ...back.removed.map((path) => [path, null]),
3390
- ]);
3391
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3392
- if (refused !== null) {
3393
- const key = pendingKey(room.locator, submitted.id);
3394
- return {
3395
- locator: room.locator,
3396
- left: [],
3397
- commit: {
3398
- rooms: [],
3399
- writes: [
3400
- {
3401
- locator: room.locator,
3402
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3403
- },
3404
- ],
3405
- },
3406
- refused,
3407
- apply() {
3408
- const state = pendings.get(key);
3409
- if (state !== undefined && !fences.has(room.locator))
3410
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3411
- return changeSet(room.locator, { queue: true });
3412
- },
3413
- };
3414
- }
3415
3321
  changes = back;
3416
3322
  }
3417
3323
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3419,10 +3325,9 @@ export function composeRuntime(options) {
3419
3325
  ...settled,
3420
3326
  apply(answer) {
3421
3327
  const set = settled.apply(answer);
3328
+ showing(room.locator, accepted.roomVersion);
3422
3329
  pendings.delete(pendingKey(room.locator, submitted.id));
3423
3330
  unreadable.delete(room.locator);
3424
- sourceRefusals.delete(room.locator);
3425
- declarationRefusals.delete(room.locator);
3426
3331
  list(room, true);
3427
3332
  return set;
3428
3333
  },
@@ -3440,13 +3345,9 @@ export function composeRuntime(options) {
3440
3345
  const failed = await exclusive(async () => {
3441
3346
  const adopting = await acceptance(room, local, submitted, accepted);
3442
3347
  await adoptedTogether(local, [adopting]);
3443
- return refusalOf(adopting);
3348
+ return failureOf(adopting);
3444
3349
  });
3445
- if (failed === null)
3446
- return true;
3447
- if (failed.kind === 'refused' && continuing())
3448
- refusedSource(room, failed);
3449
- return false;
3350
+ return failed === null;
3450
3351
  }
3451
3352
  async function adoptedMove(room, partner, local, submitted, accepted) {
3452
3353
  const other = discovered.get(partner);
@@ -3460,18 +3361,10 @@ export function composeRuntime(options) {
3460
3361
  ...accepted.destination,
3461
3362
  });
3462
3363
  await adoptedTogether(local, [left, arrived]);
3463
- const refusedArrival = refusalOf(arrived);
3464
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3465
- refusedSource(other, refusedArrival);
3466
- }
3467
- return refusalOf(left);
3364
+ return failureOf(left);
3468
3365
  });
3469
3366
  submitLater(partner);
3470
- if (failed === null)
3471
- return true;
3472
- if (failed.kind === 'refused' && continuing())
3473
- refusedSource(room, failed);
3474
- return false;
3367
+ return failed === null;
3475
3368
  }
3476
3369
  function queueIn(locator) {
3477
3370
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3582,20 +3475,13 @@ export function composeRuntime(options) {
3582
3475
  return;
3583
3476
  moveTo({ ...status, ...attention() });
3584
3477
  }
3585
- async function discardHeld(head, room, local, signal) {
3586
- const partner = partnerOf(head, room.locator);
3587
- const other = partner === null ? undefined : discovered.get(partner);
3588
- await discardPending(other === undefined ? [room] : [room, other], local, signal);
3589
- }
3590
3478
  async function submitRoom(room, local, authorized, progress, signal) {
3591
3479
  while (continuing() && authorization !== undefined && !signal.aborted) {
3592
3480
  const replica = replicas.get(room.locator);
3593
- const head = replica?.queue[0];
3481
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3594
3482
  if (replica === undefined || head === undefined)
3595
3483
  return 'done';
3596
3484
  const state = pendings.get(pendingKey(room.locator, head.id));
3597
- if (state?.accepted !== undefined)
3598
- return 'done';
3599
3485
  const partner = partnerOf(head, room.locator);
3600
3486
  if (partner !== null && !leavingIn(head, room.locator)) {
3601
3487
  submitLater(partner);
@@ -3619,10 +3505,6 @@ export function composeRuntime(options) {
3619
3505
  return 'done';
3620
3506
  }
3621
3507
  const lost = earlier || submission.state === 'submitted';
3622
- if (lost && pastHorizon(head.id)) {
3623
- await discardHeld(head, room, local, signal);
3624
- return 'done';
3625
- }
3626
3508
  let result;
3627
3509
  journal.record(`submission ${room.locator} ${head.id}`);
3628
3510
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
@@ -3700,32 +3582,12 @@ export function composeRuntime(options) {
3700
3582
  const other = partner === null ? undefined : discovered.get(partner);
3701
3583
  if (result.kind === 'conflicted' ||
3702
3584
  result.kind === 'rejected' ||
3703
- result.kind === 'expired' ||
3704
3585
  result.kind === 'retryable-refusal') {
3705
- if (result.kind === 'expired' && lost && result.restored !== true) {
3706
- journal.record(`outcome ${room.locator} ${head.id} expired`);
3707
- await discardHeld(head, room, local, signal);
3708
- return 'done';
3709
- }
3710
- if (result.kind === 'expired') {
3711
- await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3712
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3713
- if (other !== undefined && there !== null) {
3714
- await withhold(there, other, local, progress);
3715
- return 'done';
3716
- }
3717
- const withheld = await advance(room, local, authorized, continuing);
3718
- if (withheld !== null) {
3719
- await withhold(withheld, room, local, progress);
3720
- return 'done';
3721
- }
3722
- }
3723
3586
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
3724
3587
  (result.kind === 'conflicted' && result.roomVersion !== undefined
3725
3588
  ? ` ${result.roomVersion}`
3726
3589
  : ''));
3727
- if (result.kind !== 'expired')
3728
- journal.record(`refusal ${result.kind} ${result.code}`);
3590
+ journal.record(`refusal ${result.kind} ${result.code}`);
3729
3591
  }
3730
3592
  if (result.kind === 'retryable-refusal') {
3731
3593
  const reset = result.reset;
@@ -3733,11 +3595,11 @@ export function composeRuntime(options) {
3733
3595
  await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3734
3596
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
3735
3597
  }
3736
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3598
+ if (result.kind === 'conflicted' || result.kind === 'rejected') {
3737
3599
  const left = leaving(head, result);
3738
3600
  const refreshed = async () => ({
3739
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3740
- here: await advance(room, local, authorized, continuing),
3601
+ there: other === undefined ? null : await advanced(other, local, authorized),
3602
+ here: await advanced(room, local, authorized),
3741
3603
  });
3742
3604
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3743
3605
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3757,16 +3619,19 @@ export function composeRuntime(options) {
3757
3619
  return;
3758
3620
  }
3759
3621
  const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3760
- const held = (result.kind === 'expired' &&
3761
- (await landedWhole(room.locator, head, confirmedLayer))) ||
3762
- (result.kind === 'conflicted' &&
3763
- (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3622
+ const held = result.kind === 'conflicted' &&
3623
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected));
3624
+ if (held &&
3625
+ result.kind === 'conflicted' &&
3626
+ partner === null &&
3627
+ !landed(head, confirmedLayer)) {
3628
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3629
+ return;
3630
+ }
3764
3631
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3765
3632
  await unpair(local);
3766
3633
  moveTo({ ...status, ...attention() });
3767
3634
  });
3768
- if (result.kind === 'expired')
3769
- continue;
3770
3635
  const { there, here } = judged ?? (await refreshed());
3771
3636
  if (other !== undefined && there !== null)
3772
3637
  await withhold(there, other, local, progress);
@@ -3786,94 +3651,55 @@ export function composeRuntime(options) {
3786
3651
  }
3787
3652
  return 'done';
3788
3653
  }
3789
- function pastHorizon(identity) {
3790
- return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
3791
- }
3792
- async function discardPending(rooms, local, signal) {
3793
- const discarded = await exclusive(async () => {
3794
- const held = rooms
3795
- .filter(({ locator }) => !fences.has(locator))
3796
- .map((room) => ({
3797
- room,
3798
- before: replicas.get(room.locator),
3799
- queue: queueIn(room.locator),
3800
- }));
3801
- if (held.length === 0)
3802
- return [];
3803
- const answer = await local.commit({
3804
- rooms: held.flatMap(({ room, before }) => before === undefined
3805
- ? []
3806
- : [
3807
- {
3808
- locator: room.locator,
3809
- key: before.record.key,
3810
- roots: before.record.typedSelection?.roots ?? [],
3811
- composed: composedOf(composed(before.record)),
3812
- overlay: { set: [], cleared: [...before.overlaid] },
3813
- },
3814
- ]),
3815
- discards: held.map(({ room }) => room.locator),
3816
- });
3817
- for (const { room, before, queue } of held) {
3818
- const { locator } = room;
3819
- lastFenced.set(locator, ++fencings);
3820
- for (const batch of queue)
3821
- pendings.delete(pendingKey(locator, batch.id));
3822
- queuesWithoutReplica.delete(locator);
3823
- if (before !== undefined) {
3824
- replicas.set(locator, {
3825
- ...before,
3826
- queue: [],
3827
- overlaid: new Set(),
3828
- count: answer.counts[locator] ?? before.count,
3829
- });
3830
- }
3831
- journal.record(`room discarded ${locator}`);
3832
- }
3833
- publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3834
- documents: answer.changed[room.locator] ?? [],
3835
- queue: true,
3836
- }))));
3837
- moveTo({ ...status, ...attention() });
3838
- return held.map(({ room }) => room);
3839
- });
3840
- for (const room of discarded) {
3841
- if (signal.aborted)
3842
- return;
3843
- const progress = { complete: true, reachable: true };
3844
- await roomTask(room.locator, async (live) => {
3845
- await Promise.race([
3846
- reload(room, local, progress, () => live() && !signal.aborted),
3847
- abandonedOn(signal),
3848
- ]);
3849
- });
3850
- if (signal.aborted)
3851
- return;
3852
- await rest(progress);
3853
- }
3854
- }
3855
3654
  function awaitingAnswer(locator) {
3655
+ answering.get(locator)?.release();
3856
3656
  let release = () => { };
3857
- const awaited = new Promise((resolve) => {
3657
+ const answered = new Promise((resolve) => {
3858
3658
  release = resolve;
3859
3659
  });
3860
- answering.set(locator, { generation, answered: awaited });
3660
+ const awaited = { answered, release };
3661
+ answering.set(locator, awaited);
3861
3662
  return () => {
3862
- if (answering.get(locator)?.answered === awaited)
3663
+ if (answering.get(locator) === awaited)
3863
3664
  answering.delete(locator);
3864
3665
  release();
3865
3666
  };
3866
3667
  }
3668
+ function declaredAsHeld(room) {
3669
+ const held = replicas.get(room.locator)?.record;
3670
+ return (held === undefined ||
3671
+ (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3672
+ JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3673
+ JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3674
+ }
3675
+ async function keepNaming(local, room) {
3676
+ const held = replicas.get(room.locator)?.record;
3677
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3678
+ return;
3679
+ await exclusive(async () => {
3680
+ if (!continuing())
3681
+ return;
3682
+ const replica = replicas.get(room.locator);
3683
+ if (replica === undefined)
3684
+ return;
3685
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3686
+ await local.commit({
3687
+ rooms: [
3688
+ {
3689
+ locator: room.locator,
3690
+ key: record.key,
3691
+ record: storedRecord(record),
3692
+ composed: composedOf(composed(record)),
3693
+ },
3694
+ ],
3695
+ });
3696
+ replicas.set(room.locator, { ...replica, record });
3697
+ });
3698
+ }
3867
3699
  async function retainModel(local, room, next) {
3868
3700
  const key = room.locator;
3869
3701
  const previous = modelOf(room.locator);
3870
- const heldBefore = replicas.get(room.locator)?.record;
3871
- if (JSON.stringify(previous) === JSON.stringify(next) &&
3872
- (heldBefore === undefined ||
3873
- (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
3874
- JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
3875
- JSON.stringify(heldBefore.typedSelection) ===
3876
- JSON.stringify(room.typedSelection ?? null))))
3702
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3877
3703
  return;
3878
3704
  if (next.status === 'unavailable') {
3879
3705
  invalidModels.add(key);
@@ -3885,26 +3711,15 @@ export function composeRuntime(options) {
3885
3711
  const held = replicas.get(room.locator);
3886
3712
  let changed = null;
3887
3713
  if (held !== undefined) {
3888
- const declared = {
3714
+ const record = {
3889
3715
  ...held.record,
3716
+ name: room.name,
3717
+ handle: room.handle,
3890
3718
  operations: room.operations,
3891
3719
  replica: room.replica,
3892
3720
  typedSelection: room.typedSelection ?? null,
3893
3721
  model: next,
3894
3722
  };
3895
- let read = 'staged';
3896
- if (!sameMembership(declared, held.record)) {
3897
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
3898
- read =
3899
- verdict.verdict !== 'refuse'
3900
- ? 'staged'
3901
- : verdict.rule === 'model-unavailable'
3902
- ? 'deferred'
3903
- : { kind: 'refused', refusal: verdict };
3904
- }
3905
- const record = read === 'staged'
3906
- ? declared
3907
- : { ...declared, typedSelection: held.record.typedSelection };
3908
3723
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3909
3724
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3910
3725
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -3926,29 +3741,22 @@ export function composeRuntime(options) {
3926
3741
  documents: answer.changed[room.locator] ?? [],
3927
3742
  room: true,
3928
3743
  });
3929
- if (read === 'staged' || read === 'deferred') {
3930
- if (declarationRefusals.delete(room.locator)) {
3931
- sourceRefusals.delete(room.locator);
3932
- unreadable.delete(room.locator);
3933
- }
3934
- }
3935
- else {
3936
- refusedSource(room, read);
3937
- declarationRefusals.add(room.locator);
3938
- }
3939
- }
3940
- models.set(key, next);
3941
- if (next.status === 'current') {
3942
- liveModels.set(key, JSON.stringify(next.snapshot));
3943
- invalidModels.delete(key);
3944
3744
  }
3945
- list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3745
+ heldModel(room, next);
3946
3746
  if (changed === null)
3947
3747
  emitRooms();
3948
3748
  else
3949
3749
  publishChangeSet(changed);
3950
3750
  });
3951
3751
  }
3752
+ function heldModel(room, next) {
3753
+ models.set(room.locator, next);
3754
+ if (next.status === 'current') {
3755
+ liveModels.set(room.locator, JSON.stringify(next.snapshot));
3756
+ invalidModels.delete(room.locator);
3757
+ }
3758
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3759
+ }
3952
3760
  async function scoped(result, room, local) {
3953
3761
  if (result.scope === 'content' && !result.retryable) {
3954
3762
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3977,6 +3785,17 @@ export function composeRuntime(options) {
3977
3785
  emitRooms();
3978
3786
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3979
3787
  }
3788
+ function name(locator) {
3789
+ const stream = options.adapter.roomStream;
3790
+ const record = replicas.get(locator)?.record;
3791
+ const room = discovered.get(locator);
3792
+ if (stream === undefined || record === undefined || room === undefined)
3793
+ return;
3794
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3795
+ return;
3796
+ naming += 1;
3797
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3798
+ }
3980
3799
  function heldRooms(listed) {
3981
3800
  return listed.flatMap((room) => {
3982
3801
  const record = replicas.get(room.locator)?.record;
@@ -3994,14 +3813,19 @@ export function composeRuntime(options) {
3994
3813
  return outcome?.status === 'fulfilled' && outcome.value;
3995
3814
  });
3996
3815
  if (attempt === 'opened') {
3816
+ naming += 1;
3997
3817
  options.adapter.roomStream?.hold(heldRooms(ready));
3998
3818
  handed = true;
3999
3819
  }
4000
- discovering?.release();
4001
- const loaded = await Promise.allSettled(ready
4002
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
4003
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
4004
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3820
+ for (const room of ready) {
3821
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3822
+ if (attempt === 'authorized')
3823
+ name(room.locator);
3824
+ continue;
3825
+ }
3826
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3827
+ }
3828
+ const failed = prepared.find((room) => room.status === 'rejected');
4005
3829
  if (failed !== undefined)
4006
3830
  throw failed.reason;
4007
3831
  }
@@ -4020,12 +3844,16 @@ export function composeRuntime(options) {
4020
3844
  }
4021
3845
  if (prior !== undefined)
4022
3846
  await retainModel(local, room, modelOf(room.locator));
3847
+ if (prior !== undefined)
3848
+ await keepNaming(local, room);
4023
3849
  if (discoveryPaused || authorization === undefined)
4024
3850
  return false;
4025
3851
  return room.replica.admission === 'admitted';
4026
3852
  }
4027
3853
  async function caughtUp(room, local, progress, live) {
4028
- if (authorization === undefined || !live() || !behind(room))
3854
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3855
+ return;
3856
+ if (!live() || !behind(room))
4029
3857
  return;
4030
3858
  const withheld = await advance(room, local, authorization, live);
4031
3859
  if (withheld !== null)
@@ -4038,6 +3866,8 @@ export function composeRuntime(options) {
4038
3866
  async function loadRoom(room, local, progress, live) {
4039
3867
  if (!live() || authorization === undefined)
4040
3868
  return;
3869
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3870
+ return;
4041
3871
  if (replicas.has(room.locator) && revoked(room.locator)) {
4042
3872
  await reload(room, local, progress, live);
4043
3873
  return;
@@ -4055,20 +3885,53 @@ export function composeRuntime(options) {
4055
3885
  reloadsDue.delete(room.locator);
4056
3886
  if (!live() || authorization === undefined)
4057
3887
  return;
3888
+ const from = standingOf(room.locator);
4058
3889
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
4059
3890
  if (bootstrapped.kind !== 'staged') {
4060
3891
  if (live())
4061
3892
  await withhold(bootstrapped, room, local, progress);
4062
3893
  return;
4063
3894
  }
4064
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3895
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4065
3896
  await caughtUp(room, local, progress, live);
4066
3897
  }
4067
3898
  finally {
4068
3899
  caughtUpTo(room.locator);
4069
3900
  }
4070
3901
  }
4071
- async function discoverRooms(local, progress, attempt, live, scope) {
3902
+ async function asked(authorized) {
3903
+ try {
3904
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3905
+ }
3906
+ catch {
3907
+ return { kind: 'transport-uncertainty' };
3908
+ }
3909
+ }
3910
+ function malformed(listed) {
3911
+ for (const room of listed) {
3912
+ if (room.operations === undefined ||
3913
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3914
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3915
+ room.replica === undefined ||
3916
+ typeof room.replica.selection !== 'string' ||
3917
+ room.replica.selection.length === 0 ||
3918
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3919
+ !['locked', 'open'].includes(room.replica.offline)) {
3920
+ return `invalid model or replica declaration ${room.locator}`;
3921
+ }
3922
+ if (!declaredSelection(room))
3923
+ return `invalid typed selection declaration ${room.locator}`;
3924
+ }
3925
+ return null;
3926
+ }
3927
+ async function signIn(local, signedIn) {
3928
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3929
+ return;
3930
+ await local.fact('member', signedIn);
3931
+ member = signedIn;
3932
+ emitMember();
3933
+ }
3934
+ async function discoverRooms(local, progress, attempt, live, found) {
4072
3935
  if (closed || authorization === undefined) {
4073
3936
  notifyReady();
4074
3937
  return;
@@ -4076,13 +3939,9 @@ export function composeRuntime(options) {
4076
3939
  moveTo(transferring(status));
4077
3940
  discoveries += 1;
4078
3941
  const started = discoveries;
4079
- let result;
4080
- try {
4081
- result = await options.adapter.discover(authorization, scope);
4082
- }
4083
- catch {
4084
- result = { kind: 'transport-uncertainty' };
4085
- }
3942
+ const result = found ?? (await asked(authorization));
3943
+ if (result.kind === 'discovery')
3944
+ roomsNamed = true;
4086
3945
  if (!live()) {
4087
3946
  progress.complete = false;
4088
3947
  notifyReady();
@@ -4090,9 +3949,7 @@ export function composeRuntime(options) {
4090
3949
  }
4091
3950
  journal.record(`connection discover ${result.kind}`);
4092
3951
  if (result.kind === 'transport-uncertainty') {
4093
- for (const room of rooms.values()) {
4094
- await retainModel(local, room, cached(modelOf(room.locator)));
4095
- }
3952
+ await cachedModels(local);
4096
3953
  progress.reachable = false;
4097
3954
  progress.complete = false;
4098
3955
  notifyReady();
@@ -4113,26 +3970,12 @@ export function composeRuntime(options) {
4113
3970
  notifyReady();
4114
3971
  return;
4115
3972
  }
4116
- for (const room of result.rooms) {
4117
- if (room.operations === undefined ||
4118
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4119
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
4120
- room.replica === undefined ||
4121
- typeof room.replica.selection !== 'string' ||
4122
- room.replica.selection.length === 0 ||
4123
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4124
- !['locked', 'open'].includes(room.replica.offline)) {
4125
- journal.record(`invalid model or replica declaration ${room.locator}`);
4126
- await refuse('missing-required-guarantee');
4127
- notifyReady();
4128
- return;
4129
- }
4130
- if (!declaredSelection(room)) {
4131
- journal.record(`invalid typed selection declaration ${room.locator}`);
4132
- await refuse('missing-required-guarantee');
4133
- notifyReady();
4134
- return;
4135
- }
3973
+ const invalid = malformed(result.rooms);
3974
+ if (invalid !== null) {
3975
+ journal.record(invalid);
3976
+ await refuse('missing-required-guarantee');
3977
+ notifyReady();
3978
+ return;
4136
3979
  }
4137
3980
  if (refusal !== null) {
4138
3981
  refusal = null;
@@ -4140,7 +3983,6 @@ export function composeRuntime(options) {
4140
3983
  }
4141
3984
  const previousCapabilities = capabilities;
4142
3985
  capabilities = result.capabilities;
4143
- outcomeRetention = result.retention.outcome;
4144
3986
  notifyReady();
4145
3987
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
4146
3988
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
@@ -4148,13 +3990,8 @@ export function composeRuntime(options) {
4148
3990
  await local.fact('providerLimits', limits);
4149
3991
  await local.fact('capabilities', capabilities);
4150
3992
  }
4151
- const signedIn = result.member ?? null;
4152
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4153
- await local.fact('member', signedIn);
4154
- member = signedIn;
4155
- emitMember();
4156
- }
4157
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
3993
+ await signIn(local, result.member ?? null);
3994
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4158
3995
  }
4159
3996
  async function ensureAuthorization() {
4160
3997
  if (authorization !== undefined)
@@ -4169,23 +4006,45 @@ export function composeRuntime(options) {
4169
4006
  }
4170
4007
  }
4171
4008
  async function admittedRead(work) {
4172
- if (roomReads.generation !== generation) {
4173
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4009
+ while (roomReads.free === 0) {
4010
+ await new Promise((admit) => roomReads.waiting.push(admit));
4174
4011
  }
4175
- const reads = roomReads;
4176
- while (reads.free === 0)
4177
- await new Promise((admit) => reads.waiting.push(admit));
4178
- reads.free -= 1;
4012
+ roomReads.free -= 1;
4179
4013
  try {
4180
4014
  await work();
4181
4015
  }
4182
4016
  finally {
4183
- reads.free += 1;
4184
- reads.waiting.shift()?.();
4017
+ roomReads.free += 1;
4018
+ roomReads.waiting.shift()?.();
4185
4019
  }
4186
4020
  }
4021
+ function inRoom(locator, work) {
4022
+ roomWork += 1;
4023
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4024
+ try {
4025
+ return await work(continuing);
4026
+ }
4027
+ finally {
4028
+ roomWork -= 1;
4029
+ }
4030
+ });
4031
+ const chain = ran.then(() => undefined, () => undefined);
4032
+ roomLanes.set(locator, chain);
4033
+ working.add(chain);
4034
+ void chain.finally(() => {
4035
+ working.delete(chain);
4036
+ if (roomLanes.get(locator) === chain)
4037
+ roomLanes.delete(locator);
4038
+ });
4039
+ return ran;
4040
+ }
4187
4041
  function roomTask(locator, work) {
4188
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4042
+ if (stopping)
4043
+ return Promise.resolve();
4044
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4045
+ }
4046
+ function advanced(room, local, authorized) {
4047
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4189
4048
  }
4190
4049
  function task(lane, work) {
4191
4050
  if (stopping)
@@ -4201,23 +4060,19 @@ export function composeRuntime(options) {
4201
4060
  void run.finally(() => working.delete(run));
4202
4061
  return run;
4203
4062
  }
4204
- function sync(attempt) {
4063
+ function sync(attempt, found) {
4205
4064
  return task(OPENING_LANE, async (live) => {
4206
4065
  if (!live()) {
4207
- notifyReady();
4066
+ readyFromStore();
4208
4067
  return;
4209
4068
  }
4210
4069
  const progress = { complete: true, reachable: true };
4211
- const gate = discover();
4212
- if (attempt === 'opened')
4213
- opened = generation;
4214
- underway = attempt === 'opened' ? { generation, progress } : null;
4070
+ underway = attempt === 'opened' ? progress : null;
4215
4071
  if (attempt === 'opened')
4216
4072
  streamed = progress;
4217
4073
  complete = false;
4218
4074
  recorded = false;
4219
4075
  moveTo(transferring(status));
4220
- handed = false;
4221
4076
  try {
4222
4077
  const local = await openLocal();
4223
4078
  if (!live())
@@ -4225,7 +4080,7 @@ export function composeRuntime(options) {
4225
4080
  await ensureAuthorization();
4226
4081
  if (!live())
4227
4082
  return;
4228
- await discoverRooms(local, progress, attempt, live);
4083
+ await discoverRooms(local, progress, attempt, live, found);
4229
4084
  }
4230
4085
  catch {
4231
4086
  progress.complete = false;
@@ -4233,7 +4088,6 @@ export function composeRuntime(options) {
4233
4088
  progress.reachable = false;
4234
4089
  }
4235
4090
  finally {
4236
- gate.release();
4237
4091
  notifyReady();
4238
4092
  }
4239
4093
  if (!live())
@@ -4245,35 +4099,59 @@ export function composeRuntime(options) {
4245
4099
  if (handed || closed || progress.reachable)
4246
4100
  return;
4247
4101
  underway = null;
4248
- opened = null;
4249
- journal.record('connection reopen');
4250
4102
  await rest(progress, live);
4251
- options.adapter.roomStream?.reopen();
4252
4103
  });
4253
4104
  }
4254
- function rediscover(scope) {
4105
+ function resynchronize() {
4255
4106
  return task(OPENING_LANE, async (live) => {
4256
- if (!live() || !socketOpen)
4107
+ if (!live() || !announced)
4257
4108
  return;
4258
4109
  const progress = { complete: true, reachable: true };
4259
- const gate = discover();
4260
4110
  moveTo(transferring(status));
4261
4111
  try {
4262
4112
  const local = await openLocal();
4263
4113
  if (!live())
4264
4114
  return;
4265
- await ensureAuthorization();
4115
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4116
+ }
4117
+ catch {
4118
+ progress.complete = false;
4119
+ }
4120
+ if (live())
4121
+ await finishOpening(progress, live);
4122
+ });
4123
+ }
4124
+ function countAuthorityChange() {
4125
+ authorityChanges += 1;
4126
+ for (const listener of authorityChangeListeners)
4127
+ listener(authorityChanges);
4128
+ }
4129
+ function changeAuthority(change) {
4130
+ return task(OPENING_LANE, async (live) => {
4131
+ if (!live() || !announced)
4132
+ return;
4133
+ countAuthorityChange();
4134
+ const progress = { complete: true, reachable: true };
4135
+ moveTo(transferring(status));
4136
+ try {
4137
+ const local = await openLocal();
4266
4138
  if (!live())
4267
4139
  return;
4268
- await discoverRooms(local, progress, 'authorized', live, scope);
4140
+ const invalid = malformed(change.rooms);
4141
+ if (invalid !== null) {
4142
+ journal.record(invalid);
4143
+ await refuse('missing-required-guarantee');
4144
+ return;
4145
+ }
4146
+ if (change.member !== undefined)
4147
+ await signIn(local, change.member);
4148
+ discoveries += 1;
4149
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4150
+ await synchronize(listed, local, progress, 'authorized', live);
4269
4151
  }
4270
4152
  catch {
4271
4153
  progress.complete = false;
4272
4154
  }
4273
- finally {
4274
- gate.release();
4275
- notifyReady();
4276
- }
4277
4155
  if (!live())
4278
4156
  return;
4279
4157
  if (!progress.complete)
@@ -4281,14 +4159,17 @@ export function composeRuntime(options) {
4281
4159
  await rest(progress, live);
4282
4160
  });
4283
4161
  }
4284
- function closedSocket(revoked, lost) {
4285
- const opened = socketOpen;
4162
+ function closedSocket(revoked, lost, authorityChanged) {
4163
+ notifyReady();
4164
+ const opened = announced;
4286
4165
  socketOpen = false;
4166
+ announced = false;
4167
+ missedChange ||= authorityChanged;
4168
+ handed = false;
4169
+ naming = 0;
4287
4170
  connectionLost();
4288
- if (lost) {
4289
- reads.lost();
4171
+ if (lost)
4290
4172
  conversations.lost();
4291
- }
4292
4173
  if (revoked) {
4293
4174
  void refuse('revoked-authorization');
4294
4175
  return;
@@ -4299,18 +4180,32 @@ export function composeRuntime(options) {
4299
4180
  moveTo({
4300
4181
  ...status,
4301
4182
  state: status.state === 'synced' ? 'syncing' : status.state,
4302
- providerUnreachable: status.providerUnreachable || !opened,
4183
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4303
4184
  });
4185
+ void closing(opened);
4304
4186
  }
4305
- function applyOpening(expired) {
4306
- const progress = underway?.generation === generation ? underway.progress : null;
4187
+ function closing(opened) {
4188
+ return task(OPENING_LANE, async (live) => {
4189
+ if (!live())
4190
+ return;
4191
+ const local = await openLocal();
4192
+ if (!opened && !socketOpen)
4193
+ await cachedModels(local);
4194
+ await rest(null, live);
4195
+ });
4196
+ }
4197
+ function cachedModels(local) {
4198
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4199
+ }
4200
+ function caughtUpRooms(expired) {
4201
+ const progress = underway;
4202
+ underway = null;
4203
+ naming = Math.max(0, naming - 1);
4307
4204
  const cycle = progress ?? { complete: true, reachable: true };
4308
- if (opened === generation)
4309
- opened = null;
4205
+ replaced(expired);
4310
4206
  const reloading = progress === null ? [] : owedReloads(expired);
4311
4207
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4312
4208
  return task(OPENING_LANE, async (live) => {
4313
- underway = null;
4314
4209
  if (!live())
4315
4210
  return;
4316
4211
  if (progress === null) {
@@ -4318,24 +4213,14 @@ export function composeRuntime(options) {
4318
4213
  await rest(cycle, live);
4319
4214
  return;
4320
4215
  }
4321
- await roomsCarried([...expired, ...reloading]);
4322
- if (!live())
4323
- return;
4324
4216
  try {
4325
4217
  const local = await openLocal();
4326
- const reloaded = await replacing;
4218
+ await replacing;
4327
4219
  if (progress.reachable) {
4328
- for (const room of discovered.values()) {
4329
- if (replicas.has(room.locator) &&
4330
- !expired.includes(room.locator) &&
4331
- !reloading.includes(room.locator)) {
4332
- await confirmModels(room, local);
4333
- }
4334
- }
4220
+ await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4221
+ !expired.includes(room.locator) &&
4222
+ !reloading.includes(room.locator)), 'current', local);
4335
4223
  }
4336
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4337
- if (failed !== undefined)
4338
- throw failed.reason;
4339
4224
  }
4340
4225
  catch {
4341
4226
  progress.complete = false;
@@ -4347,11 +4232,8 @@ export function composeRuntime(options) {
4347
4232
  }
4348
4233
  function owedReloads(expired) {
4349
4234
  const reloading = [...discovered.values()]
4350
- .filter((room) => replicas.has(room.locator) &&
4351
- !expired.includes(room.locator) &&
4352
- (owed.has(room.locator) || revoked(room.locator)))
4235
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4353
4236
  .map((room) => room.locator);
4354
- owed.clear();
4355
4237
  for (const locator of reloading)
4356
4238
  catching.add(locator);
4357
4239
  liveQueries.recover();
@@ -4360,22 +4242,44 @@ export function composeRuntime(options) {
4360
4242
  async function reloadNow(replaced, progress) {
4361
4243
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4362
4244
  for (const room of reloaded)
4363
- reloadsDue.set(room.locator, generation);
4245
+ reloadsDue.add(room.locator);
4364
4246
  try {
4365
4247
  const local = await openLocal();
4366
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4248
+ for (const room of reloaded) {
4249
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4250
+ }
4367
4251
  }
4368
4252
  catch {
4369
4253
  for (const room of reloaded)
4370
4254
  reloadsDue.delete(room.locator);
4371
4255
  progress.reachable = false;
4372
- return [];
4373
4256
  }
4374
4257
  }
4375
- async function confirmModels(room, local) {
4376
- const model = modelOf(room.locator);
4377
- if (model.status === 'cached')
4378
- await retainModel(local, room, { ...model, status: 'current' });
4258
+ async function settleModels(held, status, local) {
4259
+ const owed = (room) => {
4260
+ const model = modelOf(room.locator);
4261
+ return model.status !== 'unavailable' && model.status !== status;
4262
+ };
4263
+ const settled = await exclusive(async () => {
4264
+ if (!continuing())
4265
+ return [];
4266
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4267
+ for (const room of unchanged) {
4268
+ const next = { ...modelOf(room.locator), status };
4269
+ const replica = replicas.get(room.locator);
4270
+ if (replica !== undefined)
4271
+ replicas.set(room.locator, { ...replica, record: { ...replica.record, model: next } });
4272
+ heldModel(room, next);
4273
+ }
4274
+ if (unchanged.length > 0)
4275
+ emitRooms();
4276
+ return unchanged;
4277
+ });
4278
+ for (const room of held) {
4279
+ if (settled.includes(room) || !owed(room))
4280
+ continue;
4281
+ await retainModel(local, room, { ...modelOf(room.locator), status });
4282
+ }
4379
4283
  }
4380
4284
  function submitLater(locator) {
4381
4285
  if (localSaves)
@@ -4419,14 +4323,10 @@ export function composeRuntime(options) {
4419
4323
  try {
4420
4324
  const local = await openLocal();
4421
4325
  await roomTask(locator, async (live) => {
4422
- const current = () => live() && !signal.aborted;
4423
- if (!current() || authorization === undefined)
4326
+ if (!live() || authorization === undefined)
4424
4327
  return;
4425
- const withheld = await Promise.race([
4426
- advance(room, local, authorization, current),
4427
- abandonedOn(signal),
4428
- ]);
4429
- if (withheld !== null && current())
4328
+ const withheld = await advance(room, local, authorization, live);
4329
+ if (withheld !== null && live())
4430
4330
  await withhold(withheld, room, local, progress);
4431
4331
  });
4432
4332
  }
@@ -4481,14 +4381,11 @@ export function composeRuntime(options) {
4481
4381
  progress.complete = false;
4482
4382
  return;
4483
4383
  }
4484
- reads.applied();
4485
4384
  submitAll();
4486
4385
  for (const locator of stale)
4487
4386
  if (repairable.has(locator))
4488
4387
  repairLater(locator, {});
4489
4388
  await conversations.reconcile();
4490
- if (status.providerUnreachable)
4491
- conversations.reconnected();
4492
4389
  const reading = { complete: true, reachable: true };
4493
4390
  await conversations.catchUp(reading);
4494
4391
  if (progress.reachable && reading.reachable)
@@ -4514,7 +4411,7 @@ export function composeRuntime(options) {
4514
4411
  if (!live())
4515
4412
  return;
4516
4413
  let lastCompleted = status.lastCompleted;
4517
- let synced = complete && stale.size === 0 && !pending();
4414
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4518
4415
  if (synced && !recorded) {
4519
4416
  const completedAt = new Date().toISOString();
4520
4417
  try {
@@ -4529,74 +4426,69 @@ export function composeRuntime(options) {
4529
4426
  }
4530
4427
  }
4531
4428
  moveTo({
4532
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4429
+ state: pending()
4430
+ ? 'pending'
4431
+ : synced && complete && roomWork === 0 && naming === 0
4432
+ ? 'synced'
4433
+ : 'syncing',
4533
4434
  lastCompleted,
4534
- providerUnreachable: !progress.reachable,
4435
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4535
4436
  ...attention(),
4536
4437
  });
4537
4438
  }
4538
- function discover() {
4539
- let release = () => { };
4540
- const done = new Promise((resolve) => {
4541
- release = resolve;
4542
- });
4543
- discovering = { generation, done, release };
4544
- return discovering;
4545
- }
4546
- function applyRoom(locator, roomVersion) {
4547
- announced.set(locator, roomVersion);
4548
- return task(roomLane(locator), async (live) => {
4549
- if (discovering?.generation === generation)
4550
- await discovering.done;
4551
- const saving = answering.get(locator);
4552
- if (saving?.generation === generation)
4553
- await saving.answered;
4554
- if (!live() || authorization === undefined)
4555
- return;
4556
- const room = discovered.get(locator);
4557
- const held = replicas.get(locator);
4558
- if (room === undefined || room.replica.admission !== 'admitted')
4559
- return;
4560
- if (held !== undefined &&
4561
- (held.record.replica.selection !== room.replica.selection ||
4562
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4563
- return;
4564
- }
4565
- const during = underway?.generation === generation ? underway.progress : null;
4566
- const progress = during ?? { complete: true, reachable: true };
4567
- if (during === null)
4568
- moveTo(transferring(status));
4439
+ function applyRoom(change) {
4440
+ const { locator } = change;
4441
+ if (stopping)
4442
+ return Promise.resolve();
4443
+ return inRoom(locator, async (live) => {
4444
+ await answering.get(locator)?.answered;
4445
+ const progress = underway ?? { complete: true, reachable: true };
4569
4446
  try {
4570
4447
  const local = await openLocal();
4571
- await admittedRead(async () => {
4572
- if (!live() || authorization === undefined)
4573
- return;
4574
- const withheld = await advance(room, local, authorization, live);
4575
- if (withheld !== null)
4576
- await withhold(withheld, room, local, progress);
4577
- });
4448
+ for (;;) {
4449
+ if (!live())
4450
+ return null;
4451
+ const room = discovered.get(locator);
4452
+ const held = replicas.get(locator);
4453
+ if (room === undefined ||
4454
+ held === undefined ||
4455
+ room.replica.admission !== 'admitted' ||
4456
+ held.record.replica.selection !== room.replica.selection) {
4457
+ return null;
4458
+ }
4459
+ const at = held.record.roomVersion;
4460
+ if (options.adapter.reached(locator, at, change.roomVersion))
4461
+ return progress;
4462
+ if (!options.adapter.reached(locator, at, change.from)) {
4463
+ name(locator);
4464
+ return progress;
4465
+ }
4466
+ if (underway === null)
4467
+ moveTo(transferring(status));
4468
+ const writing = () => live() && !full;
4469
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4470
+ if (refreshed.kind !== 'advanced')
4471
+ return progress;
4472
+ const landing = {
4473
+ kind: 'refreshed',
4474
+ from: refreshed.from,
4475
+ remodelled: refreshed.remodelled,
4476
+ };
4477
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4478
+ stale.delete(locator);
4479
+ await retainModel(local, room, modelOf(locator));
4480
+ return progress;
4481
+ }
4482
+ }
4578
4483
  }
4579
4484
  catch {
4485
+ if (!stale.has(locator))
4486
+ name(locator);
4580
4487
  stale.add(locator);
4488
+ return progress;
4581
4489
  }
4582
- finally {
4583
- if (during === null)
4584
- await rest(progress, live);
4585
- }
4586
- });
4490
+ }).then((progress) => rest(progress), () => undefined);
4587
4491
  }
4588
- function roomsCarried(replacing) {
4589
- const replaced = new Set(replacing.map(roomLane));
4590
- return Promise.all([...lanes]
4591
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4592
- .map(([, held]) => held.chain));
4593
- }
4594
- const receipts = {
4595
- heads: async () => (await openLocal()).receivedHeads(),
4596
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4597
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4598
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4599
- };
4600
4492
  function observeStream() {
4601
4493
  const stream = options.adapter.roomStream;
4602
4494
  if (stream === undefined || withdrawStream !== undefined)
@@ -4606,29 +4498,33 @@ export function composeRuntime(options) {
4606
4498
  return;
4607
4499
  if (event.kind === 'opened') {
4608
4500
  socketOpen = true;
4501
+ announced = false;
4502
+ handed = false;
4503
+ naming = 0;
4609
4504
  connectionLive();
4610
4505
  generation += 1;
4611
- reads.opened();
4612
4506
  conversations.reopened();
4613
4507
  recovery.resume();
4614
- void sync('opened');
4508
+ }
4509
+ else if (event.kind === 'discovered') {
4510
+ if (!socketOpen)
4511
+ return;
4512
+ announced = true;
4513
+ if (missedChange) {
4514
+ missedChange = false;
4515
+ countAuthorityChange();
4516
+ }
4517
+ void sync('opened', event.discovery);
4615
4518
  }
4616
4519
  else if (event.kind === 'closed')
4617
- closedSocket(event.revoked, event.lost);
4618
- else if (event.kind === 'opening')
4619
- void applyOpening(event.expired);
4520
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4521
+ else if (event.kind === 'caught-up')
4522
+ void caughtUpRooms(event.expired);
4620
4523
  else if (event.kind === 'authority-changed')
4621
- void rediscover(event.scope);
4524
+ void changeAuthority(event);
4622
4525
  else
4623
- void applyRoom(event.locator, event.roomVersion);
4624
- }, receipts, heldRevisions);
4625
- }
4626
- async function heldRevisions(locator, paths) {
4627
- const held = replicas.get(locator);
4628
- if (held === undefined || paths.length === 0)
4629
- return new Map();
4630
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4631
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4526
+ void applyRoom(event);
4527
+ });
4632
4528
  }
4633
4529
  function writesHeld() {
4634
4530
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4698,10 +4594,9 @@ export function composeRuntime(options) {
4698
4594
  if (closed)
4699
4595
  return;
4700
4596
  closed = true;
4597
+ notifyDiscovered();
4701
4598
  if (probing !== null)
4702
4599
  clearInterval(probing);
4703
- for (const resolve of shownWaiters.splice(0))
4704
- resolve();
4705
4600
  connectionLive();
4706
4601
  lockListeners.clear();
4707
4602
  readinessWatches.clear();
@@ -4713,11 +4608,13 @@ export function composeRuntime(options) {
4713
4608
  conversations.close();
4714
4609
  void recovery.retire();
4715
4610
  roomListeners.clear();
4611
+ unlistedRooms.clear();
4716
4612
  refusalListeners.clear();
4717
4613
  storageLossListeners.clear();
4718
4614
  statusListeners.clear();
4719
4615
  serviceModeListeners.clear();
4720
4616
  memberListeners.clear();
4617
+ authorityChangeListeners.clear();
4721
4618
  liveQueries.close();
4722
4619
  queueSubscriptions.clear();
4723
4620
  roomDraftListeners.clear();
@@ -4725,6 +4622,7 @@ export function composeRuntime(options) {
4725
4622
  }
4726
4623
  async function stop() {
4727
4624
  stopping = true;
4625
+ inFlight.abort();
4728
4626
  await recovery.retire();
4729
4627
  await Promise.all(working);
4730
4628
  await mutations;
@@ -4738,20 +4636,20 @@ export function composeRuntime(options) {
4738
4636
  observeStream();
4739
4637
  void openLocal().then(() => {
4740
4638
  if (capabilities.length > 0)
4741
- notifyReady();
4639
+ readyFromStore();
4742
4640
  }, () => {
4743
- notifyReady();
4641
+ readyFromStore();
4744
4642
  });
4745
4643
  if (options.adapter.roomStream === undefined)
4746
- void sync('unstreamed');
4644
+ void sync('unstreamed', null);
4747
4645
  function planned(input, reach) {
4748
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4646
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4749
4647
  const named = plan.paths !== null &&
4750
4648
  plan.rooms !== null &&
4751
4649
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4752
4650
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4753
4651
  }
4754
- function watchPlan(plan, deliver) {
4652
+ function watchPlan(plan, deliver, fail) {
4755
4653
  let seeded = false;
4756
4654
  let pending = true;
4757
4655
  const shown = () => {
@@ -4777,7 +4675,7 @@ export function composeRuntime(options) {
4777
4675
  }
4778
4676
  seeded = false;
4779
4677
  deliver(message);
4780
- });
4678
+ }, fail);
4781
4679
  return () => {
4782
4680
  watchers.delete(shown);
4783
4681
  if (watchers.size === 0)
@@ -4785,61 +4683,59 @@ export function composeRuntime(options) {
4785
4683
  stop();
4786
4684
  };
4787
4685
  }
4788
- async function searched(input, reach, locked) {
4686
+ async function searched(input, reach) {
4789
4687
  const asked = searchPlanOf(input);
4790
- const filter = {
4791
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4792
- locked,
4793
- };
4794
- const unlocking = (filter.rooms ?? [])
4795
- .filter((room) => locked.includes(room))
4796
- .map((room) => ({ room, awaits: 'unlock' }));
4797
- for (;;) {
4798
- const cover = covered(filter);
4799
- awaitPreparation(cover);
4800
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4801
- const complete = pending.length === 0 && !('pending' in cover);
4802
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4803
- const answering = complete
4804
- ? filter
4805
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4806
- const answered = covered(answering);
4807
- if ('pending' in answered)
4808
- continue;
4809
- const listed = conversations.searchable();
4810
- const named = reach !== null && !reach.conversations
4811
- ? []
4812
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4813
- const hits = await (await openLocal()).search({
4814
- text: asked.text,
4815
- limit: capped(asked.limit),
4816
- after: asked.after,
4817
- total: asked.total,
4818
- documents: {
4819
- plan: answering,
4820
- rooms: answered.rooms,
4821
- beneath: asked.collection
4822
- ? answered.rooms.map(({ locator, key }) => ({
4823
- key,
4824
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4825
- }))
4826
- : [],
4827
- },
4828
- conversations: named,
4829
- });
4830
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4831
- continue;
4832
- const coverage = {
4833
- rooms: answered.coverage.rooms,
4834
- conversations: named,
4835
- overlay: answered.coverage.overlay,
4688
+ await ready;
4689
+ const search = options.adapter.search;
4690
+ if (search === undefined) {
4691
+ return {
4692
+ kind: 'undeclared',
4693
+ code: 'action-undeclared',
4694
+ fields: { name: 'search' },
4695
+ retryable: false,
4696
+ };
4697
+ }
4698
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4699
+ return OFFLINE;
4700
+ const reached = reach === null ? null : new Set(reach.rooms);
4701
+ const named = asked.rooms === null
4702
+ ? reached === null
4703
+ ? null
4704
+ : [...reached]
4705
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4706
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4707
+ if (rooms !== null && rooms.length === 0)
4708
+ return { kind: 'found', documents: [] };
4709
+ try {
4710
+ await ensureAuthorization();
4711
+ }
4712
+ catch {
4713
+ return UNCERTAIN;
4714
+ }
4715
+ if (closed || authorization === undefined || refusal !== null)
4716
+ return UNCERTAIN;
4717
+ journal.record('transfer search');
4718
+ try {
4719
+ const result = await search(authorization, { text: asked.text, rooms });
4720
+ if (result.kind === 'revoked-authorization')
4721
+ await refuse('revoked-authorization');
4722
+ journal.record(`outcome search ${result.kind}`);
4723
+ if (result.kind === 'rejected')
4724
+ return result;
4725
+ if (result.kind !== 'found')
4726
+ return failure(result);
4727
+ const kept = rooms === null ? null : new Set(rooms);
4728
+ return {
4729
+ kind: 'found',
4730
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4836
4731
  };
4837
- return complete
4838
- ? { ...hits, coverage }
4839
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4732
+ }
4733
+ catch {
4734
+ journal.record('outcome search transport-uncertainty');
4735
+ return UNCERTAIN;
4840
4736
  }
4841
4737
  }
4842
- const watch = (topic, input, deliver) => {
4738
+ const watch = (topic, input, deliver, fail) => {
4843
4739
  if (topic === 'room-drafts') {
4844
4740
  const listener = { notify: deliver, ready: false };
4845
4741
  roomDraftListeners.add(listener);
@@ -4888,12 +4784,13 @@ export function composeRuntime(options) {
4888
4784
  };
4889
4785
  }
4890
4786
  if (topic === 'readiness') {
4891
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4787
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
4892
4788
  }
4893
4789
  if (topic === 'rooms') {
4894
- roomListeners.add(deliver);
4895
- deliver(listed());
4790
+ unlistedRooms.add(deliver);
4791
+ emitKnownRooms();
4896
4792
  return () => {
4793
+ unlistedRooms.delete(deliver);
4897
4794
  roomListeners.delete(deliver);
4898
4795
  };
4899
4796
  }
@@ -4932,6 +4829,13 @@ export function composeRuntime(options) {
4932
4829
  memberListeners.delete(deliver);
4933
4830
  };
4934
4831
  }
4832
+ if (topic === 'authority-changes') {
4833
+ authorityChangeListeners.add(deliver);
4834
+ deliver(authorityChanges);
4835
+ return () => {
4836
+ authorityChangeListeners.delete(deliver);
4837
+ };
4838
+ }
4935
4839
  if (topic === 'queue') {
4936
4840
  const queue = { locator: input[0], notify: deliver };
4937
4841
  queueSubscriptions.add(queue);
@@ -4940,17 +4844,27 @@ export function composeRuntime(options) {
4940
4844
  queueSubscriptions.delete(queue);
4941
4845
  };
4942
4846
  }
4943
- return watchPlan(planned(input[0], null), deliver);
4847
+ return watchPlan(planned(input[0], null), deliver, fail);
4944
4848
  };
4945
4849
  const md = {
4946
4850
  loaded() {
4947
4851
  return openLocal().then(() => undefined);
4948
4852
  },
4949
4853
  rooms() {
4950
- return listed();
4854
+ if (closed)
4855
+ return Promise.resolve([]);
4856
+ return new Promise((settle) => {
4857
+ const answer = (value) => {
4858
+ unlistedRooms.delete(answer);
4859
+ roomListeners.delete(answer);
4860
+ settle(value);
4861
+ };
4862
+ unlistedRooms.add(answer);
4863
+ emitKnownRooms();
4864
+ });
4951
4865
  },
4952
4866
  query: ((input) => rowsQuery(planned(input, null))),
4953
- search: (input) => searched(input, null, []),
4867
+ search: (input) => searched(input, null),
4954
4868
  types(locator) {
4955
4869
  return {
4956
4870
  collection: modelTypes(modelOf(locator)),
@@ -5053,6 +4967,8 @@ export function composeRuntime(options) {
5053
4967
  return journal.export();
5054
4968
  },
5055
4969
  cleanUp,
4970
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
4971
+ reset: () => options.shell.reset(),
5056
4972
  persistence() {
5057
4973
  return persistence === 'granted';
5058
4974
  },
@@ -5075,7 +4991,7 @@ export function composeRuntime(options) {
5075
4991
  authorization = undefined;
5076
4992
  discoveryPaused = false;
5077
4993
  if (options.adapter.roomStream === undefined)
5078
- await sync('unstreamed');
4994
+ await sync('unstreamed', null);
5079
4995
  else
5080
4996
  await task(OPENING_LANE, ensureAuthorization);
5081
4997
  },
@@ -5092,12 +5008,12 @@ export function composeRuntime(options) {
5092
5008
  lockedRooms,
5093
5009
  watchLocks,
5094
5010
  located,
5011
+ room: (locator) => listed().find((one) => one.locator === locator),
5095
5012
  };
5096
5013
  return {
5097
5014
  md,
5098
5015
  seed,
5099
5016
  stop,
5100
- lose: loseRooms,
5101
5017
  watch,
5102
5018
  persisted(answer) {
5103
5019
  if (answer === persistence)
@@ -5106,7 +5022,6 @@ export function composeRuntime(options) {
5106
5022
  journal.record(`persistence ${answer}`);
5107
5023
  queueReadiness();
5108
5024
  },
5109
- shown: screen,
5110
5025
  within: (reach, port) => {
5111
5026
  if (full)
5112
5027
  void probed();