@agentshouse/mdruntime 0.1.0-c09bf842921ff → 0.1.0-c0b4d0492dec3

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 (108) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +15 -16
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +8 -4
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +206 -76
  7. package/dist/change.d.ts +1 -0
  8. package/dist/change.d.ts.map +1 -1
  9. package/dist/chat-types.d.ts +3 -0
  10. package/dist/chat-types.d.ts.map +1 -0
  11. package/dist/chat-types.js +2 -0
  12. package/dist/client.d.ts +5 -0
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +1 -0
  15. package/dist/compile.d.ts +5 -0
  16. package/dist/compile.d.ts.map +1 -1
  17. package/dist/compile.js +186 -55
  18. package/dist/connection.d.ts +8 -2
  19. package/dist/connection.d.ts.map +1 -1
  20. package/dist/connection.js +1386 -1040
  21. package/dist/conversation-row.d.ts +33 -0
  22. package/dist/conversation-row.d.ts.map +1 -0
  23. package/dist/conversation-row.js +47 -0
  24. package/dist/conversation.d.ts +13 -5
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +354 -192
  27. package/dist/entry.d.ts.map +1 -1
  28. package/dist/entry.js +1 -0
  29. package/dist/figures.d.ts +48 -0
  30. package/dist/figures.d.ts.map +1 -0
  31. package/dist/figures.js +197 -0
  32. package/dist/host.d.ts.map +1 -1
  33. package/dist/host.js +3 -3
  34. package/dist/index.d.ts +9 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/job.d.ts +20 -1
  38. package/dist/job.d.ts.map +1 -1
  39. package/dist/job.js +26 -0
  40. package/dist/live.d.ts +4 -4
  41. package/dist/live.d.ts.map +1 -1
  42. package/dist/live.js +53 -15
  43. package/dist/memory.d.ts +1 -0
  44. package/dist/memory.d.ts.map +1 -1
  45. package/dist/memory.js +11 -3
  46. package/dist/messages.d.ts +25 -1
  47. package/dist/messages.d.ts.map +1 -1
  48. package/dist/messages.js +82 -3
  49. package/dist/model.d.ts +0 -2
  50. package/dist/model.d.ts.map +1 -1
  51. package/dist/model.js +0 -6
  52. package/dist/outcome.d.ts +1 -0
  53. package/dist/outcome.d.ts.map +1 -1
  54. package/dist/outcome.js +3 -0
  55. package/dist/overlay.d.ts +1 -0
  56. package/dist/overlay.d.ts.map +1 -1
  57. package/dist/overlay.js +11 -0
  58. package/dist/parts.d.ts +59 -0
  59. package/dist/parts.d.ts.map +1 -0
  60. package/dist/parts.js +118 -0
  61. package/dist/query.d.ts +10 -0
  62. package/dist/query.d.ts.map +1 -1
  63. package/dist/questions.d.ts +32 -0
  64. package/dist/questions.d.ts.map +1 -0
  65. package/dist/questions.js +118 -0
  66. package/dist/reach.d.ts +5 -5
  67. package/dist/reach.d.ts.map +1 -1
  68. package/dist/reach.js +156 -99
  69. package/dist/read.d.ts +7 -0
  70. package/dist/read.d.ts.map +1 -0
  71. package/dist/read.js +17 -0
  72. package/dist/reads.d.ts +35 -0
  73. package/dist/reads.d.ts.map +1 -0
  74. package/dist/reads.js +128 -0
  75. package/dist/recovery.d.ts.map +1 -1
  76. package/dist/recovery.js +9 -1
  77. package/dist/refresh.d.ts +1 -1
  78. package/dist/refresh.d.ts.map +1 -1
  79. package/dist/refresh.js +4 -0
  80. package/dist/replica.d.ts.map +1 -1
  81. package/dist/replica.js +23 -6
  82. package/dist/save.d.ts.map +1 -1
  83. package/dist/save.js +10 -7
  84. package/dist/source.d.ts +61 -0
  85. package/dist/source.d.ts.map +1 -0
  86. package/dist/source.js +81 -0
  87. package/dist/status.d.ts +0 -2
  88. package/dist/status.d.ts.map +1 -1
  89. package/dist/status.js +0 -6
  90. package/dist/store.d.ts +85 -25
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +136 -98
  93. package/dist/stored.d.ts +55 -28
  94. package/dist/stored.d.ts.map +1 -1
  95. package/dist/stored.js +187 -74
  96. package/dist/tables.d.ts +99 -19
  97. package/dist/tables.d.ts.map +1 -1
  98. package/dist/tables.js +171 -34
  99. package/dist/tallies.d.ts +18 -0
  100. package/dist/tallies.d.ts.map +1 -0
  101. package/dist/tallies.js +129 -0
  102. package/dist/wire.d.ts +6 -0
  103. package/dist/wire.d.ts.map +1 -1
  104. package/dist/wire.js +3 -0
  105. package/package.json +2 -2
  106. package/dist/fence.d.ts +0 -6
  107. package/dist/fence.d.ts.map +0 -1
  108. package/dist/fence.js +0 -10
@@ -1,8 +1,9 @@
1
1
  import { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
2
2
  import { attachedModel, builtInModel, roomTypesOf, schemaTypes } from './attached-model.js';
3
- import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
+ import { modelTypes, NO_MODEL, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
5
  import { planWithin, } from './query.js';
6
+ import { createFigures, ROOM_TOTALS } from './figures.js';
6
7
  import { createLiveQueries } from './live.js';
7
8
  import { seededResult } from './seed.js';
8
9
  import { documentKey, roomOfKey } from './rows.js';
@@ -19,15 +20,15 @@ import { binaryRecord } from './binary.js';
19
20
  import { attachedFile } from './file.js';
20
21
  import { openedFiles } from './opened.js';
21
22
  import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
23
+ import { readAnswers } from './reads.js';
22
24
  import { createRecovery } from './recovery.js';
23
25
  import { advancement } from './refresh.js';
24
26
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
27
  import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
28
  import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
27
- import { same, saving, STARTING, transferring, } from './status.js';
29
+ import { same, STARTING, } from './status.js';
28
30
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
31
  import { readinessOf, } from './readiness.js';
30
- import { FencedMap } from './fence.js';
31
32
  import { sameBinary } from './overlay.js';
32
33
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
34
  import { roomDraftRevision } from './identity.js';
@@ -68,6 +69,8 @@ const STAGE_AIM_MS = 50;
68
69
  const STREAMS_HELD = 4;
69
70
  const NOTHING_HELD = { bytes: 0 };
70
71
  const QUEUED_TEXT = 1 << 23;
72
+ // A Room the Provider answers not ready is asked again after these waits, the last repeating, so a chunk build that catches up within seconds installs at once and a long one is asked every 15 seconds.
73
+ const NOT_READY_MS = [1_000, 2_000, 3_000, 5_000, 8_000, 11_000, 15_000];
71
74
  // 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.
72
75
  const ARRIVAL_REPORT_MS = 250;
73
76
  // 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.
@@ -93,33 +96,25 @@ export function composeRuntime(options) {
93
96
  const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
94
97
  let authorization = options.authorization;
95
98
  let opening;
96
- const fences = new Map();
97
- const isFenced = (locator) => fences.has(locator);
99
+ const fencedAt = new Map();
100
+ const isFenced = (locator) => fencedAt.has(locator);
98
101
  let fencings = 0;
99
102
  const lastFenced = new Map();
100
- const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
103
+ const fencedSince = (locator, since) => fencedAt.has(locator) || (lastFenced.get(locator) ?? 0) > since;
101
104
  let discoveries = 0;
102
- const replicas = new FencedMap(isFenced);
103
- const reopened = new Set();
105
+ const confirmations = new Map();
104
106
  const usableWhenHeld = options.usableWhenHeld ?? [];
105
- const queuesWithoutReplica = new FencedMap(isFenced);
106
- const pendings = new Map();
107
+ const build = options.build ?? '';
107
108
  const landedAhead = new Map();
108
109
  const seeds = new Map();
109
110
  const seedWatchers = new Map();
110
- const roomDrafts = new FencedMap(isFenced);
111
- const attachedFiles = new FencedMap(isFenced);
112
- const presentations = new Map();
113
- let limits = [];
114
- let capabilities = [];
115
- const rooms = new FencedMap(isFenced);
116
- const counted = new Map();
111
+ const attachedFiles = new Map();
112
+ const declared = new Map();
113
+ const unheldModels = new Map();
114
+ const typesHeld = new Map();
117
115
  const unreadable = new Set();
118
116
  const builtIn = builtInModel(roomTypes);
119
117
  const typedPaths = options.adapter.typedPaths ?? [];
120
- const models = new FencedMap(isFenced);
121
- const liveModels = new FencedMap(isFenced);
122
- const invalidModels = new Set();
123
118
  const discovered = new Map();
124
119
  let inFlight = new AbortController();
125
120
  const refreshed = options.adapter.refresh?.bind(options.adapter);
@@ -134,23 +129,74 @@ export function composeRuntime(options) {
134
129
  };
135
130
  let roomsListed = false;
136
131
  const scopedRefusals = new Map();
137
- function modelOf(locator) {
138
- return models.get(locator) ?? NO_MODEL;
132
+ function typed(locator, persisted) {
133
+ if (persisted.status === 'unavailable')
134
+ return persisted;
135
+ const schema = persisted.snapshot.schema;
136
+ const held = typesHeld.get(locator);
137
+ if (held !== undefined && held.schema === schema)
138
+ return { ...persisted, types: held.types };
139
+ const types = schemaTypes(schema, roomTypes);
140
+ typesHeld.set(locator, { schema, types });
141
+ return { ...persisted, types };
139
142
  }
140
- function savedModel(locator) {
141
- return invalidModels.has(locator)
142
- ? { status: 'unavailable', reason: 'revoked' }
143
- : modelOf(locator);
143
+ function recordOf(stored) {
144
+ return { ...stored, model: typed(stored.locator, stored.model) };
144
145
  }
145
146
  function composed(record) {
146
147
  return compositionOf(record, builtIn, typedPaths);
147
148
  }
149
+ async function storedRooms(locators) {
150
+ const held = await (await openLocal()).rooms(locators);
151
+ return new Map(held
152
+ .filter(({ record }) => !isFenced(record.locator))
153
+ .map(({ record, indexed, documents }) => [
154
+ record.locator,
155
+ { record: recordOf(record), indexed, documents },
156
+ ]));
157
+ }
158
+ async function recordAt(locator) {
159
+ return (await storedRooms([locator])).get(locator)?.record;
160
+ }
161
+ async function workOf(locator) {
162
+ if (isFenced(locator))
163
+ return { replica: undefined, queue: [], states: new Map() };
164
+ const { room, queue } = await (await openLocal()).room(locator);
165
+ const batches = queue.map(({ batch }) => batch);
166
+ const states = new Map(queue.map(({ batch, state }) => [batch.id, state]));
167
+ return {
168
+ replica: room === null
169
+ ? undefined
170
+ : {
171
+ record: recordOf(room.record),
172
+ queue: batches,
173
+ states,
174
+ overlaid: room.overlaid,
175
+ count: room.documents,
176
+ confirmed: confirmations.get(locator) ?? 0,
177
+ indexed: room.indexed,
178
+ },
179
+ queue: batches,
180
+ states,
181
+ };
182
+ }
183
+ async function replicaOf(locator) {
184
+ return (await workOf(locator)).replica;
185
+ }
186
+ function modelIn(locator, stored) {
187
+ if (refusal?.code === 'revoked-authorization')
188
+ return { status: 'unavailable', reason: 'revoked' };
189
+ return stored?.record.model ?? unheldModels.get(locator) ?? NO_MODEL;
190
+ }
148
191
  function compositionFor(record) {
149
- return composed({ ...record, model: savedModel(record.locator) });
192
+ return composed({
193
+ ...record,
194
+ model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
195
+ });
150
196
  }
151
197
  let refusal = null;
152
198
  let storageLoss = null;
153
- let status = STARTING;
199
+ let status = { state: 'syncing', providerUnreachable: false, resting: false };
154
200
  let shown = STARTING;
155
201
  let discoveryPaused = false;
156
202
  let closed = false;
@@ -164,6 +210,7 @@ export function composeRuntime(options) {
164
210
  const repairing = new Set();
165
211
  const calling = new Set();
166
212
  const repairable = new Map();
213
+ const unready = new Map();
167
214
  const answering = new Map();
168
215
  let complete = false;
169
216
  let recorded = false;
@@ -187,7 +234,17 @@ export function composeRuntime(options) {
187
234
  const queueSubscriptions = new Set();
188
235
  const roomDraftListeners = new Set();
189
236
  const presentationListeners = new Set();
190
- const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
237
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover), () => covering());
238
+ const figures = createFigures({
239
+ cover: figuresCover,
240
+ rooms: async (rooms, since) => database.ask({
241
+ kind: 'replica.tallies',
242
+ totals: { ...(await planned(ROOM_TOTALS, null)), limit: null },
243
+ rooms,
244
+ since,
245
+ }),
246
+ conversations: (conversations, since) => database.ask({ kind: 'conversation.tallies', conversations, since }),
247
+ });
191
248
  let withdrawConversations;
192
249
  let currencyQueued = null;
193
250
  let socketOpen = false;
@@ -200,15 +257,14 @@ export function composeRuntime(options) {
200
257
  let haltedRefusal = false;
201
258
  let openSignals = 0;
202
259
  const serviceModeListeners = new Set();
203
- let member = null;
204
260
  const memberListeners = new Set();
205
261
  let authorityChanges = 0;
206
262
  const authorityChangeListeners = new Set();
207
263
  const unpreparedDemand = new Set();
208
264
  const unpreparable = new Set();
209
265
  let preparing = false;
210
- let targets = {};
211
266
  let persistence = options.shell.persistence;
267
+ let heldWhenOpened = false;
212
268
  let full = false;
213
269
  let probing = null;
214
270
  let testing = null;
@@ -227,7 +283,7 @@ export function composeRuntime(options) {
227
283
  const readinessWatches = new Set();
228
284
  let readinessQueued = false;
229
285
  const catching = new Set();
230
- const transfers = new FencedMap(isFenced);
286
+ const transfers = new Map();
231
287
  const installs = new Set();
232
288
  const arrivals = new Map();
233
289
  const streaming = new Set();
@@ -255,13 +311,31 @@ export function composeRuntime(options) {
255
311
  call: (name, input) => invokeAction(name, input),
256
312
  history: (request) => readConversationHistory(request),
257
313
  durably,
314
+ local: () => openLocal(),
315
+ again: () => {
316
+ void ready.then(scheduleCurrency);
317
+ },
318
+ settled: () => {
319
+ show();
320
+ },
258
321
  stream: () => options.adapter.conversations,
259
322
  publish: publishChangeSet,
260
- online: () => connectionOf() !== 'unreachable' && refusal === null,
323
+ online: providerReached,
261
324
  full: () => full,
262
325
  closed: () => closed,
263
326
  unanswered: resendLater,
264
327
  });
328
+ const lastAnswers = readAnswers({
329
+ database,
330
+ build,
331
+ unkept: options.adapter.unkeptReads ?? [],
332
+ recovery: recovery.scope(),
333
+ call: (name, input) => md.call(name, input),
334
+ online: providerReached,
335
+ closed: () => closed,
336
+ full: () => full,
337
+ filled,
338
+ });
265
339
  const journal = createLog();
266
340
  const keptFiles = openedFiles(database, isFenced);
267
341
  let readyFromStore = () => { };
@@ -277,26 +351,13 @@ export function composeRuntime(options) {
277
351
  notifyDiscovered();
278
352
  readyFromStore();
279
353
  }
280
- function rememberRoomDraft(record) {
281
- const room = roomDrafts.get(record.locator) ?? new Map();
282
- room.set(record.path, record);
283
- roomDrafts.set(record.locator, room);
284
- }
285
- function forgetRoomDraft(locator, path) {
286
- const room = roomDrafts.get(locator);
287
- if (room === undefined)
288
- return;
289
- room.delete(path);
290
- if (room.size === 0)
291
- roomDrafts.delete(locator);
292
- }
293
354
  async function visibleOf(locator, paths) {
294
- const held = replicas.get(locator);
355
+ const held = await recordAt(locator);
295
356
  if (held === undefined)
296
357
  return undefined;
297
358
  const local = await openLocal();
298
- const slice = await stepped(slicing(await local.entries(held.record.key, 'visible', paths), paths));
299
- return { ...slice, collection: collectionOf(slice.documents, composed(held.record)) };
359
+ const slice = await stepped(slicing(await local.entries(held.key, 'visible', paths), paths));
360
+ return { ...slice, collection: collectionOf(slice.documents, composed(held)) };
300
361
  }
301
362
  function roomDraftOf(record, visible) {
302
363
  return structuredClone({
@@ -320,25 +381,45 @@ export function composeRuntime(options) {
320
381
  layers.set(locator, await visibleOf(locator, paths));
321
382
  return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
322
383
  }
323
- function snapshotRoomDrafts() {
324
- const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
325
- records.sort((left, right) => {
384
+ async function draftsOf(locator, paths) {
385
+ const held = await (await openLocal()).roomDrafts(locator, paths);
386
+ return held.filter((record) => !isFenced(record.locator));
387
+ }
388
+ async function draftOf(locator, path) {
389
+ return (await draftsOf(locator, [path]))[0];
390
+ }
391
+ function inOrder(records) {
392
+ return [...records].sort((left, right) => {
326
393
  if (left.locator !== right.locator)
327
394
  return left.locator < right.locator ? -1 : 1;
328
395
  if (left.path !== right.path)
329
396
  return left.path < right.path ? -1 : 1;
330
397
  return 0;
331
398
  });
332
- return roomDraftsOf(records);
333
399
  }
334
- let draftsShown = Promise.resolve();
335
- function publishRoomDrafts(waitingOnly) {
336
- if (closed)
337
- return;
338
- const snapshot = snapshotRoomDrafts();
339
- draftsShown = draftsShown.then(async () => {
340
- const value = await snapshot.catch(() => null);
341
- if (value === null || closed)
400
+ async function snapshotRoomDrafts() {
401
+ return roomDraftsOf(inOrder(await draftsOf()));
402
+ }
403
+ let published = Promise.resolve();
404
+ function publication(work) {
405
+ published = published.then(work).catch(unpublished);
406
+ return published;
407
+ }
408
+ function publishRoomDrafts(waitingOnly, touched = null) {
409
+ if (closed || roomDraftListeners.size === 0)
410
+ return Promise.resolve();
411
+ return publication(async () => {
412
+ if (closed || roomDraftListeners.size === 0)
413
+ return;
414
+ const records = await draftsOf();
415
+ if (closed)
416
+ return;
417
+ if (touched !== null &&
418
+ !records.some((record) => touched.some(({ locator, path }) => locator === record.locator && (path === null || path === record.path)))) {
419
+ return;
420
+ }
421
+ const value = await roomDraftsOf(inOrder(records));
422
+ if (closed)
342
423
  return;
343
424
  for (const listener of roomDraftListeners) {
344
425
  if (waitingOnly && listener.ready)
@@ -348,8 +429,8 @@ export function composeRuntime(options) {
348
429
  }
349
430
  });
350
431
  }
351
- function presentationOf(app) {
352
- const value = presentations.get(app);
432
+ async function presentationOf(app) {
433
+ const value = await (await openLocal()).presentation(app);
353
434
  return value === undefined ? null : structuredClone(value);
354
435
  }
355
436
  async function writtenRoomDraft(room, path, revision, input, next) {
@@ -357,7 +438,7 @@ export function composeRuntime(options) {
357
438
  if (path === '' || normalize(path) !== path)
358
439
  throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
359
440
  const local = await openLocal();
360
- const current = roomDrafts.get(room)?.get(path);
441
+ const current = await draftOf(room, path);
361
442
  if ((current?.revision ?? null) !== revision) {
362
443
  return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
363
444
  }
@@ -376,55 +457,34 @@ export function composeRuntime(options) {
376
457
  await local.keepRoomDraft(record, parts.file === undefined || file === undefined
377
458
  ? []
378
459
  : [{ key: file.localId, blob: parts.file.blob }]);
379
- rememberRoomDraft(record);
380
- publishChangeSet(changeSet(room, { drafts: [path] }));
460
+ await publishChangeSet(changeSet(room, { drafts: [path] }));
381
461
  return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
382
462
  }
383
463
  async function keptPresentation(app, next) {
384
464
  const local = await openLocal();
385
- const stored = next(presentations.get(app));
465
+ const stored = next(await local.presentation(app));
386
466
  await local.present({ app, value: stored });
387
- presentations.set(app, stored);
388
- publishChangeSet(changeSet('', { presentation: app }));
467
+ await publishChangeSet(changeSet('', { presentation: app }));
389
468
  }
390
469
  function emitPresentation(app) {
391
- if (closed)
392
- return;
393
- for (const listener of presentationListeners) {
394
- if (listener.app === app && listener.ready)
395
- listener.notify(presentationOf(app));
396
- }
397
- }
398
- function pending() {
399
- return (!localSaves &&
400
- (queuesWithoutReplica.size > 0 ||
401
- [...replicas.values()].some((replica) => replica.queue.length > 0)));
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);
470
+ const listening = () => [...presentationListeners].filter((listener) => listener.app === app && listener.ready);
471
+ if (closed || listening().length === 0)
472
+ return Promise.resolve();
473
+ return publication(async () => {
474
+ if (closed || listening().length === 0)
475
+ return;
476
+ const value = await presentationOf(app);
477
+ if (closed)
478
+ return;
479
+ for (const listener of listening())
480
+ listener.notify(value);
481
+ });
416
482
  }
417
- function attention() {
418
- const attentionRequired = [];
419
- for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
420
- if (record.returned === null)
421
- continue;
422
- attentionRequired.push({
423
- room: record.locator,
424
- path: record.path,
425
- returned: structuredClone(record.returned),
426
- });
427
- }
483
+ async function attended() {
484
+ const held = await (await openLocal()).status();
485
+ const attentionRequired = held.returned.flatMap((record) => record.returned === null || isFenced(record.locator)
486
+ ? []
487
+ : [{ room: record.locator, path: record.path, returned: structuredClone(record.returned) }]);
428
488
  attentionRequired.sort((left, right) => {
429
489
  if (left.room !== right.room)
430
490
  return left.room < right.room ? -1 : 1;
@@ -432,41 +492,130 @@ export function composeRuntime(options) {
432
492
  return left.path < right.path ? -1 : 1;
433
493
  return 0;
434
494
  });
435
- let reset = null;
436
- for (const { submission } of pendings.values()) {
437
- if (submission.state !== 'reset')
438
- continue;
439
- if (reset === null || submission.reset < reset)
440
- reset = submission.reset;
495
+ const changed = new Map();
496
+ if (!localSaves) {
497
+ for (const change of held.unsent) {
498
+ if (!isFenced(change.room))
499
+ changed.set(`${change.room}\u0000${change.path}`, change);
500
+ }
441
501
  }
442
- return { attentionRequired, unsent: unsent(), reset };
502
+ const unsent = [...changed]
503
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
504
+ .map(([, change]) => change);
505
+ return {
506
+ queued: !localSaves && held.queued.some((locator) => !isFenced(locator)),
507
+ lastCompleted: held.lastCompleted,
508
+ attentionRequired,
509
+ unsent,
510
+ reset: held.resets.reduce((earliest, { locator, reset }) => isFenced(locator) || (earliest !== null && earliest <= reset) ? earliest : reset, null),
511
+ };
443
512
  }
444
- function documentsIn(locator) {
445
- return replicas.get(locator)?.count ?? 0;
513
+ async function queuedWork() {
514
+ const held = await (await openLocal()).queued();
515
+ return !localSaves && held.some((locator) => !isFenced(locator));
446
516
  }
447
- function listed() {
448
- return structuredClone([...rooms.values()].map((room) => {
449
- const documents = documentsIn(room.locator);
450
- counted.set(room.locator, documents);
451
- return locked(room.locator)
452
- ? {
453
- ...room,
454
- model: modelTypes(NO_MODEL),
455
- typedSelection: null,
456
- documents: 0,
457
- locked: true,
458
- }
459
- : { ...room, documents, locked: false };
460
- }));
517
+ function declarationOf(locator, stored) {
518
+ const held = declared.get(locator);
519
+ const room = stored?.record ?? held?.room ?? discovered.get(locator);
520
+ return room === undefined ? undefined : { room, available: held?.available ?? true };
521
+ }
522
+ function listedLocators(held) {
523
+ return [
524
+ ...declared.keys(),
525
+ ...[...held.keys()].filter((locator) => !declared.has(locator)),
526
+ ].filter((locator) => !isFenced(locator));
527
+ }
528
+ function selectionModel() {
529
+ return refusal?.code === 'revoked-authorization'
530
+ ? { status: 'unavailable', reason: 'revoked' }
531
+ : builtIn;
532
+ }
533
+ function listedRoom(locator, stored) {
534
+ const declaration = declarationOf(locator, stored);
535
+ if (declaration === undefined)
536
+ return undefined;
537
+ const { room, available } = declaration;
538
+ const selection = room.typedSelection ?? null;
539
+ return {
540
+ name: room.name,
541
+ locator,
542
+ handle: room.handle,
543
+ operations: room.operations,
544
+ replica: room.replica,
545
+ model: modelTypes(modelIn(locator, stored)),
546
+ typedSelection: selection === null ? null : { roots: selection.roots, model: modelTypes(selectionModel()) },
547
+ providerOwnedPaths: room.providerOwnedPaths,
548
+ roomVersion: stored?.record.roomVersion ?? null,
549
+ synchronizationAvailable: available,
550
+ refusal: scopedRefusals.get(locator) ?? null,
551
+ documents: stored?.documents ?? 0,
552
+ };
553
+ }
554
+ function listingOf(held) {
555
+ const listing = new Map();
556
+ for (const locator of listedLocators(held)) {
557
+ const room = listedRoom(locator, held.get(locator));
558
+ if (room !== undefined)
559
+ listing.set(locator, room);
560
+ }
561
+ return listing;
562
+ }
563
+ async function patchedListing(changed) {
564
+ if (roomsShown === null)
565
+ return null;
566
+ const held = await storedRooms([...changed]);
567
+ const listing = new Map(roomsShown);
568
+ for (const locator of changed) {
569
+ const room = isFenced(locator) ? undefined : listedRoom(locator, held.get(locator));
570
+ if (room === undefined || !listing.has(locator))
571
+ return null;
572
+ listing.set(locator, room);
573
+ }
574
+ return listing;
575
+ }
576
+ async function listingNow() {
577
+ return listingOf(await storedRooms());
461
578
  }
462
- function locked(locator) {
463
- return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
579
+ function lockedIn(listing, locator) {
580
+ return listing.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
464
581
  }
465
- function lockedRooms() {
466
- return [...rooms.keys()].filter(locked).sort();
582
+ function shownRooms(listing) {
583
+ return structuredClone([...listing.values()].map(({ documents, ...room }) => lockedIn(listing, room.locator)
584
+ ? {
585
+ ...room,
586
+ model: modelTypes(NO_MODEL),
587
+ typedSelection: null,
588
+ documents: 0,
589
+ locked: true,
590
+ }
591
+ : { ...room, documents, locked: false }));
467
592
  }
468
- function relocked(before) {
469
- const after = lockedRooms();
593
+ async function offlineModes() {
594
+ const modes = new Map();
595
+ for (const [locator, { room }] of declared) {
596
+ const declaration = room ?? discovered.get(locator);
597
+ if (declaration !== undefined)
598
+ modes.set(locator, declaration.replica.offline);
599
+ }
600
+ for (const [locator, { offline }] of (await covering()).rooms)
601
+ modes.set(locator, offline);
602
+ return modes;
603
+ }
604
+ async function locked(locator) {
605
+ if (isFenced(locator) || confirmed.has(locator))
606
+ return false;
607
+ const held = declared.get(locator)?.room ?? discovered.get(locator);
608
+ const mode = (await (await openLocal()).offline(locator)) ?? held?.replica.offline;
609
+ return mode === 'locked';
610
+ }
611
+ async function lockedRooms() {
612
+ return [...(await offlineModes())]
613
+ .filter(([locator, mode]) => mode === 'locked' && !confirmed.has(locator) && !isFenced(locator))
614
+ .map(([locator]) => locator)
615
+ .sort();
616
+ }
617
+ async function relocked(before) {
618
+ const after = await lockedRooms();
470
619
  if (JSON.stringify(after) === JSON.stringify(before))
471
620
  return;
472
621
  journal.record(`locked ${after.length}`);
@@ -489,9 +638,10 @@ export function composeRuntime(options) {
489
638
  lockGrace = null;
490
639
  if (socketOpen || closed)
491
640
  return;
492
- const before = lockedRooms();
493
- confirmed.clear();
494
- relocked(before);
641
+ void lockedRooms().then((before) => {
642
+ confirmed.clear();
643
+ return relocked(before);
644
+ }, () => { });
495
645
  }, LOCK_GRACE_MS);
496
646
  }
497
647
  function connectionLive() {
@@ -505,13 +655,14 @@ export function composeRuntime(options) {
505
655
  lockListeners.delete(notify);
506
656
  };
507
657
  }
508
- function roomFacts(reach = null) {
509
- const held = [...rooms.values()]
658
+ async function roomFacts(reach = null) {
659
+ const held = await storedRooms();
660
+ const rooms = [...listingOf(held).values()]
510
661
  .filter((room) => holds(reach, room.locator))
511
662
  .map((room) => {
512
663
  const governed = governs(reach, room);
513
664
  const attached = [
514
- ...(governed.collection ? [modelOf(room.locator)] : []),
665
+ ...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
515
666
  ...(governed.selection ? [builtIn] : []),
516
667
  ];
517
668
  return {
@@ -520,28 +671,86 @@ export function composeRuntime(options) {
520
671
  types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
521
672
  };
522
673
  });
523
- return { listed: roomsListed, rooms: held };
674
+ return { listed: roomsListed, rooms };
524
675
  }
525
- function emitRooms() {
526
- liveQueries.recover();
676
+ function emitRooms(changed) {
677
+ recoverReads();
527
678
  queueReadiness();
528
- const value = listed();
529
- for (const listener of roomListeners)
530
- listener(value);
531
- emitKnownRooms();
679
+ return changed === undefined ? publishRooms('all') : publishRooms('changed', changed);
532
680
  }
533
681
  function emitKnownRooms() {
534
682
  conversations.learned();
535
683
  if (unlistedRooms.size === 0)
536
684
  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);
685
+ void publishRooms('known');
686
+ }
687
+ let roomsOwed = null;
688
+ let roomsShown = null;
689
+ function publishRooms(mode, touched = []) {
690
+ if (closed)
691
+ return Promise.resolve();
692
+ if (roomsOwed !== null) {
693
+ roomsOwed.all ||= mode === 'all';
694
+ for (const locator of touched) {
695
+ (mode === 'changed' ? roomsOwed.changed : roomsOwed.counted).add(locator);
696
+ }
697
+ return roomsOwed.shown;
544
698
  }
699
+ const owing = {
700
+ all: mode === 'all',
701
+ changed: new Set(mode === 'changed' ? touched : []),
702
+ counted: new Set(mode === 'changed' ? [] : touched),
703
+ shown: Promise.resolve(),
704
+ };
705
+ roomsOwed = owing;
706
+ owing.shown = publication(async () => {
707
+ const owed = owing;
708
+ if (roomsOwed === owing)
709
+ roomsOwed = null;
710
+ if (closed || (roomListeners.size === 0 && unlistedRooms.size === 0))
711
+ return;
712
+ const patched = owed.all || owed.changed.size === 0 || unlistedRooms.size > 0
713
+ ? null
714
+ : await patchedListing(new Set([...owed.changed, ...owed.counted]));
715
+ if (patched === null && owed.changed.size > 0)
716
+ owed.all = true;
717
+ if (patched === null && !owed.all && owed.counted.size > 0) {
718
+ const held = await covering().catch(() => null);
719
+ if (held === null)
720
+ return;
721
+ owed.all = [...owed.counted].some((locator) => held.rooms.has(locator) &&
722
+ roomsShown?.get(locator)?.documents !== held.rooms.get(locator).documents);
723
+ if (owed.all) {
724
+ recoverReads();
725
+ queueReadiness();
726
+ }
727
+ else if (unlistedRooms.size === 0)
728
+ return;
729
+ }
730
+ const listing = patched ?? (await listingNow().catch(() => null));
731
+ if (listing === null || closed)
732
+ return;
733
+ const value = shownRooms(listing);
734
+ const joining = unlistedRooms.size > 0 &&
735
+ (roomsListed || listing.size > 0 || refusal !== null || status.providerUnreachable);
736
+ if (!owed.all && patched === null && !joining)
737
+ return;
738
+ const moved = owed.all ||
739
+ patched !== null ||
740
+ (roomsShown !== null && JSON.stringify(shownRooms(roomsShown)) !== JSON.stringify(value));
741
+ if (moved)
742
+ for (const listener of roomListeners)
743
+ listener(value);
744
+ if (joining) {
745
+ for (const listener of [...unlistedRooms]) {
746
+ unlistedRooms.delete(listener);
747
+ roomListeners.add(listener);
748
+ listener(value);
749
+ }
750
+ }
751
+ roomsShown = roomListeners.size === 0 ? null : listing;
752
+ });
753
+ return owing.shown;
545
754
  }
546
755
  function emitRefusal() {
547
756
  queueReadiness();
@@ -554,58 +763,98 @@ export function composeRuntime(options) {
554
763
  for (const listener of storageLossListeners)
555
764
  listener(storageLoss);
556
765
  }
557
- function emitMember() {
766
+ function emitMember(member) {
558
767
  for (const listener of memberListeners)
559
768
  listener(member);
560
769
  }
561
770
  function moveTo(next) {
562
771
  if (closed)
563
- return;
772
+ return Promise.resolve();
564
773
  status = next;
565
- show();
774
+ return show();
566
775
  }
776
+ function transferring() {
777
+ return { ...status, state: 'syncing', resting: false };
778
+ }
779
+ let statusOwed = null;
780
+ let statusShown = Promise.resolve();
567
781
  function show() {
568
782
  if (closed)
569
- return;
783
+ return Promise.resolve();
570
784
  queueReadiness();
571
785
  emitKnownRooms();
572
- const next = status.state === 'synced' && !conversations.current()
573
- ? { ...status, state: 'syncing' }
574
- : status;
575
- if (same(shown, next))
576
- return;
577
- shown = next;
578
- for (const listener of statusListeners)
579
- listener(shown);
786
+ const moved = { moving: status, current: conversations.current() };
787
+ if (statusOwed !== null) {
788
+ statusOwed.push(moved);
789
+ return statusShown;
790
+ }
791
+ const owed = [moved];
792
+ statusOwed = owed;
793
+ statusShown = publication(async () => {
794
+ if (statusOwed === owed)
795
+ statusOwed = null;
796
+ const held = await attended();
797
+ if (closed)
798
+ return;
799
+ for (const { moving, current } of owed) {
800
+ const state = moving.resting && held.queued ? 'pending' : moving.state;
801
+ const next = {
802
+ state: state === 'synced' && !current ? 'syncing' : state,
803
+ lastCompleted: held.lastCompleted,
804
+ providerUnreachable: moving.providerUnreachable,
805
+ attentionRequired: held.attentionRequired,
806
+ unsent: held.unsent,
807
+ reset: held.reset,
808
+ };
809
+ if (same(shown, next))
810
+ continue;
811
+ shown = next;
812
+ for (const listener of statusListeners)
813
+ listener(shown);
814
+ }
815
+ });
816
+ return statusShown;
580
817
  }
581
- function entryOf(locator, batch) {
818
+ function entryOf(states, batch) {
582
819
  if (localSaves)
583
820
  return Object.freeze({ batch, state: 'local' });
584
- const state = pendings.get(pendingKey(locator, batch.id));
821
+ const state = states.get(batch.id);
585
822
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
586
823
  }
587
- function entriesOf(locator) {
588
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
824
+ async function entriesOf(locator) {
825
+ if (isFenced(locator))
826
+ return [];
827
+ const { queue, states } = await workOf(locator);
828
+ return queue.map((batch) => entryOf(states, batch));
589
829
  }
590
830
  function emitQueue(locator) {
591
- const entries = entriesOf(locator);
592
- for (const subscription of queueSubscriptions) {
593
- if (subscription.locator === locator)
831
+ const listening = () => [...queueSubscriptions].filter((subscription) => subscription.locator === locator);
832
+ if (closed || listening().length === 0)
833
+ return Promise.resolve();
834
+ return publication(async () => {
835
+ if (closed || listening().length === 0)
836
+ return;
837
+ const entries = await entriesOf(locator);
838
+ if (closed)
839
+ return;
840
+ for (const subscription of listening())
594
841
  subscription.notify(entries);
595
- }
842
+ });
596
843
  }
597
844
  function applyChangeSet(set) {
598
845
  liveQueries.touched(set);
846
+ figures.touched(set);
847
+ }
848
+ function recoverReads() {
849
+ liveQueries.recover();
850
+ figures.recover();
599
851
  }
600
852
  function unpublished(error) {
601
853
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
602
854
  }
603
- function indexed(locator) {
604
- return replicas.get(locator)?.indexed === true;
605
- }
606
855
  async function prepareReplica(locator) {
607
856
  for (;;) {
608
- const held = replicas.get(locator);
857
+ const held = (await storedRooms([locator])).get(locator);
609
858
  if (held === undefined || closed || held.indexed)
610
859
  return;
611
860
  const key = held.record.key;
@@ -613,20 +862,19 @@ export function composeRuntime(options) {
613
862
  const local = await openLocal();
614
863
  return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
615
864
  });
616
- const now = replicas.get(locator);
617
- if (now?.record.key !== key)
618
- continue;
619
865
  if (!done.done)
620
866
  continue;
621
- replicas.set(locator, { ...now, indexed: true });
867
+ if ((await storedRooms([locator])).get(locator)?.record.key !== key)
868
+ continue;
622
869
  publishChangeSet(changeSet(locator, { documents: null }));
623
- liveQueries.recover();
870
+ recoverReads();
624
871
  queueReadiness();
625
872
  return;
626
873
  }
627
874
  }
628
- function unprepared() {
629
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
875
+ async function unprepared() {
876
+ const held = (await covering()).rooms;
877
+ return [...unpreparedDemand, ...held.keys()].find((locator) => !(held.get(locator)?.indexed ?? false) && !unpreparable.has(locator));
630
878
  }
631
879
  function unpreparedBy(locator, error) {
632
880
  unpreparable.add(locator);
@@ -641,7 +889,7 @@ export function composeRuntime(options) {
641
889
  preparing = true;
642
890
  void (async () => {
643
891
  try {
644
- for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
892
+ for (let waiting = await unprepared(); waiting !== undefined; waiting = await unprepared()) {
645
893
  await yielded();
646
894
  if (closed)
647
895
  return;
@@ -649,6 +897,9 @@ export function composeRuntime(options) {
649
897
  await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
650
898
  }
651
899
  }
900
+ catch {
901
+ journal.record('preparation failed');
902
+ }
652
903
  finally {
653
904
  preparing = false;
654
905
  }
@@ -657,31 +908,48 @@ export function composeRuntime(options) {
657
908
  function roomsOfReach(reach) {
658
909
  return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
659
910
  }
660
- function reachedRooms(plan) {
911
+ async function covering() {
912
+ const held = await (await openLocal()).covering();
913
+ return {
914
+ rooms: new Map([...held.rooms].filter(([locator]) => !isFenced(locator))),
915
+ queues: new Map([...held.queues].filter(([locator]) => !isFenced(locator))),
916
+ };
917
+ }
918
+ function everyRoom(held) {
919
+ return [...new Set([...declared.keys(), ...held.rooms.keys(), ...held.queues.keys()])];
920
+ }
921
+ function reachedRooms(plan, held) {
661
922
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
662
- const named = plan.rooms ??
663
- (reached === null
664
- ? [...new Set([...rooms.keys(), ...replicas.keys(), ...queuesWithoutReplica.keys()])]
665
- : [...reached]);
923
+ const named = plan.rooms ?? (reached === null ? everyRoom(held) : [...reached]);
666
924
  return [...new Set(named)]
667
925
  .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
668
926
  .sort();
669
927
  }
670
928
  function awaited(locator) {
671
929
  return (!roomsListed ||
672
- (rooms.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
673
- }
674
- function covered(plan) {
675
- const locators = reachedRooms(plan);
676
- if (locators.length === 0 && plan.rooms === null && plan.reach === null && !roomsListed) {
930
+ (declared.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
931
+ }
932
+ function covered(plan, held) {
933
+ return coveredRooms(reachedRooms(plan, held), plan.rooms === null && plan.reach === null, held);
934
+ }
935
+ async function figuresCover() {
936
+ const held = await covering();
937
+ const locked = await lockedRooms();
938
+ const cover = coveredRooms(everyRoom(held)
939
+ .filter((locator) => !locked.includes(locator))
940
+ .sort(), true, held);
941
+ awaitPreparation(cover);
942
+ return 'pending' in cover ? cover : { rooms: cover.rooms };
943
+ }
944
+ function coveredRooms(locators, whole, held) {
945
+ if (locators.length === 0 && whole && !roomsListed)
677
946
  return { pending: [] };
678
- }
679
947
  const pending = [];
680
948
  const coverage = [];
681
949
  const overlay = [];
682
950
  const read = [];
683
951
  for (const locator of locators) {
684
- const replica = replicas.get(locator);
952
+ const replica = held.rooms.get(locator);
685
953
  if (replica === undefined) {
686
954
  if (awaited(locator))
687
955
  pending.push({ room: locator, awaits: 'acquisition' });
@@ -693,12 +961,12 @@ export function composeRuntime(options) {
693
961
  }
694
962
  coverage.push({
695
963
  room: locator,
696
- generation: replica.record.transfer,
964
+ generation: replica.transfer,
697
965
  current: !(catching.has(locator) || transfers.has(locator)),
698
966
  });
699
- read.push({ locator, key: replica.record.key });
700
- for (const batch of replica.queue)
701
- overlay.push(batch.id);
967
+ read.push({ locator, key: replica.key });
968
+ for (const id of held.queues.get(locator) ?? [])
969
+ overlay.push(id);
702
970
  }
703
971
  if (pending.length > 0)
704
972
  return { pending };
@@ -711,46 +979,54 @@ export function composeRuntime(options) {
711
979
  if (waiting.length > 0)
712
980
  prepare(waiting.map(({ room }) => room));
713
981
  }
714
- function requiredRooms(given) {
982
+ function requiredRooms(given, held) {
715
983
  const reach = planReach(given);
984
+ const listed = new Set(listedLocators(held));
716
985
  if (reach === null)
717
- return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
986
+ return { listed: roomsListed, rooms: [...listed].sort() };
718
987
  const named = [...roomsOfReach(reach)].sort();
719
988
  if (roomsListed)
720
- return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
721
- return { listed: named.every((room) => replicas.has(room)), rooms: named };
989
+ return { listed: true, rooms: named.filter((room) => listed.has(room)) };
990
+ return { listed: named.every((room) => held.has(room)), rooms: named };
722
991
  }
723
- function roomState(locator) {
992
+ function roomState(locator, held) {
724
993
  const listedRoom = discovered.get(locator);
725
994
  if (roomsListed &&
726
995
  (listedRoom === undefined ||
727
996
  listedRoom.replica.admission !== 'admitted' ||
728
997
  unreadable.has(locator)))
729
998
  return 'unadmitted';
730
- if (!replicas.has(locator))
999
+ const stored = held.get(locator);
1000
+ if (stored === undefined)
731
1001
  return 'acquiring';
732
- if (!indexed(locator))
1002
+ if (!stored.indexed)
733
1003
  return 'preparing';
734
1004
  return 'complete';
735
1005
  }
736
1006
  function conversationCoverage(reach) {
737
- const declared = reach === null || reach.conversations;
738
- if (!declared || options.adapter.conversations === undefined)
1007
+ const declaredConversations = reach === null || reach.conversations;
1008
+ if (!declaredConversations || options.adapter.conversations === undefined)
739
1009
  return 'undeclared';
740
1010
  if (!conversations.known() || !conversations.current())
741
1011
  return 'acquiring';
742
1012
  return 'complete';
743
1013
  }
744
- function connectionOf() {
1014
+ function connectionOf(lastCompleted) {
745
1015
  if (socketOpen)
746
1016
  return 'connected';
747
1017
  if (status.providerUnreachable)
748
1018
  return 'unreachable';
749
- return options.adapter.roomStream === undefined && status.lastCompleted !== null
1019
+ return options.adapter.roomStream === undefined && lastCompleted !== null
750
1020
  ? 'connected'
751
1021
  : 'connecting';
752
1022
  }
753
- function bytesOf(room, underway) {
1023
+ async function connectionNow() {
1024
+ if (socketOpen || status.providerUnreachable || options.adapter.roomStream !== undefined) {
1025
+ return connectionOf(null);
1026
+ }
1027
+ return connectionOf((await (await openLocal()).facts()).lastCompleted);
1028
+ }
1029
+ function bytesOf(room, underway, held) {
754
1030
  const transfer = transfers.get(room);
755
1031
  if (transfer !== undefined) {
756
1032
  const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
@@ -759,10 +1035,10 @@ export function composeRuntime(options) {
759
1035
  total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
760
1036
  };
761
1037
  }
762
- const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
763
- return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
1038
+ const kept = underway ? NOTHING_HELD.bytes : (held.get(room)?.bytes ?? NOTHING_HELD.bytes);
1039
+ return { done: kept, total: arrivals.get(room) ?? kept };
764
1040
  }
765
- function installationOf(coverage) {
1041
+ function installationOf(coverage, held) {
766
1042
  const rooms = { done: 0, total: 0 };
767
1043
  const bytes = { done: 0, total: 0 };
768
1044
  let installing = 0;
@@ -778,7 +1054,7 @@ export function composeRuntime(options) {
778
1054
  installing += 1;
779
1055
  if (underway && (streaming.has(room) || !arrivals.has(room)))
780
1056
  downloading = true;
781
- const now = bytesOf(room, underway);
1057
+ const now = bytesOf(room, underway, held);
782
1058
  const earlier = underway ? startedOver.get(room) : undefined;
783
1059
  bytes.done += Math.max(now.done, earlier?.done ?? 0);
784
1060
  bytes.total += Math.max(now.total, earlier?.total ?? 0);
@@ -792,9 +1068,14 @@ export function composeRuntime(options) {
792
1068
  : 'replaying';
793
1069
  return { step, rooms, bytes };
794
1070
  }
795
- function readinessFor(port, reach) {
796
- const required = requiredRooms(reach);
797
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
1071
+ async function readinessHeld() {
1072
+ const local = await openLocal();
1073
+ const [held, facts] = await Promise.all([covering(), local.facts()]);
1074
+ return { held: held.rooms, facts };
1075
+ }
1076
+ function readinessFor(port, reach, { held, facts }) {
1077
+ const required = requiredRooms(reach, held);
1078
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, held) }));
798
1079
  const preparing = coverage
799
1080
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
800
1081
  .map(({ room }) => room);
@@ -808,36 +1089,65 @@ export function composeRuntime(options) {
808
1089
  conversations: conversationCoverage(reach),
809
1090
  },
810
1091
  persistence,
811
- connection: connectionOf(),
1092
+ connection: connectionOf(facts.lastCompleted),
812
1093
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
813
1094
  space: full ? 'full' : null,
814
- installation: installationOf(coverage),
1095
+ installation: installationOf(coverage, held),
815
1096
  lost: storageLoss !== null,
816
- established: targets[port.target]?.established === true ||
817
- (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
1097
+ established: facts.targets[port.target]?.established === true ||
1098
+ (heldWhenOpened && usableWhenHeld.includes(port.target)),
818
1099
  });
819
1100
  }
1101
+ const establishing = new Set();
820
1102
  function establish(name) {
821
- targets = { ...targets, [name]: { established: true } };
822
- queueReadiness();
823
- probeEachMinute();
824
- void durably((local) => local.fact('targets', targets)).catch(() => {
1103
+ if (establishing.has(name))
1104
+ return;
1105
+ establishing.add(name);
1106
+ void durably(async (local) => {
1107
+ const { targets } = await local.facts();
1108
+ await local.fact('targets', { ...targets, [name]: { established: true } });
1109
+ })
1110
+ .then(() => {
1111
+ queueReadiness();
1112
+ void probeEachMinute();
1113
+ }, () => {
825
1114
  journal.record(`target record failed ${name}`);
826
- });
1115
+ })
1116
+ .finally(() => establishing.delete(name));
827
1117
  }
828
- function deliverReadiness(watch) {
1118
+ function deliverReadiness(watch, held) {
829
1119
  if (!readinessWatches.has(watch))
830
1120
  return;
831
- const readiness = readinessFor(watch.port, watch.reach);
832
- if (readiness.ready && targets[watch.port.target]?.established !== true) {
1121
+ const readiness = readinessFor(watch.port, watch.reach, held);
1122
+ if (readiness.ready && held.facts.targets[watch.port.target]?.established !== true) {
833
1123
  establish(watch.port.target);
834
1124
  }
835
- const shown = JSON.stringify(readiness);
836
- if (shown === watch.last)
1125
+ const shownReadiness = JSON.stringify(readiness);
1126
+ if (shownReadiness === watch.last)
837
1127
  return;
838
- watch.last = shown;
1128
+ watch.last = shownReadiness;
839
1129
  watch.deliver(structuredClone(readiness));
840
1130
  }
1131
+ let readinessShown = Promise.resolve();
1132
+ const readinessOwed = new Set();
1133
+ function deliverAll(watches) {
1134
+ const queued = readinessOwed.size > 0;
1135
+ for (const watch of watches)
1136
+ readinessOwed.add(watch);
1137
+ if (queued)
1138
+ return;
1139
+ readinessShown = readinessShown
1140
+ .then(async () => {
1141
+ const owed = [...readinessOwed];
1142
+ readinessOwed.clear();
1143
+ const held = await readinessHeld();
1144
+ if (closed)
1145
+ return;
1146
+ for (const watch of owed)
1147
+ deliverReadiness(watch, held);
1148
+ })
1149
+ .catch(unpublished);
1150
+ }
841
1151
  function queueReadiness() {
842
1152
  if (readinessQueued || closed || readinessWatches.size === 0)
843
1153
  return;
@@ -846,32 +1156,40 @@ export function composeRuntime(options) {
846
1156
  readinessQueued = false;
847
1157
  if (closed)
848
1158
  return;
849
- for (const watch of [...readinessWatches])
850
- deliverReadiness(watch);
1159
+ deliverAll([...readinessWatches]);
851
1160
  });
852
1161
  }
853
1162
  function watchReadiness(port, reach, deliver) {
854
1163
  const watch = { port, reach, deliver, last: null };
855
1164
  readinessWatches.add(watch);
856
- void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
1165
+ void openLocal().then(() => deliverAll([watch]), () => deliverAll([watch]));
857
1166
  return () => {
858
1167
  readinessWatches.delete(watch);
859
1168
  };
860
1169
  }
1170
+ function providerReached() {
1171
+ return (socketOpen || !status.providerUnreachable) && refusal === null;
1172
+ }
861
1173
  function filled() {
862
1174
  if (full || closed)
863
1175
  return;
864
1176
  full = true;
865
1177
  journal.record('space full');
866
1178
  queueReadiness();
867
- probeEachMinute();
1179
+ void probeEachMinute();
868
1180
  }
869
- function installedTarget() {
870
- return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
871
- Object.values(targets).some((target) => target.established));
1181
+ async function installedTarget() {
1182
+ if (heldWhenOpened && usableWhenHeld.length > 0)
1183
+ return true;
1184
+ const { targets } = await (await openLocal()).facts();
1185
+ return Object.values(targets).some((target) => target.established);
872
1186
  }
873
- function probeEachMinute() {
874
- if (!full || probing !== null || closed || !installedTarget())
1187
+ async function probeEachMinute() {
1188
+ if (!full || probing !== null || closed)
1189
+ return;
1190
+ if (!(await installedTarget().catch(() => false)))
1191
+ return;
1192
+ if (!full || probing !== null || closed)
875
1193
  return;
876
1194
  // A device whose storage filled after its install tries a write each minute, so space freed elsewhere ends its read-only mode without a Retry.
877
1195
  probing = setInterval(() => {
@@ -906,39 +1224,41 @@ export function composeRuntime(options) {
906
1224
  else
907
1225
  void resynchronize();
908
1226
  }
909
- function seedRoomHeld(room, version) {
910
- return indexed(room) && reachedBy(room, version);
1227
+ function seedRoomHeld(room, version, held) {
1228
+ const stored = held.rooms.get(room);
1229
+ return (stored !== undefined &&
1230
+ stored.indexed &&
1231
+ options.adapter.reached(room, stored.roomVersion, version));
911
1232
  }
912
- function seedHeld(at) {
913
- return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
1233
+ function seedHeld(at, held) {
1234
+ return Object.entries(at).every(([room, version]) => (roomsListed && !declared.has(room) && !held.rooms.has(room)) ||
1235
+ seedRoomHeld(room, version, held));
914
1236
  }
915
- function seedApplies(plan) {
1237
+ function seedApplies(plan, held) {
916
1238
  const seed = seeds.get(plan.key);
917
1239
  if (seed === undefined)
918
1240
  return null;
919
- if (!seedHeld(seed.at))
1241
+ if (!seedHeld(seed.at, held))
920
1242
  return seed;
921
1243
  seeds.delete(plan.key);
922
1244
  return null;
923
1245
  }
924
- function seedBehind(plan, cover) {
925
- const seed = seedApplies(plan);
1246
+ function seedBehind(plan, cover, held) {
1247
+ const seed = seedApplies(plan, held);
926
1248
  if (seed === null || 'pending' in cover)
927
1249
  return cover;
928
1250
  return {
929
1251
  pending: Object.entries(seed.at)
930
- .filter(([room, version]) => !seedRoomHeld(room, version))
1252
+ .filter(([room, version]) => !seedRoomHeld(room, version, held))
931
1253
  .map(([room]) => ({ room, awaits: 'acquisition' })),
932
1254
  };
933
1255
  }
934
1256
  function seed(entry) {
935
- if (seedHeld(entry.at))
936
- return;
937
1257
  for (const { query, rows } of entry.queries) {
938
1258
  const key = queryKey(query);
939
1259
  seeds.set(key, { rows, at: entry.at });
940
- for (const shown of seedWatchers.get(key) ?? [])
941
- shown();
1260
+ for (const shownSeed of seedWatchers.get(key) ?? [])
1261
+ shownSeed();
942
1262
  }
943
1263
  }
944
1264
  function sameCover(left, right) {
@@ -946,7 +1266,7 @@ export function composeRuntime(options) {
946
1266
  }
947
1267
  async function coveredRows(plan, cover) {
948
1268
  for (;;) {
949
- const before = cover();
1269
+ const before = await cover();
950
1270
  if ('pending' in before)
951
1271
  return { pending: before };
952
1272
  const local = await openLocal();
@@ -954,7 +1274,7 @@ export function composeRuntime(options) {
954
1274
  const changes = local.changes(keys);
955
1275
  const current = () => local.changes(keys) === changes;
956
1276
  const rows = await windowRows(local, plan, before.rooms, current);
957
- const after = cover();
1277
+ const after = await cover();
958
1278
  if (rows !== null && sameCover(before, after))
959
1279
  return { rows, cover: before, current };
960
1280
  }
@@ -1010,23 +1330,25 @@ export function composeRuntime(options) {
1010
1330
  const held = answered.cover;
1011
1331
  return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
1012
1332
  }
1333
+ async function coverNow(plan) {
1334
+ const cover = covered(plan, await covering());
1335
+ awaitPreparation(cover);
1336
+ return cover;
1337
+ }
1013
1338
  async function rowsQuery(plan) {
1014
- const before = covered(plan);
1339
+ const held = await covering();
1340
+ const before = covered(plan, held);
1015
1341
  awaitPreparation(before);
1016
- const seeded = seedApplies(plan);
1342
+ const seeded = seedApplies(plan, held);
1017
1343
  if (seeded !== null)
1018
1344
  return seededResult(seeded).result;
1019
1345
  if ('pending' in before)
1020
1346
  return before;
1021
- const answered = await coveredRows(plan, () => {
1022
- const cover = covered(plan);
1023
- awaitPreparation(cover);
1024
- return cover;
1025
- });
1347
+ const answered = await coveredRows(plan, () => coverNow(plan));
1026
1348
  if ('pending' in answered)
1027
1349
  return answered.pending;
1028
- const held = answered.cover;
1029
- return { ...answered.rows, coverage: held.coverage };
1350
+ const shownCover = answered.cover;
1351
+ return { ...answered.rows, coverage: shownCover.coverage };
1030
1352
  }
1031
1353
  function publishChangeSet(set) {
1032
1354
  applyChangeSet(set);
@@ -1034,21 +1356,24 @@ export function composeRuntime(options) {
1034
1356
  if (path !== null)
1035
1357
  attachedFiles.get(locator)?.delete(path);
1036
1358
  }
1037
- if (set.rooms.length > 0 ||
1038
- [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
1039
- emitRooms();
1040
- for (const locator of set.queues)
1041
- emitQueue(locator);
1042
- if (set.drafts.length > 0 ||
1043
- set.documents.some((change) => {
1044
- const held = roomDrafts.get(change.locator);
1045
- return held !== undefined && (change.path === null || held.has(change.path));
1046
- })) {
1047
- publishRoomDrafts(false);
1359
+ const shownNow = [];
1360
+ if (set.queues.length > 0 || set.drafts.length > 0)
1361
+ shownNow.push(show());
1362
+ if (set.rooms.length > 0)
1363
+ shownNow.push(emitRooms(set.rooms));
1364
+ else if (set.documents.length > 0) {
1365
+ shownNow.push(publishRooms('counted', [...new Set(set.documents.map(({ locator }) => locator))]));
1048
1366
  }
1367
+ for (const locator of set.queues)
1368
+ shownNow.push(emitQueue(locator));
1369
+ if (set.drafts.length > 0)
1370
+ shownNow.push(publishRoomDrafts(false));
1371
+ else if (set.documents.length > 0)
1372
+ shownNow.push(publishRoomDrafts(false, set.documents));
1049
1373
  for (const app of set.presentations)
1050
- emitPresentation(app);
1374
+ shownNow.push(emitPresentation(app));
1051
1375
  conversations.changed(set);
1376
+ return Promise.all(shownNow).then(() => undefined);
1052
1377
  }
1053
1378
  async function refuse(reason, provider = null) {
1054
1379
  discoveryPaused = true;
@@ -1058,53 +1383,31 @@ export function composeRuntime(options) {
1058
1383
  if (reason === 'revoked-authorization') {
1059
1384
  await exclusive(async () => {
1060
1385
  const revoked = { status: 'unavailable', reason: 'revoked' };
1061
- const records = [...replicas].map(([locator, held]) => ({
1062
- locator,
1063
- held,
1064
- record: { ...held.record, model: revoked },
1065
- }));
1066
- let answer = null;
1386
+ let held = new Map();
1067
1387
  try {
1068
1388
  const local = await openLocal();
1069
- answer = await local.commit({
1070
- rooms: records.map(({ locator, record }) => ({
1071
- locator,
1072
- key: record.key,
1073
- record: storedRecord(record),
1074
- composed: composedOf(composed(record)),
1075
- })),
1389
+ held = await storedRooms();
1390
+ await local.commit({
1391
+ rooms: [...held.values()].map(({ record: kept }) => {
1392
+ const record = { ...kept, model: revoked };
1393
+ return {
1394
+ locator: record.locator,
1395
+ key: record.key,
1396
+ record: storedRecord(record),
1397
+ composed: composedOf(composed(record)),
1398
+ };
1399
+ }),
1076
1400
  });
1077
1401
  }
1078
1402
  catch {
1079
1403
  journal.record('model revocation persistence failed');
1080
1404
  }
1081
- const keys = new Set([...models.keys(), ...rooms.keys()]);
1082
- for (const key of keys) {
1083
- invalidModels.add(key);
1084
- models.set(key, revoked);
1085
- liveModels.delete(key);
1086
- }
1087
- const changed = [];
1088
- for (const { locator, held, record } of records) {
1089
- if (replicas.get(locator) !== held)
1090
- continue;
1091
- const indexed = answer === null ? held.indexed : committedRoom(locator, answer).indexed;
1092
- replicas.set(locator, { ...held, record, indexed });
1093
- }
1094
- for (const locator of [...rooms.keys()]) {
1095
- const room = rooms.get(locator);
1096
- if (room === undefined)
1097
- continue;
1098
- rooms.set(locator, {
1099
- ...room,
1100
- model: modelTypes(revoked),
1101
- typedSelection: room.typedSelection === null
1102
- ? null
1103
- : { ...room.typedSelection, model: modelTypes(revoked) },
1104
- });
1105
- changed.push(changeSet(locator, { documents: null, room: true }));
1405
+ const listedNow = listedLocators(held);
1406
+ for (const locator of listedNow) {
1407
+ if (!held.has(locator))
1408
+ unheldModels.set(locator, revoked);
1106
1409
  }
1107
- publishChangeSet(mergeChangeSets(changed));
1410
+ publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
1108
1411
  prepare();
1109
1412
  });
1110
1413
  }
@@ -1121,49 +1424,66 @@ export function composeRuntime(options) {
1121
1424
  }
1122
1425
  }
1123
1426
  function list(room, synchronizationAvailable) {
1124
- const selection = room.typedSelection ?? null;
1125
- rooms.set(room.locator, {
1126
- name: room.name,
1127
- locator: room.locator,
1128
- handle: room.handle,
1129
- operations: room.operations,
1130
- replica: room.replica,
1131
- model: modelTypes(modelOf(room.locator)),
1132
- typedSelection: selection === null
1133
- ? null
1134
- : {
1135
- roots: selection.roots,
1136
- model: modelTypes(builtIn),
1137
- },
1138
- providerOwnedPaths: room.providerOwnedPaths,
1139
- roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1140
- synchronizationAvailable,
1141
- refusal: scopedRefusals.get(room.locator) ?? null,
1427
+ if (isFenced(room.locator))
1428
+ return;
1429
+ declared.set(room.locator, {
1430
+ room: {
1431
+ name: room.name,
1432
+ locator: room.locator,
1433
+ handle: room.handle,
1434
+ operations: room.operations,
1435
+ replica: room.replica,
1436
+ typedSelection: room.typedSelection ?? null,
1437
+ providerOwnedPaths: room.providerOwnedPaths,
1438
+ },
1439
+ available: synchronizationAvailable,
1142
1440
  });
1143
1441
  }
1442
+ function listHeld(locator) {
1443
+ if (isFenced(locator))
1444
+ return;
1445
+ declared.set(locator, { room: null, available: false });
1446
+ }
1144
1447
  async function applyDiscovery(found, started, left) {
1145
- const lockedBefore = lockedRooms();
1146
- for (const room of found) {
1147
- const fence = fences.get(room.locator);
1148
- if (fence !== undefined && fence.erased && fence.at < started)
1149
- fences.delete(room.locator);
1448
+ const lockedBefore = await lockedRooms();
1449
+ const local = await openLocal();
1450
+ const stillFenced = new Set(await local.fences());
1451
+ const relisting = found
1452
+ .map(({ locator }) => locator)
1453
+ .filter((locator) => {
1454
+ const fence = fencedAt.get(locator);
1455
+ return stillFenced.has(locator) && (fence === undefined || fence.at < started);
1456
+ });
1457
+ const relisted = relisting.length === 0 ? new Set() : await durably((held) => held.unfence(relisting));
1458
+ for (const { locator } of found) {
1459
+ const fence = fencedAt.get(locator);
1460
+ if (fence === undefined || fence.at >= started)
1461
+ continue;
1462
+ if (relisted.has(locator) || (!stillFenced.has(locator) && !fence.written)) {
1463
+ fencedAt.delete(locator);
1464
+ }
1150
1465
  }
1466
+ const kept = await local.held();
1151
1467
  await loseRooms(found
1152
- .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1468
+ .filter((room) => room.replica.admission !== 'admitted' &&
1469
+ (kept.held.has(room.locator) || transfers.has(room.locator)))
1153
1470
  .map((room) => room.locator));
1154
- const listed = found.filter((room) => !fences.has(room.locator));
1155
- const listedLocators = new Set(listed.map((room) => room.locator));
1156
- for (const locator of listedLocators)
1471
+ const listed = found.filter(({ locator }) => !fencedAt.has(locator) && (!stillFenced.has(locator) || relisted.has(locator)));
1472
+ const listedLocatorsNow = new Set(listed.map((room) => room.locator));
1473
+ for (const locator of listedLocatorsNow)
1157
1474
  scopedRefusals.delete(locator);
1158
1475
  if (left === undefined)
1159
1476
  discovered.clear();
1160
1477
  for (const room of listed)
1161
1478
  discovered.set(room.locator, room);
1162
- await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1479
+ const named = new Set(found.map((room) => room.locator));
1480
+ await loseRooms(left ??
1481
+ [...new Set([...declared.keys(), ...kept.held, ...transfers.keys()])].filter((locator) => !named.has(locator)));
1482
+ const held = await storedRooms(listed.map((room) => room.locator));
1163
1483
  for (const room of listed) {
1164
- const held = replicas.get(room.locator)?.record;
1165
- if (held !== undefined && held.replica.selection !== room.replica.selection) {
1166
- list(held, false);
1484
+ const record = held.get(room.locator)?.record;
1485
+ if (record !== undefined && record.replica.selection !== room.replica.selection) {
1486
+ listHeld(room.locator);
1167
1487
  continue;
1168
1488
  }
1169
1489
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
@@ -1171,74 +1491,36 @@ export function composeRuntime(options) {
1171
1491
  confirmLocks(listed);
1172
1492
  roomsListed = true;
1173
1493
  emitRooms();
1174
- relocked(lockedBefore);
1494
+ await relocked(lockedBefore);
1175
1495
  return listed;
1176
1496
  }
1177
- function heldLocally(locator) {
1178
- return (replicas.has(locator) ||
1179
- roomDrafts.has(locator) ||
1180
- transfers.has(locator) ||
1181
- queuesWithoutReplica.has(locator));
1182
- }
1183
- function unlistedHeld(listed) {
1184
- return [
1185
- ...new Set([
1186
- ...rooms.keys(),
1187
- ...roomDrafts.keys(),
1188
- ...transfers.keys(),
1189
- ...queuesWithoutReplica.keys(),
1190
- ]),
1191
- ].filter((locator) => !listed.has(locator));
1192
- }
1193
- function fence(locators) {
1194
- const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1195
- const removed = fresh.map((locator) => ({
1196
- locator,
1197
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
1198
- }));
1199
- for (const locator of fresh) {
1200
- fences.set(locator, { at: discoveries, erased: false });
1201
- lastFenced.set(locator, ++fencings);
1202
- confirmed.delete(locator);
1203
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1204
- pendings.delete(pendingKey(locator, batch.id));
1205
- landedAhead.delete(pendingKey(locator, batch.id));
1206
- }
1207
- replicas.delete(locator);
1208
- highest.delete(locator);
1209
- beyondHorizon.delete(locator);
1210
- queuesWithoutReplica.delete(locator);
1211
- rooms.delete(locator);
1212
- roomDrafts.delete(locator);
1213
- attachedFiles.delete(locator);
1214
- untransferred(locator);
1215
- startedOver.delete(locator);
1216
- catching.delete(locator);
1217
- discovered.delete(locator);
1218
- unreadable.delete(locator);
1219
- stale.delete(locator);
1220
- repairable.delete(locator);
1221
- unpreparable.delete(locator);
1222
- scopedRefusals.delete(locator);
1223
- models.delete(locator);
1224
- liveModels.delete(locator);
1225
- invalidModels.delete(locator);
1226
- journal.record(`room lost ${locator}`);
1227
- }
1228
- if (fresh.length === 0)
1229
- return fresh;
1230
- publishChangeSet(mergeChangeSets(removed.map(({ locator, drafts }) => changeSet(locator, { documents: null, room: true, queue: true, drafts }))));
1231
- liveQueries.recover();
1232
- moveTo({ ...status, ...attention() });
1233
- return fresh;
1497
+ function forget(locator) {
1498
+ confirmed.delete(locator);
1499
+ for (const key of [...landedAhead.keys()]) {
1500
+ if (key.startsWith(`${locator}\u0000`))
1501
+ landedAhead.delete(key);
1502
+ }
1503
+ confirmations.delete(locator);
1504
+ highest.delete(locator);
1505
+ beyondHorizon.delete(locator);
1506
+ declared.delete(locator);
1507
+ attachedFiles.delete(locator);
1508
+ untransferred(locator);
1509
+ startedOver.delete(locator);
1510
+ catching.delete(locator);
1511
+ discovered.delete(locator);
1512
+ unreadable.delete(locator);
1513
+ stale.delete(locator);
1514
+ repairable.delete(locator);
1515
+ unpreparable.delete(locator);
1516
+ scopedRefusals.delete(locator);
1517
+ unheldModels.delete(locator);
1518
+ typesHeld.delete(locator);
1234
1519
  }
1235
1520
  async function eraseFenced(locators) {
1236
1521
  for (const locator of locators) {
1237
1522
  while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1238
1523
  await yielded();
1239
- const held = fences.get(locator);
1240
- if (held !== undefined)
1241
- held.erased = true;
1242
1524
  }
1243
1525
  }
1244
1526
  let sweeping = false;
@@ -1260,147 +1542,60 @@ export function composeRuntime(options) {
1260
1542
  }
1261
1543
  })();
1262
1544
  }
1545
+ function fenceLive(locators) {
1546
+ for (const locator of new Set(locators)) {
1547
+ if (fencedAt.has(locator))
1548
+ continue;
1549
+ fencedAt.set(locator, { at: discoveries, written: false });
1550
+ lastFenced.set(locator, ++fencings);
1551
+ forget(locator);
1552
+ journal.record(`room lost ${locator}`);
1553
+ }
1554
+ }
1263
1555
  async function loseRooms(locators) {
1264
- const fresh = fence(locators);
1556
+ fenceLive(locators);
1557
+ const fresh = [...new Set(locators)].filter((locator) => {
1558
+ const fence = fencedAt.get(locator);
1559
+ if (fence === undefined || fence.written)
1560
+ return false;
1561
+ fence.written = true;
1562
+ return true;
1563
+ });
1265
1564
  if (fresh.length === 0)
1266
1565
  return;
1267
- await durably(async (local) => {
1268
- await local.fence(fresh);
1269
- await unpair(local);
1270
- });
1271
- await eraseFenced(fresh);
1272
- }
1273
- function retyped(persisted) {
1274
- if (persisted.status === 'unavailable')
1275
- return persisted;
1276
- return { ...persisted, types: schemaTypes(persisted.snapshot.schema, roomTypes) };
1277
- }
1278
- function restored(key, held) {
1279
- const persisted = retyped(held);
1280
- const prior = models.get(key);
1281
- if (prior !== undefined &&
1282
- prior.status !== 'unavailable' &&
1283
- (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
1284
- invalidModels.add(key);
1285
- }
1286
- const model = persisted.status === 'unavailable' ||
1287
- liveModels.get(key) === JSON.stringify(persisted.snapshot)
1288
- ? persisted
1289
- : cached(persisted);
1290
- models.set(key, model);
1291
- if (model.status === 'unavailable') {
1292
- liveModels.delete(key);
1293
- invalidModels.delete(key);
1294
- }
1295
- return model;
1296
- }
1297
- async function hydrate(opened) {
1298
- const [held, queues, kept, heldConversations, chatDrafts] = await Promise.all([
1299
- opened.opened(),
1300
- opened.queues(),
1301
- opened.roomDrafts(),
1302
- opened.conversations(),
1303
- opened.chatDrafts(),
1304
- ]);
1305
- const records = [...held.rooms.values()];
1306
- const counts = await opened.counts(records.map(({ key, typedSelection }) => ({ key, roots: typedSelection?.roots ?? [] })));
1307
- const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
1308
- targets = held.targets;
1309
- transfers.clear();
1310
- arrivals.clear();
1311
- streaming.clear();
1312
- startedOver.clear();
1313
- limits = held.providerLimits;
1314
- capabilities = held.capabilities;
1315
- member = held.member;
1316
- roomDrafts.clear();
1317
- for (const record of kept)
1318
- rememberRoomDraft(record);
1319
- presentations.clear();
1320
- for (const record of held.presentation)
1321
- presentations.set(record.app, record.value);
1322
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1323
- pendings.clear();
1324
- landedAhead.clear();
1325
- for (const queue of queues) {
1326
- for (const { batch, state } of queue.pending) {
1327
- pendings.set(pendingKey(queue.locator, batch.id), state);
1328
- }
1329
- }
1330
- replicas.clear();
1331
- reopened.clear();
1332
- queuesWithoutReplica.clear();
1333
- for (const [locator, queue] of queuedBatches) {
1334
- if (!held.rooms.has(locator))
1335
- queuesWithoutReplica.set(locator, queue);
1336
- }
1337
- records.forEach((stored, index) => {
1338
- if (fences.has(stored.locator))
1339
- return;
1340
- const record = { ...stored, model: restored(stored.locator, stored.model) };
1341
- reopened.add(record.locator);
1342
- replicas.set(record.locator, {
1343
- record,
1344
- queue: queuedBatches.get(record.locator) ?? [],
1345
- overlaid: held.overlaid.get(record.key) ?? new Set(),
1346
- count: counts[index] ?? 0,
1347
- confirmed: 0,
1348
- indexed: !held.unindexed.has(record.key),
1349
- });
1350
- const existing = rooms.get(record.locator);
1351
- if (existing === undefined)
1352
- list(record, record.roomVersion !== null);
1353
- else {
1354
- rooms.set(record.locator, {
1355
- ...existing,
1356
- roomVersion: record.roomVersion,
1357
- operations: record.operations,
1358
- replica: record.replica,
1359
- model: modelTypes(record.model),
1360
- typedSelection: record.typedSelection === null
1361
- ? null
1362
- : { roots: record.typedSelection.roots, model: modelTypes(builtIn) },
1363
- });
1364
- }
1365
- });
1566
+ let held = [];
1366
1567
  try {
1367
- for (const key of held.orphans)
1368
- await opened.release(key);
1568
+ await durably(async (local) => {
1569
+ held = await local.roomDrafts();
1570
+ await local.fence(fresh);
1571
+ });
1369
1572
  }
1370
1573
  catch (error) {
1371
- if (!refusedForSpace(error))
1372
- throw error;
1574
+ for (const locator of fresh) {
1575
+ const fence = fencedAt.get(locator);
1576
+ if (fence !== undefined)
1577
+ fence.written = false;
1578
+ }
1579
+ throw error;
1373
1580
  }
1374
- if (held.erasing || held.orphans.length > 0)
1375
- queueMicrotask(sweep);
1376
- return {
1377
- lastCompleted: held.lastCompleted,
1378
- locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
1379
- read: mergeChangeSets([...replicas.keys()].map((locator) => changeSet(locator, { documents: null }))),
1380
- };
1381
- }
1382
- async function readState(opened) {
1383
- const { lastCompleted, locators, read } = await hydrate(opened);
1384
- applyChangeSet(read);
1385
- prepare();
1386
- if (replicas.size > 0)
1387
- emitRooms();
1388
- if (member !== null)
1389
- emitMember();
1390
- for (const locator of locators)
1391
- emitQueue(locator);
1392
- publishRoomDrafts(false);
1393
- return lastCompleted;
1581
+ finally {
1582
+ publishChangeSet(mergeChangeSets(fresh.map((locator) => changeSet(locator, {
1583
+ documents: null,
1584
+ room: true,
1585
+ queue: true,
1586
+ drafts: held.filter((draft) => draft.locator === locator).map(({ path }) => path),
1587
+ }))));
1588
+ recoverReads();
1589
+ moveTo(status);
1590
+ }
1591
+ await durably(unpair);
1592
+ await eraseFenced(fresh);
1394
1593
  }
1395
1594
  async function open() {
1396
1595
  if (closed)
1397
1596
  throw new Error('runtime closed');
1398
- const opened = openRuntimeStore(isFenced, database);
1597
+ const opened = openRuntimeStore(database);
1399
1598
  const interrupted = await opened.fences();
1400
- for (const locator of interrupted) {
1401
- fences.set(locator, { at: discoveries, erased: false });
1402
- lastFenced.set(locator, ++fencings);
1403
- }
1404
1599
  const kept = options.shell.generation;
1405
1600
  const storedGeneration = await opened.generation();
1406
1601
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -1413,7 +1608,21 @@ export function composeRuntime(options) {
1413
1608
  storageLoss = loss;
1414
1609
  emitStorageLoss();
1415
1610
  }
1416
- const lastCompleted = await readState(opened);
1611
+ const held = await opened.opened();
1612
+ heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
1613
+ try {
1614
+ await opened.demote();
1615
+ await database.ask({ kind: 'read.builds', build });
1616
+ for (const key of held.orphans)
1617
+ await opened.release(key);
1618
+ }
1619
+ catch (error) {
1620
+ if (!refusedForSpace(error))
1621
+ throw error;
1622
+ }
1623
+ if (held.erasing || held.orphans.length > 0)
1624
+ queueMicrotask(sweep);
1625
+ await conversations.opened(opened);
1417
1626
  if (interrupted.length > 0) {
1418
1627
  queueMicrotask(() => {
1419
1628
  void eraseFenced(interrupted).catch(() => {
@@ -1421,7 +1630,9 @@ export function composeRuntime(options) {
1421
1630
  });
1422
1631
  });
1423
1632
  }
1424
- moveTo({ ...status, lastCompleted, ...attention() });
1633
+ moveTo(status);
1634
+ prepare();
1635
+ emitRooms();
1425
1636
  void exclusive(() => unpair(opened)).catch(() => {
1426
1637
  journal.record('unpair failed');
1427
1638
  });
@@ -1464,9 +1675,9 @@ export function composeRuntime(options) {
1464
1675
  gathering = entry;
1465
1676
  return entry.done;
1466
1677
  }
1467
- function withdrawable(locator) {
1678
+ function withdrawable(states) {
1468
1679
  return (batch) => {
1469
- const state = pendings.get(pendingKey(locator, batch.id));
1680
+ const state = states.get(batch.id);
1470
1681
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1471
1682
  };
1472
1683
  }
@@ -1488,13 +1699,13 @@ export function composeRuntime(options) {
1488
1699
  const partner = partnerOf(batch, locator);
1489
1700
  if (partner === null)
1490
1701
  return true;
1491
- const other = replicas.get(partner);
1702
+ const other = await replicaOf(partner);
1492
1703
  const half = other?.queue.find((one) => one.id === batch.id);
1493
1704
  if (other === undefined || half === undefined)
1494
1705
  return false;
1495
1706
  return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1496
1707
  }
1497
- function carriedBack(locator, unlanded, written) {
1708
+ async function carriedBack(locator, unlanded, written) {
1498
1709
  const back = new Map();
1499
1710
  const taken = [];
1500
1711
  for (const batch of unlanded) {
@@ -1503,13 +1714,15 @@ export function composeRuntime(options) {
1503
1714
  continue;
1504
1715
  const source = between.source.room;
1505
1716
  const destination = between.destination.room;
1506
- for (const { path: to } of pendings.get(pendingKey(destination, batch.id))?.drafts ?? []) {
1507
- const record = roomDrafts.get(destination)?.get(to);
1717
+ const local = await openLocal();
1718
+ const arrived = (await local.pending(destination, batch.id)).state?.drafts ?? [];
1719
+ for (const { path: to } of arrived) {
1720
+ const record = await draftOf(destination, to);
1508
1721
  const path = between.moves.find((move) => move.to === to)?.path;
1509
1722
  if (record === undefined || path === undefined)
1510
1723
  continue;
1511
1724
  const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1512
- const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1725
+ const existing = into.get(key) ?? (await draftOf(source, path));
1513
1726
  taken.push({ locator: destination, path: to });
1514
1727
  into.set(key, {
1515
1728
  locator: source,
@@ -1535,19 +1748,14 @@ export function composeRuntime(options) {
1535
1748
  .filter((one) => one.locator === room)
1536
1749
  .map(({ path }) => path),
1537
1750
  })),
1538
- remember() {
1539
- for (const one of taken)
1540
- forgetRoomDraft(one.locator, one.path);
1541
- for (const record of records)
1542
- rememberRoomDraft(record);
1543
- },
1544
1751
  };
1545
1752
  }
1546
- function committedRoom(locator, answer) {
1547
- const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1548
- if (!indexed)
1753
+ function preparedAfter(locator, answer) {
1754
+ if (answer.indexed[locator] === false)
1549
1755
  prepare([locator]);
1550
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1756
+ }
1757
+ function confirmedAgain(locator) {
1758
+ confirmations.set(locator, (confirmations.get(locator) ?? 0) + 1);
1551
1759
  }
1552
1760
  function replayed(record, confirmedLayer, queue) {
1553
1761
  return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
@@ -1564,13 +1772,14 @@ export function composeRuntime(options) {
1564
1772
  touched,
1565
1773
  };
1566
1774
  }
1567
- async function settle(local, locator, before, record, queue, settling = {}) {
1568
- return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1775
+ async function settle(local, locator, work, record, queue, settling = {}) {
1776
+ return together(local, [await settlement(local, locator, work, record, queue, settling)]);
1569
1777
  }
1570
- function unpairedIn(locator, batch) {
1571
- if (!queueIn(locator).some((held) => held.id === batch.id))
1778
+ async function unpairedIn(locator, batch) {
1779
+ const { state } = await (await openLocal()).pending(locator, batch.id);
1780
+ if (state === undefined)
1572
1781
  return false;
1573
- return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1782
+ return !leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch);
1574
1783
  }
1575
1784
  async function together(local, settlements) {
1576
1785
  const settled = new Set(settlements.map(({ locator }) => locator));
@@ -1580,8 +1789,9 @@ export function composeRuntime(options) {
1580
1789
  for (const { locator, left } of wave) {
1581
1790
  for (const batch of left) {
1582
1791
  const partner = partnerOf(batch, locator);
1583
- if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1792
+ if (partner === null || settled.has(partner) || !(await unpairedIn(partner, batch))) {
1584
1793
  continue;
1794
+ }
1585
1795
  partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1586
1796
  }
1587
1797
  }
@@ -1599,9 +1809,10 @@ export function composeRuntime(options) {
1599
1809
  });
1600
1810
  return mergeChangeSets(all.map((one) => one.apply(answer)));
1601
1811
  }
1602
- async function settlement(local, locator, before, record, queue, settling = {}) {
1812
+ async function settlement(local, locator, work, record, queue, settling = {}) {
1813
+ const before = work.replica;
1603
1814
  const view = settling.view ?? 'confirmed';
1604
- const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1815
+ const previousQueue = work.queue;
1605
1816
  const overrides = settling.changes;
1606
1817
  const read = await sliceAt(record, view, queue, [
1607
1818
  ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
@@ -1618,7 +1829,7 @@ export function composeRuntime(options) {
1618
1829
  Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1619
1830
  });
1620
1831
  const unshown = queue.filter((batch) => !shown.includes(batch));
1621
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1832
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(work.states), localSaves, locator, composition));
1622
1833
  const founded = kept.filter((batch) => !unshown.includes(batch));
1623
1834
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1624
1835
  const unqueued = shown.map((batch) => ({ batch, landed: true }));
@@ -1636,8 +1847,8 @@ export function composeRuntime(options) {
1636
1847
  for (const { batch, returned: reason } of returning) {
1637
1848
  if (reason === null || batch.between?.destination.room === locator)
1638
1849
  continue;
1639
- for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
1640
- const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
1850
+ for (const saved of work.states.get(batch.id)?.drafts ?? []) {
1851
+ const existing = written.get(saved.path) ?? (await draftOf(locator, saved.path));
1641
1852
  written.set(saved.path, {
1642
1853
  locator,
1643
1854
  path: saved.path,
@@ -1647,12 +1858,12 @@ export function composeRuntime(options) {
1647
1858
  });
1648
1859
  }
1649
1860
  }
1650
- const carried = carriedBack(locator, [
1861
+ const carried = await carriedBack(locator, [
1651
1862
  ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1652
1863
  ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1653
1864
  ], written);
1654
1865
  const replayedLayer = await replayed(record, confirmedLayer, kept);
1655
- const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1866
+ const { overlay } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1656
1867
  const commit = {
1657
1868
  rooms: [
1658
1869
  {
@@ -1672,6 +1883,7 @@ export function composeRuntime(options) {
1672
1883
  {
1673
1884
  locator,
1674
1885
  queue: kept,
1886
+ stored: previousQueue,
1675
1887
  founded,
1676
1888
  roomDrafts: [...written.values()],
1677
1889
  consumedRoomDrafts: carried.taken,
@@ -1686,33 +1898,21 @@ export function composeRuntime(options) {
1686
1898
  commit,
1687
1899
  apply: (answer) => {
1688
1900
  for (const batch of previousQueue) {
1689
- if (staying.has(batch.id))
1690
- continue;
1691
- pendings.delete(pendingKey(locator, batch.id));
1692
- landedAhead.delete(pendingKey(locator, batch.id));
1901
+ if (!staying.has(batch.id))
1902
+ landedAhead.delete(pendingKey(locator, batch.id));
1693
1903
  }
1694
- for (const draft of written.values())
1695
- rememberRoomDraft(draft);
1696
- carried.remember();
1697
1904
  const drafts = [...written.keys(), ...carried.taken];
1698
- queuesWithoutReplica.delete(locator);
1699
1905
  for (const { batch, landed: held } of unqueued) {
1700
1906
  const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1701
1907
  journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1702
1908
  }
1703
1909
  const changed = answer.changed[locator];
1704
- const confirmedChanged = settling.commit?.published !== undefined ||
1910
+ if (settling.commit?.published !== undefined ||
1705
1911
  settling.commit?.refreshed === true ||
1706
- overrides !== undefined;
1707
- const { count, indexed } = committedRoom(locator, answer);
1708
- replicas.set(locator, {
1709
- record,
1710
- queue: kept,
1711
- overlaid: touched,
1712
- count,
1713
- confirmed: (before?.confirmed ?? 0) + (confirmedChanged ? 1 : 0),
1714
- indexed,
1715
- });
1912
+ overrides !== undefined) {
1913
+ confirmedAgain(locator);
1914
+ }
1915
+ preparedAfter(locator, answer);
1716
1916
  return mergeChangeSets([
1717
1917
  changeSet(locator, {
1718
1918
  documents: changed === undefined ? [] : changed,
@@ -1725,22 +1925,11 @@ export function composeRuntime(options) {
1725
1925
  },
1726
1926
  };
1727
1927
  }
1728
- function adoptModels(locator, record) {
1729
- const model = record.model;
1730
- models.set(locator, model);
1731
- invalidModels.delete(locator);
1732
- if (model.status === 'current')
1733
- liveModels.set(locator, JSON.stringify(model.snapshot));
1734
- else if (model.status === 'unavailable')
1735
- liveModels.delete(locator);
1736
- }
1737
- function remodel(locator, answers, held) {
1928
+ function remodel(answers, held) {
1738
1929
  const answer = answers?.collection;
1739
1930
  if (answer === undefined)
1740
1931
  return held;
1741
- const state = attachedModel(answer, roomTypes);
1742
- models.set(locator, state);
1743
- return { collection: state };
1932
+ return { collection: attachedModel(answer, roomTypes) };
1744
1933
  }
1745
1934
  function publish(local, room, reached, landing, live) {
1746
1935
  const landed = exclusive(async () => {
@@ -1748,8 +1937,9 @@ export function composeRuntime(options) {
1748
1937
  caughtUpTo(room.locator);
1749
1938
  return true;
1750
1939
  }
1751
- const prior = replicas.get(room.locator);
1752
- if (!stands(room.locator, landing.from)) {
1940
+ const work = await workOf(room.locator);
1941
+ const prior = work.replica;
1942
+ if (!standsAt(room.locator, prior, landing.from)) {
1753
1943
  if (landing.kind === 'refreshed') {
1754
1944
  await local.unrefresh(reached.key);
1755
1945
  return false;
@@ -1765,11 +1955,9 @@ export function composeRuntime(options) {
1765
1955
  ...(landing.remodelled ? { model: reached.model } : {}),
1766
1956
  }
1767
1957
  : reached;
1768
- adoptModels(room.locator, record);
1769
- const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1770
1958
  let set;
1771
1959
  try {
1772
- set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1960
+ set = await settle(local, room.locator, work, record, work.queue, landing.kind === 'refreshed'
1773
1961
  ? {
1774
1962
  room: true,
1775
1963
  view: 'refreshed',
@@ -1791,14 +1979,16 @@ export function composeRuntime(options) {
1791
1979
  return true;
1792
1980
  }
1793
1981
  if (landing.kind === 'bootstrapped') {
1982
+ readied(room.locator);
1794
1983
  highest.delete(room.locator);
1795
1984
  beyondHorizon.delete(room.locator);
1796
1985
  untransferred(room.locator);
1797
1986
  startedOver.delete(room.locator);
1798
1987
  catching.delete(room.locator);
1988
+ unheldModels.delete(room.locator);
1799
1989
  if (prior !== undefined && prior.record.key !== record.key)
1800
1990
  sweep();
1801
- name(room.locator);
1991
+ void name(room.locator);
1802
1992
  }
1803
1993
  stale.delete(room.locator);
1804
1994
  unpreparable.delete(room.locator);
@@ -1807,28 +1997,27 @@ export function composeRuntime(options) {
1807
1997
  list(room, true);
1808
1998
  publishChangeSet(set);
1809
1999
  await unpair(local);
1810
- moveTo({ ...status, ...attention() });
1811
- if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
2000
+ moveTo(status);
2001
+ if (landing.kind === 'bootstrapped' && work.queue.length > 0)
1812
2002
  submitLater(room.locator);
1813
- }
1814
2003
  return true;
1815
2004
  });
1816
2005
  return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1817
2006
  }
1818
2007
  async function savedRoom(locator) {
1819
2008
  const local = await openLocal();
1820
- const replica = replicas.get(locator);
1821
- const room = rooms.get(locator);
1822
- if (replica === undefined || room === undefined) {
2009
+ const replica = await replicaOf(locator);
2010
+ if (replica === undefined || isFenced(locator)) {
1823
2011
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1824
2012
  }
1825
- return { local, replica, room };
2013
+ return { local, replica };
1826
2014
  }
1827
2015
  async function saveRoomDrafts(locator, named, writtenBy) {
1828
2016
  const target = await savedRoom(locator);
2017
+ const drafts = new Map((await draftsOf(locator, named.map(({ path }) => path))).map((record) => [record.path, record]));
1829
2018
  const saved = [];
1830
2019
  for (const { path, revision } of named) {
1831
- const record = roomDrafts.get(locator)?.get(path);
2020
+ const record = drafts.get(path);
1832
2021
  if (record === undefined || record.revision !== revision) {
1833
2022
  return { outcome: 'refused', code: 'stale', retryable: false, file: path };
1834
2023
  }
@@ -1855,14 +2044,16 @@ export function composeRuntime(options) {
1855
2044
  }
1856
2045
  return committed(locator, target, saved, new Set(), writtenBy, files);
1857
2046
  }
1858
- function carriedDrafts(locator, saved, consumed) {
1859
- const holding = roomDrafts.get(locator);
2047
+ async function carriedDrafts(locator, saved, consumed) {
2048
+ if (!saved.some(({ parts }) => parts.move !== undefined))
2049
+ return new Map();
2050
+ const holding = new Map((await draftsOf(locator)).map((record) => [record.path, record]));
1860
2051
  return new Map(saved.flatMap(({ path, parts }) => {
1861
- const record = holding?.get(path);
2052
+ const record = holding.get(path);
1862
2053
  if (parts.move === undefined || record === undefined || consumed.has(path))
1863
2054
  return [];
1864
2055
  const to = moveTarget(parts.move);
1865
- if (to === path || holding?.has(to) === true)
2056
+ if (to === path || holding.has(to))
1866
2057
  return [];
1867
2058
  return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1868
2059
  }));
@@ -1977,15 +2168,15 @@ export function composeRuntime(options) {
1977
2168
  ...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
1978
2169
  ];
1979
2170
  }
1980
- async function sideOf(locator, replica, room, saving) {
2171
+ async function sideOf(locator, replica, saving) {
1981
2172
  const record = replica.record;
1982
2173
  return {
1983
2174
  room: locator,
1984
- handle: room.handle,
2175
+ handle: record.handle,
1985
2176
  baseRoomVersion: record.roomVersion,
1986
2177
  composition: compositionFor(record),
1987
- operations: room.operations,
1988
- providerOwnedPaths: room.providerOwnedPaths,
2178
+ operations: record.operations,
2179
+ providerOwnedPaths: record.providerOwnedPaths,
1989
2180
  visible: saving.visible,
1990
2181
  folders: saving.folders,
1991
2182
  queued: queuedDocuments(replica.queue),
@@ -1998,12 +2189,10 @@ export function composeRuntime(options) {
1998
2189
  return into?.parts.move?.room ?? null;
1999
2190
  }
2000
2191
  async function saveCommit(local, files, rooms) {
2001
- const touched = new Map();
2002
2192
  const commits = [];
2003
2193
  for (const { locator, saving, before, queue } of rooms) {
2004
2194
  const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
2005
2195
  const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
2006
- touched.set(locator, diff.touched);
2007
2196
  commits.push({
2008
2197
  locator,
2009
2198
  key: saving.record.key,
@@ -2013,27 +2202,18 @@ export function composeRuntime(options) {
2013
2202
  });
2014
2203
  }
2015
2204
  try {
2016
- const answer = await local.commit({
2205
+ return await local.commit({
2017
2206
  rooms: commits,
2018
2207
  writes: rooms.map(({ write }) => write),
2019
2208
  files,
2020
2209
  whole: true,
2021
2210
  });
2022
- return { answer, touched };
2023
2211
  }
2024
2212
  catch {
2025
2213
  return null;
2026
2214
  }
2027
2215
  }
2028
- function savedReplica(locator, before, queue, touched, answer) {
2029
- replicas.set(locator, {
2030
- ...before,
2031
- queue,
2032
- overlaid: touched,
2033
- count: answer.counts[locator] ?? before.count,
2034
- });
2035
- }
2036
- async function committedMove(locator, destination, { local, replica, room }, saved, consumed, writtenBy, files) {
2216
+ async function committedMove(locator, destination, { local, replica }, saved, consumed, writtenBy, files) {
2037
2217
  const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2038
2218
  if (mixed !== undefined) {
2039
2219
  return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
@@ -2043,27 +2223,28 @@ export function composeRuntime(options) {
2043
2223
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2044
2224
  if (changed.length > 0)
2045
2225
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2046
- const target = replicas.get(destination);
2047
- const listedRoom = rooms.get(destination);
2048
- if (target === undefined || listedRoom === undefined) {
2226
+ const target = await replicaOf(destination);
2227
+ if (target === undefined || isFenced(destination)) {
2049
2228
  throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2050
2229
  }
2051
2230
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2052
2231
  const built = await buildMove({
2053
- source: await sideOf(locator, replica, room, sourceSaving),
2054
- destination: await sideOf(destination, target, listedRoom, arriving),
2232
+ source: await sideOf(locator, replica, sourceSaving),
2233
+ destination: await sideOf(destination, target, arriving),
2055
2234
  moves,
2056
- providerLimits: limits,
2235
+ providerLimits: (await local.facts()).providerLimits,
2057
2236
  local: localSaves,
2058
2237
  ...(writtenBy === undefined ? {} : { writtenBy }),
2059
2238
  });
2060
2239
  if (built.kind === 'refused')
2061
2240
  return { outcome: 'refused', ...built.refusal };
2062
2241
  const id = built.leaving.id;
2242
+ const leavingDrafts = new Map((await draftsOf(locator, saved.map(({ path }) => path))).map((record) => [record.path, record]));
2243
+ const arrivingDrafts = new Set((await draftsOf(destination, moves.map(({ to }) => to))).map(({ path }) => path));
2063
2244
  const carrying = saved.flatMap(({ path, parts }) => {
2064
- const record = roomDrafts.get(locator)?.get(path);
2245
+ const record = leavingDrafts.get(path);
2065
2246
  const to = moveTarget(parts.move);
2066
- if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2247
+ if (record === undefined || consumed.has(path) || arrivingDrafts.has(to)) {
2067
2248
  return [];
2068
2249
  }
2069
2250
  const moved = {
@@ -2079,7 +2260,7 @@ export function composeRuntime(options) {
2079
2260
  const left = [...consumed, ...carrying.map(({ from }) => from)];
2080
2261
  const leavingQueue = [...replica.queue, built.leaving];
2081
2262
  const arrivingQueue = [...target.queue, built.arriving];
2082
- const committedBoth = await saveCommit(local, files, [
2263
+ const answer = await saveCommit(local, files, [
2083
2264
  {
2084
2265
  locator,
2085
2266
  saving: sourceSaving,
@@ -2088,6 +2269,7 @@ export function composeRuntime(options) {
2088
2269
  write: {
2089
2270
  locator,
2090
2271
  queue: leavingQueue,
2272
+ stored: replica.queue,
2091
2273
  saved: new Map([[id, saved]]),
2092
2274
  consumedRoomDrafts: left,
2093
2275
  },
@@ -2100,37 +2282,23 @@ export function composeRuntime(options) {
2100
2282
  write: {
2101
2283
  locator: destination,
2102
2284
  queue: arrivingQueue,
2285
+ stored: target.queue,
2103
2286
  saved: new Map([[id, marked]]),
2104
2287
  roomDrafts: carried,
2105
2288
  },
2106
2289
  },
2107
2290
  ]);
2108
- if (committedBoth === null)
2291
+ if (answer === null)
2109
2292
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2110
- const { answer, touched } = committedBoth;
2111
2293
  if (answer.fenced !== null) {
2112
2294
  throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2113
2295
  }
2114
- if (fences.has(locator) || fences.has(destination)) {
2296
+ if (isFenced(locator) || isFenced(destination)) {
2115
2297
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2116
2298
  }
2117
- pendings.set(pendingKey(locator, id), {
2118
- drafts: saved,
2119
- submission: { state: 'waiting' },
2120
- lost: false,
2121
- });
2122
- pendings.set(pendingKey(destination, id), {
2123
- drafts: marked,
2124
- submission: { state: 'waiting' },
2125
- lost: false,
2126
- });
2127
- for (const path of left)
2128
- forgetRoomDraft(locator, path);
2129
- for (const record of carried)
2130
- rememberRoomDraft(record);
2131
- savedReplica(locator, replica, leavingQueue, touched.get(locator), answer);
2132
- savedReplica(destination, target, arrivingQueue, touched.get(destination), answer);
2133
- publishChangeSet(mergeChangeSets([
2299
+ preparedAfter(locator, answer);
2300
+ preparedAfter(destination, answer);
2301
+ const shownNow = publishChangeSet(mergeChangeSets([
2134
2302
  changeSet(locator, {
2135
2303
  documents: answer.changed[locator] ?? [],
2136
2304
  queue: true,
@@ -2142,14 +2310,15 @@ export function composeRuntime(options) {
2142
2310
  drafts: carried.map(({ path }) => path),
2143
2311
  }),
2144
2312
  ]));
2145
- queuedSave(locator);
2313
+ await Promise.all([shownNow, queuedSave(locator)]);
2146
2314
  return { outcome: 'saved', identifier: id };
2147
2315
  }
2148
2316
  function queuedSave(locator) {
2149
2317
  if (!localSaves)
2150
2318
  recorded = false;
2151
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2319
+ const shownNow = moveTo(localSaves ? status : { ...status, state: 'pending', resting: false });
2152
2320
  submitLater(locator);
2321
+ return shownNow;
2153
2322
  }
2154
2323
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2155
2324
  if (full)
@@ -2158,7 +2327,8 @@ export function composeRuntime(options) {
2158
2327
  if (into !== null) {
2159
2328
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2160
2329
  }
2161
- const { local, replica, room } = target;
2330
+ const { local, replica } = target;
2331
+ const { providerLimits } = await local.facts();
2162
2332
  const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2163
2333
  const changes = parts.flatMap(({ changes: made }) => made);
2164
2334
  const saving = await savingIn(replica, changes, [
@@ -2168,7 +2338,7 @@ export function composeRuntime(options) {
2168
2338
  const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2169
2339
  if (changed.length > 0)
2170
2340
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2171
- const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(locator), saving.visibleOver));
2341
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(replica.states), saving.visibleOver));
2172
2342
  if (requeue.kind === 'refused')
2173
2343
  return { outcome: 'refused', ...requeue.refusal };
2174
2344
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -2188,10 +2358,10 @@ export function composeRuntime(options) {
2188
2358
  local: localSaves,
2189
2359
  baseRoomVersion: replica.record.roomVersion,
2190
2360
  composition: compositionFor(replica.record),
2191
- operations: room.operations,
2192
- providerOwnedPaths: room.providerOwnedPaths,
2193
- providerLimits: limits,
2194
- handle: room.handle,
2361
+ operations: replica.record.operations,
2362
+ providerOwnedPaths: replica.record.providerOwnedPaths,
2363
+ providerLimits,
2364
+ handle: replica.record.handle,
2195
2365
  visible: saving.visible,
2196
2366
  folders: saving.folders,
2197
2367
  changes,
@@ -2243,7 +2413,7 @@ export function composeRuntime(options) {
2243
2413
  for (const [origin, replaced] of withdrawn) {
2244
2414
  if (replaced === null)
2245
2415
  continue;
2246
- const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
2416
+ const carried = withdrawnDrafts(replica.states.get(origin)?.drafts ?? [], changes);
2247
2417
  drafted.set(replaced.id, replaced === join?.batch
2248
2418
  ? [
2249
2419
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -2257,14 +2427,14 @@ export function composeRuntime(options) {
2257
2427
  for (const replaced of withdrawn.values()) {
2258
2428
  if (replaced === null || !batches.includes(replaced))
2259
2429
  continue;
2260
- const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
2430
+ const limited = await stepped(limiting(batches, replaced, providerLimits, saving.visibleOver));
2261
2431
  if (limited !== null)
2262
2432
  return { outcome: 'refused', ...limited };
2263
2433
  }
2264
- const carried = carriedDrafts(locator, saved, consumed);
2434
+ const carried = await carriedDrafts(locator, saved, consumed);
2265
2435
  const left = [...consumed, ...carried.keys()];
2266
2436
  const arrived = [...carried.values()];
2267
- const done = await saveCommit(local, files, [
2437
+ const answer = await saveCommit(local, files, [
2268
2438
  {
2269
2439
  locator,
2270
2440
  saving,
@@ -2273,40 +2443,27 @@ export function composeRuntime(options) {
2273
2443
  write: {
2274
2444
  locator,
2275
2445
  queue: batches,
2446
+ stored: replica.queue,
2276
2447
  saved: drafted,
2277
2448
  consumedRoomDrafts: left,
2278
2449
  roomDrafts: arrived,
2279
2450
  },
2280
2451
  },
2281
2452
  ]);
2282
- if (done === null)
2453
+ if (answer === null)
2283
2454
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2284
- const { answer, touched } = done;
2285
2455
  if (answer.fenced !== null) {
2286
2456
  throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2287
2457
  }
2288
- if (fences.has(locator))
2458
+ if (isFenced(locator))
2289
2459
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2290
- for (const origin of withdrawn.keys())
2291
- pendings.delete(pendingKey(locator, origin));
2292
- for (const [id, drafts] of drafted) {
2293
- pendings.set(pendingKey(locator, id), {
2294
- drafts,
2295
- submission: { state: 'waiting' },
2296
- lost: false,
2297
- });
2298
- }
2299
- for (const path of left)
2300
- forgetRoomDraft(locator, path);
2301
- for (const record of arrived)
2302
- rememberRoomDraft(record);
2303
- savedReplica(locator, replica, batches, touched.get(locator), answer);
2304
- publishChangeSet(changeSet(locator, {
2460
+ preparedAfter(locator, answer);
2461
+ const shownNow = publishChangeSet(changeSet(locator, {
2305
2462
  documents: answer.changed[locator] ?? [],
2306
2463
  queue: true,
2307
2464
  drafts: [...left, ...arrived.map(({ path }) => path)],
2308
2465
  }));
2309
- queuedSave(locator);
2466
+ await Promise.all([shownNow, queuedSave(locator)]);
2310
2467
  const identifier = join?.batch.id ??
2311
2468
  built?.batch.id ??
2312
2469
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
@@ -2315,29 +2472,26 @@ export function composeRuntime(options) {
2315
2472
  async function mark(local, locator, batch, submission, lost) {
2316
2473
  const partner = partnerOf(batch, locator);
2317
2474
  for (const held of partner === null ? [locator] : [locator, partner]) {
2318
- const state = pendings.get(pendingKey(held, batch.id));
2475
+ const { state, room } = await local.pending(held, batch.id);
2319
2476
  if (!continuing() ||
2320
2477
  state === undefined ||
2321
2478
  (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2322
2479
  continue;
2323
2480
  }
2324
2481
  await local.submit(held, batch.id, submission, lost);
2325
- if (fences.has(held))
2482
+ if (isFenced(held))
2326
2483
  continue;
2327
- pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2328
- if (replicas.has(held))
2484
+ if (room)
2329
2485
  publishChangeSet(changeSet(held, { queue: true }));
2330
2486
  }
2331
- moveTo({ ...status, ...attention() });
2487
+ moveTo(status);
2332
2488
  }
2333
2489
  async function keepUpload(local, locator, identifier, upload) {
2334
2490
  if (!continuing())
2335
2491
  throw new Error('connection refused');
2336
2492
  await local.keepUpload(locator, identifier, upload);
2337
- if (fences.has(locator))
2493
+ if (isFenced(locator))
2338
2494
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2339
- const key = pendingKey(locator, identifier);
2340
- pendings.set(key, { ...pendings.get(key), upload });
2341
2495
  if (!continuing())
2342
2496
  throw new Error('connection refused');
2343
2497
  }
@@ -2373,7 +2527,7 @@ export function composeRuntime(options) {
2373
2527
  if (fencedSince(locator, since))
2374
2528
  return failure({ kind: 'room-not-readable', locator });
2375
2529
  if (result.kind === 'scoped-refusal') {
2376
- const room = replicas.get(result.locator)?.record;
2530
+ const room = await recordAt(result.locator);
2377
2531
  if (room !== undefined)
2378
2532
  await scoped(result, room, await openLocal());
2379
2533
  }
@@ -2386,27 +2540,28 @@ export function composeRuntime(options) {
2386
2540
  return UNCERTAIN;
2387
2541
  }
2388
2542
  }
2389
- function located(room, path, destination) {
2543
+ async function located(room, path, destination) {
2390
2544
  const crossing = roomTarget(destination);
2545
+ const listing = await listingNow();
2391
2546
  if (crossing === null) {
2392
2547
  const named = target(path, destination);
2393
- return named === null || !rooms.has(room) ? null : { room, path: named };
2548
+ return named === null || !listing.has(room) ? null : { room, path: named };
2394
2549
  }
2395
- const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2550
+ const held = [...listing.values()].find((listed) => listed.handle === crossing.handle);
2396
2551
  return held === undefined ? null : { room: held.locator, path: crossing.path };
2397
2552
  }
2398
2553
  async function resolveReference(room, path, destination) {
2399
2554
  if (roomTarget(destination) === null && target(path, destination) === null) {
2400
2555
  return ABSENT_TARGET;
2401
2556
  }
2402
- const at = located(room, path, destination);
2557
+ const at = await located(room, path, destination);
2403
2558
  if (at === null)
2404
2559
  return UNREADABLE;
2405
- if (locked(at.room))
2560
+ if (await locked(at.room))
2406
2561
  return LOCKED_UNREADABLE;
2407
- const held = replicas.get(at.room);
2562
+ const held = await recordAt(at.room);
2408
2563
  if (held !== undefined &&
2409
- (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2564
+ (await (await openLocal()).entries(held.key, 'visible', [at.path])).length > 0) {
2410
2565
  return { kind: 'document', ...at };
2411
2566
  }
2412
2567
  const history = await readHistory(at.room, at.path);
@@ -2421,13 +2576,13 @@ export function composeRuntime(options) {
2421
2576
  ? { kind: 'deleted', ...at, revision: last }
2422
2577
  : { kind: 'document', ...at };
2423
2578
  }
2424
- function queuedFacts(locator, path, localId) {
2425
- for (const batch of replicas.get(locator)?.queue ?? []) {
2579
+ async function queuedFacts(locator, path, localId) {
2580
+ for (const batch of (await replicaOf(locator))?.queue ?? []) {
2426
2581
  const change = batch.changes[queuedFile(batch, path, localId)];
2427
2582
  if (change?.kind === 'binary')
2428
2583
  return change;
2429
2584
  }
2430
- const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2585
+ const drafted = (await draftOf(locator, path))?.parts.file;
2431
2586
  return drafted?.localId === localId ? drafted : null;
2432
2587
  }
2433
2588
  function batchFile(batch) {
@@ -2459,7 +2614,7 @@ export function composeRuntime(options) {
2459
2614
  if (fencedSince(request.locator, since))
2460
2615
  return failure({ kind: 'room-not-readable', locator: request.locator });
2461
2616
  if (settled.kind === 'scoped-refusal') {
2462
- const room = replicas.get(settled.locator)?.record;
2617
+ const room = await recordAt(settled.locator);
2463
2618
  if (room !== undefined)
2464
2619
  await scoped(settled, room, await openLocal());
2465
2620
  }
@@ -2486,7 +2641,7 @@ export function composeRuntime(options) {
2486
2641
  async function binaryPath(locator, path, revision) {
2487
2642
  if (revision !== undefined)
2488
2643
  return { kind: 'path', locator, path, revision };
2489
- const replica = replicas.get(locator);
2644
+ const replica = await replicaOf(locator);
2490
2645
  const local = await openLocal();
2491
2646
  const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2492
2647
  const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
@@ -2526,13 +2681,13 @@ export function composeRuntime(options) {
2526
2681
  }
2527
2682
  async function askedBinary(locator, path, revision) {
2528
2683
  await ready;
2529
- const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2684
+ const drafted = (await draftOf(locator, path))?.parts.file;
2530
2685
  return drafted !== undefined && drafted.localId === revision
2531
2686
  ? { kind: 'pending', localId: revision }
2532
2687
  : binaryPath(locator, path, revision);
2533
2688
  }
2534
- function pendingBinary(locator, path, localId) {
2535
- const facts = queuedFacts(locator, path, localId);
2689
+ async function pendingBinary(locator, path, localId) {
2690
+ const facts = await queuedFacts(locator, path, localId);
2536
2691
  return facts === null
2537
2692
  ? absentBinary(path)
2538
2693
  : {
@@ -2551,10 +2706,10 @@ export function composeRuntime(options) {
2551
2706
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2552
2707
  }
2553
2708
  async function heldMediaType(locator, path) {
2554
- const replica = replicas.get(locator);
2709
+ const replica = await recordAt(locator);
2555
2710
  const [held] = replica === undefined
2556
2711
  ? []
2557
- : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2712
+ : await (await openLocal()).entries(replica.key, 'visible', [path]);
2558
2713
  return held === undefined
2559
2714
  ? attachedFiles.get(locator)?.get(path)?.mediaType
2560
2715
  : held.binary?.mediaType;
@@ -2589,7 +2744,7 @@ export function composeRuntime(options) {
2589
2744
  async function attachBinary(locator, file) {
2590
2745
  await ready;
2591
2746
  await firstDiscovery;
2592
- if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2747
+ if (closed || !roomsNamed || (await connectionNow()) === 'unreachable' || refusal !== null)
2593
2748
  return OFFLINE;
2594
2749
  const request = await attachedFile(locator, file);
2595
2750
  return transferBinary({ locator, path: file.name }, async (held) => {
@@ -2609,8 +2764,8 @@ export function composeRuntime(options) {
2609
2764
  const freed = await database.ask({ kind: 'opened.clean' });
2610
2765
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2611
2766
  }
2612
- function declaredAction(name) {
2613
- for (const capability of capabilities) {
2767
+ async function declaredAction(name) {
2768
+ for (const capability of (await (await openLocal()).facts()).capabilities) {
2614
2769
  for (const action of capability.actions ?? []) {
2615
2770
  if (action.name === name)
2616
2771
  return action;
@@ -2618,8 +2773,8 @@ export function composeRuntime(options) {
2618
2773
  }
2619
2774
  return undefined;
2620
2775
  }
2621
- function calledRooms(name, input) {
2622
- const member = declaredAction(name)?.room;
2776
+ async function calledRooms(name, input) {
2777
+ const member = (await declaredAction(name))?.room;
2623
2778
  if (member === undefined || input === null || typeof input !== 'object')
2624
2779
  return [];
2625
2780
  const named = input[member];
@@ -2629,7 +2784,7 @@ export function composeRuntime(options) {
2629
2784
  await ready;
2630
2785
  if (closed || refusal !== null)
2631
2786
  return UNCERTAIN;
2632
- if (declaredAction(name) === undefined)
2787
+ if ((await declaredAction(name)) === undefined)
2633
2788
  return undeclared(name);
2634
2789
  try {
2635
2790
  await ensureAuthorization();
@@ -2644,7 +2799,7 @@ export function composeRuntime(options) {
2644
2799
  try {
2645
2800
  const result = await options.adapter.action(authorization, { name, input });
2646
2801
  if (result.kind === 'scoped-refusal') {
2647
- const room = replicas.get(result.locator)?.record;
2802
+ const room = await recordAt(result.locator);
2648
2803
  if (room !== undefined)
2649
2804
  await scoped(result, room, await openLocal());
2650
2805
  }
@@ -2731,31 +2886,35 @@ export function composeRuntime(options) {
2731
2886
  function continuing() {
2732
2887
  return !closed && !discoveryPaused;
2733
2888
  }
2734
- function reachedBy(locator, roomVersion) {
2735
- const held = replicas.get(locator);
2736
- return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2889
+ async function reachedBy(locator, roomVersion) {
2890
+ const held = await recordAt(locator);
2891
+ return held !== undefined && options.adapter.reached(locator, held.roomVersion, roomVersion);
2737
2892
  }
2738
- function shownAt(locator) {
2739
- const held = replicas.get(locator)?.record.roomVersion;
2893
+ function shownAt(locator, held) {
2740
2894
  const raised = highest.get(locator);
2741
2895
  if (held === undefined || raised === undefined)
2742
2896
  return held ?? raised;
2743
2897
  return options.adapter.reached(locator, held, raised) ? held : raised;
2744
2898
  }
2745
- function showing(locator, roomVersion) {
2746
- const at = shownAt(locator);
2899
+ function showing(locator, roomVersion, held) {
2900
+ const at = shownAt(locator, held);
2747
2901
  if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2748
2902
  highest.set(locator, roomVersion);
2749
2903
  }
2750
2904
  }
2751
- function older(locator, roomVersion) {
2905
+ function olderThan(locator, roomVersion, held) {
2752
2906
  if (beyondHorizon.has(locator))
2753
2907
  return false;
2754
- const at = shownAt(locator);
2908
+ const at = shownAt(locator, held);
2755
2909
  return (at !== undefined &&
2756
2910
  options.adapter.reached(locator, at, roomVersion) &&
2757
2911
  !options.adapter.reached(locator, roomVersion, at));
2758
2912
  }
2913
+ async function older(locator, roomVersion) {
2914
+ if (beyondHorizon.has(locator))
2915
+ return false;
2916
+ return olderThan(locator, roomVersion, (await recordAt(locator))?.roomVersion);
2917
+ }
2759
2918
  function replaced(locators) {
2760
2919
  for (const locator of locators) {
2761
2920
  highest.delete(locator);
@@ -2763,17 +2922,22 @@ export function composeRuntime(options) {
2763
2922
  }
2764
2923
  }
2765
2924
  async function writtenAfter(locator, roomVersion, path) {
2766
- const held = replicas.get(locator);
2925
+ const held = await recordAt(locator);
2767
2926
  if (held === undefined)
2768
2927
  return false;
2769
- const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2928
+ const [entry] = await (await openLocal()).entries(held.key, 'confirmed', [path]);
2770
2929
  const written = entry?.written ?? undefined;
2771
2930
  return (written !== undefined &&
2772
2931
  options.adapter.reached(locator, written, roomVersion) &&
2773
2932
  !options.adapter.reached(locator, roomVersion, written));
2774
2933
  }
2775
- async function writtenLater(local, locator, key, paths, roomVersion) {
2776
- if (paths.length === 0 || !older(locator, roomVersion))
2934
+ async function writtenLater(local, locator, key, paths, roomVersion, heldAt) {
2935
+ if (paths.length === 0)
2936
+ return new Set();
2937
+ const behindShown = heldAt === undefined
2938
+ ? await older(locator, roomVersion)
2939
+ : olderThan(locator, roomVersion, heldAt);
2940
+ if (!behindShown)
2777
2941
  return new Set();
2778
2942
  const entries = await local.entries(key, 'confirmed', paths);
2779
2943
  return new Set(entries.flatMap(({ path, written }) => written !== null &&
@@ -2782,7 +2946,7 @@ export function composeRuntime(options) {
2782
2946
  ? [path]
2783
2947
  : []));
2784
2948
  }
2785
- function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2949
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false, heldAt) {
2786
2950
  const write = replaying ? finishingWrite : exclusive;
2787
2951
  return {
2788
2952
  turn() {
@@ -2790,7 +2954,7 @@ export function composeRuntime(options) {
2790
2954
  installing(locator);
2791
2955
  },
2792
2956
  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),
2957
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion, heldAt),
2794
2958
  async stage(page) {
2795
2959
  for (let written = 0;;) {
2796
2960
  const end = batchEnd(page.entries, written);
@@ -2928,7 +3092,7 @@ export function composeRuntime(options) {
2928
3092
  unindexed.delete(locator);
2929
3093
  }
2930
3094
  function setBack(locator) {
2931
- const now = bytesOf(locator, true);
3095
+ const now = bytesOf(locator, true, new Map());
2932
3096
  const earlier = startedOver.get(locator);
2933
3097
  startedOver.set(locator, {
2934
3098
  done: Math.max(now.done, earlier?.done ?? 0),
@@ -2964,11 +3128,13 @@ export function composeRuntime(options) {
2964
3128
  if (installs.delete(locator))
2965
3129
  queueReadiness();
2966
3130
  }
2967
- async function pullRefresh(authorized, held, liveness, hold, refresh) {
3131
+ async function pullRefresh(authorized, record, liveness, hold, refresh) {
2968
3132
  if (!liveness())
2969
3133
  return unconfirmed;
2970
- const { record } = held;
2971
- const from = standingOf(record.locator);
3134
+ const from = {
3135
+ key: record.key,
3136
+ confirmed: confirmations.get(record.locator) ?? 0,
3137
+ };
2972
3138
  const local = await openLocal();
2973
3139
  try {
2974
3140
  await exclusive(() => local.unrefresh(record.key));
@@ -2980,7 +3146,7 @@ export function composeRuntime(options) {
2980
3146
  finally {
2981
3147
  hold.take();
2982
3148
  }
2983
- }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3149
+ }, authorized, record, (models, kept) => (liveness() ? remodel(models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness, false, record.roomVersion));
2984
3150
  if (result.kind !== 'advanced') {
2985
3151
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2986
3152
  return result;
@@ -3000,30 +3166,14 @@ export function composeRuntime(options) {
3000
3166
  throw error;
3001
3167
  }
3002
3168
  }
3003
- const heldContents = new Map();
3004
- function heldContentOf(local, key) {
3005
- const known = heldContents.get(key);
3006
- if (known !== undefined)
3007
- return known;
3008
- if (heldContents.size > ROOM_READS)
3009
- heldContents.clear();
3010
- const made = {
3011
- files: (after, limit) => local.files(key, 'confirmed', after, limit),
3012
- async contents(paths) {
3013
- return new Map((await local.entries(key, 'confirmed', paths)).flatMap(({ path, content, revision }) => content === null || revision === null ? [] : [[path, { content, revision }]]));
3014
- },
3015
- };
3016
- heldContents.set(key, made);
3017
- return made;
3018
- }
3019
3169
  async function abandoned(local, locator) {
3020
3170
  await local.abandon(locator);
3021
3171
  setBack(locator);
3022
3172
  untransferred(locator);
3023
- liveQueries.recover();
3173
+ recoverReads();
3024
3174
  sweep();
3025
3175
  }
3026
- function staging(local, liveness) {
3176
+ function staging(local, liveness, unheld) {
3027
3177
  const derived = async (locator, key, composition) => {
3028
3178
  underived.delete(locator);
3029
3179
  for (;;) {
@@ -3093,9 +3243,9 @@ export function composeRuntime(options) {
3093
3243
  const held = { ...record, key };
3094
3244
  transfers.set(locator, held);
3095
3245
  if (arrived)
3096
- liveQueries.recover();
3246
+ recoverReads();
3097
3247
  installing(locator);
3098
- liveQueries.recover();
3248
+ recoverReads();
3099
3249
  const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3100
3250
  owedIndexes(locator, record.done.bytes - prior, text);
3101
3251
  const owed = text + (underived.get(locator) ?? 0);
@@ -3104,6 +3254,37 @@ export function composeRuntime(options) {
3104
3254
  return held;
3105
3255
  return (await derived(locator, key, composition)) ? held : null;
3106
3256
  },
3257
+ async resume(record, parts, composition) {
3258
+ const { locator } = record;
3259
+ installing(locator);
3260
+ if (!liveness())
3261
+ return null;
3262
+ const prior = unheld === undefined ? await recordAt(locator) : undefined;
3263
+ const published = prior !== undefined && prior.replica.selection === record.selection ? prior.key : null;
3264
+ const kept = unheld?.staged ?? (await local.held()).staged.has(locator);
3265
+ if (published === null && !kept)
3266
+ return { record: null, staged: new Set() };
3267
+ const resumed = await installWrite(async () => {
3268
+ if (!liveness())
3269
+ return null;
3270
+ return local.resume(record, composition, parts.map((part) => part.id), published);
3271
+ });
3272
+ if (resumed === null || !liveness())
3273
+ return null;
3274
+ const parted = new Set(resumed.staged);
3275
+ if (resumed.key === null)
3276
+ return { record: null, staged: parted };
3277
+ const bytes = parts.reduce((sum, part) => sum + (parted.has(part.id) ? part.bytes : 0), 0);
3278
+ const held = {
3279
+ ...record,
3280
+ transfer: resumed.transfer,
3281
+ key: resumed.key,
3282
+ done: { bytes },
3283
+ };
3284
+ transfers.set(locator, held);
3285
+ recoverReads();
3286
+ return { record: held, staged: parted };
3287
+ },
3107
3288
  derive: derived,
3108
3289
  async refreshing(locator, key, composition) {
3109
3290
  await exclusive(() => local.unrefresh(key));
@@ -3118,33 +3299,26 @@ export function composeRuntime(options) {
3118
3299
  return;
3119
3300
  setBack(locator);
3120
3301
  untransferred(locator);
3121
- liveQueries.recover();
3302
+ recoverReads();
3122
3303
  },
3123
3304
  release: (key) => exclusive(async () => {
3124
3305
  await local.release(key);
3125
3306
  sweep();
3126
3307
  }),
3127
- content: (key) => heldContentOf(local, key),
3128
3308
  };
3129
3309
  }
3130
- function reusable(room, local) {
3131
- const record = replicas.get(room.locator)?.record;
3132
- if (record === undefined || record.replica.selection !== room.replica.selection)
3133
- return null;
3134
- return heldContentOf(local, record.key);
3135
- }
3136
- async function pullBootstrap(authorized, room, local, liveness) {
3310
+ async function pullBootstrap(authorized, room, local, liveness, prior, from = reads, unheld) {
3137
3311
  if (!liveness())
3138
3312
  return unconfirmed;
3139
- if (replicas.has(room.locator)) {
3313
+ if (prior !== undefined) {
3140
3314
  catching.add(room.locator);
3141
- liveQueries.recover();
3315
+ recoverReads();
3142
3316
  }
3143
3317
  try {
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));
3318
+ const result = await bootstrapRoom(from, authorized, room, { collection: prior?.model ?? unheldModels.get(room.locator) ?? NO_MODEL }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(models, held) : held), liveness, staging(local, liveness, unheld));
3145
3319
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3146
3320
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3147
- if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3321
+ if (result.kind === 'staged' && (await older(room.locator, result.record.roomVersion))) {
3148
3322
  caughtUpTo(room.locator);
3149
3323
  installed(room.locator);
3150
3324
  await abandoned(local, room.locator);
@@ -3168,28 +3342,31 @@ export function composeRuntime(options) {
3168
3342
  }
3169
3343
  function caughtUpTo(locator) {
3170
3344
  if (catching.delete(locator))
3171
- liveQueries.recover();
3345
+ recoverReads();
3172
3346
  }
3173
- function behind(room) {
3174
- const held = replicas.get(room.locator)?.record.roomVersion;
3347
+ async function behind(room) {
3348
+ const held = (await recordAt(room.locator))?.roomVersion;
3175
3349
  return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3176
3350
  }
3177
- function standingOf(locator) {
3178
- const held = replicas.get(locator);
3179
- return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3351
+ function standingFrom(locator, held) {
3352
+ return { key: held?.record.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
3353
+ }
3354
+ function standingAt(locator, held) {
3355
+ return { key: held?.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
3180
3356
  }
3181
- function stands(locator, from) {
3182
- const now = standingOf(locator);
3357
+ function standsAt(locator, held, from) {
3358
+ const now = standingFrom(locator, held);
3183
3359
  return now.key === from.key && now.confirmed === from.confirmed;
3184
3360
  }
3185
- async function advance(room, local, authorized, running) {
3361
+ async function advance(room, local, authorized, running, unheld) {
3186
3362
  const live = () => running() && !full;
3187
3363
  if (!live())
3188
3364
  return unconfirmed;
3189
- const held = replicas.get(room.locator);
3190
- if (held !== undefined && options.adapter.roomStream !== undefined) {
3365
+ const held = unheld === undefined ? await recordAt(room.locator) : undefined;
3366
+ if (options.adapter.roomStream !== undefined &&
3367
+ (held !== undefined || !beyondHorizon.has(room.locator))) {
3191
3368
  stale.delete(room.locator);
3192
- name(room.locator);
3369
+ await name(room.locator);
3193
3370
  return null;
3194
3371
  }
3195
3372
  const refresh = reads.refresh;
@@ -3203,15 +3380,16 @@ export function composeRuntime(options) {
3203
3380
  return unconfirmed;
3204
3381
  replaced([room.locator]);
3205
3382
  }
3206
- const from = standingOf(room.locator);
3207
- const bootstrapped = await pullBootstrap(authorized, room, local, live);
3383
+ const prior = held === undefined ? undefined : await recordAt(room.locator);
3384
+ const from = standingAt(room.locator, prior);
3385
+ const bootstrapped = await pullBootstrap(authorized, room, local, live, prior, reads, unheld);
3208
3386
  if (bootstrapped.kind !== 'staged')
3209
3387
  return live() ? bootstrapped : unconfirmed;
3210
3388
  const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3211
3389
  if (await publish(local, room, bootstrapped.record, landing, live)) {
3212
3390
  return live() ? null : unconfirmed;
3213
3391
  }
3214
- return reachedBy(room.locator, bootstrapped.record.roomVersion)
3392
+ return (await reachedBy(room.locator, bootstrapped.record.roomVersion))
3215
3393
  ? null
3216
3394
  : advance(room, local, authorized, live);
3217
3395
  }
@@ -3235,12 +3413,12 @@ export function composeRuntime(options) {
3235
3413
  remodelled: refreshed.remodelled,
3236
3414
  };
3237
3415
  if (!(await publish(local, room, refreshed.record, landing, live))) {
3238
- const now = replicas.get(room.locator)?.record.roomVersion;
3416
+ const now = (await recordAt(room.locator))?.roomVersion;
3239
3417
  const reached = now !== undefined &&
3240
3418
  options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3241
3419
  return reached ? null : 'behind';
3242
3420
  }
3243
- await retainModel(local, room, modelOf(room.locator));
3421
+ await retainModel(local, room, null);
3244
3422
  return null;
3245
3423
  }
3246
3424
  return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
@@ -3248,6 +3426,11 @@ export function composeRuntime(options) {
3248
3426
  async function withhold(withheld, room, local, progress) {
3249
3427
  if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3250
3428
  return;
3429
+ if (withheld.kind === 'not-ready') {
3430
+ progress.complete = false;
3431
+ askAgain(room.locator);
3432
+ return;
3433
+ }
3251
3434
  stale.add(room.locator);
3252
3435
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3253
3436
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
@@ -3274,6 +3457,37 @@ export function composeRuntime(options) {
3274
3457
  await refuse('revoked-authorization');
3275
3458
  }
3276
3459
  }
3460
+ function askAgain(locator) {
3461
+ const held = unready.get(locator) ?? { tries: 0, timer: null };
3462
+ unready.set(locator, held);
3463
+ if (held.timer !== null)
3464
+ return;
3465
+ const wait = NOT_READY_MS[Math.min(held.tries, NOT_READY_MS.length - 1)];
3466
+ held.tries += 1;
3467
+ held.timer = setTimeout(() => {
3468
+ held.timer = null;
3469
+ const room = discovered.get(locator);
3470
+ const unstreamed = options.adapter.roomStream === undefined;
3471
+ if (closed || stopping || (!unstreamed && !socketOpen) || room === undefined)
3472
+ return;
3473
+ const progress = { complete: true, reachable: true };
3474
+ void openLocal()
3475
+ .then((local) => roomTask(locator, (live) => loadRoom(room, local, progress, live)))
3476
+ .then(() => rest(progress), () => undefined);
3477
+ }, wait);
3478
+ }
3479
+ function readied(locator) {
3480
+ const held = unready.get(locator);
3481
+ if (held === undefined)
3482
+ return;
3483
+ if (held.timer !== null)
3484
+ clearTimeout(held.timer);
3485
+ unready.delete(locator);
3486
+ }
3487
+ function unreadied() {
3488
+ for (const locator of [...unready.keys()])
3489
+ readied(locator);
3490
+ }
3277
3491
  function leaving(batch, result) {
3278
3492
  return {
3279
3493
  batch,
@@ -3299,7 +3513,8 @@ export function composeRuntime(options) {
3299
3513
  async function acceptance(room, local, submitted, accepted) {
3300
3514
  if (!continuing())
3301
3515
  return unconfirmed;
3302
- const current = replicas.get(room.locator);
3516
+ const work = await workOf(room.locator);
3517
+ const current = work.replica;
3303
3518
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
3304
3519
  return null;
3305
3520
  }
@@ -3308,7 +3523,7 @@ export function composeRuntime(options) {
3308
3523
  let record = current.record;
3309
3524
  let changes;
3310
3525
  if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3311
- !older(room.locator, accepted.roomVersion)) {
3526
+ !olderThan(room.locator, accepted.roomVersion, held)) {
3312
3527
  const paths = [...returnedPaths(submitted, accepted)];
3313
3528
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3314
3529
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
@@ -3320,13 +3535,13 @@ export function composeRuntime(options) {
3320
3535
  };
3321
3536
  changes = back;
3322
3537
  }
3323
- const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3538
+ const settled = await settlement(local, room.locator, work, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3539
+ const reached = record.roomVersion;
3324
3540
  return {
3325
3541
  ...settled,
3326
3542
  apply(answer) {
3327
3543
  const set = settled.apply(answer);
3328
- showing(room.locator, accepted.roomVersion);
3329
- pendings.delete(pendingKey(room.locator, submitted.id));
3544
+ showing(room.locator, accepted.roomVersion, reached);
3330
3545
  unreadable.delete(room.locator);
3331
3546
  list(room, true);
3332
3547
  return set;
@@ -3336,8 +3551,9 @@ export function composeRuntime(options) {
3336
3551
  async function adoptedTogether(local, acceptances) {
3337
3552
  const settlements = acceptances.filter(settling);
3338
3553
  if (settlements.length > 0) {
3339
- publishChangeSet(await together(local, settlements));
3340
- moveTo({ ...status, ...attention() });
3554
+ const set = await together(local, settlements);
3555
+ void moveTo(status);
3556
+ void publishChangeSet(set);
3341
3557
  }
3342
3558
  await unpair(local);
3343
3559
  }
@@ -3353,7 +3569,7 @@ export function composeRuntime(options) {
3353
3569
  const other = discovered.get(partner);
3354
3570
  const failed = await exclusive(async () => {
3355
3571
  const left = await acceptance(room, local, submitted, accepted);
3356
- const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3572
+ const arriving = (await replicaOf(partner))?.queue.find((batch) => batch.id === submitted.id);
3357
3573
  const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3358
3574
  ? null
3359
3575
  : await acceptance(other, local, arriving, {
@@ -3366,25 +3582,24 @@ export function composeRuntime(options) {
3366
3582
  submitLater(partner);
3367
3583
  return failed === null;
3368
3584
  }
3369
- function queueIn(locator) {
3370
- return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3585
+ async function behindIn(locator, batch) {
3586
+ const local = await openLocal();
3587
+ const [order, { room }] = await Promise.all([
3588
+ local.order(locator),
3589
+ local.pending(locator, batch.id),
3590
+ ]);
3591
+ return room ? order.indexOf(batch.id) > 0 : order.length > 0;
3371
3592
  }
3372
- function behindIn(locator, batch) {
3373
- const held = replicas.get(locator)?.queue;
3374
- if (held === undefined)
3375
- return queuesWithoutReplica.get(locator) !== undefined;
3376
- return held.findIndex((one) => one.id === batch.id) > 0;
3377
- }
3378
- function unpaired(skipped) {
3379
- for (const locator of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
3380
- for (const batch of queueIn(locator)) {
3381
- const partner = partnerOf(batch, locator);
3382
- if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3383
- continue;
3384
- if (queueIn(partner).some((held) => held.id === batch.id))
3385
- continue;
3386
- if (unpairedIn(locator, batch))
3387
- return { locator, batch };
3593
+ async function unpaired(local, skipped) {
3594
+ const { queued, moving } = await local.moving();
3595
+ for (const { locator, batch, state } of moving) {
3596
+ const partner = partnerOf(batch, locator);
3597
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3598
+ continue;
3599
+ if (queued.get(partner)?.includes(batch.id) === true)
3600
+ continue;
3601
+ if (!leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch)) {
3602
+ return { locator, batch };
3388
3603
  }
3389
3604
  }
3390
3605
  return null;
@@ -3396,9 +3611,10 @@ export function composeRuntime(options) {
3396
3611
  returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3397
3612
  }));
3398
3613
  const outcome = localSaves ? 'dropped' : 'unpaired';
3399
- const current = replicas.get(locator);
3614
+ const work = await workOf(locator);
3615
+ const current = work.replica;
3400
3616
  if (current !== undefined) {
3401
- const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3617
+ const settled = await settlement(local, locator, work, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3402
3618
  return {
3403
3619
  ...settled,
3404
3620
  apply(answer) {
@@ -3410,22 +3626,22 @@ export function composeRuntime(options) {
3410
3626
  },
3411
3627
  };
3412
3628
  }
3413
- const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3629
+ const queue = work.queue.filter((one) => !ids.has(one.id));
3414
3630
  const written = new Map();
3415
3631
  for (const { batch, returned } of leaving) {
3416
3632
  if (returned === null)
3417
3633
  continue;
3418
- for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3634
+ for (const saved of work.states.get(batch.id)?.drafts ?? []) {
3419
3635
  written.set(saved.path, {
3420
3636
  locator,
3421
3637
  path: saved.path,
3422
3638
  revision: roomDraftRevision(),
3423
- parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3639
+ parts: returnedInto((written.get(saved.path) ?? (await draftOf(locator, saved.path)))?.parts, saved.parts),
3424
3640
  returned,
3425
3641
  });
3426
3642
  }
3427
3643
  }
3428
- const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3644
+ const carried = await carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3429
3645
  return {
3430
3646
  locator,
3431
3647
  left: batches,
@@ -3435,6 +3651,7 @@ export function composeRuntime(options) {
3435
3651
  {
3436
3652
  locator,
3437
3653
  queue,
3654
+ stored: work.queue,
3438
3655
  roomDrafts: [...written.values()],
3439
3656
  consumedRoomDrafts: carried.taken,
3440
3657
  },
@@ -3442,17 +3659,8 @@ export function composeRuntime(options) {
3442
3659
  ],
3443
3660
  },
3444
3661
  apply() {
3445
- for (const batch of batches) {
3662
+ for (const batch of batches)
3446
3663
  journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
3447
- pendings.delete(pendingKey(locator, batch.id));
3448
- }
3449
- if (queue.length > 0)
3450
- queuesWithoutReplica.set(locator, queue);
3451
- else
3452
- queuesWithoutReplica.delete(locator);
3453
- for (const record of written.values())
3454
- rememberRoomDraft(record);
3455
- carried.remember();
3456
3664
  submitLater(locator);
3457
3665
  return mergeChangeSets([
3458
3666
  changeSet(locator, {
@@ -3466,28 +3674,33 @@ export function composeRuntime(options) {
3466
3674
  }
3467
3675
  async function unpair(local) {
3468
3676
  const handled = new Set();
3469
- for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3677
+ for (let held = await unpaired(local, handled); held !== null && continuing(); held = await unpaired(local, handled)) {
3470
3678
  const { locator, batch } = held;
3471
3679
  handled.add(pendingKey(locator, batch.id));
3472
3680
  publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3473
3681
  }
3474
3682
  if (handled.size === 0)
3475
3683
  return;
3476
- moveTo({ ...status, ...attention() });
3684
+ moveTo(status);
3477
3685
  }
3478
3686
  async function submitRoom(room, local, authorized, progress, signal) {
3479
3687
  while (continuing() && authorization !== undefined && !signal.aborted) {
3480
- const replica = replicas.get(room.locator);
3481
- const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3482
- if (replica === undefined || head === undefined)
3688
+ const headId = (await local.order(room.locator)).find((id) => !landedAhead.has(pendingKey(room.locator, id)));
3689
+ if (headId === undefined)
3690
+ return 'done';
3691
+ const [head, pending] = await Promise.all([
3692
+ local.batch(room.locator, headId),
3693
+ local.pending(room.locator, headId),
3694
+ ]);
3695
+ if (head === null || !pending.room)
3483
3696
  return 'done';
3484
- const state = pendings.get(pendingKey(room.locator, head.id));
3697
+ const { state } = pending;
3485
3698
  const partner = partnerOf(head, room.locator);
3486
3699
  if (partner !== null && !leavingIn(head, room.locator)) {
3487
3700
  submitLater(partner);
3488
3701
  return 'done';
3489
3702
  }
3490
- if (partner !== null && behindIn(partner, head)) {
3703
+ if (partner !== null && (await behindIn(partner, head))) {
3491
3704
  progress.complete = false;
3492
3705
  return 'done';
3493
3706
  }
@@ -3508,7 +3721,7 @@ export function composeRuntime(options) {
3508
3721
  let result;
3509
3722
  journal.record(`submission ${room.locator} ${head.id}`);
3510
3723
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
3511
- if (discoveryPaused || fences.has(room.locator))
3724
+ if (discoveryPaused || isFenced(room.locator))
3512
3725
  return 'done';
3513
3726
  if (signal.aborted || writesHeld()) {
3514
3727
  await exclusive(() => mark(local, room.locator, head, submission, earlier));
@@ -3614,7 +3827,8 @@ export function composeRuntime(options) {
3614
3827
  await exclusive(async () => {
3615
3828
  if (!continuing())
3616
3829
  return;
3617
- const current = replicas.get(room.locator);
3830
+ const work = await workOf(room.locator);
3831
+ const current = work.replica;
3618
3832
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
3619
3833
  return;
3620
3834
  }
@@ -3628,9 +3842,9 @@ export function composeRuntime(options) {
3628
3842
  landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3629
3843
  return;
3630
3844
  }
3631
- publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3845
+ publishChangeSet(await settle(local, room.locator, work, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3632
3846
  await unpair(local);
3633
- moveTo({ ...status, ...attention() });
3847
+ moveTo(status);
3634
3848
  });
3635
3849
  const { there, here } = judged ?? (await refreshed());
3636
3850
  if (other !== undefined && there !== null)
@@ -3665,24 +3879,24 @@ export function composeRuntime(options) {
3665
3879
  release();
3666
3880
  };
3667
3881
  }
3668
- function declaredAsHeld(room) {
3669
- const held = replicas.get(room.locator)?.record;
3882
+ function declaredAsHeld(held, room) {
3670
3883
  return (held === undefined ||
3671
3884
  (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3672
3885
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3886
+ JSON.stringify(held.providerOwnedPaths) === JSON.stringify(room.providerOwnedPaths) &&
3673
3887
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3674
3888
  }
3675
3889
  async function keepNaming(local, room) {
3676
- const held = replicas.get(room.locator)?.record;
3890
+ const held = await recordAt(room.locator);
3677
3891
  if (held === undefined || (held.name === room.name && held.handle === room.handle))
3678
3892
  return;
3679
3893
  await exclusive(async () => {
3680
3894
  if (!continuing())
3681
3895
  return;
3682
- const replica = replicas.get(room.locator);
3683
- if (replica === undefined)
3896
+ const current = await recordAt(room.locator);
3897
+ if (current === undefined)
3684
3898
  return;
3685
- const record = { ...replica.record, name: room.name, handle: room.handle };
3899
+ const record = { ...current, name: room.name, handle: room.handle };
3686
3900
  await local.commit({
3687
3901
  rooms: [
3688
3902
  {
@@ -3693,36 +3907,38 @@ export function composeRuntime(options) {
3693
3907
  },
3694
3908
  ],
3695
3909
  });
3696
- replicas.set(room.locator, { ...replica, record });
3910
+ emitRooms();
3697
3911
  });
3698
3912
  }
3699
- async function retainModel(local, room, next) {
3700
- const key = room.locator;
3701
- const previous = modelOf(room.locator);
3702
- if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(room))
3913
+ async function modelNow(locator) {
3914
+ return (await recordAt(locator))?.model ?? unheldModels.get(locator) ?? NO_MODEL;
3915
+ }
3916
+ async function retainModel(local, room, given) {
3917
+ const held = await recordAt(room.locator);
3918
+ const previous = held?.model ?? unheldModels.get(room.locator) ?? NO_MODEL;
3919
+ const next = given ?? previous;
3920
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(held, room))
3703
3921
  return;
3704
- if (next.status === 'unavailable') {
3705
- invalidModels.add(key);
3706
- liveModels.delete(key);
3707
- }
3708
3922
  await exclusive(async () => {
3709
3923
  if (!continuing())
3710
3924
  return;
3711
- const held = replicas.get(room.locator);
3925
+ const work = await workOf(room.locator);
3926
+ const current = work.replica;
3712
3927
  let changed = null;
3713
- if (held !== undefined) {
3928
+ if (current !== undefined) {
3714
3929
  const record = {
3715
- ...held.record,
3930
+ ...current.record,
3716
3931
  name: room.name,
3717
3932
  handle: room.handle,
3718
3933
  operations: room.operations,
3719
3934
  replica: room.replica,
3935
+ providerOwnedPaths: room.providerOwnedPaths,
3720
3936
  typedSelection: room.typedSelection ?? null,
3721
3937
  model: next,
3722
3938
  };
3723
- const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
3724
- const replayedLayer = await replayed(record, confirmedLayer, held.queue);
3725
- const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
3939
+ const confirmedLayer = await sliceAt(record, 'confirmed', current.queue, current.overlaid);
3940
+ const replayedLayer = await replayed(record, confirmedLayer, current.queue);
3941
+ const diff = await overlayDiff(record, current, replayedLayer.visible, replayedLayer.touched);
3726
3942
  const answer = await local.commit({
3727
3943
  rooms: [
3728
3944
  {
@@ -3735,28 +3951,21 @@ export function composeRuntime(options) {
3735
3951
  },
3736
3952
  ],
3737
3953
  });
3738
- const { count, indexed } = committedRoom(room.locator, answer);
3739
- replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3954
+ preparedAfter(room.locator, answer);
3740
3955
  changed = changeSet(room.locator, {
3741
3956
  documents: answer.changed[room.locator] ?? [],
3742
3957
  room: true,
3743
3958
  });
3744
3959
  }
3745
- heldModel(room, next);
3960
+ else
3961
+ unheldModels.set(room.locator, next);
3962
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3746
3963
  if (changed === null)
3747
3964
  emitRooms();
3748
3965
  else
3749
3966
  publishChangeSet(changed);
3750
3967
  });
3751
3968
  }
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
- }
3760
3969
  async function scoped(result, room, local) {
3761
3970
  if (result.scope === 'content' && !result.retryable) {
3762
3971
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3777,7 +3986,7 @@ export function composeRuntime(options) {
3777
3986
  else if (result.scope !== 'operation') {
3778
3987
  unreadable.add(room.locator);
3779
3988
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3780
- await retainModel(local, scopedAway, modelOf(room.locator));
3989
+ await retainModel(local, scopedAway, null);
3781
3990
  }
3782
3991
  if (!continuing())
3783
3992
  return;
@@ -3785,42 +3994,55 @@ export function composeRuntime(options) {
3785
3994
  emitRooms();
3786
3995
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3787
3996
  }
3788
- function name(locator) {
3997
+ async function name(locator) {
3789
3998
  const stream = options.adapter.roomStream;
3790
- const record = replicas.get(locator)?.record;
3791
3999
  const room = discovered.get(locator);
3792
- if (stream === undefined || record === undefined || room === undefined)
4000
+ if (stream === undefined || room === undefined || !socketOpen || !handed)
3793
4001
  return;
3794
- if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
4002
+ const [held] = await heldRooms([room]);
4003
+ if (held === undefined || !socketOpen || !handed)
3795
4004
  return;
3796
4005
  naming += 1;
3797
- stream.hold([{ locator, roomVersion: record.roomVersion }]);
4006
+ stream.hold([held]);
3798
4007
  }
3799
- function heldRooms(listed) {
4008
+ async function heldRooms(listed) {
4009
+ const stored = await storedRooms(listed.map((room) => room.locator));
3800
4010
  return listed.flatMap((room) => {
3801
- const record = replicas.get(room.locator)?.record;
3802
- if (record === undefined || record.replica.selection !== room.replica.selection)
4011
+ const record = stored.get(room.locator)?.record;
4012
+ if (record === undefined) {
4013
+ return room.replica.admission === 'admitted'
4014
+ ? [{ locator: room.locator, roomVersion: null }]
4015
+ : [];
4016
+ }
4017
+ if (record.replica.selection !== room.replica.selection)
3803
4018
  return [];
3804
4019
  return [{ locator: room.locator, roomVersion: record.roomVersion }];
3805
4020
  });
3806
4021
  }
3807
4022
  async function synchronize(listed, local, progress, attempt, live) {
3808
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
4023
+ const heldNow = await storedRooms(listed.map(({ locator }) => locator));
4024
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress, heldNow.get(room.locator)?.record)));
3809
4025
  if (!live())
3810
4026
  return;
3811
4027
  const ready = listed.filter((_, index) => {
3812
4028
  const outcome = prepared[index];
3813
4029
  return outcome?.status === 'fulfilled' && outcome.value;
3814
4030
  });
4031
+ const held = await heldRooms(ready);
4032
+ if (!live())
4033
+ return;
3815
4034
  if (attempt === 'opened') {
3816
4035
  naming += 1;
3817
- options.adapter.roomStream?.hold(heldRooms(ready));
4036
+ options.adapter.roomStream?.hold(held);
3818
4037
  handed = true;
3819
4038
  }
4039
+ const kept = attempt === 'unstreamed'
4040
+ ? new Set()
4041
+ : new Set((await storedRooms(ready.map(({ locator }) => locator))).keys());
3820
4042
  for (const room of ready) {
3821
- if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4043
+ if (attempt === 'opened' || kept.has(room.locator)) {
3822
4044
  if (attempt === 'authorized')
3823
- name(room.locator);
4045
+ await name(room.locator);
3824
4046
  continue;
3825
4047
  }
3826
4048
  void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
@@ -3829,23 +4051,23 @@ export function composeRuntime(options) {
3829
4051
  if (failed !== undefined)
3830
4052
  throw failed.reason;
3831
4053
  }
3832
- async function prepareRoom(room, local, progress) {
4054
+ async function prepareRoom(room, local, progress, prior) {
3833
4055
  if (closed || authorization === undefined) {
3834
4056
  progress.complete = false;
3835
4057
  return false;
3836
4058
  }
3837
- const prior = replicas.get(room.locator)?.record;
3838
4059
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
3839
- list(prior, false);
4060
+ listHeld(room.locator);
3840
4061
  emitRooms();
3841
4062
  journal.record(`selection changed ${room.locator}`);
3842
4063
  progress.complete = false;
3843
4064
  return false;
3844
4065
  }
3845
- if (prior !== undefined)
3846
- await retainModel(local, room, modelOf(room.locator));
3847
- if (prior !== undefined)
4066
+ if (prior !== undefined && !declaredAsHeld(prior, room))
4067
+ await retainModel(local, room, null);
4068
+ if (prior !== undefined && (prior.name !== room.name || prior.handle !== room.handle)) {
3848
4069
  await keepNaming(local, room);
4070
+ }
3849
4071
  if (discoveryPaused || authorization === undefined)
3850
4072
  return false;
3851
4073
  return room.replica.admission === 'admitted';
@@ -3853,26 +4075,28 @@ export function composeRuntime(options) {
3853
4075
  async function caughtUp(room, local, progress, live) {
3854
4076
  if (options.adapter.roomStream !== undefined || authorization === undefined)
3855
4077
  return;
3856
- if (!live() || !behind(room))
4078
+ if (!live() || !(await behind(room)))
3857
4079
  return;
3858
4080
  const withheld = await advance(room, local, authorization, live);
3859
4081
  if (withheld !== null)
3860
4082
  await withhold(withheld, room, local, progress);
3861
4083
  }
3862
- function revoked(locator) {
3863
- const model = modelOf(locator);
3864
- return model.status === 'unavailable' && model.reason === 'revoked';
4084
+ function revoked(model) {
4085
+ return model?.status === 'unavailable' && model.reason === 'revoked';
3865
4086
  }
3866
- async function loadRoom(room, local, progress, live) {
4087
+ async function loadRoom(room, local, progress, live, unheld) {
3867
4088
  if (!live() || authorization === undefined)
3868
4089
  return;
3869
- if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3870
- return;
3871
- if (replicas.has(room.locator) && revoked(room.locator)) {
3872
- await reload(room, local, progress, live);
3873
- return;
4090
+ const prior = unheld === undefined ? await recordAt(room.locator) : undefined;
4091
+ if (prior !== undefined) {
4092
+ if (!(await prepareRoom(room, local, progress, prior)))
4093
+ return;
4094
+ if (revoked(prior.model)) {
4095
+ await reload(room, local, progress, live);
4096
+ return;
4097
+ }
3874
4098
  }
3875
- const withheld = await advance(room, local, authorization, live);
4099
+ const withheld = await advance(room, local, authorization, live, unheld);
3876
4100
  if (withheld !== null) {
3877
4101
  await withhold(withheld, room, local, progress);
3878
4102
  return;
@@ -3885,8 +4109,9 @@ export function composeRuntime(options) {
3885
4109
  reloadsDue.delete(room.locator);
3886
4110
  if (!live() || authorization === undefined)
3887
4111
  return;
3888
- const from = standingOf(room.locator);
3889
- const bootstrapped = await pullBootstrap(authorization, room, local, live);
4112
+ const prior = await recordAt(room.locator);
4113
+ const from = standingAt(room.locator, prior);
4114
+ const bootstrapped = await pullBootstrap(authorization, room, local, live, prior);
3890
4115
  if (bootstrapped.kind !== 'staged') {
3891
4116
  if (live())
3892
4117
  await withhold(bootstrapped, room, local, progress);
@@ -3910,7 +4135,7 @@ export function composeRuntime(options) {
3910
4135
  function malformed(listed) {
3911
4136
  for (const room of listed) {
3912
4137
  if (room.operations === undefined ||
3913
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4138
+ !['write', 'erase'].every((operation) => Array.isArray(room.operations[operation]) &&
3914
4139
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
3915
4140
  room.replica === undefined ||
3916
4141
  typeof room.replica.selection !== 'string' ||
@@ -3925,18 +4150,17 @@ export function composeRuntime(options) {
3925
4150
  return null;
3926
4151
  }
3927
4152
  async function signIn(local, signedIn) {
3928
- if (JSON.stringify(signedIn) === JSON.stringify(member))
4153
+ if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
3929
4154
  return;
3930
4155
  await local.fact('member', signedIn);
3931
- member = signedIn;
3932
- emitMember();
4156
+ emitMember(signedIn);
3933
4157
  }
3934
4158
  async function discoverRooms(local, progress, attempt, live, found) {
3935
4159
  if (closed || authorization === undefined) {
3936
4160
  notifyReady();
3937
4161
  return;
3938
4162
  }
3939
- moveTo(transferring(status));
4163
+ moveTo(transferring());
3940
4164
  discoveries += 1;
3941
4165
  const started = discoveries;
3942
4166
  const result = found ?? (await asked(authorization));
@@ -3981,15 +4205,13 @@ export function composeRuntime(options) {
3981
4205
  refusal = null;
3982
4206
  emitRefusal();
3983
4207
  }
3984
- const previousCapabilities = capabilities;
3985
- capabilities = result.capabilities;
3986
- notifyReady();
3987
- if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
3988
- JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
3989
- limits = result.providerLimits;
3990
- await local.fact('providerLimits', limits);
3991
- await local.fact('capabilities', capabilities);
4208
+ const held = await local.facts();
4209
+ if (JSON.stringify(result.providerLimits) !== JSON.stringify(held.providerLimits) ||
4210
+ JSON.stringify(result.capabilities) !== JSON.stringify(held.capabilities)) {
4211
+ await local.fact('providerLimits', result.providerLimits);
4212
+ await local.fact('capabilities', result.capabilities);
3992
4213
  }
4214
+ notifyReady();
3993
4215
  await signIn(local, result.member ?? null);
3994
4216
  await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3995
4217
  }
@@ -4072,7 +4294,7 @@ export function composeRuntime(options) {
4072
4294
  streamed = progress;
4073
4295
  complete = false;
4074
4296
  recorded = false;
4075
- moveTo(transferring(status));
4297
+ moveTo(transferring());
4076
4298
  try {
4077
4299
  const local = await openLocal();
4078
4300
  if (!live())
@@ -4107,7 +4329,7 @@ export function composeRuntime(options) {
4107
4329
  if (!live() || !announced)
4108
4330
  return;
4109
4331
  const progress = { complete: true, reachable: true };
4110
- moveTo(transferring(status));
4332
+ moveTo(transferring());
4111
4333
  try {
4112
4334
  const local = await openLocal();
4113
4335
  if (!live())
@@ -4125,6 +4347,7 @@ export function composeRuntime(options) {
4125
4347
  authorityChanges += 1;
4126
4348
  for (const listener of authorityChangeListeners)
4127
4349
  listener(authorityChanges);
4350
+ lastAnswers.again();
4128
4351
  }
4129
4352
  function changeAuthority(change) {
4130
4353
  return task(OPENING_LANE, async (live) => {
@@ -4132,7 +4355,7 @@ export function composeRuntime(options) {
4132
4355
  return;
4133
4356
  countAuthorityChange();
4134
4357
  const progress = { complete: true, reachable: true };
4135
- moveTo(transferring(status));
4358
+ moveTo(transferring());
4136
4359
  try {
4137
4360
  const local = await openLocal();
4138
4361
  if (!live())
@@ -4167,6 +4390,7 @@ export function composeRuntime(options) {
4167
4390
  missedChange ||= authorityChanged;
4168
4391
  handed = false;
4169
4392
  naming = 0;
4393
+ unreadied();
4170
4394
  connectionLost();
4171
4395
  if (lost)
4172
4396
  conversations.lost();
@@ -4194,8 +4418,12 @@ export function composeRuntime(options) {
4194
4418
  await rest(null, live);
4195
4419
  });
4196
4420
  }
4197
- function cachedModels(local) {
4198
- return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4421
+ async function cachedModels(local) {
4422
+ const held = await storedRooms();
4423
+ await settleModels(listedLocators(held).flatMap((locator) => {
4424
+ const room = discovered.get(locator) ?? declarationOf(locator, held.get(locator))?.room;
4425
+ return room === undefined ? [] : [room];
4426
+ }), 'cached', local);
4199
4427
  }
4200
4428
  function caughtUpRooms(expired) {
4201
4429
  const progress = underway;
@@ -4203,8 +4431,12 @@ export function composeRuntime(options) {
4203
4431
  naming = Math.max(0, naming - 1);
4204
4432
  const cycle = progress ?? { complete: true, reachable: true };
4205
4433
  replaced(expired);
4206
- const reloading = progress === null ? [] : owedReloads(expired);
4207
- const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4434
+ const installing = new Set(installs);
4435
+ const held = progress === null ? storedRooms(expired) : storedRooms();
4436
+ const reloading = progress === null
4437
+ ? Promise.resolve([])
4438
+ : held.then((stored) => owedReloads(expired, stored));
4439
+ const replacing = Promise.all([held, reloading]).then(([stored, owed]) => reloadNow((room) => expired.includes(room.locator) || owed.includes(room.locator), cycle, stored, installing));
4208
4440
  return task(OPENING_LANE, async (live) => {
4209
4441
  if (!live())
4210
4442
  return;
@@ -4217,9 +4449,11 @@ export function composeRuntime(options) {
4217
4449
  const local = await openLocal();
4218
4450
  await replacing;
4219
4451
  if (progress.reachable) {
4220
- await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4452
+ const stored = await held;
4453
+ const owed = await reloading;
4454
+ await settleModels([...discovered.values()].filter((room) => stored.has(room.locator) &&
4221
4455
  !expired.includes(room.locator) &&
4222
- !reloading.includes(room.locator)), 'current', local);
4456
+ !owed.includes(room.locator)), 'current', local);
4223
4457
  }
4224
4458
  }
4225
4459
  catch {
@@ -4230,24 +4464,34 @@ export function composeRuntime(options) {
4230
4464
  await finishOpening(progress, live);
4231
4465
  });
4232
4466
  }
4233
- function owedReloads(expired) {
4467
+ function owedReloads(expired, held) {
4234
4468
  const reloading = [...discovered.values()]
4235
- .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4469
+ .filter((room) => !expired.includes(room.locator) && revoked(held.get(room.locator)?.record.model))
4236
4470
  .map((room) => room.locator);
4237
4471
  for (const locator of reloading)
4238
4472
  catching.add(locator);
4239
- liveQueries.recover();
4473
+ recoverReads();
4240
4474
  return reloading;
4241
4475
  }
4242
- async function reloadNow(replaced, progress) {
4243
- const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4476
+ async function reloadNow(replaced, progress, held, installing) {
4477
+ const reloaded = [...discovered.values()].filter((room) => held.has(room.locator) && !installing.has(room.locator) && replaced(room));
4478
+ const acquired = [...discovered.values()].filter((room) => (!held.has(room.locator) || installing.has(room.locator)) &&
4479
+ room.replica.admission === 'admitted' &&
4480
+ replaced(room));
4244
4481
  for (const room of reloaded)
4245
4482
  reloadsDue.add(room.locator);
4246
4483
  try {
4247
4484
  const local = await openLocal();
4485
+ const kept = acquired.length === 0 ? new Set() : (await local.held()).staged;
4248
4486
  for (const room of reloaded) {
4249
4487
  void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4250
4488
  }
4489
+ for (const room of acquired) {
4490
+ const unheld = installing.has(room.locator)
4491
+ ? undefined
4492
+ : { staged: kept.has(room.locator) };
4493
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live, unheld)).then(() => rest(progress));
4494
+ }
4251
4495
  }
4252
4496
  catch {
4253
4497
  for (const room of reloaded)
@@ -4256,29 +4500,56 @@ export function composeRuntime(options) {
4256
4500
  }
4257
4501
  }
4258
4502
  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 () => {
4503
+ const owed = (model) => model.status !== 'unavailable' && model.status !== status;
4504
+ const declaredAnew = await exclusive(async () => {
4264
4505
  if (!continuing())
4265
4506
  return [];
4266
- const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4507
+ const stored = await storedRooms(held.map(({ locator }) => locator));
4508
+ const unchanged = held.filter((room) => {
4509
+ const record = stored.get(room.locator)?.record;
4510
+ return (owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL) &&
4511
+ declaredAsHeld(record, room));
4512
+ });
4513
+ const records = unchanged.flatMap((room) => {
4514
+ const record = stored.get(room.locator)?.record;
4515
+ return record === undefined
4516
+ ? []
4517
+ : [{ ...record, model: { ...record.model, status } }];
4518
+ });
4519
+ if (records.length > 0) {
4520
+ try {
4521
+ await local.commit({
4522
+ rooms: records.map((record) => ({
4523
+ locator: record.locator,
4524
+ key: record.key,
4525
+ record: storedRecord(record),
4526
+ composed: composedOf(composed(record)),
4527
+ })),
4528
+ });
4529
+ }
4530
+ catch {
4531
+ journal.record(`model ${status} persistence failed`);
4532
+ }
4533
+ }
4267
4534
  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);
4535
+ const unheld = unheldModels.get(room.locator);
4536
+ if (!stored.has(room.locator) && unheld !== undefined) {
4537
+ unheldModels.set(room.locator, { ...unheld, status });
4538
+ }
4539
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
4273
4540
  }
4274
4541
  if (unchanged.length > 0)
4275
4542
  emitRooms();
4276
- return unchanged;
4543
+ return held.filter((room) => {
4544
+ const record = stored.get(room.locator)?.record;
4545
+ return (!unchanged.includes(room) &&
4546
+ owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL));
4547
+ });
4277
4548
  });
4278
- for (const room of held) {
4279
- if (settled.includes(room) || !owed(room))
4280
- continue;
4281
- await retainModel(local, room, { ...modelOf(room.locator), status });
4549
+ for (const room of declaredAnew) {
4550
+ const model = await modelNow(room.locator);
4551
+ if (owed(model))
4552
+ await retainModel(local, room, { ...model, status });
4282
4553
  }
4283
4554
  }
4284
4555
  function submitLater(locator) {
@@ -4341,7 +4612,7 @@ export function composeRuntime(options) {
4341
4612
  return 'done';
4342
4613
  await rest(progress);
4343
4614
  if (!stale.has(locator)) {
4344
- if ((replicas.get(locator)?.queue.length ?? 0) > 0)
4615
+ if ((await (await openLocal()).order(locator)).length > 0)
4345
4616
  submitLater(locator);
4346
4617
  return 'done';
4347
4618
  }
@@ -4369,9 +4640,11 @@ export function composeRuntime(options) {
4369
4640
  return 'done';
4370
4641
  return reading.reachable && reading.complete ? 'done' : 'again';
4371
4642
  }
4372
- function submitAll() {
4373
- for (const [locator, replica] of replicas) {
4374
- if (replica.queue.length > 0)
4643
+ async function submitAll() {
4644
+ if (localSaves)
4645
+ return;
4646
+ for (const locator of await (await openLocal()).queued()) {
4647
+ if (!isFenced(locator))
4375
4648
  submitLater(locator);
4376
4649
  }
4377
4650
  }
@@ -4381,7 +4654,7 @@ export function composeRuntime(options) {
4381
4654
  progress.complete = false;
4382
4655
  return;
4383
4656
  }
4384
- submitAll();
4657
+ await submitAll();
4385
4658
  for (const locator of stale)
4386
4659
  if (repairable.has(locator))
4387
4660
  repairLater(locator, {});
@@ -4410,8 +4683,14 @@ export function composeRuntime(options) {
4410
4683
  async function rest(progress, live = () => !closed) {
4411
4684
  if (!live())
4412
4685
  return;
4413
- let lastCompleted = status.lastCompleted;
4414
- let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4686
+ const settled = () => complete && roomWork === 0 && naming === 0 && stale.size === 0;
4687
+ let synced = settled();
4688
+ if (synced && !recorded) {
4689
+ const queued = await queuedWork().catch(() => true);
4690
+ if (!live())
4691
+ return;
4692
+ synced = !queued && settled();
4693
+ }
4415
4694
  if (synced && !recorded) {
4416
4695
  const completedAt = new Date().toISOString();
4417
4696
  try {
@@ -4419,21 +4698,15 @@ export function composeRuntime(options) {
4419
4698
  if (!live())
4420
4699
  return;
4421
4700
  recorded = true;
4422
- lastCompleted = completedAt;
4423
4701
  }
4424
4702
  catch {
4425
4703
  synced = false;
4426
4704
  }
4427
4705
  }
4428
4706
  moveTo({
4429
- state: pending()
4430
- ? 'pending'
4431
- : synced && complete && roomWork === 0 && naming === 0
4432
- ? 'synced'
4433
- : 'syncing',
4434
- lastCompleted,
4707
+ state: synced && complete && roomWork === 0 && naming === 0 ? 'synced' : 'syncing',
4435
4708
  providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4436
- ...attention(),
4709
+ resting: true,
4437
4710
  });
4438
4711
  }
4439
4712
  function applyRoom(change) {
@@ -4449,22 +4722,26 @@ export function composeRuntime(options) {
4449
4722
  if (!live())
4450
4723
  return null;
4451
4724
  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) {
4725
+ if (room === undefined || room.replica.admission !== 'admitted')
4457
4726
  return null;
4727
+ const held = await recordAt(locator);
4728
+ if (held === undefined) {
4729
+ if (change.from !== null)
4730
+ return null;
4731
+ await installCarried(room, local, change, live, progress);
4732
+ return progress;
4458
4733
  }
4459
- const at = held.record.roomVersion;
4734
+ if (held.replica.selection !== room.replica.selection)
4735
+ return null;
4736
+ const at = held.roomVersion;
4460
4737
  if (options.adapter.reached(locator, at, change.roomVersion))
4461
4738
  return progress;
4462
- if (!options.adapter.reached(locator, at, change.from)) {
4463
- name(locator);
4739
+ if (change.from === null || !options.adapter.reached(locator, at, change.from)) {
4740
+ await name(locator);
4464
4741
  return progress;
4465
4742
  }
4466
4743
  if (underway === null)
4467
- moveTo(transferring(status));
4744
+ moveTo(transferring());
4468
4745
  const writing = () => live() && !full;
4469
4746
  const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4470
4747
  if (refreshed.kind !== 'advanced')
@@ -4476,19 +4753,52 @@ export function composeRuntime(options) {
4476
4753
  };
4477
4754
  if (await publish(local, room, refreshed.record, landing, writing)) {
4478
4755
  stale.delete(locator);
4479
- await retainModel(local, room, modelOf(locator));
4756
+ if (!declaredAsHeld(held, room))
4757
+ await retainModel(local, room, null);
4480
4758
  return progress;
4481
4759
  }
4482
4760
  }
4483
4761
  }
4484
4762
  catch {
4485
4763
  if (!stale.has(locator))
4486
- name(locator);
4764
+ void name(locator);
4487
4765
  stale.add(locator);
4488
4766
  return progress;
4489
4767
  }
4490
4768
  }).then((progress) => rest(progress), () => undefined);
4491
4769
  }
4770
+ async function installCarried(room, local, change, live, progress) {
4771
+ if (authorization === undefined)
4772
+ return;
4773
+ if (underway === null)
4774
+ moveTo(transferring());
4775
+ const prior = await recordAt(room.locator);
4776
+ const from = standingAt(room.locator, prior);
4777
+ const page = await change.read();
4778
+ const carried = {
4779
+ bootstrap: () => Promise.resolve({
4780
+ kind: 'bootstrap',
4781
+ roomVersion: page.roomVersion,
4782
+ ...(page.models === undefined ? {} : { models: page.models }),
4783
+ documents: page.documents,
4784
+ facts: page.facts,
4785
+ revisions: page.revisions,
4786
+ ...(page.fileIds === undefined ? {} : { fileIds: page.fileIds }),
4787
+ ...(page.created === undefined ? {} : { created: page.created }),
4788
+ changes: page.changes,
4789
+ binaryFiles: page.binaryFiles,
4790
+ ...(page.folders === undefined ? {} : { folders: page.folders }),
4791
+ }),
4792
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
4793
+ };
4794
+ const bootstrapped = await pullBootstrap(authorization, room, local, live, prior, carried);
4795
+ if (bootstrapped.kind !== 'staged') {
4796
+ if (live())
4797
+ await withhold(bootstrapped, room, local, progress);
4798
+ return;
4799
+ }
4800
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4801
+ }
4492
4802
  function observeStream() {
4493
4803
  const stream = options.adapter.roomStream;
4494
4804
  if (stream === undefined || withdrawStream !== undefined)
@@ -4504,6 +4814,7 @@ export function composeRuntime(options) {
4504
4814
  connectionLive();
4505
4815
  generation += 1;
4506
4816
  conversations.reopened();
4817
+ lastAnswers.reopened();
4507
4818
  recovery.resume();
4508
4819
  }
4509
4820
  else if (event.kind === 'discovered') {
@@ -4520,8 +4831,11 @@ export function composeRuntime(options) {
4520
4831
  closedSocket(event.revoked, event.lost, event.authorityChanged);
4521
4832
  else if (event.kind === 'caught-up')
4522
4833
  void caughtUpRooms(event.expired);
4523
- else if (event.kind === 'authority-changed')
4834
+ else if (event.kind === 'authority-changed') {
4835
+ if (announced && socketOpen)
4836
+ fenceLive(event.left);
4524
4837
  void changeAuthority(event);
4838
+ }
4525
4839
  else
4526
4840
  void applyRoom(event);
4527
4841
  });
@@ -4554,10 +4868,7 @@ export function composeRuntime(options) {
4554
4868
  withdrawConversations = stream.observe((event) => {
4555
4869
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
4556
4870
  queueReadiness();
4557
- if (conversations.receive(event)) {
4558
- void ready.then(scheduleCurrency);
4559
- return;
4560
- }
4871
+ conversations.receive(event);
4561
4872
  if (event.kind !== 'gap' || conversations.current())
4562
4873
  return;
4563
4874
  show();
@@ -4581,7 +4892,7 @@ export function composeRuntime(options) {
4581
4892
  return;
4582
4893
  }
4583
4894
  const progress = { complete: false, reachable: !status.providerUnreachable };
4584
- moveTo(transferring(status));
4895
+ moveTo(transferring());
4585
4896
  try {
4586
4897
  await conversations.catchUp(progress);
4587
4898
  }
@@ -4595,6 +4906,7 @@ export function composeRuntime(options) {
4595
4906
  return;
4596
4907
  closed = true;
4597
4908
  notifyDiscovered();
4909
+ unreadied();
4598
4910
  if (probing !== null)
4599
4911
  clearInterval(probing);
4600
4912
  connectionLive();
@@ -4616,12 +4928,14 @@ export function composeRuntime(options) {
4616
4928
  memberListeners.clear();
4617
4929
  authorityChangeListeners.clear();
4618
4930
  liveQueries.close();
4931
+ figures.close();
4619
4932
  queueSubscriptions.clear();
4620
4933
  roomDraftListeners.clear();
4621
4934
  presentationListeners.clear();
4622
4935
  }
4623
4936
  async function stop() {
4624
4937
  stopping = true;
4938
+ unreadied();
4625
4939
  inFlight.abort();
4626
4940
  await recovery.retire();
4627
4941
  await Promise.all(working);
@@ -4634,16 +4948,18 @@ export function composeRuntime(options) {
4634
4948
  if (options.signal?.aborted)
4635
4949
  close();
4636
4950
  observeStream();
4637
- void openLocal().then(() => {
4638
- if (capabilities.length > 0)
4951
+ void openLocal()
4952
+ .then(async (local) => {
4953
+ if ((await local.facts()).capabilities.length > 0)
4639
4954
  readyFromStore();
4640
- }, () => {
4955
+ })
4956
+ .catch(() => {
4641
4957
  readyFromStore();
4642
4958
  });
4643
4959
  if (options.adapter.roomStream === undefined)
4644
4960
  void sync('unstreamed', null);
4645
- function planned(input, reach) {
4646
- const plan = planWithin(input, roomFacts(reach), planReach(reach));
4961
+ async function planned(input, reach) {
4962
+ const plan = planWithin(input, await roomFacts(reach), planReach(reach));
4647
4963
  const named = plan.paths !== null &&
4648
4964
  plan.rooms !== null &&
4649
4965
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
@@ -4662,10 +4978,10 @@ export function composeRuntime(options) {
4662
4978
  const watchers = seedWatchers.get(plan.key) ?? new Set();
4663
4979
  seedWatchers.set(plan.key, watchers);
4664
4980
  watchers.add(shown);
4665
- const stop = liveQueries.watch(plan, identityOf(plan), () => {
4666
- const cover = covered(plan);
4981
+ const stop = liveQueries.watch(plan, identityOf(plan), (held) => {
4982
+ const cover = covered(plan, held);
4667
4983
  awaitPreparation(cover);
4668
- return seedBehind(plan, cover);
4984
+ return seedBehind(plan, cover, held);
4669
4985
  }, (message) => {
4670
4986
  const answered = message;
4671
4987
  pending = whole(answered) && 'pending' in answered.result;
@@ -4683,6 +4999,21 @@ export function composeRuntime(options) {
4683
4999
  stop();
4684
5000
  };
4685
5001
  }
5002
+ function watchPlanned(planning, deliver, fail) {
5003
+ let stop = null;
5004
+ let stopped = false;
5005
+ planning.then((plan) => {
5006
+ if (!stopped)
5007
+ stop = watchPlan(plan, deliver, fail);
5008
+ }, (error) => {
5009
+ if (!stopped)
5010
+ fail(error);
5011
+ });
5012
+ return () => {
5013
+ stopped = true;
5014
+ stop?.();
5015
+ };
5016
+ }
4686
5017
  async function searched(input, reach) {
4687
5018
  const asked = searchPlanOf(input);
4688
5019
  await ready;
@@ -4695,7 +5026,7 @@ export function composeRuntime(options) {
4695
5026
  retryable: false,
4696
5027
  };
4697
5028
  }
4698
- if (closed || refusal !== null || connectionOf() !== 'connected')
5029
+ if (closed || refusal !== null || (await connectionNow()) !== 'connected')
4699
5030
  return OFFLINE;
4700
5031
  const reached = reach === null ? null : new Set(reach.rooms);
4701
5032
  const named = asked.rooms === null
@@ -4703,7 +5034,8 @@ export function composeRuntime(options) {
4703
5034
  ? null
4704
5035
  : [...reached]
4705
5036
  : asked.rooms.filter((room) => reached === null || reached.has(room));
4706
- const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
5037
+ const held = named === null ? null : await storedRooms(named);
5038
+ const rooms = named === null ? null : named.filter((room) => held.has(room)).sort();
4707
5039
  if (rooms !== null && rooms.length === 0)
4708
5040
  return { kind: 'found', documents: [] };
4709
5041
  try {
@@ -4741,18 +5073,15 @@ export function composeRuntime(options) {
4741
5073
  roomDraftListeners.add(listener);
4742
5074
  void exclusive(async () => {
4743
5075
  await openLocal();
4744
- }).then(() => {
5076
+ }).then(() => publication(async () => {
4745
5077
  if (closed || !roomDraftListeners.has(listener))
4746
5078
  return;
4747
- const snapshot = snapshotRoomDrafts();
4748
- draftsShown = draftsShown.then(async () => {
4749
- const value = await snapshot.catch(() => null);
4750
- if (value === null || closed || !roomDraftListeners.has(listener))
4751
- return;
4752
- listener.ready = true;
4753
- listener.notify(value);
4754
- });
4755
- }, () => { });
5079
+ const value = await snapshotRoomDrafts();
5080
+ if (closed || !roomDraftListeners.has(listener))
5081
+ return;
5082
+ listener.ready = true;
5083
+ listener.notify(value);
5084
+ }), () => { });
4756
5085
  return () => {
4757
5086
  roomDraftListeners.delete(listener);
4758
5087
  };
@@ -4764,6 +5093,10 @@ export function composeRuntime(options) {
4764
5093
  return conversations.watchList(deliver);
4765
5094
  if (topic === 'agents')
4766
5095
  return conversations.watchAgents(deliver);
5096
+ if (topic === 'figures')
5097
+ return figures.watch(deliver, fail);
5098
+ if (topic === 'read')
5099
+ return lastAnswers.watch(input[0], input[1], deliver);
4767
5100
  if (topic === 'presentation') {
4768
5101
  const listener = {
4769
5102
  app: input[0],
@@ -4772,13 +5105,12 @@ export function composeRuntime(options) {
4772
5105
  };
4773
5106
  presentationListeners.add(listener);
4774
5107
  void exclusive(async () => {
4775
- await openLocal();
4776
- }).then(() => {
5108
+ const value = await presentationOf(listener.app);
4777
5109
  if (closed || !presentationListeners.has(listener))
4778
5110
  return;
4779
5111
  listener.ready = true;
4780
- listener.notify(presentationOf(listener.app));
4781
- }, () => { });
5112
+ listener.notify(value);
5113
+ }).catch(() => { });
4782
5114
  return () => {
4783
5115
  presentationListeners.delete(listener);
4784
5116
  };
@@ -4792,6 +5124,8 @@ export function composeRuntime(options) {
4792
5124
  return () => {
4793
5125
  unlistedRooms.delete(deliver);
4794
5126
  roomListeners.delete(deliver);
5127
+ if (roomListeners.size === 0)
5128
+ roomsShown = null;
4795
5129
  };
4796
5130
  }
4797
5131
  if (topic === 'connection-refusal') {
@@ -4824,7 +5158,12 @@ export function composeRuntime(options) {
4824
5158
  }
4825
5159
  if (topic === 'member') {
4826
5160
  memberListeners.add(deliver);
4827
- deliver(member);
5161
+ void openLocal()
5162
+ .then((local) => local.facts())
5163
+ .then(({ member }) => {
5164
+ if (memberListeners.has(deliver))
5165
+ deliver(member);
5166
+ }, () => { });
4828
5167
  return () => {
4829
5168
  memberListeners.delete(deliver);
4830
5169
  };
@@ -4839,12 +5178,18 @@ export function composeRuntime(options) {
4839
5178
  if (topic === 'queue') {
4840
5179
  const queue = { locator: input[0], notify: deliver };
4841
5180
  queueSubscriptions.add(queue);
4842
- queue.notify(entriesOf(queue.locator));
5181
+ void publication(async () => {
5182
+ if (!queueSubscriptions.has(queue))
5183
+ return;
5184
+ const entries = await entriesOf(queue.locator);
5185
+ if (queueSubscriptions.has(queue))
5186
+ queue.notify(entries);
5187
+ });
4843
5188
  return () => {
4844
5189
  queueSubscriptions.delete(queue);
4845
5190
  };
4846
5191
  }
4847
- return watchPlan(planned(input[0], null), deliver, fail);
5192
+ return watchPlanned(planned(input[0], null), deliver, fail);
4848
5193
  };
4849
5194
  const md = {
4850
5195
  loaded() {
@@ -4863,15 +5208,14 @@ export function composeRuntime(options) {
4863
5208
  emitKnownRooms();
4864
5209
  });
4865
5210
  },
4866
- query: ((input) => rowsQuery(planned(input, null))),
5211
+ query: ((input) => planned(input, null).then(rowsQuery)),
4867
5212
  search: (input) => searched(input, null),
4868
- types(locator) {
5213
+ async types(locator) {
5214
+ const held = (await storedRooms([locator])).get(locator);
5215
+ const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
4869
5216
  return {
4870
- collection: modelTypes(modelOf(locator)),
4871
- typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4872
- rooms.get(locator)?.typedSelection === null
4873
- ? null
4874
- : modelTypes(builtIn),
5217
+ collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
5218
+ typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
4875
5219
  };
4876
5220
  },
4877
5221
  roomDraft: {
@@ -4885,7 +5229,7 @@ export function composeRuntime(options) {
4885
5229
  });
4886
5230
  },
4887
5231
  async read(room, path) {
4888
- const record = roomDrafts.get(room)?.get(path);
5232
+ const record = await draftOf(room, path);
4889
5233
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4890
5234
  },
4891
5235
  write(room, path, revision, input, next = roomDraftRevision()) {
@@ -4903,14 +5247,15 @@ export function composeRuntime(options) {
4903
5247
  discard(room, path, revision) {
4904
5248
  return exclusive(async () => {
4905
5249
  const local = await openLocal();
4906
- const current = roomDrafts.get(room)?.get(path);
5250
+ const current = await draftOf(room, path);
4907
5251
  if (current === undefined || current.revision !== revision) {
4908
5252
  return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
4909
5253
  }
4910
5254
  await local.discardRoomDraft(room, path);
4911
- forgetRoomDraft(room, path);
4912
- publishChangeSet(changeSet(room, { drafts: [path] }));
4913
- moveTo({ ...status, ...attention() });
5255
+ await Promise.all([
5256
+ publishChangeSet(changeSet(room, { drafts: [path] })),
5257
+ moveTo(status),
5258
+ ]);
4914
5259
  return { outcome: 'written', roomDraft: null };
4915
5260
  });
4916
5261
  },
@@ -4919,10 +5264,7 @@ export function composeRuntime(options) {
4919
5264
  chatDraft: conversations.chatDraft,
4920
5265
  presentation: {
4921
5266
  read(app) {
4922
- return exclusive(async () => {
4923
- await openLocal();
4924
- return presentationOf(app);
4925
- });
5267
+ return exclusive(() => presentationOf(app));
4926
5268
  },
4927
5269
  write(app, value) {
4928
5270
  return exclusive(() => keptPresentation(app, () => structuredClone(value)));
@@ -4955,7 +5297,8 @@ export function composeRuntime(options) {
4955
5297
  },
4956
5298
  async call(name, input) {
4957
5299
  await ready;
4958
- const room = calledRooms(name, input).find((one) => locked(one));
5300
+ const lockedNow = await lockedRooms();
5301
+ const room = (await calledRooms(name, input)).find((one) => lockedNow.includes(one));
4959
5302
  return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
4960
5303
  },
4961
5304
  async reached(locator, roomVersion, path) {
@@ -4969,6 +5312,9 @@ export function composeRuntime(options) {
4969
5312
  cleanUp,
4970
5313
  unsent: () => database.ask({ kind: 'authored.unsent' }),
4971
5314
  reset: () => options.shell.reset(),
5315
+ async readAgain() {
5316
+ lastAnswers.again();
5317
+ },
4972
5318
  persistence() {
4973
5319
  return persistence === 'granted';
4974
5320
  },
@@ -5008,7 +5354,7 @@ export function composeRuntime(options) {
5008
5354
  lockedRooms,
5009
5355
  watchLocks,
5010
5356
  located,
5011
- room: (locator) => listed().find((one) => one.locator === locator),
5357
+ room: async (locator) => shownRooms(await listingNow()).find((one) => one.locator === locator),
5012
5358
  };
5013
5359
  return {
5014
5360
  md,