@agentshouse/mdruntime 0.1.0-c09bf842921ff → 0.1.0-c0c1e81da9166

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 (99) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +14 -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 +2 -0
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/compile.d.ts +5 -0
  15. package/dist/compile.d.ts.map +1 -1
  16. package/dist/compile.js +186 -55
  17. package/dist/connection.d.ts +4 -2
  18. package/dist/connection.d.ts.map +1 -1
  19. package/dist/connection.js +1362 -1040
  20. package/dist/conversation-row.d.ts +33 -0
  21. package/dist/conversation-row.d.ts.map +1 -0
  22. package/dist/conversation-row.js +47 -0
  23. package/dist/conversation.d.ts +13 -5
  24. package/dist/conversation.d.ts.map +1 -1
  25. package/dist/conversation.js +354 -192
  26. package/dist/entry.d.ts.map +1 -1
  27. package/dist/entry.js +1 -0
  28. package/dist/figures.d.ts +48 -0
  29. package/dist/figures.d.ts.map +1 -0
  30. package/dist/figures.js +197 -0
  31. package/dist/index.d.ts +8 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -1
  34. package/dist/job.d.ts +20 -1
  35. package/dist/job.d.ts.map +1 -1
  36. package/dist/job.js +26 -0
  37. package/dist/live.d.ts +4 -4
  38. package/dist/live.d.ts.map +1 -1
  39. package/dist/live.js +53 -15
  40. package/dist/memory.d.ts +1 -0
  41. package/dist/memory.d.ts.map +1 -1
  42. package/dist/memory.js +11 -3
  43. package/dist/messages.d.ts +25 -1
  44. package/dist/messages.d.ts.map +1 -1
  45. package/dist/messages.js +82 -3
  46. package/dist/model.d.ts +0 -2
  47. package/dist/model.d.ts.map +1 -1
  48. package/dist/model.js +0 -6
  49. package/dist/overlay.d.ts +1 -0
  50. package/dist/overlay.d.ts.map +1 -1
  51. package/dist/overlay.js +11 -0
  52. package/dist/parts.d.ts +59 -0
  53. package/dist/parts.d.ts.map +1 -0
  54. package/dist/parts.js +118 -0
  55. package/dist/query.d.ts +10 -0
  56. package/dist/query.d.ts.map +1 -1
  57. package/dist/questions.d.ts +32 -0
  58. package/dist/questions.d.ts.map +1 -0
  59. package/dist/questions.js +118 -0
  60. package/dist/reach.d.ts +5 -5
  61. package/dist/reach.d.ts.map +1 -1
  62. package/dist/reach.js +151 -99
  63. package/dist/read.d.ts +7 -0
  64. package/dist/read.d.ts.map +1 -0
  65. package/dist/read.js +17 -0
  66. package/dist/recovery.d.ts.map +1 -1
  67. package/dist/recovery.js +9 -1
  68. package/dist/refresh.d.ts +1 -1
  69. package/dist/refresh.d.ts.map +1 -1
  70. package/dist/refresh.js +4 -0
  71. package/dist/replica.d.ts.map +1 -1
  72. package/dist/replica.js +23 -6
  73. package/dist/save.d.ts.map +1 -1
  74. package/dist/save.js +10 -7
  75. package/dist/source.d.ts +61 -0
  76. package/dist/source.d.ts.map +1 -0
  77. package/dist/source.js +81 -0
  78. package/dist/status.d.ts +0 -2
  79. package/dist/status.d.ts.map +1 -1
  80. package/dist/status.js +0 -6
  81. package/dist/store.d.ts +85 -25
  82. package/dist/store.d.ts.map +1 -1
  83. package/dist/store.js +136 -98
  84. package/dist/stored.d.ts +55 -28
  85. package/dist/stored.d.ts.map +1 -1
  86. package/dist/stored.js +187 -74
  87. package/dist/tables.d.ts +91 -19
  88. package/dist/tables.d.ts.map +1 -1
  89. package/dist/tables.js +160 -34
  90. package/dist/tallies.d.ts +18 -0
  91. package/dist/tallies.d.ts.map +1 -0
  92. package/dist/tallies.js +129 -0
  93. package/dist/wire.d.ts +1 -0
  94. package/dist/wire.d.ts.map +1 -1
  95. package/dist/wire.js +1 -0
  96. package/package.json +2 -2
  97. package/dist/fence.d.ts +0 -6
  98. package/dist/fence.d.ts.map +0 -1
  99. 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';
@@ -24,10 +25,9 @@ import { advancement } from './refresh.js';
24
25
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
25
26
  import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
26
27
  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';
28
+ import { same, STARTING, } from './status.js';
28
29
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
29
30
  import { readinessOf, } from './readiness.js';
30
- import { FencedMap } from './fence.js';
31
31
  import { sameBinary } from './overlay.js';
32
32
  import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
33
33
  import { roomDraftRevision } from './identity.js';
@@ -68,6 +68,8 @@ const STAGE_AIM_MS = 50;
68
68
  const STREAMS_HELD = 4;
69
69
  const NOTHING_HELD = { bytes: 0 };
70
70
  const QUEUED_TEXT = 1 << 23;
71
+ // 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.
72
+ const NOT_READY_MS = [1_000, 2_000, 3_000, 5_000, 8_000, 11_000, 15_000];
71
73
  // 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
74
  const ARRIVAL_REPORT_MS = 250;
73
75
  // 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 +95,24 @@ export function composeRuntime(options) {
93
95
  const roomTypes = roomTypesOf(options.adapter.roomTypes, options.adapter.nameableRoomTypes);
94
96
  let authorization = options.authorization;
95
97
  let opening;
96
- const fences = new Map();
97
- const isFenced = (locator) => fences.has(locator);
98
+ const fencedAt = new Map();
99
+ const isFenced = (locator) => fencedAt.has(locator);
98
100
  let fencings = 0;
99
101
  const lastFenced = new Map();
100
- const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
102
+ const fencedSince = (locator, since) => fencedAt.has(locator) || (lastFenced.get(locator) ?? 0) > since;
101
103
  let discoveries = 0;
102
- const replicas = new FencedMap(isFenced);
103
- const reopened = new Set();
104
+ const confirmations = new Map();
104
105
  const usableWhenHeld = options.usableWhenHeld ?? [];
105
- const queuesWithoutReplica = new FencedMap(isFenced);
106
- const pendings = new Map();
107
106
  const landedAhead = new Map();
108
107
  const seeds = new Map();
109
108
  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();
109
+ const attachedFiles = new Map();
110
+ const declared = new Map();
111
+ const unheldModels = new Map();
112
+ const typesHeld = new Map();
117
113
  const unreadable = new Set();
118
114
  const builtIn = builtInModel(roomTypes);
119
115
  const typedPaths = options.adapter.typedPaths ?? [];
120
- const models = new FencedMap(isFenced);
121
- const liveModels = new FencedMap(isFenced);
122
- const invalidModels = new Set();
123
116
  const discovered = new Map();
124
117
  let inFlight = new AbortController();
125
118
  const refreshed = options.adapter.refresh?.bind(options.adapter);
@@ -134,23 +127,74 @@ export function composeRuntime(options) {
134
127
  };
135
128
  let roomsListed = false;
136
129
  const scopedRefusals = new Map();
137
- function modelOf(locator) {
138
- return models.get(locator) ?? NO_MODEL;
130
+ function typed(locator, persisted) {
131
+ if (persisted.status === 'unavailable')
132
+ return persisted;
133
+ const schema = persisted.snapshot.schema;
134
+ const held = typesHeld.get(locator);
135
+ if (held !== undefined && held.schema === schema)
136
+ return { ...persisted, types: held.types };
137
+ const types = schemaTypes(schema, roomTypes);
138
+ typesHeld.set(locator, { schema, types });
139
+ return { ...persisted, types };
139
140
  }
140
- function savedModel(locator) {
141
- return invalidModels.has(locator)
142
- ? { status: 'unavailable', reason: 'revoked' }
143
- : modelOf(locator);
141
+ function recordOf(stored) {
142
+ return { ...stored, model: typed(stored.locator, stored.model) };
144
143
  }
145
144
  function composed(record) {
146
145
  return compositionOf(record, builtIn, typedPaths);
147
146
  }
147
+ async function storedRooms(locators) {
148
+ const held = await (await openLocal()).rooms(locators);
149
+ return new Map(held
150
+ .filter(({ record }) => !isFenced(record.locator))
151
+ .map(({ record, indexed, documents }) => [
152
+ record.locator,
153
+ { record: recordOf(record), indexed, documents },
154
+ ]));
155
+ }
156
+ async function recordAt(locator) {
157
+ return (await storedRooms([locator])).get(locator)?.record;
158
+ }
159
+ async function workOf(locator) {
160
+ if (isFenced(locator))
161
+ return { replica: undefined, queue: [], states: new Map() };
162
+ const { room, queue } = await (await openLocal()).room(locator);
163
+ const batches = queue.map(({ batch }) => batch);
164
+ const states = new Map(queue.map(({ batch, state }) => [batch.id, state]));
165
+ return {
166
+ replica: room === null
167
+ ? undefined
168
+ : {
169
+ record: recordOf(room.record),
170
+ queue: batches,
171
+ states,
172
+ overlaid: room.overlaid,
173
+ count: room.documents,
174
+ confirmed: confirmations.get(locator) ?? 0,
175
+ indexed: room.indexed,
176
+ },
177
+ queue: batches,
178
+ states,
179
+ };
180
+ }
181
+ async function replicaOf(locator) {
182
+ return (await workOf(locator)).replica;
183
+ }
184
+ function modelIn(locator, stored) {
185
+ if (refusal?.code === 'revoked-authorization')
186
+ return { status: 'unavailable', reason: 'revoked' };
187
+ return stored?.record.model ?? unheldModels.get(locator) ?? NO_MODEL;
188
+ }
148
189
  function compositionFor(record) {
149
- return composed({ ...record, model: savedModel(record.locator) });
190
+ return composed({
191
+ ...record,
192
+ model: modelIn(record.locator, { record, indexed: true, documents: 0 }),
193
+ });
150
194
  }
151
195
  let refusal = null;
152
196
  let storageLoss = null;
153
- let status = STARTING;
197
+ let status = { state: 'syncing', providerUnreachable: false, resting: false };
154
198
  let shown = STARTING;
155
199
  let discoveryPaused = false;
156
200
  let closed = false;
@@ -164,6 +208,7 @@ export function composeRuntime(options) {
164
208
  const repairing = new Set();
165
209
  const calling = new Set();
166
210
  const repairable = new Map();
211
+ const unready = new Map();
167
212
  const answering = new Map();
168
213
  let complete = false;
169
214
  let recorded = false;
@@ -187,7 +232,17 @@ export function composeRuntime(options) {
187
232
  const queueSubscriptions = new Set();
188
233
  const roomDraftListeners = new Set();
189
234
  const presentationListeners = new Set();
190
- const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover));
235
+ const liveQueries = createLiveQueries((plan, cover) => answeredPlan(plan, cover), () => covering());
236
+ const figures = createFigures({
237
+ cover: figuresCover,
238
+ rooms: async (rooms, since) => database.ask({
239
+ kind: 'replica.tallies',
240
+ totals: { ...(await planned(ROOM_TOTALS, null)), limit: null },
241
+ rooms,
242
+ since,
243
+ }),
244
+ conversations: (conversations, since) => database.ask({ kind: 'conversation.tallies', conversations, since }),
245
+ });
191
246
  let withdrawConversations;
192
247
  let currencyQueued = null;
193
248
  let socketOpen = false;
@@ -200,15 +255,14 @@ export function composeRuntime(options) {
200
255
  let haltedRefusal = false;
201
256
  let openSignals = 0;
202
257
  const serviceModeListeners = new Set();
203
- let member = null;
204
258
  const memberListeners = new Set();
205
259
  let authorityChanges = 0;
206
260
  const authorityChangeListeners = new Set();
207
261
  const unpreparedDemand = new Set();
208
262
  const unpreparable = new Set();
209
263
  let preparing = false;
210
- let targets = {};
211
264
  let persistence = options.shell.persistence;
265
+ let heldWhenOpened = false;
212
266
  let full = false;
213
267
  let probing = null;
214
268
  let testing = null;
@@ -227,7 +281,7 @@ export function composeRuntime(options) {
227
281
  const readinessWatches = new Set();
228
282
  let readinessQueued = false;
229
283
  const catching = new Set();
230
- const transfers = new FencedMap(isFenced);
284
+ const transfers = new Map();
231
285
  const installs = new Set();
232
286
  const arrivals = new Map();
233
287
  const streaming = new Set();
@@ -255,9 +309,16 @@ export function composeRuntime(options) {
255
309
  call: (name, input) => invokeAction(name, input),
256
310
  history: (request) => readConversationHistory(request),
257
311
  durably,
312
+ local: () => openLocal(),
313
+ again: () => {
314
+ void ready.then(scheduleCurrency);
315
+ },
316
+ settled: () => {
317
+ show();
318
+ },
258
319
  stream: () => options.adapter.conversations,
259
320
  publish: publishChangeSet,
260
- online: () => connectionOf() !== 'unreachable' && refusal === null,
321
+ online: () => (socketOpen || !status.providerUnreachable) && refusal === null,
261
322
  full: () => full,
262
323
  closed: () => closed,
263
324
  unanswered: resendLater,
@@ -277,26 +338,13 @@ export function composeRuntime(options) {
277
338
  notifyDiscovered();
278
339
  readyFromStore();
279
340
  }
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
341
  async function visibleOf(locator, paths) {
294
- const held = replicas.get(locator);
342
+ const held = await recordAt(locator);
295
343
  if (held === undefined)
296
344
  return undefined;
297
345
  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)) };
346
+ const slice = await stepped(slicing(await local.entries(held.key, 'visible', paths), paths));
347
+ return { ...slice, collection: collectionOf(slice.documents, composed(held)) };
300
348
  }
301
349
  function roomDraftOf(record, visible) {
302
350
  return structuredClone({
@@ -320,25 +368,45 @@ export function composeRuntime(options) {
320
368
  layers.set(locator, await visibleOf(locator, paths));
321
369
  return records.map((record) => roomDraftOf(record, layers.get(record.locator)));
322
370
  }
323
- function snapshotRoomDrafts() {
324
- const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
325
- records.sort((left, right) => {
371
+ async function draftsOf(locator, paths) {
372
+ const held = await (await openLocal()).roomDrafts(locator, paths);
373
+ return held.filter((record) => !isFenced(record.locator));
374
+ }
375
+ async function draftOf(locator, path) {
376
+ return (await draftsOf(locator, [path]))[0];
377
+ }
378
+ function inOrder(records) {
379
+ return [...records].sort((left, right) => {
326
380
  if (left.locator !== right.locator)
327
381
  return left.locator < right.locator ? -1 : 1;
328
382
  if (left.path !== right.path)
329
383
  return left.path < right.path ? -1 : 1;
330
384
  return 0;
331
385
  });
332
- return roomDraftsOf(records);
333
386
  }
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)
387
+ async function snapshotRoomDrafts() {
388
+ return roomDraftsOf(inOrder(await draftsOf()));
389
+ }
390
+ let published = Promise.resolve();
391
+ function publication(work) {
392
+ published = published.then(work).catch(unpublished);
393
+ return published;
394
+ }
395
+ function publishRoomDrafts(waitingOnly, touched = null) {
396
+ if (closed || roomDraftListeners.size === 0)
397
+ return Promise.resolve();
398
+ return publication(async () => {
399
+ if (closed || roomDraftListeners.size === 0)
400
+ return;
401
+ const records = await draftsOf();
402
+ if (closed)
403
+ return;
404
+ if (touched !== null &&
405
+ !records.some((record) => touched.some(({ locator, path }) => locator === record.locator && (path === null || path === record.path)))) {
406
+ return;
407
+ }
408
+ const value = await roomDraftsOf(inOrder(records));
409
+ if (closed)
342
410
  return;
343
411
  for (const listener of roomDraftListeners) {
344
412
  if (waitingOnly && listener.ready)
@@ -348,8 +416,8 @@ export function composeRuntime(options) {
348
416
  }
349
417
  });
350
418
  }
351
- function presentationOf(app) {
352
- const value = presentations.get(app);
419
+ async function presentationOf(app) {
420
+ const value = await (await openLocal()).presentation(app);
353
421
  return value === undefined ? null : structuredClone(value);
354
422
  }
355
423
  async function writtenRoomDraft(room, path, revision, input, next) {
@@ -357,7 +425,7 @@ export function composeRuntime(options) {
357
425
  if (path === '' || normalize(path) !== path)
358
426
  throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
359
427
  const local = await openLocal();
360
- const current = roomDrafts.get(room)?.get(path);
428
+ const current = await draftOf(room, path);
361
429
  if ((current?.revision ?? null) !== revision) {
362
430
  return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
363
431
  }
@@ -376,55 +444,34 @@ export function composeRuntime(options) {
376
444
  await local.keepRoomDraft(record, parts.file === undefined || file === undefined
377
445
  ? []
378
446
  : [{ key: file.localId, blob: parts.file.blob }]);
379
- rememberRoomDraft(record);
380
- publishChangeSet(changeSet(room, { drafts: [path] }));
447
+ await publishChangeSet(changeSet(room, { drafts: [path] }));
381
448
  return { outcome: 'written', roomDraft: (await roomDraftsOf([record]))[0] ?? null };
382
449
  }
383
450
  async function keptPresentation(app, next) {
384
451
  const local = await openLocal();
385
- const stored = next(presentations.get(app));
452
+ const stored = next(await local.presentation(app));
386
453
  await local.present({ app, value: stored });
387
- presentations.set(app, stored);
388
- publishChangeSet(changeSet('', { presentation: app }));
454
+ await publishChangeSet(changeSet('', { presentation: app }));
389
455
  }
390
456
  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);
457
+ const listening = () => [...presentationListeners].filter((listener) => listener.app === app && listener.ready);
458
+ if (closed || listening().length === 0)
459
+ return Promise.resolve();
460
+ return publication(async () => {
461
+ if (closed || listening().length === 0)
462
+ return;
463
+ const value = await presentationOf(app);
464
+ if (closed)
465
+ return;
466
+ for (const listener of listening())
467
+ listener.notify(value);
468
+ });
416
469
  }
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
- }
470
+ async function attended() {
471
+ const held = await (await openLocal()).status();
472
+ const attentionRequired = held.returned.flatMap((record) => record.returned === null || isFenced(record.locator)
473
+ ? []
474
+ : [{ room: record.locator, path: record.path, returned: structuredClone(record.returned) }]);
428
475
  attentionRequired.sort((left, right) => {
429
476
  if (left.room !== right.room)
430
477
  return left.room < right.room ? -1 : 1;
@@ -432,41 +479,130 @@ export function composeRuntime(options) {
432
479
  return left.path < right.path ? -1 : 1;
433
480
  return 0;
434
481
  });
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;
482
+ const changed = new Map();
483
+ if (!localSaves) {
484
+ for (const change of held.unsent) {
485
+ if (!isFenced(change.room))
486
+ changed.set(`${change.room}\u0000${change.path}`, change);
487
+ }
441
488
  }
442
- return { attentionRequired, unsent: unsent(), reset };
489
+ const unsent = [...changed]
490
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
491
+ .map(([, change]) => change);
492
+ return {
493
+ queued: !localSaves && held.queued.some((locator) => !isFenced(locator)),
494
+ lastCompleted: held.lastCompleted,
495
+ attentionRequired,
496
+ unsent,
497
+ reset: held.resets.reduce((earliest, { locator, reset }) => isFenced(locator) || (earliest !== null && earliest <= reset) ? earliest : reset, null),
498
+ };
443
499
  }
444
- function documentsIn(locator) {
445
- return replicas.get(locator)?.count ?? 0;
500
+ async function queuedWork() {
501
+ const held = await (await openLocal()).queued();
502
+ return !localSaves && held.some((locator) => !isFenced(locator));
446
503
  }
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
- }));
504
+ function declarationOf(locator, stored) {
505
+ const held = declared.get(locator);
506
+ const room = stored?.record ?? held?.room ?? discovered.get(locator);
507
+ return room === undefined ? undefined : { room, available: held?.available ?? true };
461
508
  }
462
- function locked(locator) {
463
- return rooms.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
509
+ function listedLocators(held) {
510
+ return [
511
+ ...declared.keys(),
512
+ ...[...held.keys()].filter((locator) => !declared.has(locator)),
513
+ ].filter((locator) => !isFenced(locator));
464
514
  }
465
- function lockedRooms() {
466
- return [...rooms.keys()].filter(locked).sort();
515
+ function selectionModel() {
516
+ return refusal?.code === 'revoked-authorization'
517
+ ? { status: 'unavailable', reason: 'revoked' }
518
+ : builtIn;
467
519
  }
468
- function relocked(before) {
469
- const after = lockedRooms();
520
+ function listedRoom(locator, stored) {
521
+ const declaration = declarationOf(locator, stored);
522
+ if (declaration === undefined)
523
+ return undefined;
524
+ const { room, available } = declaration;
525
+ const selection = room.typedSelection ?? null;
526
+ return {
527
+ name: room.name,
528
+ locator,
529
+ handle: room.handle,
530
+ operations: room.operations,
531
+ replica: room.replica,
532
+ model: modelTypes(modelIn(locator, stored)),
533
+ typedSelection: selection === null ? null : { roots: selection.roots, model: modelTypes(selectionModel()) },
534
+ providerOwnedPaths: room.providerOwnedPaths,
535
+ roomVersion: stored?.record.roomVersion ?? null,
536
+ synchronizationAvailable: available,
537
+ refusal: scopedRefusals.get(locator) ?? null,
538
+ documents: stored?.documents ?? 0,
539
+ };
540
+ }
541
+ function listingOf(held) {
542
+ const listing = new Map();
543
+ for (const locator of listedLocators(held)) {
544
+ const room = listedRoom(locator, held.get(locator));
545
+ if (room !== undefined)
546
+ listing.set(locator, room);
547
+ }
548
+ return listing;
549
+ }
550
+ async function patchedListing(changed) {
551
+ if (roomsShown === null)
552
+ return null;
553
+ const held = await storedRooms([...changed]);
554
+ const listing = new Map(roomsShown);
555
+ for (const locator of changed) {
556
+ const room = isFenced(locator) ? undefined : listedRoom(locator, held.get(locator));
557
+ if (room === undefined || !listing.has(locator))
558
+ return null;
559
+ listing.set(locator, room);
560
+ }
561
+ return listing;
562
+ }
563
+ async function listingNow() {
564
+ return listingOf(await storedRooms());
565
+ }
566
+ function lockedIn(listing, locator) {
567
+ return listing.get(locator)?.replica.offline === 'locked' && !confirmed.has(locator);
568
+ }
569
+ function shownRooms(listing) {
570
+ return structuredClone([...listing.values()].map(({ documents, ...room }) => lockedIn(listing, room.locator)
571
+ ? {
572
+ ...room,
573
+ model: modelTypes(NO_MODEL),
574
+ typedSelection: null,
575
+ documents: 0,
576
+ locked: true,
577
+ }
578
+ : { ...room, documents, locked: false }));
579
+ }
580
+ async function offlineModes() {
581
+ const modes = new Map();
582
+ for (const [locator, { room }] of declared) {
583
+ const declaration = room ?? discovered.get(locator);
584
+ if (declaration !== undefined)
585
+ modes.set(locator, declaration.replica.offline);
586
+ }
587
+ for (const [locator, { offline }] of (await covering()).rooms)
588
+ modes.set(locator, offline);
589
+ return modes;
590
+ }
591
+ async function locked(locator) {
592
+ if (isFenced(locator) || confirmed.has(locator))
593
+ return false;
594
+ const held = declared.get(locator)?.room ?? discovered.get(locator);
595
+ const mode = (await (await openLocal()).offline(locator)) ?? held?.replica.offline;
596
+ return mode === 'locked';
597
+ }
598
+ async function lockedRooms() {
599
+ return [...(await offlineModes())]
600
+ .filter(([locator, mode]) => mode === 'locked' && !confirmed.has(locator) && !isFenced(locator))
601
+ .map(([locator]) => locator)
602
+ .sort();
603
+ }
604
+ async function relocked(before) {
605
+ const after = await lockedRooms();
470
606
  if (JSON.stringify(after) === JSON.stringify(before))
471
607
  return;
472
608
  journal.record(`locked ${after.length}`);
@@ -489,9 +625,10 @@ export function composeRuntime(options) {
489
625
  lockGrace = null;
490
626
  if (socketOpen || closed)
491
627
  return;
492
- const before = lockedRooms();
493
- confirmed.clear();
494
- relocked(before);
628
+ void lockedRooms().then((before) => {
629
+ confirmed.clear();
630
+ return relocked(before);
631
+ }, () => { });
495
632
  }, LOCK_GRACE_MS);
496
633
  }
497
634
  function connectionLive() {
@@ -505,13 +642,14 @@ export function composeRuntime(options) {
505
642
  lockListeners.delete(notify);
506
643
  };
507
644
  }
508
- function roomFacts(reach = null) {
509
- const held = [...rooms.values()]
645
+ async function roomFacts(reach = null) {
646
+ const held = await storedRooms();
647
+ const rooms = [...listingOf(held).values()]
510
648
  .filter((room) => holds(reach, room.locator))
511
649
  .map((room) => {
512
650
  const governed = governs(reach, room);
513
651
  const attached = [
514
- ...(governed.collection ? [modelOf(room.locator)] : []),
652
+ ...(governed.collection ? [modelIn(room.locator, held.get(room.locator))] : []),
515
653
  ...(governed.selection ? [builtIn] : []),
516
654
  ];
517
655
  return {
@@ -520,28 +658,86 @@ export function composeRuntime(options) {
520
658
  types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
521
659
  };
522
660
  });
523
- return { listed: roomsListed, rooms: held };
661
+ return { listed: roomsListed, rooms };
524
662
  }
525
- function emitRooms() {
526
- liveQueries.recover();
663
+ function emitRooms(changed) {
664
+ recoverReads();
527
665
  queueReadiness();
528
- const value = listed();
529
- for (const listener of roomListeners)
530
- listener(value);
531
- emitKnownRooms();
666
+ return changed === undefined ? publishRooms('all') : publishRooms('changed', changed);
532
667
  }
533
668
  function emitKnownRooms() {
534
669
  conversations.learned();
535
670
  if (unlistedRooms.size === 0)
536
671
  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);
672
+ void publishRooms('known');
673
+ }
674
+ let roomsOwed = null;
675
+ let roomsShown = null;
676
+ function publishRooms(mode, touched = []) {
677
+ if (closed)
678
+ return Promise.resolve();
679
+ if (roomsOwed !== null) {
680
+ roomsOwed.all ||= mode === 'all';
681
+ for (const locator of touched) {
682
+ (mode === 'changed' ? roomsOwed.changed : roomsOwed.counted).add(locator);
683
+ }
684
+ return roomsOwed.shown;
544
685
  }
686
+ const owing = {
687
+ all: mode === 'all',
688
+ changed: new Set(mode === 'changed' ? touched : []),
689
+ counted: new Set(mode === 'changed' ? [] : touched),
690
+ shown: Promise.resolve(),
691
+ };
692
+ roomsOwed = owing;
693
+ owing.shown = publication(async () => {
694
+ const owed = owing;
695
+ if (roomsOwed === owing)
696
+ roomsOwed = null;
697
+ if (closed || (roomListeners.size === 0 && unlistedRooms.size === 0))
698
+ return;
699
+ const patched = owed.all || owed.changed.size === 0 || unlistedRooms.size > 0
700
+ ? null
701
+ : await patchedListing(new Set([...owed.changed, ...owed.counted]));
702
+ if (patched === null && owed.changed.size > 0)
703
+ owed.all = true;
704
+ if (patched === null && !owed.all && owed.counted.size > 0) {
705
+ const held = await covering().catch(() => null);
706
+ if (held === null)
707
+ return;
708
+ owed.all = [...owed.counted].some((locator) => held.rooms.has(locator) &&
709
+ roomsShown?.get(locator)?.documents !== held.rooms.get(locator).documents);
710
+ if (owed.all) {
711
+ recoverReads();
712
+ queueReadiness();
713
+ }
714
+ else if (unlistedRooms.size === 0)
715
+ return;
716
+ }
717
+ const listing = patched ?? (await listingNow().catch(() => null));
718
+ if (listing === null || closed)
719
+ return;
720
+ const value = shownRooms(listing);
721
+ const joining = unlistedRooms.size > 0 &&
722
+ (roomsListed || listing.size > 0 || refusal !== null || status.providerUnreachable);
723
+ if (!owed.all && patched === null && !joining)
724
+ return;
725
+ const moved = owed.all ||
726
+ patched !== null ||
727
+ (roomsShown !== null && JSON.stringify(shownRooms(roomsShown)) !== JSON.stringify(value));
728
+ if (moved)
729
+ for (const listener of roomListeners)
730
+ listener(value);
731
+ if (joining) {
732
+ for (const listener of [...unlistedRooms]) {
733
+ unlistedRooms.delete(listener);
734
+ roomListeners.add(listener);
735
+ listener(value);
736
+ }
737
+ }
738
+ roomsShown = roomListeners.size === 0 ? null : listing;
739
+ });
740
+ return owing.shown;
545
741
  }
546
742
  function emitRefusal() {
547
743
  queueReadiness();
@@ -554,58 +750,98 @@ export function composeRuntime(options) {
554
750
  for (const listener of storageLossListeners)
555
751
  listener(storageLoss);
556
752
  }
557
- function emitMember() {
753
+ function emitMember(member) {
558
754
  for (const listener of memberListeners)
559
755
  listener(member);
560
756
  }
561
757
  function moveTo(next) {
562
758
  if (closed)
563
- return;
759
+ return Promise.resolve();
564
760
  status = next;
565
- show();
761
+ return show();
566
762
  }
763
+ function transferring() {
764
+ return { ...status, state: 'syncing', resting: false };
765
+ }
766
+ let statusOwed = null;
767
+ let statusShown = Promise.resolve();
567
768
  function show() {
568
769
  if (closed)
569
- return;
770
+ return Promise.resolve();
570
771
  queueReadiness();
571
772
  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);
773
+ const moved = { moving: status, current: conversations.current() };
774
+ if (statusOwed !== null) {
775
+ statusOwed.push(moved);
776
+ return statusShown;
777
+ }
778
+ const owed = [moved];
779
+ statusOwed = owed;
780
+ statusShown = publication(async () => {
781
+ if (statusOwed === owed)
782
+ statusOwed = null;
783
+ const held = await attended();
784
+ if (closed)
785
+ return;
786
+ for (const { moving, current } of owed) {
787
+ const state = moving.resting && held.queued ? 'pending' : moving.state;
788
+ const next = {
789
+ state: state === 'synced' && !current ? 'syncing' : state,
790
+ lastCompleted: held.lastCompleted,
791
+ providerUnreachable: moving.providerUnreachable,
792
+ attentionRequired: held.attentionRequired,
793
+ unsent: held.unsent,
794
+ reset: held.reset,
795
+ };
796
+ if (same(shown, next))
797
+ continue;
798
+ shown = next;
799
+ for (const listener of statusListeners)
800
+ listener(shown);
801
+ }
802
+ });
803
+ return statusShown;
580
804
  }
581
- function entryOf(locator, batch) {
805
+ function entryOf(states, batch) {
582
806
  if (localSaves)
583
807
  return Object.freeze({ batch, state: 'local' });
584
- const state = pendings.get(pendingKey(locator, batch.id));
808
+ const state = states.get(batch.id);
585
809
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
586
810
  }
587
- function entriesOf(locator) {
588
- return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map((batch) => entryOf(locator, batch));
811
+ async function entriesOf(locator) {
812
+ if (isFenced(locator))
813
+ return [];
814
+ const { queue, states } = await workOf(locator);
815
+ return queue.map((batch) => entryOf(states, batch));
589
816
  }
590
817
  function emitQueue(locator) {
591
- const entries = entriesOf(locator);
592
- for (const subscription of queueSubscriptions) {
593
- if (subscription.locator === locator)
818
+ const listening = () => [...queueSubscriptions].filter((subscription) => subscription.locator === locator);
819
+ if (closed || listening().length === 0)
820
+ return Promise.resolve();
821
+ return publication(async () => {
822
+ if (closed || listening().length === 0)
823
+ return;
824
+ const entries = await entriesOf(locator);
825
+ if (closed)
826
+ return;
827
+ for (const subscription of listening())
594
828
  subscription.notify(entries);
595
- }
829
+ });
596
830
  }
597
831
  function applyChangeSet(set) {
598
832
  liveQueries.touched(set);
833
+ figures.touched(set);
834
+ }
835
+ function recoverReads() {
836
+ liveQueries.recover();
837
+ figures.recover();
599
838
  }
600
839
  function unpublished(error) {
601
840
  journal.record(`publication failed ${error instanceof Error ? error.name : 'unknown'}`);
602
841
  }
603
- function indexed(locator) {
604
- return replicas.get(locator)?.indexed === true;
605
- }
606
842
  async function prepareReplica(locator) {
607
843
  for (;;) {
608
- const held = replicas.get(locator);
844
+ const held = (await storedRooms([locator])).get(locator);
609
845
  if (held === undefined || closed || held.indexed)
610
846
  return;
611
847
  const key = held.record.key;
@@ -613,20 +849,19 @@ export function composeRuntime(options) {
613
849
  const local = await openLocal();
614
850
  return timed(deriveBudget, () => local.rederive(key, composed(held.record), deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR));
615
851
  });
616
- const now = replicas.get(locator);
617
- if (now?.record.key !== key)
618
- continue;
619
852
  if (!done.done)
620
853
  continue;
621
- replicas.set(locator, { ...now, indexed: true });
854
+ if ((await storedRooms([locator])).get(locator)?.record.key !== key)
855
+ continue;
622
856
  publishChangeSet(changeSet(locator, { documents: null }));
623
- liveQueries.recover();
857
+ recoverReads();
624
858
  queueReadiness();
625
859
  return;
626
860
  }
627
861
  }
628
- function unprepared() {
629
- return [...unpreparedDemand, ...replicas.keys()].find((locator) => !indexed(locator) && !unpreparable.has(locator));
862
+ async function unprepared() {
863
+ const held = (await covering()).rooms;
864
+ return [...unpreparedDemand, ...held.keys()].find((locator) => !(held.get(locator)?.indexed ?? false) && !unpreparable.has(locator));
630
865
  }
631
866
  function unpreparedBy(locator, error) {
632
867
  unpreparable.add(locator);
@@ -641,7 +876,7 @@ export function composeRuntime(options) {
641
876
  preparing = true;
642
877
  void (async () => {
643
878
  try {
644
- for (let waiting = unprepared(); waiting !== undefined; waiting = unprepared()) {
879
+ for (let waiting = await unprepared(); waiting !== undefined; waiting = await unprepared()) {
645
880
  await yielded();
646
881
  if (closed)
647
882
  return;
@@ -649,6 +884,9 @@ export function composeRuntime(options) {
649
884
  await prepareReplica(waiting).catch((error) => unpreparedBy(waiting, error));
650
885
  }
651
886
  }
887
+ catch {
888
+ journal.record('preparation failed');
889
+ }
652
890
  finally {
653
891
  preparing = false;
654
892
  }
@@ -657,31 +895,48 @@ export function composeRuntime(options) {
657
895
  function roomsOfReach(reach) {
658
896
  return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
659
897
  }
660
- function reachedRooms(plan) {
898
+ async function covering() {
899
+ const held = await (await openLocal()).covering();
900
+ return {
901
+ rooms: new Map([...held.rooms].filter(([locator]) => !isFenced(locator))),
902
+ queues: new Map([...held.queues].filter(([locator]) => !isFenced(locator))),
903
+ };
904
+ }
905
+ function everyRoom(held) {
906
+ return [...new Set([...declared.keys(), ...held.rooms.keys(), ...held.queues.keys()])];
907
+ }
908
+ function reachedRooms(plan, held) {
661
909
  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]);
910
+ const named = plan.rooms ?? (reached === null ? everyRoom(held) : [...reached]);
666
911
  return [...new Set(named)]
667
912
  .filter((locator) => (reached === null || reached.has(locator)) && !plan.locked.includes(locator))
668
913
  .sort();
669
914
  }
670
915
  function awaited(locator) {
671
916
  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) {
917
+ (declared.has(locator) && discovered.get(locator)?.replica.admission === 'admitted'));
918
+ }
919
+ function covered(plan, held) {
920
+ return coveredRooms(reachedRooms(plan, held), plan.rooms === null && plan.reach === null, held);
921
+ }
922
+ async function figuresCover() {
923
+ const held = await covering();
924
+ const locked = await lockedRooms();
925
+ const cover = coveredRooms(everyRoom(held)
926
+ .filter((locator) => !locked.includes(locator))
927
+ .sort(), true, held);
928
+ awaitPreparation(cover);
929
+ return 'pending' in cover ? cover : { rooms: cover.rooms };
930
+ }
931
+ function coveredRooms(locators, whole, held) {
932
+ if (locators.length === 0 && whole && !roomsListed)
677
933
  return { pending: [] };
678
- }
679
934
  const pending = [];
680
935
  const coverage = [];
681
936
  const overlay = [];
682
937
  const read = [];
683
938
  for (const locator of locators) {
684
- const replica = replicas.get(locator);
939
+ const replica = held.rooms.get(locator);
685
940
  if (replica === undefined) {
686
941
  if (awaited(locator))
687
942
  pending.push({ room: locator, awaits: 'acquisition' });
@@ -693,12 +948,12 @@ export function composeRuntime(options) {
693
948
  }
694
949
  coverage.push({
695
950
  room: locator,
696
- generation: replica.record.transfer,
951
+ generation: replica.transfer,
697
952
  current: !(catching.has(locator) || transfers.has(locator)),
698
953
  });
699
- read.push({ locator, key: replica.record.key });
700
- for (const batch of replica.queue)
701
- overlay.push(batch.id);
954
+ read.push({ locator, key: replica.key });
955
+ for (const id of held.queues.get(locator) ?? [])
956
+ overlay.push(id);
702
957
  }
703
958
  if (pending.length > 0)
704
959
  return { pending };
@@ -711,46 +966,54 @@ export function composeRuntime(options) {
711
966
  if (waiting.length > 0)
712
967
  prepare(waiting.map(({ room }) => room));
713
968
  }
714
- function requiredRooms(given) {
969
+ function requiredRooms(given, held) {
715
970
  const reach = planReach(given);
971
+ const listed = new Set(listedLocators(held));
716
972
  if (reach === null)
717
- return { listed: roomsListed, rooms: [...rooms.keys()].sort() };
973
+ return { listed: roomsListed, rooms: [...listed].sort() };
718
974
  const named = [...roomsOfReach(reach)].sort();
719
975
  if (roomsListed)
720
- return { listed: true, rooms: named.filter((room) => rooms.has(room)) };
721
- return { listed: named.every((room) => replicas.has(room)), rooms: named };
976
+ return { listed: true, rooms: named.filter((room) => listed.has(room)) };
977
+ return { listed: named.every((room) => held.has(room)), rooms: named };
722
978
  }
723
- function roomState(locator) {
979
+ function roomState(locator, held) {
724
980
  const listedRoom = discovered.get(locator);
725
981
  if (roomsListed &&
726
982
  (listedRoom === undefined ||
727
983
  listedRoom.replica.admission !== 'admitted' ||
728
984
  unreadable.has(locator)))
729
985
  return 'unadmitted';
730
- if (!replicas.has(locator))
986
+ const stored = held.get(locator);
987
+ if (stored === undefined)
731
988
  return 'acquiring';
732
- if (!indexed(locator))
989
+ if (!stored.indexed)
733
990
  return 'preparing';
734
991
  return 'complete';
735
992
  }
736
993
  function conversationCoverage(reach) {
737
- const declared = reach === null || reach.conversations;
738
- if (!declared || options.adapter.conversations === undefined)
994
+ const declaredConversations = reach === null || reach.conversations;
995
+ if (!declaredConversations || options.adapter.conversations === undefined)
739
996
  return 'undeclared';
740
997
  if (!conversations.known() || !conversations.current())
741
998
  return 'acquiring';
742
999
  return 'complete';
743
1000
  }
744
- function connectionOf() {
1001
+ function connectionOf(lastCompleted) {
745
1002
  if (socketOpen)
746
1003
  return 'connected';
747
1004
  if (status.providerUnreachable)
748
1005
  return 'unreachable';
749
- return options.adapter.roomStream === undefined && status.lastCompleted !== null
1006
+ return options.adapter.roomStream === undefined && lastCompleted !== null
750
1007
  ? 'connected'
751
1008
  : 'connecting';
752
1009
  }
753
- function bytesOf(room, underway) {
1010
+ async function connectionNow() {
1011
+ if (socketOpen || status.providerUnreachable || options.adapter.roomStream !== undefined) {
1012
+ return connectionOf(null);
1013
+ }
1014
+ return connectionOf((await (await openLocal()).facts()).lastCompleted);
1015
+ }
1016
+ function bytesOf(room, underway, held) {
754
1017
  const transfer = transfers.get(room);
755
1018
  if (transfer !== undefined) {
756
1019
  const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
@@ -759,10 +1022,10 @@ export function composeRuntime(options) {
759
1022
  total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
760
1023
  };
761
1024
  }
762
- const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
763
- return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
1025
+ const kept = underway ? NOTHING_HELD.bytes : (held.get(room)?.bytes ?? NOTHING_HELD.bytes);
1026
+ return { done: kept, total: arrivals.get(room) ?? kept };
764
1027
  }
765
- function installationOf(coverage) {
1028
+ function installationOf(coverage, held) {
766
1029
  const rooms = { done: 0, total: 0 };
767
1030
  const bytes = { done: 0, total: 0 };
768
1031
  let installing = 0;
@@ -778,7 +1041,7 @@ export function composeRuntime(options) {
778
1041
  installing += 1;
779
1042
  if (underway && (streaming.has(room) || !arrivals.has(room)))
780
1043
  downloading = true;
781
- const now = bytesOf(room, underway);
1044
+ const now = bytesOf(room, underway, held);
782
1045
  const earlier = underway ? startedOver.get(room) : undefined;
783
1046
  bytes.done += Math.max(now.done, earlier?.done ?? 0);
784
1047
  bytes.total += Math.max(now.total, earlier?.total ?? 0);
@@ -792,9 +1055,14 @@ export function composeRuntime(options) {
792
1055
  : 'replaying';
793
1056
  return { step, rooms, bytes };
794
1057
  }
795
- function readinessFor(port, reach) {
796
- const required = requiredRooms(reach);
797
- const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
1058
+ async function readinessHeld() {
1059
+ const local = await openLocal();
1060
+ const [held, facts] = await Promise.all([covering(), local.facts()]);
1061
+ return { held: held.rooms, facts };
1062
+ }
1063
+ function readinessFor(port, reach, { held, facts }) {
1064
+ const required = requiredRooms(reach, held);
1065
+ const coverage = required.rooms.map((room) => ({ room, state: roomState(room, held) }));
798
1066
  const preparing = coverage
799
1067
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
800
1068
  .map(({ room }) => room);
@@ -808,36 +1076,65 @@ export function composeRuntime(options) {
808
1076
  conversations: conversationCoverage(reach),
809
1077
  },
810
1078
  persistence,
811
- connection: connectionOf(),
1079
+ connection: connectionOf(facts.lastCompleted),
812
1080
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
813
1081
  space: full ? 'full' : null,
814
- installation: installationOf(coverage),
1082
+ installation: installationOf(coverage, held),
815
1083
  lost: storageLoss !== null,
816
- established: targets[port.target]?.established === true ||
817
- (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
1084
+ established: facts.targets[port.target]?.established === true ||
1085
+ (heldWhenOpened && usableWhenHeld.includes(port.target)),
818
1086
  });
819
1087
  }
1088
+ const establishing = new Set();
820
1089
  function establish(name) {
821
- targets = { ...targets, [name]: { established: true } };
822
- queueReadiness();
823
- probeEachMinute();
824
- void durably((local) => local.fact('targets', targets)).catch(() => {
1090
+ if (establishing.has(name))
1091
+ return;
1092
+ establishing.add(name);
1093
+ void durably(async (local) => {
1094
+ const { targets } = await local.facts();
1095
+ await local.fact('targets', { ...targets, [name]: { established: true } });
1096
+ })
1097
+ .then(() => {
1098
+ queueReadiness();
1099
+ void probeEachMinute();
1100
+ }, () => {
825
1101
  journal.record(`target record failed ${name}`);
826
- });
1102
+ })
1103
+ .finally(() => establishing.delete(name));
827
1104
  }
828
- function deliverReadiness(watch) {
1105
+ function deliverReadiness(watch, held) {
829
1106
  if (!readinessWatches.has(watch))
830
1107
  return;
831
- const readiness = readinessFor(watch.port, watch.reach);
832
- if (readiness.ready && targets[watch.port.target]?.established !== true) {
1108
+ const readiness = readinessFor(watch.port, watch.reach, held);
1109
+ if (readiness.ready && held.facts.targets[watch.port.target]?.established !== true) {
833
1110
  establish(watch.port.target);
834
1111
  }
835
- const shown = JSON.stringify(readiness);
836
- if (shown === watch.last)
1112
+ const shownReadiness = JSON.stringify(readiness);
1113
+ if (shownReadiness === watch.last)
837
1114
  return;
838
- watch.last = shown;
1115
+ watch.last = shownReadiness;
839
1116
  watch.deliver(structuredClone(readiness));
840
1117
  }
1118
+ let readinessShown = Promise.resolve();
1119
+ const readinessOwed = new Set();
1120
+ function deliverAll(watches) {
1121
+ const queued = readinessOwed.size > 0;
1122
+ for (const watch of watches)
1123
+ readinessOwed.add(watch);
1124
+ if (queued)
1125
+ return;
1126
+ readinessShown = readinessShown
1127
+ .then(async () => {
1128
+ const owed = [...readinessOwed];
1129
+ readinessOwed.clear();
1130
+ const held = await readinessHeld();
1131
+ if (closed)
1132
+ return;
1133
+ for (const watch of owed)
1134
+ deliverReadiness(watch, held);
1135
+ })
1136
+ .catch(unpublished);
1137
+ }
841
1138
  function queueReadiness() {
842
1139
  if (readinessQueued || closed || readinessWatches.size === 0)
843
1140
  return;
@@ -846,14 +1143,13 @@ export function composeRuntime(options) {
846
1143
  readinessQueued = false;
847
1144
  if (closed)
848
1145
  return;
849
- for (const watch of [...readinessWatches])
850
- deliverReadiness(watch);
1146
+ deliverAll([...readinessWatches]);
851
1147
  });
852
1148
  }
853
1149
  function watchReadiness(port, reach, deliver) {
854
1150
  const watch = { port, reach, deliver, last: null };
855
1151
  readinessWatches.add(watch);
856
- void openLocal().then(() => deliverReadiness(watch), () => deliverReadiness(watch));
1152
+ void openLocal().then(() => deliverAll([watch]), () => deliverAll([watch]));
857
1153
  return () => {
858
1154
  readinessWatches.delete(watch);
859
1155
  };
@@ -864,14 +1160,20 @@ export function composeRuntime(options) {
864
1160
  full = true;
865
1161
  journal.record('space full');
866
1162
  queueReadiness();
867
- probeEachMinute();
1163
+ void probeEachMinute();
868
1164
  }
869
- function installedTarget() {
870
- return ((usableWhenHeld.length > 0 && reopened.size > 0) ||
871
- Object.values(targets).some((target) => target.established));
1165
+ async function installedTarget() {
1166
+ if (heldWhenOpened && usableWhenHeld.length > 0)
1167
+ return true;
1168
+ const { targets } = await (await openLocal()).facts();
1169
+ return Object.values(targets).some((target) => target.established);
872
1170
  }
873
- function probeEachMinute() {
874
- if (!full || probing !== null || closed || !installedTarget())
1171
+ async function probeEachMinute() {
1172
+ if (!full || probing !== null || closed)
1173
+ return;
1174
+ if (!(await installedTarget().catch(() => false)))
1175
+ return;
1176
+ if (!full || probing !== null || closed)
875
1177
  return;
876
1178
  // 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
1179
  probing = setInterval(() => {
@@ -906,39 +1208,41 @@ export function composeRuntime(options) {
906
1208
  else
907
1209
  void resynchronize();
908
1210
  }
909
- function seedRoomHeld(room, version) {
910
- return indexed(room) && reachedBy(room, version);
1211
+ function seedRoomHeld(room, version, held) {
1212
+ const stored = held.rooms.get(room);
1213
+ return (stored !== undefined &&
1214
+ stored.indexed &&
1215
+ options.adapter.reached(room, stored.roomVersion, version));
911
1216
  }
912
- function seedHeld(at) {
913
- return Object.entries(at).every(([room, version]) => (roomsListed && !rooms.has(room)) || seedRoomHeld(room, version));
1217
+ function seedHeld(at, held) {
1218
+ return Object.entries(at).every(([room, version]) => (roomsListed && !declared.has(room) && !held.rooms.has(room)) ||
1219
+ seedRoomHeld(room, version, held));
914
1220
  }
915
- function seedApplies(plan) {
1221
+ function seedApplies(plan, held) {
916
1222
  const seed = seeds.get(plan.key);
917
1223
  if (seed === undefined)
918
1224
  return null;
919
- if (!seedHeld(seed.at))
1225
+ if (!seedHeld(seed.at, held))
920
1226
  return seed;
921
1227
  seeds.delete(plan.key);
922
1228
  return null;
923
1229
  }
924
- function seedBehind(plan, cover) {
925
- const seed = seedApplies(plan);
1230
+ function seedBehind(plan, cover, held) {
1231
+ const seed = seedApplies(plan, held);
926
1232
  if (seed === null || 'pending' in cover)
927
1233
  return cover;
928
1234
  return {
929
1235
  pending: Object.entries(seed.at)
930
- .filter(([room, version]) => !seedRoomHeld(room, version))
1236
+ .filter(([room, version]) => !seedRoomHeld(room, version, held))
931
1237
  .map(([room]) => ({ room, awaits: 'acquisition' })),
932
1238
  };
933
1239
  }
934
1240
  function seed(entry) {
935
- if (seedHeld(entry.at))
936
- return;
937
1241
  for (const { query, rows } of entry.queries) {
938
1242
  const key = queryKey(query);
939
1243
  seeds.set(key, { rows, at: entry.at });
940
- for (const shown of seedWatchers.get(key) ?? [])
941
- shown();
1244
+ for (const shownSeed of seedWatchers.get(key) ?? [])
1245
+ shownSeed();
942
1246
  }
943
1247
  }
944
1248
  function sameCover(left, right) {
@@ -946,7 +1250,7 @@ export function composeRuntime(options) {
946
1250
  }
947
1251
  async function coveredRows(plan, cover) {
948
1252
  for (;;) {
949
- const before = cover();
1253
+ const before = await cover();
950
1254
  if ('pending' in before)
951
1255
  return { pending: before };
952
1256
  const local = await openLocal();
@@ -954,7 +1258,7 @@ export function composeRuntime(options) {
954
1258
  const changes = local.changes(keys);
955
1259
  const current = () => local.changes(keys) === changes;
956
1260
  const rows = await windowRows(local, plan, before.rooms, current);
957
- const after = cover();
1261
+ const after = await cover();
958
1262
  if (rows !== null && sameCover(before, after))
959
1263
  return { rows, cover: before, current };
960
1264
  }
@@ -1010,23 +1314,25 @@ export function composeRuntime(options) {
1010
1314
  const held = answered.cover;
1011
1315
  return { ...keyedOf(answered.rows, held.coverage), current: answered.current };
1012
1316
  }
1317
+ async function coverNow(plan) {
1318
+ const cover = covered(plan, await covering());
1319
+ awaitPreparation(cover);
1320
+ return cover;
1321
+ }
1013
1322
  async function rowsQuery(plan) {
1014
- const before = covered(plan);
1323
+ const held = await covering();
1324
+ const before = covered(plan, held);
1015
1325
  awaitPreparation(before);
1016
- const seeded = seedApplies(plan);
1326
+ const seeded = seedApplies(plan, held);
1017
1327
  if (seeded !== null)
1018
1328
  return seededResult(seeded).result;
1019
1329
  if ('pending' in before)
1020
1330
  return before;
1021
- const answered = await coveredRows(plan, () => {
1022
- const cover = covered(plan);
1023
- awaitPreparation(cover);
1024
- return cover;
1025
- });
1331
+ const answered = await coveredRows(plan, () => coverNow(plan));
1026
1332
  if ('pending' in answered)
1027
1333
  return answered.pending;
1028
- const held = answered.cover;
1029
- return { ...answered.rows, coverage: held.coverage };
1334
+ const shownCover = answered.cover;
1335
+ return { ...answered.rows, coverage: shownCover.coverage };
1030
1336
  }
1031
1337
  function publishChangeSet(set) {
1032
1338
  applyChangeSet(set);
@@ -1034,21 +1340,24 @@ export function composeRuntime(options) {
1034
1340
  if (path !== null)
1035
1341
  attachedFiles.get(locator)?.delete(path);
1036
1342
  }
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);
1343
+ const shownNow = [];
1344
+ if (set.queues.length > 0 || set.drafts.length > 0)
1345
+ shownNow.push(show());
1346
+ if (set.rooms.length > 0)
1347
+ shownNow.push(emitRooms(set.rooms));
1348
+ else if (set.documents.length > 0) {
1349
+ shownNow.push(publishRooms('counted', [...new Set(set.documents.map(({ locator }) => locator))]));
1048
1350
  }
1351
+ for (const locator of set.queues)
1352
+ shownNow.push(emitQueue(locator));
1353
+ if (set.drafts.length > 0)
1354
+ shownNow.push(publishRoomDrafts(false));
1355
+ else if (set.documents.length > 0)
1356
+ shownNow.push(publishRoomDrafts(false, set.documents));
1049
1357
  for (const app of set.presentations)
1050
- emitPresentation(app);
1358
+ shownNow.push(emitPresentation(app));
1051
1359
  conversations.changed(set);
1360
+ return Promise.all(shownNow).then(() => undefined);
1052
1361
  }
1053
1362
  async function refuse(reason, provider = null) {
1054
1363
  discoveryPaused = true;
@@ -1058,53 +1367,31 @@ export function composeRuntime(options) {
1058
1367
  if (reason === 'revoked-authorization') {
1059
1368
  await exclusive(async () => {
1060
1369
  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;
1370
+ let held = new Map();
1067
1371
  try {
1068
1372
  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
- })),
1373
+ held = await storedRooms();
1374
+ await local.commit({
1375
+ rooms: [...held.values()].map(({ record: kept }) => {
1376
+ const record = { ...kept, model: revoked };
1377
+ return {
1378
+ locator: record.locator,
1379
+ key: record.key,
1380
+ record: storedRecord(record),
1381
+ composed: composedOf(composed(record)),
1382
+ };
1383
+ }),
1076
1384
  });
1077
1385
  }
1078
1386
  catch {
1079
1387
  journal.record('model revocation persistence failed');
1080
1388
  }
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 }));
1389
+ const listedNow = listedLocators(held);
1390
+ for (const locator of listedNow) {
1391
+ if (!held.has(locator))
1392
+ unheldModels.set(locator, revoked);
1106
1393
  }
1107
- publishChangeSet(mergeChangeSets(changed));
1394
+ publishChangeSet(mergeChangeSets(listedNow.map((locator) => changeSet(locator, { documents: null, room: true }))));
1108
1395
  prepare();
1109
1396
  });
1110
1397
  }
@@ -1121,49 +1408,66 @@ export function composeRuntime(options) {
1121
1408
  }
1122
1409
  }
1123
1410
  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,
1411
+ if (isFenced(room.locator))
1412
+ return;
1413
+ declared.set(room.locator, {
1414
+ room: {
1415
+ name: room.name,
1416
+ locator: room.locator,
1417
+ handle: room.handle,
1418
+ operations: room.operations,
1419
+ replica: room.replica,
1420
+ typedSelection: room.typedSelection ?? null,
1421
+ providerOwnedPaths: room.providerOwnedPaths,
1422
+ },
1423
+ available: synchronizationAvailable,
1142
1424
  });
1143
1425
  }
1426
+ function listHeld(locator) {
1427
+ if (isFenced(locator))
1428
+ return;
1429
+ declared.set(locator, { room: null, available: false });
1430
+ }
1144
1431
  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);
1432
+ const lockedBefore = await lockedRooms();
1433
+ const local = await openLocal();
1434
+ const stillFenced = new Set(await local.fences());
1435
+ const relisting = found
1436
+ .map(({ locator }) => locator)
1437
+ .filter((locator) => {
1438
+ const fence = fencedAt.get(locator);
1439
+ return stillFenced.has(locator) && (fence === undefined || fence.at < started);
1440
+ });
1441
+ const relisted = relisting.length === 0 ? new Set() : await durably((held) => held.unfence(relisting));
1442
+ for (const { locator } of found) {
1443
+ const fence = fencedAt.get(locator);
1444
+ if (fence === undefined || fence.at >= started)
1445
+ continue;
1446
+ if (relisted.has(locator) || (!stillFenced.has(locator) && !fence.written)) {
1447
+ fencedAt.delete(locator);
1448
+ }
1150
1449
  }
1450
+ const kept = await local.held();
1151
1451
  await loseRooms(found
1152
- .filter((room) => room.replica.admission !== 'admitted' && heldLocally(room.locator))
1452
+ .filter((room) => room.replica.admission !== 'admitted' &&
1453
+ (kept.held.has(room.locator) || transfers.has(room.locator)))
1153
1454
  .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)
1455
+ const listed = found.filter(({ locator }) => !fencedAt.has(locator) && (!stillFenced.has(locator) || relisted.has(locator)));
1456
+ const listedLocatorsNow = new Set(listed.map((room) => room.locator));
1457
+ for (const locator of listedLocatorsNow)
1157
1458
  scopedRefusals.delete(locator);
1158
1459
  if (left === undefined)
1159
1460
  discovered.clear();
1160
1461
  for (const room of listed)
1161
1462
  discovered.set(room.locator, room);
1162
- await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1463
+ const named = new Set(found.map((room) => room.locator));
1464
+ await loseRooms(left ??
1465
+ [...new Set([...declared.keys(), ...kept.held, ...transfers.keys()])].filter((locator) => !named.has(locator)));
1466
+ const held = await storedRooms(listed.map((room) => room.locator));
1163
1467
  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);
1468
+ const record = held.get(room.locator)?.record;
1469
+ if (record !== undefined && record.replica.selection !== room.replica.selection) {
1470
+ listHeld(room.locator);
1167
1471
  continue;
1168
1472
  }
1169
1473
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
@@ -1171,74 +1475,36 @@ export function composeRuntime(options) {
1171
1475
  confirmLocks(listed);
1172
1476
  roomsListed = true;
1173
1477
  emitRooms();
1174
- relocked(lockedBefore);
1478
+ await relocked(lockedBefore);
1175
1479
  return listed;
1176
1480
  }
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;
1481
+ function forget(locator) {
1482
+ confirmed.delete(locator);
1483
+ for (const key of [...landedAhead.keys()]) {
1484
+ if (key.startsWith(`${locator}\u0000`))
1485
+ landedAhead.delete(key);
1486
+ }
1487
+ confirmations.delete(locator);
1488
+ highest.delete(locator);
1489
+ beyondHorizon.delete(locator);
1490
+ declared.delete(locator);
1491
+ attachedFiles.delete(locator);
1492
+ untransferred(locator);
1493
+ startedOver.delete(locator);
1494
+ catching.delete(locator);
1495
+ discovered.delete(locator);
1496
+ unreadable.delete(locator);
1497
+ stale.delete(locator);
1498
+ repairable.delete(locator);
1499
+ unpreparable.delete(locator);
1500
+ scopedRefusals.delete(locator);
1501
+ unheldModels.delete(locator);
1502
+ typesHeld.delete(locator);
1234
1503
  }
1235
1504
  async function eraseFenced(locators) {
1236
1505
  for (const locator of locators) {
1237
1506
  while (await durably((local) => local.erase(locator, ERASE_BATCH)))
1238
1507
  await yielded();
1239
- const held = fences.get(locator);
1240
- if (held !== undefined)
1241
- held.erased = true;
1242
1508
  }
1243
1509
  }
1244
1510
  let sweeping = false;
@@ -1260,147 +1526,60 @@ export function composeRuntime(options) {
1260
1526
  }
1261
1527
  })();
1262
1528
  }
1529
+ function fenceLive(locators) {
1530
+ for (const locator of new Set(locators)) {
1531
+ if (fencedAt.has(locator))
1532
+ continue;
1533
+ fencedAt.set(locator, { at: discoveries, written: false });
1534
+ lastFenced.set(locator, ++fencings);
1535
+ forget(locator);
1536
+ journal.record(`room lost ${locator}`);
1537
+ }
1538
+ }
1263
1539
  async function loseRooms(locators) {
1264
- const fresh = fence(locators);
1540
+ fenceLive(locators);
1541
+ const fresh = [...new Set(locators)].filter((locator) => {
1542
+ const fence = fencedAt.get(locator);
1543
+ if (fence === undefined || fence.written)
1544
+ return false;
1545
+ fence.written = true;
1546
+ return true;
1547
+ });
1265
1548
  if (fresh.length === 0)
1266
1549
  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
- });
1550
+ let held = [];
1366
1551
  try {
1367
- for (const key of held.orphans)
1368
- await opened.release(key);
1552
+ await durably(async (local) => {
1553
+ held = await local.roomDrafts();
1554
+ await local.fence(fresh);
1555
+ });
1369
1556
  }
1370
1557
  catch (error) {
1371
- if (!refusedForSpace(error))
1372
- throw error;
1558
+ for (const locator of fresh) {
1559
+ const fence = fencedAt.get(locator);
1560
+ if (fence !== undefined)
1561
+ fence.written = false;
1562
+ }
1563
+ throw error;
1373
1564
  }
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;
1565
+ finally {
1566
+ publishChangeSet(mergeChangeSets(fresh.map((locator) => changeSet(locator, {
1567
+ documents: null,
1568
+ room: true,
1569
+ queue: true,
1570
+ drafts: held.filter((draft) => draft.locator === locator).map(({ path }) => path),
1571
+ }))));
1572
+ recoverReads();
1573
+ moveTo(status);
1574
+ }
1575
+ await durably(unpair);
1576
+ await eraseFenced(fresh);
1394
1577
  }
1395
1578
  async function open() {
1396
1579
  if (closed)
1397
1580
  throw new Error('runtime closed');
1398
- const opened = openRuntimeStore(isFenced, database);
1581
+ const opened = openRuntimeStore(database);
1399
1582
  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
1583
  const kept = options.shell.generation;
1405
1584
  const storedGeneration = await opened.generation();
1406
1585
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -1413,7 +1592,20 @@ export function composeRuntime(options) {
1413
1592
  storageLoss = loss;
1414
1593
  emitStorageLoss();
1415
1594
  }
1416
- const lastCompleted = await readState(opened);
1595
+ const held = await opened.opened();
1596
+ heldWhenOpened = usableWhenHeld.length > 0 && (await opened.covering()).rooms.size > 0;
1597
+ try {
1598
+ await opened.demote();
1599
+ for (const key of held.orphans)
1600
+ await opened.release(key);
1601
+ }
1602
+ catch (error) {
1603
+ if (!refusedForSpace(error))
1604
+ throw error;
1605
+ }
1606
+ if (held.erasing || held.orphans.length > 0)
1607
+ queueMicrotask(sweep);
1608
+ await conversations.opened(opened);
1417
1609
  if (interrupted.length > 0) {
1418
1610
  queueMicrotask(() => {
1419
1611
  void eraseFenced(interrupted).catch(() => {
@@ -1421,7 +1613,9 @@ export function composeRuntime(options) {
1421
1613
  });
1422
1614
  });
1423
1615
  }
1424
- moveTo({ ...status, lastCompleted, ...attention() });
1616
+ moveTo(status);
1617
+ prepare();
1618
+ emitRooms();
1425
1619
  void exclusive(() => unpair(opened)).catch(() => {
1426
1620
  journal.record('unpair failed');
1427
1621
  });
@@ -1464,9 +1658,9 @@ export function composeRuntime(options) {
1464
1658
  gathering = entry;
1465
1659
  return entry.done;
1466
1660
  }
1467
- function withdrawable(locator) {
1661
+ function withdrawable(states) {
1468
1662
  return (batch) => {
1469
- const state = pendings.get(pendingKey(locator, batch.id));
1663
+ const state = states.get(batch.id);
1470
1664
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
1471
1665
  };
1472
1666
  }
@@ -1488,13 +1682,13 @@ export function composeRuntime(options) {
1488
1682
  const partner = partnerOf(batch, locator);
1489
1683
  if (partner === null)
1490
1684
  return true;
1491
- const other = replicas.get(partner);
1685
+ const other = await replicaOf(partner);
1492
1686
  const half = other?.queue.find((one) => one.id === batch.id);
1493
1687
  if (other === undefined || half === undefined)
1494
1688
  return false;
1495
1689
  return landed(half, await sliceAt(other.record, 'confirmed', [half]));
1496
1690
  }
1497
- function carriedBack(locator, unlanded, written) {
1691
+ async function carriedBack(locator, unlanded, written) {
1498
1692
  const back = new Map();
1499
1693
  const taken = [];
1500
1694
  for (const batch of unlanded) {
@@ -1503,13 +1697,15 @@ export function composeRuntime(options) {
1503
1697
  continue;
1504
1698
  const source = between.source.room;
1505
1699
  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);
1700
+ const local = await openLocal();
1701
+ const arrived = (await local.pending(destination, batch.id)).state?.drafts ?? [];
1702
+ for (const { path: to } of arrived) {
1703
+ const record = await draftOf(destination, to);
1508
1704
  const path = between.moves.find((move) => move.to === to)?.path;
1509
1705
  if (record === undefined || path === undefined)
1510
1706
  continue;
1511
1707
  const [into, key] = source === locator ? [written, path] : [back, documentKey(source, path)];
1512
- const existing = into.get(key) ?? roomDrafts.get(source)?.get(path);
1708
+ const existing = into.get(key) ?? (await draftOf(source, path));
1513
1709
  taken.push({ locator: destination, path: to });
1514
1710
  into.set(key, {
1515
1711
  locator: source,
@@ -1535,19 +1731,14 @@ export function composeRuntime(options) {
1535
1731
  .filter((one) => one.locator === room)
1536
1732
  .map(({ path }) => path),
1537
1733
  })),
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
1734
  };
1545
1735
  }
1546
- function committedRoom(locator, answer) {
1547
- const indexed = answer.indexed[locator] ?? replicas.get(locator)?.indexed ?? false;
1548
- if (!indexed)
1736
+ function preparedAfter(locator, answer) {
1737
+ if (answer.indexed[locator] === false)
1549
1738
  prepare([locator]);
1550
- return { count: answer.counts[locator] ?? replicas.get(locator)?.count ?? 0, indexed };
1739
+ }
1740
+ function confirmedAgain(locator) {
1741
+ confirmations.set(locator, (confirmations.get(locator) ?? 0) + 1);
1551
1742
  }
1552
1743
  function replayed(record, confirmedLayer, queue) {
1553
1744
  return stepped(replaying(confirmedLayer, queue, record.locator, composed(record)));
@@ -1564,13 +1755,14 @@ export function composeRuntime(options) {
1564
1755
  touched,
1565
1756
  };
1566
1757
  }
1567
- async function settle(local, locator, before, record, queue, settling = {}) {
1568
- return together(local, [await settlement(local, locator, before, record, queue, settling)]);
1758
+ async function settle(local, locator, work, record, queue, settling = {}) {
1759
+ return together(local, [await settlement(local, locator, work, record, queue, settling)]);
1569
1760
  }
1570
- function unpairedIn(locator, batch) {
1571
- if (!queueIn(locator).some((held) => held.id === batch.id))
1761
+ async function unpairedIn(locator, batch) {
1762
+ const { state } = await (await openLocal()).pending(locator, batch.id);
1763
+ if (state === undefined)
1572
1764
  return false;
1573
- return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1765
+ return !leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch);
1574
1766
  }
1575
1767
  async function together(local, settlements) {
1576
1768
  const settled = new Set(settlements.map(({ locator }) => locator));
@@ -1580,8 +1772,9 @@ export function composeRuntime(options) {
1580
1772
  for (const { locator, left } of wave) {
1581
1773
  for (const batch of left) {
1582
1774
  const partner = partnerOf(batch, locator);
1583
- if (partner === null || settled.has(partner) || !unpairedIn(partner, batch))
1775
+ if (partner === null || settled.has(partner) || !(await unpairedIn(partner, batch))) {
1584
1776
  continue;
1777
+ }
1585
1778
  partnered.set(partner, [...(partnered.get(partner) ?? []), batch]);
1586
1779
  }
1587
1780
  }
@@ -1599,9 +1792,10 @@ export function composeRuntime(options) {
1599
1792
  });
1600
1793
  return mergeChangeSets(all.map((one) => one.apply(answer)));
1601
1794
  }
1602
- async function settlement(local, locator, before, record, queue, settling = {}) {
1795
+ async function settlement(local, locator, work, record, queue, settling = {}) {
1796
+ const before = work.replica;
1603
1797
  const view = settling.view ?? 'confirmed';
1604
- const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
1798
+ const previousQueue = work.queue;
1605
1799
  const overrides = settling.changes;
1606
1800
  const read = await sliceAt(record, view, queue, [
1607
1801
  ...(before !== undefined && before.record.key === record.key ? before.overlaid : []),
@@ -1618,7 +1812,7 @@ export function composeRuntime(options) {
1618
1812
  Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1619
1813
  });
1620
1814
  const unshown = queue.filter((batch) => !shown.includes(batch));
1621
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1815
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(work.states), localSaves, locator, composition));
1622
1816
  const founded = kept.filter((batch) => !unshown.includes(batch));
1623
1817
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1624
1818
  const unqueued = shown.map((batch) => ({ batch, landed: true }));
@@ -1636,8 +1830,8 @@ export function composeRuntime(options) {
1636
1830
  for (const { batch, returned: reason } of returning) {
1637
1831
  if (reason === null || batch.between?.destination.room === locator)
1638
1832
  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);
1833
+ for (const saved of work.states.get(batch.id)?.drafts ?? []) {
1834
+ const existing = written.get(saved.path) ?? (await draftOf(locator, saved.path));
1641
1835
  written.set(saved.path, {
1642
1836
  locator,
1643
1837
  path: saved.path,
@@ -1647,12 +1841,12 @@ export function composeRuntime(options) {
1647
1841
  });
1648
1842
  }
1649
1843
  }
1650
- const carried = carriedBack(locator, [
1844
+ const carried = await carriedBack(locator, [
1651
1845
  ...[...unqueued].reverse().flatMap(({ batch, landed: held }) => (held ? [] : [batch])),
1652
1846
  ...(settling.leaving ?? []).flatMap(({ batch, returned }) => returned === null ? [] : [batch]),
1653
1847
  ], written);
1654
1848
  const replayedLayer = await replayed(record, confirmedLayer, kept);
1655
- const { overlay, touched } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1849
+ const { overlay } = await overlayDiff(record, before, replayedLayer.visible, replayedLayer.touched);
1656
1850
  const commit = {
1657
1851
  rooms: [
1658
1852
  {
@@ -1672,6 +1866,7 @@ export function composeRuntime(options) {
1672
1866
  {
1673
1867
  locator,
1674
1868
  queue: kept,
1869
+ stored: previousQueue,
1675
1870
  founded,
1676
1871
  roomDrafts: [...written.values()],
1677
1872
  consumedRoomDrafts: carried.taken,
@@ -1686,33 +1881,21 @@ export function composeRuntime(options) {
1686
1881
  commit,
1687
1882
  apply: (answer) => {
1688
1883
  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));
1884
+ if (!staying.has(batch.id))
1885
+ landedAhead.delete(pendingKey(locator, batch.id));
1693
1886
  }
1694
- for (const draft of written.values())
1695
- rememberRoomDraft(draft);
1696
- carried.remember();
1697
1887
  const drafts = [...written.keys(), ...carried.taken];
1698
- queuesWithoutReplica.delete(locator);
1699
1888
  for (const { batch, landed: held } of unqueued) {
1700
1889
  const outcome = localSaves ? 'dropped' : held ? 'landed' : 'withdrawn';
1701
1890
  journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
1702
1891
  }
1703
1892
  const changed = answer.changed[locator];
1704
- const confirmedChanged = settling.commit?.published !== undefined ||
1893
+ if (settling.commit?.published !== undefined ||
1705
1894
  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
- });
1895
+ overrides !== undefined) {
1896
+ confirmedAgain(locator);
1897
+ }
1898
+ preparedAfter(locator, answer);
1716
1899
  return mergeChangeSets([
1717
1900
  changeSet(locator, {
1718
1901
  documents: changed === undefined ? [] : changed,
@@ -1725,22 +1908,11 @@ export function composeRuntime(options) {
1725
1908
  },
1726
1909
  };
1727
1910
  }
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) {
1911
+ function remodel(answers, held) {
1738
1912
  const answer = answers?.collection;
1739
1913
  if (answer === undefined)
1740
1914
  return held;
1741
- const state = attachedModel(answer, roomTypes);
1742
- models.set(locator, state);
1743
- return { collection: state };
1915
+ return { collection: attachedModel(answer, roomTypes) };
1744
1916
  }
1745
1917
  function publish(local, room, reached, landing, live) {
1746
1918
  const landed = exclusive(async () => {
@@ -1748,8 +1920,9 @@ export function composeRuntime(options) {
1748
1920
  caughtUpTo(room.locator);
1749
1921
  return true;
1750
1922
  }
1751
- const prior = replicas.get(room.locator);
1752
- if (!stands(room.locator, landing.from)) {
1923
+ const work = await workOf(room.locator);
1924
+ const prior = work.replica;
1925
+ if (!standsAt(room.locator, prior, landing.from)) {
1753
1926
  if (landing.kind === 'refreshed') {
1754
1927
  await local.unrefresh(reached.key);
1755
1928
  return false;
@@ -1765,11 +1938,9 @@ export function composeRuntime(options) {
1765
1938
  ...(landing.remodelled ? { model: reached.model } : {}),
1766
1939
  }
1767
1940
  : reached;
1768
- adoptModels(room.locator, record);
1769
- const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1770
1941
  let set;
1771
1942
  try {
1772
- set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1943
+ set = await settle(local, room.locator, work, record, work.queue, landing.kind === 'refreshed'
1773
1944
  ? {
1774
1945
  room: true,
1775
1946
  view: 'refreshed',
@@ -1791,14 +1962,16 @@ export function composeRuntime(options) {
1791
1962
  return true;
1792
1963
  }
1793
1964
  if (landing.kind === 'bootstrapped') {
1965
+ readied(room.locator);
1794
1966
  highest.delete(room.locator);
1795
1967
  beyondHorizon.delete(room.locator);
1796
1968
  untransferred(room.locator);
1797
1969
  startedOver.delete(room.locator);
1798
1970
  catching.delete(room.locator);
1971
+ unheldModels.delete(room.locator);
1799
1972
  if (prior !== undefined && prior.record.key !== record.key)
1800
1973
  sweep();
1801
- name(room.locator);
1974
+ void name(room.locator);
1802
1975
  }
1803
1976
  stale.delete(room.locator);
1804
1977
  unpreparable.delete(room.locator);
@@ -1807,28 +1980,27 @@ export function composeRuntime(options) {
1807
1980
  list(room, true);
1808
1981
  publishChangeSet(set);
1809
1982
  await unpair(local);
1810
- moveTo({ ...status, ...attention() });
1811
- if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1983
+ moveTo(status);
1984
+ if (landing.kind === 'bootstrapped' && work.queue.length > 0)
1812
1985
  submitLater(room.locator);
1813
- }
1814
1986
  return true;
1815
1987
  });
1816
1988
  return landing.kind === 'refreshed' ? landed : landed.finally(() => installed(room.locator));
1817
1989
  }
1818
1990
  async function savedRoom(locator) {
1819
1991
  const local = await openLocal();
1820
- const replica = replicas.get(locator);
1821
- const room = rooms.get(locator);
1822
- if (replica === undefined || room === undefined) {
1992
+ const replica = await replicaOf(locator);
1993
+ if (replica === undefined || isFenced(locator)) {
1823
1994
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1824
1995
  }
1825
- return { local, replica, room };
1996
+ return { local, replica };
1826
1997
  }
1827
1998
  async function saveRoomDrafts(locator, named, writtenBy) {
1828
1999
  const target = await savedRoom(locator);
2000
+ const drafts = new Map((await draftsOf(locator, named.map(({ path }) => path))).map((record) => [record.path, record]));
1829
2001
  const saved = [];
1830
2002
  for (const { path, revision } of named) {
1831
- const record = roomDrafts.get(locator)?.get(path);
2003
+ const record = drafts.get(path);
1832
2004
  if (record === undefined || record.revision !== revision) {
1833
2005
  return { outcome: 'refused', code: 'stale', retryable: false, file: path };
1834
2006
  }
@@ -1855,14 +2027,16 @@ export function composeRuntime(options) {
1855
2027
  }
1856
2028
  return committed(locator, target, saved, new Set(), writtenBy, files);
1857
2029
  }
1858
- function carriedDrafts(locator, saved, consumed) {
1859
- const holding = roomDrafts.get(locator);
2030
+ async function carriedDrafts(locator, saved, consumed) {
2031
+ if (!saved.some(({ parts }) => parts.move !== undefined))
2032
+ return new Map();
2033
+ const holding = new Map((await draftsOf(locator)).map((record) => [record.path, record]));
1860
2034
  return new Map(saved.flatMap(({ path, parts }) => {
1861
- const record = holding?.get(path);
2035
+ const record = holding.get(path);
1862
2036
  if (parts.move === undefined || record === undefined || consumed.has(path))
1863
2037
  return [];
1864
2038
  const to = moveTarget(parts.move);
1865
- if (to === path || holding?.has(to) === true)
2039
+ if (to === path || holding.has(to))
1866
2040
  return [];
1867
2041
  return [[path, { ...record, path: to, revision: roomDraftRevision() }]];
1868
2042
  }));
@@ -1977,15 +2151,15 @@ export function composeRuntime(options) {
1977
2151
  ...new Set((await local.referrers(key, view, targets)).map(({ path }) => path)),
1978
2152
  ];
1979
2153
  }
1980
- async function sideOf(locator, replica, room, saving) {
2154
+ async function sideOf(locator, replica, saving) {
1981
2155
  const record = replica.record;
1982
2156
  return {
1983
2157
  room: locator,
1984
- handle: room.handle,
2158
+ handle: record.handle,
1985
2159
  baseRoomVersion: record.roomVersion,
1986
2160
  composition: compositionFor(record),
1987
- operations: room.operations,
1988
- providerOwnedPaths: room.providerOwnedPaths,
2161
+ operations: record.operations,
2162
+ providerOwnedPaths: record.providerOwnedPaths,
1989
2163
  visible: saving.visible,
1990
2164
  folders: saving.folders,
1991
2165
  queued: queuedDocuments(replica.queue),
@@ -1998,12 +2172,10 @@ export function composeRuntime(options) {
1998
2172
  return into?.parts.move?.room ?? null;
1999
2173
  }
2000
2174
  async function saveCommit(local, files, rooms) {
2001
- const touched = new Map();
2002
2175
  const commits = [];
2003
2176
  for (const { locator, saving, before, queue } of rooms) {
2004
2177
  const replayedLayer = await replayed(saving.record, saving.confirmed, queue);
2005
2178
  const diff = await overlayDiff(saving.record, before, replayedLayer.visible, replayedLayer.touched);
2006
- touched.set(locator, diff.touched);
2007
2179
  commits.push({
2008
2180
  locator,
2009
2181
  key: saving.record.key,
@@ -2013,27 +2185,18 @@ export function composeRuntime(options) {
2013
2185
  });
2014
2186
  }
2015
2187
  try {
2016
- const answer = await local.commit({
2188
+ return await local.commit({
2017
2189
  rooms: commits,
2018
2190
  writes: rooms.map(({ write }) => write),
2019
2191
  files,
2020
2192
  whole: true,
2021
2193
  });
2022
- return { answer, touched };
2023
2194
  }
2024
2195
  catch {
2025
2196
  return null;
2026
2197
  }
2027
2198
  }
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) {
2199
+ async function committedMove(locator, destination, { local, replica }, saved, consumed, writtenBy, files) {
2037
2200
  const mixed = saved.find(({ parts }) => Object.keys(parts).length !== 1 || parts.move?.room !== destination);
2038
2201
  if (mixed !== undefined) {
2039
2202
  return { outcome: 'refused', code: 'mixed-room-move', retryable: false, file: mixed.path };
@@ -2043,27 +2206,28 @@ export function composeRuntime(options) {
2043
2206
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2044
2207
  if (changed.length > 0)
2045
2208
  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) {
2209
+ const target = await replicaOf(destination);
2210
+ if (target === undefined || isFenced(destination)) {
2049
2211
  throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2050
2212
  }
2051
2213
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2052
2214
  const built = await buildMove({
2053
- source: await sideOf(locator, replica, room, sourceSaving),
2054
- destination: await sideOf(destination, target, listedRoom, arriving),
2215
+ source: await sideOf(locator, replica, sourceSaving),
2216
+ destination: await sideOf(destination, target, arriving),
2055
2217
  moves,
2056
- providerLimits: limits,
2218
+ providerLimits: (await local.facts()).providerLimits,
2057
2219
  local: localSaves,
2058
2220
  ...(writtenBy === undefined ? {} : { writtenBy }),
2059
2221
  });
2060
2222
  if (built.kind === 'refused')
2061
2223
  return { outcome: 'refused', ...built.refusal };
2062
2224
  const id = built.leaving.id;
2225
+ const leavingDrafts = new Map((await draftsOf(locator, saved.map(({ path }) => path))).map((record) => [record.path, record]));
2226
+ const arrivingDrafts = new Set((await draftsOf(destination, moves.map(({ to }) => to))).map(({ path }) => path));
2063
2227
  const carrying = saved.flatMap(({ path, parts }) => {
2064
- const record = roomDrafts.get(locator)?.get(path);
2228
+ const record = leavingDrafts.get(path);
2065
2229
  const to = moveTarget(parts.move);
2066
- if (record === undefined || consumed.has(path) || roomDrafts.get(destination)?.has(to)) {
2230
+ if (record === undefined || consumed.has(path) || arrivingDrafts.has(to)) {
2067
2231
  return [];
2068
2232
  }
2069
2233
  const moved = {
@@ -2079,7 +2243,7 @@ export function composeRuntime(options) {
2079
2243
  const left = [...consumed, ...carrying.map(({ from }) => from)];
2080
2244
  const leavingQueue = [...replica.queue, built.leaving];
2081
2245
  const arrivingQueue = [...target.queue, built.arriving];
2082
- const committedBoth = await saveCommit(local, files, [
2246
+ const answer = await saveCommit(local, files, [
2083
2247
  {
2084
2248
  locator,
2085
2249
  saving: sourceSaving,
@@ -2088,6 +2252,7 @@ export function composeRuntime(options) {
2088
2252
  write: {
2089
2253
  locator,
2090
2254
  queue: leavingQueue,
2255
+ stored: replica.queue,
2091
2256
  saved: new Map([[id, saved]]),
2092
2257
  consumedRoomDrafts: left,
2093
2258
  },
@@ -2100,37 +2265,23 @@ export function composeRuntime(options) {
2100
2265
  write: {
2101
2266
  locator: destination,
2102
2267
  queue: arrivingQueue,
2268
+ stored: target.queue,
2103
2269
  saved: new Map([[id, marked]]),
2104
2270
  roomDrafts: carried,
2105
2271
  },
2106
2272
  },
2107
2273
  ]);
2108
- if (committedBoth === null)
2274
+ if (answer === null)
2109
2275
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2110
- const { answer, touched } = committedBoth;
2111
2276
  if (answer.fenced !== null) {
2112
2277
  throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2113
2278
  }
2114
- if (fences.has(locator) || fences.has(destination)) {
2279
+ if (isFenced(locator) || isFenced(destination)) {
2115
2280
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2116
2281
  }
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([
2282
+ preparedAfter(locator, answer);
2283
+ preparedAfter(destination, answer);
2284
+ const shownNow = publishChangeSet(mergeChangeSets([
2134
2285
  changeSet(locator, {
2135
2286
  documents: answer.changed[locator] ?? [],
2136
2287
  queue: true,
@@ -2142,14 +2293,15 @@ export function composeRuntime(options) {
2142
2293
  drafts: carried.map(({ path }) => path),
2143
2294
  }),
2144
2295
  ]));
2145
- queuedSave(locator);
2296
+ await Promise.all([shownNow, queuedSave(locator)]);
2146
2297
  return { outcome: 'saved', identifier: id };
2147
2298
  }
2148
2299
  function queuedSave(locator) {
2149
2300
  if (!localSaves)
2150
2301
  recorded = false;
2151
- moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
2302
+ const shownNow = moveTo(localSaves ? status : { ...status, state: 'pending', resting: false });
2152
2303
  submitLater(locator);
2304
+ return shownNow;
2153
2305
  }
2154
2306
  async function committed(locator, target, saved, consumed, writtenBy, files = []) {
2155
2307
  if (full)
@@ -2158,7 +2310,8 @@ export function composeRuntime(options) {
2158
2310
  if (into !== null) {
2159
2311
  return committedMove(locator, into, target, saved, consumed, writtenBy, files);
2160
2312
  }
2161
- const { local, replica, room } = target;
2313
+ const { local, replica } = target;
2314
+ const { providerLimits } = await local.facts();
2162
2315
  const parts = saved.map(({ path, parts: held }) => changesOf(path, held));
2163
2316
  const changes = parts.flatMap(({ changes: made }) => made);
2164
2317
  const saving = await savingIn(replica, changes, [
@@ -2168,7 +2321,7 @@ export function composeRuntime(options) {
2168
2321
  const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2169
2322
  if (changed.length > 0)
2170
2323
  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));
2324
+ const requeue = await stepped(requeuing(replica.queue, changes, saving.visible, withdrawable(replica.states), saving.visibleOver));
2172
2325
  if (requeue.kind === 'refused')
2173
2326
  return { outcome: 'refused', ...requeue.refusal };
2174
2327
  const foreign = [...requeue.withdrawn.keys()].some((origin) => {
@@ -2188,10 +2341,10 @@ export function composeRuntime(options) {
2188
2341
  local: localSaves,
2189
2342
  baseRoomVersion: replica.record.roomVersion,
2190
2343
  composition: compositionFor(replica.record),
2191
- operations: room.operations,
2192
- providerOwnedPaths: room.providerOwnedPaths,
2193
- providerLimits: limits,
2194
- handle: room.handle,
2344
+ operations: replica.record.operations,
2345
+ providerOwnedPaths: replica.record.providerOwnedPaths,
2346
+ providerLimits,
2347
+ handle: replica.record.handle,
2195
2348
  visible: saving.visible,
2196
2349
  folders: saving.folders,
2197
2350
  changes,
@@ -2243,7 +2396,7 @@ export function composeRuntime(options) {
2243
2396
  for (const [origin, replaced] of withdrawn) {
2244
2397
  if (replaced === null)
2245
2398
  continue;
2246
- const carried = withdrawnDrafts(pendings.get(pendingKey(locator, origin))?.drafts ?? [], changes);
2399
+ const carried = withdrawnDrafts(replica.states.get(origin)?.drafts ?? [], changes);
2247
2400
  drafted.set(replaced.id, replaced === join?.batch
2248
2401
  ? [
2249
2402
  ...carried.map((held) => joinedDraft(held, saved)),
@@ -2257,14 +2410,14 @@ export function composeRuntime(options) {
2257
2410
  for (const replaced of withdrawn.values()) {
2258
2411
  if (replaced === null || !batches.includes(replaced))
2259
2412
  continue;
2260
- const limited = await stepped(limiting(batches, replaced, limits, saving.visibleOver));
2413
+ const limited = await stepped(limiting(batches, replaced, providerLimits, saving.visibleOver));
2261
2414
  if (limited !== null)
2262
2415
  return { outcome: 'refused', ...limited };
2263
2416
  }
2264
- const carried = carriedDrafts(locator, saved, consumed);
2417
+ const carried = await carriedDrafts(locator, saved, consumed);
2265
2418
  const left = [...consumed, ...carried.keys()];
2266
2419
  const arrived = [...carried.values()];
2267
- const done = await saveCommit(local, files, [
2420
+ const answer = await saveCommit(local, files, [
2268
2421
  {
2269
2422
  locator,
2270
2423
  saving,
@@ -2273,40 +2426,27 @@ export function composeRuntime(options) {
2273
2426
  write: {
2274
2427
  locator,
2275
2428
  queue: batches,
2429
+ stored: replica.queue,
2276
2430
  saved: drafted,
2277
2431
  consumedRoomDrafts: left,
2278
2432
  roomDrafts: arrived,
2279
2433
  },
2280
2434
  },
2281
2435
  ]);
2282
- if (done === null)
2436
+ if (answer === null)
2283
2437
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2284
- const { answer, touched } = done;
2285
2438
  if (answer.fenced !== null) {
2286
2439
  throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2287
2440
  }
2288
- if (fences.has(locator))
2441
+ if (isFenced(locator))
2289
2442
  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, {
2443
+ preparedAfter(locator, answer);
2444
+ const shownNow = publishChangeSet(changeSet(locator, {
2305
2445
  documents: answer.changed[locator] ?? [],
2306
2446
  queue: true,
2307
2447
  drafts: [...left, ...arrived.map(({ path }) => path)],
2308
2448
  }));
2309
- queuedSave(locator);
2449
+ await Promise.all([shownNow, queuedSave(locator)]);
2310
2450
  const identifier = join?.batch.id ??
2311
2451
  built?.batch.id ??
2312
2452
  [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
@@ -2315,29 +2455,26 @@ export function composeRuntime(options) {
2315
2455
  async function mark(local, locator, batch, submission, lost) {
2316
2456
  const partner = partnerOf(batch, locator);
2317
2457
  for (const held of partner === null ? [locator] : [locator, partner]) {
2318
- const state = pendings.get(pendingKey(held, batch.id));
2458
+ const { state, room } = await local.pending(held, batch.id);
2319
2459
  if (!continuing() ||
2320
2460
  state === undefined ||
2321
2461
  (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
2322
2462
  continue;
2323
2463
  }
2324
2464
  await local.submit(held, batch.id, submission, lost);
2325
- if (fences.has(held))
2465
+ if (isFenced(held))
2326
2466
  continue;
2327
- pendings.set(pendingKey(held, batch.id), { ...state, submission, lost });
2328
- if (replicas.has(held))
2467
+ if (room)
2329
2468
  publishChangeSet(changeSet(held, { queue: true }));
2330
2469
  }
2331
- moveTo({ ...status, ...attention() });
2470
+ moveTo(status);
2332
2471
  }
2333
2472
  async function keepUpload(local, locator, identifier, upload) {
2334
2473
  if (!continuing())
2335
2474
  throw new Error('connection refused');
2336
2475
  await local.keepUpload(locator, identifier, upload);
2337
- if (fences.has(locator))
2476
+ if (isFenced(locator))
2338
2477
  throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2339
- const key = pendingKey(locator, identifier);
2340
- pendings.set(key, { ...pendings.get(key), upload });
2341
2478
  if (!continuing())
2342
2479
  throw new Error('connection refused');
2343
2480
  }
@@ -2373,7 +2510,7 @@ export function composeRuntime(options) {
2373
2510
  if (fencedSince(locator, since))
2374
2511
  return failure({ kind: 'room-not-readable', locator });
2375
2512
  if (result.kind === 'scoped-refusal') {
2376
- const room = replicas.get(result.locator)?.record;
2513
+ const room = await recordAt(result.locator);
2377
2514
  if (room !== undefined)
2378
2515
  await scoped(result, room, await openLocal());
2379
2516
  }
@@ -2386,27 +2523,28 @@ export function composeRuntime(options) {
2386
2523
  return UNCERTAIN;
2387
2524
  }
2388
2525
  }
2389
- function located(room, path, destination) {
2526
+ async function located(room, path, destination) {
2390
2527
  const crossing = roomTarget(destination);
2528
+ const listing = await listingNow();
2391
2529
  if (crossing === null) {
2392
2530
  const named = target(path, destination);
2393
- return named === null || !rooms.has(room) ? null : { room, path: named };
2531
+ return named === null || !listing.has(room) ? null : { room, path: named };
2394
2532
  }
2395
- const held = [...rooms.values()].find((listed) => listed.handle === crossing.handle);
2533
+ const held = [...listing.values()].find((listed) => listed.handle === crossing.handle);
2396
2534
  return held === undefined ? null : { room: held.locator, path: crossing.path };
2397
2535
  }
2398
2536
  async function resolveReference(room, path, destination) {
2399
2537
  if (roomTarget(destination) === null && target(path, destination) === null) {
2400
2538
  return ABSENT_TARGET;
2401
2539
  }
2402
- const at = located(room, path, destination);
2540
+ const at = await located(room, path, destination);
2403
2541
  if (at === null)
2404
2542
  return UNREADABLE;
2405
- if (locked(at.room))
2543
+ if (await locked(at.room))
2406
2544
  return LOCKED_UNREADABLE;
2407
- const held = replicas.get(at.room);
2545
+ const held = await recordAt(at.room);
2408
2546
  if (held !== undefined &&
2409
- (await (await openLocal()).entries(held.record.key, 'visible', [at.path])).length > 0) {
2547
+ (await (await openLocal()).entries(held.key, 'visible', [at.path])).length > 0) {
2410
2548
  return { kind: 'document', ...at };
2411
2549
  }
2412
2550
  const history = await readHistory(at.room, at.path);
@@ -2421,13 +2559,13 @@ export function composeRuntime(options) {
2421
2559
  ? { kind: 'deleted', ...at, revision: last }
2422
2560
  : { kind: 'document', ...at };
2423
2561
  }
2424
- function queuedFacts(locator, path, localId) {
2425
- for (const batch of replicas.get(locator)?.queue ?? []) {
2562
+ async function queuedFacts(locator, path, localId) {
2563
+ for (const batch of (await replicaOf(locator))?.queue ?? []) {
2426
2564
  const change = batch.changes[queuedFile(batch, path, localId)];
2427
2565
  if (change?.kind === 'binary')
2428
2566
  return change;
2429
2567
  }
2430
- const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2568
+ const drafted = (await draftOf(locator, path))?.parts.file;
2431
2569
  return drafted?.localId === localId ? drafted : null;
2432
2570
  }
2433
2571
  function batchFile(batch) {
@@ -2459,7 +2597,7 @@ export function composeRuntime(options) {
2459
2597
  if (fencedSince(request.locator, since))
2460
2598
  return failure({ kind: 'room-not-readable', locator: request.locator });
2461
2599
  if (settled.kind === 'scoped-refusal') {
2462
- const room = replicas.get(settled.locator)?.record;
2600
+ const room = await recordAt(settled.locator);
2463
2601
  if (room !== undefined)
2464
2602
  await scoped(settled, room, await openLocal());
2465
2603
  }
@@ -2486,7 +2624,7 @@ export function composeRuntime(options) {
2486
2624
  async function binaryPath(locator, path, revision) {
2487
2625
  if (revision !== undefined)
2488
2626
  return { kind: 'path', locator, path, revision };
2489
- const replica = replicas.get(locator);
2627
+ const replica = await replicaOf(locator);
2490
2628
  const local = await openLocal();
2491
2629
  const [held] = replica === undefined ? [] : await local.entries(replica.record.key, 'visible', [path]);
2492
2630
  const attached = held === undefined ? attachedFiles.get(locator)?.get(path) : undefined;
@@ -2526,13 +2664,13 @@ export function composeRuntime(options) {
2526
2664
  }
2527
2665
  async function askedBinary(locator, path, revision) {
2528
2666
  await ready;
2529
- const drafted = roomDrafts.get(locator)?.get(path)?.parts.file;
2667
+ const drafted = (await draftOf(locator, path))?.parts.file;
2530
2668
  return drafted !== undefined && drafted.localId === revision
2531
2669
  ? { kind: 'pending', localId: revision }
2532
2670
  : binaryPath(locator, path, revision);
2533
2671
  }
2534
- function pendingBinary(locator, path, localId) {
2535
- const facts = queuedFacts(locator, path, localId);
2672
+ async function pendingBinary(locator, path, localId) {
2673
+ const facts = await queuedFacts(locator, path, localId);
2536
2674
  return facts === null
2537
2675
  ? absentBinary(path)
2538
2676
  : {
@@ -2551,10 +2689,10 @@ export function composeRuntime(options) {
2551
2689
  return readBinary(asked, { locator, path: asked.path, revision: asked.revision });
2552
2690
  }
2553
2691
  async function heldMediaType(locator, path) {
2554
- const replica = replicas.get(locator);
2692
+ const replica = await recordAt(locator);
2555
2693
  const [held] = replica === undefined
2556
2694
  ? []
2557
- : await (await openLocal()).entries(replica.record.key, 'visible', [path]);
2695
+ : await (await openLocal()).entries(replica.key, 'visible', [path]);
2558
2696
  return held === undefined
2559
2697
  ? attachedFiles.get(locator)?.get(path)?.mediaType
2560
2698
  : held.binary?.mediaType;
@@ -2589,7 +2727,7 @@ export function composeRuntime(options) {
2589
2727
  async function attachBinary(locator, file) {
2590
2728
  await ready;
2591
2729
  await firstDiscovery;
2592
- if (closed || !roomsNamed || connectionOf() === 'unreachable' || refusal !== null)
2730
+ if (closed || !roomsNamed || (await connectionNow()) === 'unreachable' || refusal !== null)
2593
2731
  return OFFLINE;
2594
2732
  const request = await attachedFile(locator, file);
2595
2733
  return transferBinary({ locator, path: file.name }, async (held) => {
@@ -2609,8 +2747,8 @@ export function composeRuntime(options) {
2609
2747
  const freed = await database.ask({ kind: 'opened.clean' });
2610
2748
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2611
2749
  }
2612
- function declaredAction(name) {
2613
- for (const capability of capabilities) {
2750
+ async function declaredAction(name) {
2751
+ for (const capability of (await (await openLocal()).facts()).capabilities) {
2614
2752
  for (const action of capability.actions ?? []) {
2615
2753
  if (action.name === name)
2616
2754
  return action;
@@ -2618,8 +2756,8 @@ export function composeRuntime(options) {
2618
2756
  }
2619
2757
  return undefined;
2620
2758
  }
2621
- function calledRooms(name, input) {
2622
- const member = declaredAction(name)?.room;
2759
+ async function calledRooms(name, input) {
2760
+ const member = (await declaredAction(name))?.room;
2623
2761
  if (member === undefined || input === null || typeof input !== 'object')
2624
2762
  return [];
2625
2763
  const named = input[member];
@@ -2629,7 +2767,7 @@ export function composeRuntime(options) {
2629
2767
  await ready;
2630
2768
  if (closed || refusal !== null)
2631
2769
  return UNCERTAIN;
2632
- if (declaredAction(name) === undefined)
2770
+ if ((await declaredAction(name)) === undefined)
2633
2771
  return undeclared(name);
2634
2772
  try {
2635
2773
  await ensureAuthorization();
@@ -2644,7 +2782,7 @@ export function composeRuntime(options) {
2644
2782
  try {
2645
2783
  const result = await options.adapter.action(authorization, { name, input });
2646
2784
  if (result.kind === 'scoped-refusal') {
2647
- const room = replicas.get(result.locator)?.record;
2785
+ const room = await recordAt(result.locator);
2648
2786
  if (room !== undefined)
2649
2787
  await scoped(result, room, await openLocal());
2650
2788
  }
@@ -2731,31 +2869,35 @@ export function composeRuntime(options) {
2731
2869
  function continuing() {
2732
2870
  return !closed && !discoveryPaused;
2733
2871
  }
2734
- function reachedBy(locator, roomVersion) {
2735
- const held = replicas.get(locator);
2736
- return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2872
+ async function reachedBy(locator, roomVersion) {
2873
+ const held = await recordAt(locator);
2874
+ return held !== undefined && options.adapter.reached(locator, held.roomVersion, roomVersion);
2737
2875
  }
2738
- function shownAt(locator) {
2739
- const held = replicas.get(locator)?.record.roomVersion;
2876
+ function shownAt(locator, held) {
2740
2877
  const raised = highest.get(locator);
2741
2878
  if (held === undefined || raised === undefined)
2742
2879
  return held ?? raised;
2743
2880
  return options.adapter.reached(locator, held, raised) ? held : raised;
2744
2881
  }
2745
- function showing(locator, roomVersion) {
2746
- const at = shownAt(locator);
2882
+ function showing(locator, roomVersion, held) {
2883
+ const at = shownAt(locator, held);
2747
2884
  if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2748
2885
  highest.set(locator, roomVersion);
2749
2886
  }
2750
2887
  }
2751
- function older(locator, roomVersion) {
2888
+ function olderThan(locator, roomVersion, held) {
2752
2889
  if (beyondHorizon.has(locator))
2753
2890
  return false;
2754
- const at = shownAt(locator);
2891
+ const at = shownAt(locator, held);
2755
2892
  return (at !== undefined &&
2756
2893
  options.adapter.reached(locator, at, roomVersion) &&
2757
2894
  !options.adapter.reached(locator, roomVersion, at));
2758
2895
  }
2896
+ async function older(locator, roomVersion) {
2897
+ if (beyondHorizon.has(locator))
2898
+ return false;
2899
+ return olderThan(locator, roomVersion, (await recordAt(locator))?.roomVersion);
2900
+ }
2759
2901
  function replaced(locators) {
2760
2902
  for (const locator of locators) {
2761
2903
  highest.delete(locator);
@@ -2763,17 +2905,22 @@ export function composeRuntime(options) {
2763
2905
  }
2764
2906
  }
2765
2907
  async function writtenAfter(locator, roomVersion, path) {
2766
- const held = replicas.get(locator);
2908
+ const held = await recordAt(locator);
2767
2909
  if (held === undefined)
2768
2910
  return false;
2769
- const [entry] = await (await openLocal()).entries(held.record.key, 'confirmed', [path]);
2911
+ const [entry] = await (await openLocal()).entries(held.key, 'confirmed', [path]);
2770
2912
  const written = entry?.written ?? undefined;
2771
2913
  return (written !== undefined &&
2772
2914
  options.adapter.reached(locator, written, roomVersion) &&
2773
2915
  !options.adapter.reached(locator, roomVersion, written));
2774
2916
  }
2775
- async function writtenLater(local, locator, key, paths, roomVersion) {
2776
- if (paths.length === 0 || !older(locator, roomVersion))
2917
+ async function writtenLater(local, locator, key, paths, roomVersion, heldAt) {
2918
+ if (paths.length === 0)
2919
+ return new Set();
2920
+ const behindShown = heldAt === undefined
2921
+ ? await older(locator, roomVersion)
2922
+ : olderThan(locator, roomVersion, heldAt);
2923
+ if (!behindShown)
2777
2924
  return new Set();
2778
2925
  const entries = await local.entries(key, 'confirmed', paths);
2779
2926
  return new Set(entries.flatMap(({ path, written }) => written !== null &&
@@ -2782,7 +2929,7 @@ export function composeRuntime(options) {
2782
2929
  ? [path]
2783
2930
  : []));
2784
2931
  }
2785
- function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2932
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false, heldAt) {
2786
2933
  const write = replaying ? finishingWrite : exclusive;
2787
2934
  return {
2788
2935
  turn() {
@@ -2790,7 +2937,7 @@ export function composeRuntime(options) {
2790
2937
  installing(locator);
2791
2938
  },
2792
2939
  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),
2940
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion, heldAt),
2794
2941
  async stage(page) {
2795
2942
  for (let written = 0;;) {
2796
2943
  const end = batchEnd(page.entries, written);
@@ -2928,7 +3075,7 @@ export function composeRuntime(options) {
2928
3075
  unindexed.delete(locator);
2929
3076
  }
2930
3077
  function setBack(locator) {
2931
- const now = bytesOf(locator, true);
3078
+ const now = bytesOf(locator, true, new Map());
2932
3079
  const earlier = startedOver.get(locator);
2933
3080
  startedOver.set(locator, {
2934
3081
  done: Math.max(now.done, earlier?.done ?? 0),
@@ -2964,11 +3111,13 @@ export function composeRuntime(options) {
2964
3111
  if (installs.delete(locator))
2965
3112
  queueReadiness();
2966
3113
  }
2967
- async function pullRefresh(authorized, held, liveness, hold, refresh) {
3114
+ async function pullRefresh(authorized, record, liveness, hold, refresh) {
2968
3115
  if (!liveness())
2969
3116
  return unconfirmed;
2970
- const { record } = held;
2971
- const from = standingOf(record.locator);
3117
+ const from = {
3118
+ key: record.key,
3119
+ confirmed: confirmations.get(record.locator) ?? 0,
3120
+ };
2972
3121
  const local = await openLocal();
2973
3122
  try {
2974
3123
  await exclusive(() => local.unrefresh(record.key));
@@ -2980,7 +3129,7 @@ export function composeRuntime(options) {
2980
3129
  finally {
2981
3130
  hold.take();
2982
3131
  }
2983
- }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3132
+ }, authorized, record, (models, kept) => (liveness() ? remodel(models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness, false, record.roomVersion));
2984
3133
  if (result.kind !== 'advanced') {
2985
3134
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
2986
3135
  return result;
@@ -3000,30 +3149,14 @@ export function composeRuntime(options) {
3000
3149
  throw error;
3001
3150
  }
3002
3151
  }
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
3152
  async function abandoned(local, locator) {
3020
3153
  await local.abandon(locator);
3021
3154
  setBack(locator);
3022
3155
  untransferred(locator);
3023
- liveQueries.recover();
3156
+ recoverReads();
3024
3157
  sweep();
3025
3158
  }
3026
- function staging(local, liveness) {
3159
+ function staging(local, liveness, unheld) {
3027
3160
  const derived = async (locator, key, composition) => {
3028
3161
  underived.delete(locator);
3029
3162
  for (;;) {
@@ -3093,9 +3226,9 @@ export function composeRuntime(options) {
3093
3226
  const held = { ...record, key };
3094
3227
  transfers.set(locator, held);
3095
3228
  if (arrived)
3096
- liveQueries.recover();
3229
+ recoverReads();
3097
3230
  installing(locator);
3098
- liveQueries.recover();
3231
+ recoverReads();
3099
3232
  const text = page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0);
3100
3233
  owedIndexes(locator, record.done.bytes - prior, text);
3101
3234
  const owed = text + (underived.get(locator) ?? 0);
@@ -3104,6 +3237,37 @@ export function composeRuntime(options) {
3104
3237
  return held;
3105
3238
  return (await derived(locator, key, composition)) ? held : null;
3106
3239
  },
3240
+ async resume(record, parts, composition) {
3241
+ const { locator } = record;
3242
+ installing(locator);
3243
+ if (!liveness())
3244
+ return null;
3245
+ const prior = unheld === undefined ? await recordAt(locator) : undefined;
3246
+ const published = prior !== undefined && prior.replica.selection === record.selection ? prior.key : null;
3247
+ const kept = unheld?.staged ?? (await local.held()).staged.has(locator);
3248
+ if (published === null && !kept)
3249
+ return { record: null, staged: new Set() };
3250
+ const resumed = await installWrite(async () => {
3251
+ if (!liveness())
3252
+ return null;
3253
+ return local.resume(record, composition, parts.map((part) => part.id), published);
3254
+ });
3255
+ if (resumed === null || !liveness())
3256
+ return null;
3257
+ const parted = new Set(resumed.staged);
3258
+ if (resumed.key === null)
3259
+ return { record: null, staged: parted };
3260
+ const bytes = parts.reduce((sum, part) => sum + (parted.has(part.id) ? part.bytes : 0), 0);
3261
+ const held = {
3262
+ ...record,
3263
+ transfer: resumed.transfer,
3264
+ key: resumed.key,
3265
+ done: { bytes },
3266
+ };
3267
+ transfers.set(locator, held);
3268
+ recoverReads();
3269
+ return { record: held, staged: parted };
3270
+ },
3107
3271
  derive: derived,
3108
3272
  async refreshing(locator, key, composition) {
3109
3273
  await exclusive(() => local.unrefresh(key));
@@ -3118,33 +3282,26 @@ export function composeRuntime(options) {
3118
3282
  return;
3119
3283
  setBack(locator);
3120
3284
  untransferred(locator);
3121
- liveQueries.recover();
3285
+ recoverReads();
3122
3286
  },
3123
3287
  release: (key) => exclusive(async () => {
3124
3288
  await local.release(key);
3125
3289
  sweep();
3126
3290
  }),
3127
- content: (key) => heldContentOf(local, key),
3128
3291
  };
3129
3292
  }
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) {
3293
+ async function pullBootstrap(authorized, room, local, liveness, prior, from = reads, unheld) {
3137
3294
  if (!liveness())
3138
3295
  return unconfirmed;
3139
- if (replicas.has(room.locator)) {
3296
+ if (prior !== undefined) {
3140
3297
  catching.add(room.locator);
3141
- liveQueries.recover();
3298
+ recoverReads();
3142
3299
  }
3143
3300
  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));
3301
+ 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
3302
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3146
3303
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3147
- if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3304
+ if (result.kind === 'staged' && (await older(room.locator, result.record.roomVersion))) {
3148
3305
  caughtUpTo(room.locator);
3149
3306
  installed(room.locator);
3150
3307
  await abandoned(local, room.locator);
@@ -3168,28 +3325,31 @@ export function composeRuntime(options) {
3168
3325
  }
3169
3326
  function caughtUpTo(locator) {
3170
3327
  if (catching.delete(locator))
3171
- liveQueries.recover();
3328
+ recoverReads();
3172
3329
  }
3173
- function behind(room) {
3174
- const held = replicas.get(room.locator)?.record.roomVersion;
3330
+ async function behind(room) {
3331
+ const held = (await recordAt(room.locator))?.roomVersion;
3175
3332
  return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3176
3333
  }
3177
- function standingOf(locator) {
3178
- const held = replicas.get(locator);
3179
- return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3334
+ function standingFrom(locator, held) {
3335
+ return { key: held?.record.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
3336
+ }
3337
+ function standingAt(locator, held) {
3338
+ return { key: held?.key ?? null, confirmed: confirmations.get(locator) ?? 0 };
3180
3339
  }
3181
- function stands(locator, from) {
3182
- const now = standingOf(locator);
3340
+ function standsAt(locator, held, from) {
3341
+ const now = standingFrom(locator, held);
3183
3342
  return now.key === from.key && now.confirmed === from.confirmed;
3184
3343
  }
3185
- async function advance(room, local, authorized, running) {
3344
+ async function advance(room, local, authorized, running, unheld) {
3186
3345
  const live = () => running() && !full;
3187
3346
  if (!live())
3188
3347
  return unconfirmed;
3189
- const held = replicas.get(room.locator);
3190
- if (held !== undefined && options.adapter.roomStream !== undefined) {
3348
+ const held = unheld === undefined ? await recordAt(room.locator) : undefined;
3349
+ if (options.adapter.roomStream !== undefined &&
3350
+ (held !== undefined || !beyondHorizon.has(room.locator))) {
3191
3351
  stale.delete(room.locator);
3192
- name(room.locator);
3352
+ await name(room.locator);
3193
3353
  return null;
3194
3354
  }
3195
3355
  const refresh = reads.refresh;
@@ -3203,15 +3363,16 @@ export function composeRuntime(options) {
3203
3363
  return unconfirmed;
3204
3364
  replaced([room.locator]);
3205
3365
  }
3206
- const from = standingOf(room.locator);
3207
- const bootstrapped = await pullBootstrap(authorized, room, local, live);
3366
+ const prior = held === undefined ? undefined : await recordAt(room.locator);
3367
+ const from = standingAt(room.locator, prior);
3368
+ const bootstrapped = await pullBootstrap(authorized, room, local, live, prior, reads, unheld);
3208
3369
  if (bootstrapped.kind !== 'staged')
3209
3370
  return live() ? bootstrapped : unconfirmed;
3210
3371
  const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3211
3372
  if (await publish(local, room, bootstrapped.record, landing, live)) {
3212
3373
  return live() ? null : unconfirmed;
3213
3374
  }
3214
- return reachedBy(room.locator, bootstrapped.record.roomVersion)
3375
+ return (await reachedBy(room.locator, bootstrapped.record.roomVersion))
3215
3376
  ? null
3216
3377
  : advance(room, local, authorized, live);
3217
3378
  }
@@ -3235,12 +3396,12 @@ export function composeRuntime(options) {
3235
3396
  remodelled: refreshed.remodelled,
3236
3397
  };
3237
3398
  if (!(await publish(local, room, refreshed.record, landing, live))) {
3238
- const now = replicas.get(room.locator)?.record.roomVersion;
3399
+ const now = (await recordAt(room.locator))?.roomVersion;
3239
3400
  const reached = now !== undefined &&
3240
3401
  options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3241
3402
  return reached ? null : 'behind';
3242
3403
  }
3243
- await retainModel(local, room, modelOf(room.locator));
3404
+ await retainModel(local, room, null);
3244
3405
  return null;
3245
3406
  }
3246
3407
  return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
@@ -3248,6 +3409,11 @@ export function composeRuntime(options) {
3248
3409
  async function withhold(withheld, room, local, progress) {
3249
3410
  if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3250
3411
  return;
3412
+ if (withheld.kind === 'not-ready') {
3413
+ progress.complete = false;
3414
+ askAgain(room.locator);
3415
+ return;
3416
+ }
3251
3417
  stale.add(room.locator);
3252
3418
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3253
3419
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
@@ -3274,6 +3440,37 @@ export function composeRuntime(options) {
3274
3440
  await refuse('revoked-authorization');
3275
3441
  }
3276
3442
  }
3443
+ function askAgain(locator) {
3444
+ const held = unready.get(locator) ?? { tries: 0, timer: null };
3445
+ unready.set(locator, held);
3446
+ if (held.timer !== null)
3447
+ return;
3448
+ const wait = NOT_READY_MS[Math.min(held.tries, NOT_READY_MS.length - 1)];
3449
+ held.tries += 1;
3450
+ held.timer = setTimeout(() => {
3451
+ held.timer = null;
3452
+ const room = discovered.get(locator);
3453
+ const unstreamed = options.adapter.roomStream === undefined;
3454
+ if (closed || stopping || (!unstreamed && !socketOpen) || room === undefined)
3455
+ return;
3456
+ const progress = { complete: true, reachable: true };
3457
+ void openLocal()
3458
+ .then((local) => roomTask(locator, (live) => loadRoom(room, local, progress, live)))
3459
+ .then(() => rest(progress), () => undefined);
3460
+ }, wait);
3461
+ }
3462
+ function readied(locator) {
3463
+ const held = unready.get(locator);
3464
+ if (held === undefined)
3465
+ return;
3466
+ if (held.timer !== null)
3467
+ clearTimeout(held.timer);
3468
+ unready.delete(locator);
3469
+ }
3470
+ function unreadied() {
3471
+ for (const locator of [...unready.keys()])
3472
+ readied(locator);
3473
+ }
3277
3474
  function leaving(batch, result) {
3278
3475
  return {
3279
3476
  batch,
@@ -3299,7 +3496,8 @@ export function composeRuntime(options) {
3299
3496
  async function acceptance(room, local, submitted, accepted) {
3300
3497
  if (!continuing())
3301
3498
  return unconfirmed;
3302
- const current = replicas.get(room.locator);
3499
+ const work = await workOf(room.locator);
3500
+ const current = work.replica;
3303
3501
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
3304
3502
  return null;
3305
3503
  }
@@ -3308,7 +3506,7 @@ export function composeRuntime(options) {
3308
3506
  let record = current.record;
3309
3507
  let changes;
3310
3508
  if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3311
- !older(room.locator, accepted.roomVersion)) {
3509
+ !olderThan(room.locator, accepted.roomVersion, held)) {
3312
3510
  const paths = [...returnedPaths(submitted, accepted)];
3313
3511
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3314
3512
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion, composed(record)));
@@ -3320,13 +3518,13 @@ export function composeRuntime(options) {
3320
3518
  };
3321
3519
  changes = back;
3322
3520
  }
3323
- const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3521
+ const settled = await settlement(local, room.locator, work, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
3522
+ const reached = record.roomVersion;
3324
3523
  return {
3325
3524
  ...settled,
3326
3525
  apply(answer) {
3327
3526
  const set = settled.apply(answer);
3328
- showing(room.locator, accepted.roomVersion);
3329
- pendings.delete(pendingKey(room.locator, submitted.id));
3527
+ showing(room.locator, accepted.roomVersion, reached);
3330
3528
  unreadable.delete(room.locator);
3331
3529
  list(room, true);
3332
3530
  return set;
@@ -3336,8 +3534,9 @@ export function composeRuntime(options) {
3336
3534
  async function adoptedTogether(local, acceptances) {
3337
3535
  const settlements = acceptances.filter(settling);
3338
3536
  if (settlements.length > 0) {
3339
- publishChangeSet(await together(local, settlements));
3340
- moveTo({ ...status, ...attention() });
3537
+ const set = await together(local, settlements);
3538
+ void moveTo(status);
3539
+ void publishChangeSet(set);
3341
3540
  }
3342
3541
  await unpair(local);
3343
3542
  }
@@ -3353,7 +3552,7 @@ export function composeRuntime(options) {
3353
3552
  const other = discovered.get(partner);
3354
3553
  const failed = await exclusive(async () => {
3355
3554
  const left = await acceptance(room, local, submitted, accepted);
3356
- const arriving = replicas.get(partner)?.queue.find((batch) => batch.id === submitted.id);
3555
+ const arriving = (await replicaOf(partner))?.queue.find((batch) => batch.id === submitted.id);
3357
3556
  const arrived = other === undefined || arriving === undefined || accepted.destination === undefined
3358
3557
  ? null
3359
3558
  : await acceptance(other, local, arriving, {
@@ -3366,25 +3565,24 @@ export function composeRuntime(options) {
3366
3565
  submitLater(partner);
3367
3566
  return failed === null;
3368
3567
  }
3369
- function queueIn(locator) {
3370
- return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
3568
+ async function behindIn(locator, batch) {
3569
+ const local = await openLocal();
3570
+ const [order, { room }] = await Promise.all([
3571
+ local.order(locator),
3572
+ local.pending(locator, batch.id),
3573
+ ]);
3574
+ return room ? order.indexOf(batch.id) > 0 : order.length > 0;
3371
3575
  }
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 };
3576
+ async function unpaired(local, skipped) {
3577
+ const { queued, moving } = await local.moving();
3578
+ for (const { locator, batch, state } of moving) {
3579
+ const partner = partnerOf(batch, locator);
3580
+ if (partner === null || skipped.has(pendingKey(locator, batch.id)))
3581
+ continue;
3582
+ if (queued.get(partner)?.includes(batch.id) === true)
3583
+ continue;
3584
+ if (!leavingIn(batch, locator) || withdrawable(new Map([[batch.id, state]]))(batch)) {
3585
+ return { locator, batch };
3388
3586
  }
3389
3587
  }
3390
3588
  return null;
@@ -3396,9 +3594,10 @@ export function composeRuntime(options) {
3396
3594
  returned: !localSaves && leavingIn(batch, locator) ? returnedBy('withdrawn') : null,
3397
3595
  }));
3398
3596
  const outcome = localSaves ? 'dropped' : 'unpaired';
3399
- const current = replicas.get(locator);
3597
+ const work = await workOf(locator);
3598
+ const current = work.replica;
3400
3599
  if (current !== undefined) {
3401
- const settled = await settlement(local, locator, current, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3600
+ const settled = await settlement(local, locator, work, current.record, current.queue.filter((one) => !ids.has(one.id)), { leaving });
3402
3601
  return {
3403
3602
  ...settled,
3404
3603
  apply(answer) {
@@ -3410,22 +3609,22 @@ export function composeRuntime(options) {
3410
3609
  },
3411
3610
  };
3412
3611
  }
3413
- const queue = queueIn(locator).filter((one) => !ids.has(one.id));
3612
+ const queue = work.queue.filter((one) => !ids.has(one.id));
3414
3613
  const written = new Map();
3415
3614
  for (const { batch, returned } of leaving) {
3416
3615
  if (returned === null)
3417
3616
  continue;
3418
- for (const saved of pendings.get(pendingKey(locator, batch.id))?.drafts ?? []) {
3617
+ for (const saved of work.states.get(batch.id)?.drafts ?? []) {
3419
3618
  written.set(saved.path, {
3420
3619
  locator,
3421
3620
  path: saved.path,
3422
3621
  revision: roomDraftRevision(),
3423
- parts: returnedInto((written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path))?.parts, saved.parts),
3622
+ parts: returnedInto((written.get(saved.path) ?? (await draftOf(locator, saved.path)))?.parts, saved.parts),
3424
3623
  returned,
3425
3624
  });
3426
3625
  }
3427
3626
  }
3428
- const carried = carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3627
+ const carried = await carriedBack(locator, leaving.flatMap(({ batch, returned }) => (returned === null ? [] : [batch])), written);
3429
3628
  return {
3430
3629
  locator,
3431
3630
  left: batches,
@@ -3435,6 +3634,7 @@ export function composeRuntime(options) {
3435
3634
  {
3436
3635
  locator,
3437
3636
  queue,
3637
+ stored: work.queue,
3438
3638
  roomDrafts: [...written.values()],
3439
3639
  consumedRoomDrafts: carried.taken,
3440
3640
  },
@@ -3442,17 +3642,8 @@ export function composeRuntime(options) {
3442
3642
  ],
3443
3643
  },
3444
3644
  apply() {
3445
- for (const batch of batches) {
3645
+ for (const batch of batches)
3446
3646
  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
3647
  submitLater(locator);
3457
3648
  return mergeChangeSets([
3458
3649
  changeSet(locator, {
@@ -3466,28 +3657,33 @@ export function composeRuntime(options) {
3466
3657
  }
3467
3658
  async function unpair(local) {
3468
3659
  const handled = new Set();
3469
- for (let held = unpaired(handled); held !== null && continuing(); held = unpaired(handled)) {
3660
+ for (let held = await unpaired(local, handled); held !== null && continuing(); held = await unpaired(local, handled)) {
3470
3661
  const { locator, batch } = held;
3471
3662
  handled.add(pendingKey(locator, batch.id));
3472
3663
  publishChangeSet(await together(local, [await unpairing(local, locator, [batch])]));
3473
3664
  }
3474
3665
  if (handled.size === 0)
3475
3666
  return;
3476
- moveTo({ ...status, ...attention() });
3667
+ moveTo(status);
3477
3668
  }
3478
3669
  async function submitRoom(room, local, authorized, progress, signal) {
3479
3670
  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)
3671
+ const headId = (await local.order(room.locator)).find((id) => !landedAhead.has(pendingKey(room.locator, id)));
3672
+ if (headId === undefined)
3673
+ return 'done';
3674
+ const [head, pending] = await Promise.all([
3675
+ local.batch(room.locator, headId),
3676
+ local.pending(room.locator, headId),
3677
+ ]);
3678
+ if (head === null || !pending.room)
3483
3679
  return 'done';
3484
- const state = pendings.get(pendingKey(room.locator, head.id));
3680
+ const { state } = pending;
3485
3681
  const partner = partnerOf(head, room.locator);
3486
3682
  if (partner !== null && !leavingIn(head, room.locator)) {
3487
3683
  submitLater(partner);
3488
3684
  return 'done';
3489
3685
  }
3490
- if (partner !== null && behindIn(partner, head)) {
3686
+ if (partner !== null && (await behindIn(partner, head))) {
3491
3687
  progress.complete = false;
3492
3688
  return 'done';
3493
3689
  }
@@ -3508,7 +3704,7 @@ export function composeRuntime(options) {
3508
3704
  let result;
3509
3705
  journal.record(`submission ${room.locator} ${head.id}`);
3510
3706
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
3511
- if (discoveryPaused || fences.has(room.locator))
3707
+ if (discoveryPaused || isFenced(room.locator))
3512
3708
  return 'done';
3513
3709
  if (signal.aborted || writesHeld()) {
3514
3710
  await exclusive(() => mark(local, room.locator, head, submission, earlier));
@@ -3614,7 +3810,8 @@ export function composeRuntime(options) {
3614
3810
  await exclusive(async () => {
3615
3811
  if (!continuing())
3616
3812
  return;
3617
- const current = replicas.get(room.locator);
3813
+ const work = await workOf(room.locator);
3814
+ const current = work.replica;
3618
3815
  if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
3619
3816
  return;
3620
3817
  }
@@ -3628,9 +3825,9 @@ export function composeRuntime(options) {
3628
3825
  landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3629
3826
  return;
3630
3827
  }
3631
- publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3828
+ publishChangeSet(await settle(local, room.locator, work, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3632
3829
  await unpair(local);
3633
- moveTo({ ...status, ...attention() });
3830
+ moveTo(status);
3634
3831
  });
3635
3832
  const { there, here } = judged ?? (await refreshed());
3636
3833
  if (other !== undefined && there !== null)
@@ -3665,24 +3862,24 @@ export function composeRuntime(options) {
3665
3862
  release();
3666
3863
  };
3667
3864
  }
3668
- function declaredAsHeld(room) {
3669
- const held = replicas.get(room.locator)?.record;
3865
+ function declaredAsHeld(held, room) {
3670
3866
  return (held === undefined ||
3671
3867
  (JSON.stringify(held.operations) === JSON.stringify(room.operations) &&
3672
3868
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3869
+ JSON.stringify(held.providerOwnedPaths) === JSON.stringify(room.providerOwnedPaths) &&
3673
3870
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3674
3871
  }
3675
3872
  async function keepNaming(local, room) {
3676
- const held = replicas.get(room.locator)?.record;
3873
+ const held = await recordAt(room.locator);
3677
3874
  if (held === undefined || (held.name === room.name && held.handle === room.handle))
3678
3875
  return;
3679
3876
  await exclusive(async () => {
3680
3877
  if (!continuing())
3681
3878
  return;
3682
- const replica = replicas.get(room.locator);
3683
- if (replica === undefined)
3879
+ const current = await recordAt(room.locator);
3880
+ if (current === undefined)
3684
3881
  return;
3685
- const record = { ...replica.record, name: room.name, handle: room.handle };
3882
+ const record = { ...current, name: room.name, handle: room.handle };
3686
3883
  await local.commit({
3687
3884
  rooms: [
3688
3885
  {
@@ -3693,36 +3890,38 @@ export function composeRuntime(options) {
3693
3890
  },
3694
3891
  ],
3695
3892
  });
3696
- replicas.set(room.locator, { ...replica, record });
3893
+ emitRooms();
3697
3894
  });
3698
3895
  }
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))
3896
+ async function modelNow(locator) {
3897
+ return (await recordAt(locator))?.model ?? unheldModels.get(locator) ?? NO_MODEL;
3898
+ }
3899
+ async function retainModel(local, room, given) {
3900
+ const held = await recordAt(room.locator);
3901
+ const previous = held?.model ?? unheldModels.get(room.locator) ?? NO_MODEL;
3902
+ const next = given ?? previous;
3903
+ if (JSON.stringify(previous) === JSON.stringify(next) && declaredAsHeld(held, room))
3703
3904
  return;
3704
- if (next.status === 'unavailable') {
3705
- invalidModels.add(key);
3706
- liveModels.delete(key);
3707
- }
3708
3905
  await exclusive(async () => {
3709
3906
  if (!continuing())
3710
3907
  return;
3711
- const held = replicas.get(room.locator);
3908
+ const work = await workOf(room.locator);
3909
+ const current = work.replica;
3712
3910
  let changed = null;
3713
- if (held !== undefined) {
3911
+ if (current !== undefined) {
3714
3912
  const record = {
3715
- ...held.record,
3913
+ ...current.record,
3716
3914
  name: room.name,
3717
3915
  handle: room.handle,
3718
3916
  operations: room.operations,
3719
3917
  replica: room.replica,
3918
+ providerOwnedPaths: room.providerOwnedPaths,
3720
3919
  typedSelection: room.typedSelection ?? null,
3721
3920
  model: next,
3722
3921
  };
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);
3922
+ const confirmedLayer = await sliceAt(record, 'confirmed', current.queue, current.overlaid);
3923
+ const replayedLayer = await replayed(record, confirmedLayer, current.queue);
3924
+ const diff = await overlayDiff(record, current, replayedLayer.visible, replayedLayer.touched);
3726
3925
  const answer = await local.commit({
3727
3926
  rooms: [
3728
3927
  {
@@ -3735,28 +3934,21 @@ export function composeRuntime(options) {
3735
3934
  },
3736
3935
  ],
3737
3936
  });
3738
- const { count, indexed } = committedRoom(room.locator, answer);
3739
- replicas.set(room.locator, { ...held, record, overlaid: diff.touched, count, indexed });
3937
+ preparedAfter(room.locator, answer);
3740
3938
  changed = changeSet(room.locator, {
3741
3939
  documents: answer.changed[room.locator] ?? [],
3742
3940
  room: true,
3743
3941
  });
3744
3942
  }
3745
- heldModel(room, next);
3943
+ else
3944
+ unheldModels.set(room.locator, next);
3945
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
3746
3946
  if (changed === null)
3747
3947
  emitRooms();
3748
3948
  else
3749
3949
  publishChangeSet(changed);
3750
3950
  });
3751
3951
  }
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
3952
  async function scoped(result, room, local) {
3761
3953
  if (result.scope === 'content' && !result.retryable) {
3762
3954
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
@@ -3777,7 +3969,7 @@ export function composeRuntime(options) {
3777
3969
  else if (result.scope !== 'operation') {
3778
3970
  unreadable.add(room.locator);
3779
3971
  scopedAway = { ...room, replica: { ...room.replica, admission: 'unavailable' } };
3780
- await retainModel(local, scopedAway, modelOf(room.locator));
3972
+ await retainModel(local, scopedAway, null);
3781
3973
  }
3782
3974
  if (!continuing())
3783
3975
  return;
@@ -3785,42 +3977,55 @@ export function composeRuntime(options) {
3785
3977
  emitRooms();
3786
3978
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
3787
3979
  }
3788
- function name(locator) {
3980
+ async function name(locator) {
3789
3981
  const stream = options.adapter.roomStream;
3790
- const record = replicas.get(locator)?.record;
3791
3982
  const room = discovered.get(locator);
3792
- if (stream === undefined || record === undefined || room === undefined)
3983
+ if (stream === undefined || room === undefined || !socketOpen || !handed)
3793
3984
  return;
3794
- if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3985
+ const [held] = await heldRooms([room]);
3986
+ if (held === undefined || !socketOpen || !handed)
3795
3987
  return;
3796
3988
  naming += 1;
3797
- stream.hold([{ locator, roomVersion: record.roomVersion }]);
3989
+ stream.hold([held]);
3798
3990
  }
3799
- function heldRooms(listed) {
3991
+ async function heldRooms(listed) {
3992
+ const stored = await storedRooms(listed.map((room) => room.locator));
3800
3993
  return listed.flatMap((room) => {
3801
- const record = replicas.get(room.locator)?.record;
3802
- if (record === undefined || record.replica.selection !== room.replica.selection)
3994
+ const record = stored.get(room.locator)?.record;
3995
+ if (record === undefined) {
3996
+ return room.replica.admission === 'admitted'
3997
+ ? [{ locator: room.locator, roomVersion: null }]
3998
+ : [];
3999
+ }
4000
+ if (record.replica.selection !== room.replica.selection)
3803
4001
  return [];
3804
4002
  return [{ locator: room.locator, roomVersion: record.roomVersion }];
3805
4003
  });
3806
4004
  }
3807
4005
  async function synchronize(listed, local, progress, attempt, live) {
3808
- const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
4006
+ const heldNow = await storedRooms(listed.map(({ locator }) => locator));
4007
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress, heldNow.get(room.locator)?.record)));
3809
4008
  if (!live())
3810
4009
  return;
3811
4010
  const ready = listed.filter((_, index) => {
3812
4011
  const outcome = prepared[index];
3813
4012
  return outcome?.status === 'fulfilled' && outcome.value;
3814
4013
  });
4014
+ const held = await heldRooms(ready);
4015
+ if (!live())
4016
+ return;
3815
4017
  if (attempt === 'opened') {
3816
4018
  naming += 1;
3817
- options.adapter.roomStream?.hold(heldRooms(ready));
4019
+ options.adapter.roomStream?.hold(held);
3818
4020
  handed = true;
3819
4021
  }
4022
+ const kept = attempt === 'unstreamed'
4023
+ ? new Set()
4024
+ : new Set((await storedRooms(ready.map(({ locator }) => locator))).keys());
3820
4025
  for (const room of ready) {
3821
- if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4026
+ if (attempt === 'opened' || kept.has(room.locator)) {
3822
4027
  if (attempt === 'authorized')
3823
- name(room.locator);
4028
+ await name(room.locator);
3824
4029
  continue;
3825
4030
  }
3826
4031
  void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
@@ -3829,23 +4034,23 @@ export function composeRuntime(options) {
3829
4034
  if (failed !== undefined)
3830
4035
  throw failed.reason;
3831
4036
  }
3832
- async function prepareRoom(room, local, progress) {
4037
+ async function prepareRoom(room, local, progress, prior) {
3833
4038
  if (closed || authorization === undefined) {
3834
4039
  progress.complete = false;
3835
4040
  return false;
3836
4041
  }
3837
- const prior = replicas.get(room.locator)?.record;
3838
4042
  if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
3839
- list(prior, false);
4043
+ listHeld(room.locator);
3840
4044
  emitRooms();
3841
4045
  journal.record(`selection changed ${room.locator}`);
3842
4046
  progress.complete = false;
3843
4047
  return false;
3844
4048
  }
3845
- if (prior !== undefined)
3846
- await retainModel(local, room, modelOf(room.locator));
3847
- if (prior !== undefined)
4049
+ if (prior !== undefined && !declaredAsHeld(prior, room))
4050
+ await retainModel(local, room, null);
4051
+ if (prior !== undefined && (prior.name !== room.name || prior.handle !== room.handle)) {
3848
4052
  await keepNaming(local, room);
4053
+ }
3849
4054
  if (discoveryPaused || authorization === undefined)
3850
4055
  return false;
3851
4056
  return room.replica.admission === 'admitted';
@@ -3853,26 +4058,28 @@ export function composeRuntime(options) {
3853
4058
  async function caughtUp(room, local, progress, live) {
3854
4059
  if (options.adapter.roomStream !== undefined || authorization === undefined)
3855
4060
  return;
3856
- if (!live() || !behind(room))
4061
+ if (!live() || !(await behind(room)))
3857
4062
  return;
3858
4063
  const withheld = await advance(room, local, authorization, live);
3859
4064
  if (withheld !== null)
3860
4065
  await withhold(withheld, room, local, progress);
3861
4066
  }
3862
- function revoked(locator) {
3863
- const model = modelOf(locator);
3864
- return model.status === 'unavailable' && model.reason === 'revoked';
4067
+ function revoked(model) {
4068
+ return model?.status === 'unavailable' && model.reason === 'revoked';
3865
4069
  }
3866
- async function loadRoom(room, local, progress, live) {
4070
+ async function loadRoom(room, local, progress, live, unheld) {
3867
4071
  if (!live() || authorization === undefined)
3868
4072
  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;
4073
+ const prior = unheld === undefined ? await recordAt(room.locator) : undefined;
4074
+ if (prior !== undefined) {
4075
+ if (!(await prepareRoom(room, local, progress, prior)))
4076
+ return;
4077
+ if (revoked(prior.model)) {
4078
+ await reload(room, local, progress, live);
4079
+ return;
4080
+ }
3874
4081
  }
3875
- const withheld = await advance(room, local, authorization, live);
4082
+ const withheld = await advance(room, local, authorization, live, unheld);
3876
4083
  if (withheld !== null) {
3877
4084
  await withhold(withheld, room, local, progress);
3878
4085
  return;
@@ -3885,8 +4092,9 @@ export function composeRuntime(options) {
3885
4092
  reloadsDue.delete(room.locator);
3886
4093
  if (!live() || authorization === undefined)
3887
4094
  return;
3888
- const from = standingOf(room.locator);
3889
- const bootstrapped = await pullBootstrap(authorization, room, local, live);
4095
+ const prior = await recordAt(room.locator);
4096
+ const from = standingAt(room.locator, prior);
4097
+ const bootstrapped = await pullBootstrap(authorization, room, local, live, prior);
3890
4098
  if (bootstrapped.kind !== 'staged') {
3891
4099
  if (live())
3892
4100
  await withhold(bootstrapped, room, local, progress);
@@ -3910,7 +4118,7 @@ export function composeRuntime(options) {
3910
4118
  function malformed(listed) {
3911
4119
  for (const room of listed) {
3912
4120
  if (room.operations === undefined ||
3913
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4121
+ !['write', 'erase'].every((operation) => Array.isArray(room.operations[operation]) &&
3914
4122
  room.operations[operation].every((scope) => typeof scope === 'string')) ||
3915
4123
  room.replica === undefined ||
3916
4124
  typeof room.replica.selection !== 'string' ||
@@ -3925,18 +4133,17 @@ export function composeRuntime(options) {
3925
4133
  return null;
3926
4134
  }
3927
4135
  async function signIn(local, signedIn) {
3928
- if (JSON.stringify(signedIn) === JSON.stringify(member))
4136
+ if (JSON.stringify(signedIn) === JSON.stringify((await local.facts()).member))
3929
4137
  return;
3930
4138
  await local.fact('member', signedIn);
3931
- member = signedIn;
3932
- emitMember();
4139
+ emitMember(signedIn);
3933
4140
  }
3934
4141
  async function discoverRooms(local, progress, attempt, live, found) {
3935
4142
  if (closed || authorization === undefined) {
3936
4143
  notifyReady();
3937
4144
  return;
3938
4145
  }
3939
- moveTo(transferring(status));
4146
+ moveTo(transferring());
3940
4147
  discoveries += 1;
3941
4148
  const started = discoveries;
3942
4149
  const result = found ?? (await asked(authorization));
@@ -3981,15 +4188,13 @@ export function composeRuntime(options) {
3981
4188
  refusal = null;
3982
4189
  emitRefusal();
3983
4190
  }
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);
4191
+ const held = await local.facts();
4192
+ if (JSON.stringify(result.providerLimits) !== JSON.stringify(held.providerLimits) ||
4193
+ JSON.stringify(result.capabilities) !== JSON.stringify(held.capabilities)) {
4194
+ await local.fact('providerLimits', result.providerLimits);
4195
+ await local.fact('capabilities', result.capabilities);
3992
4196
  }
4197
+ notifyReady();
3993
4198
  await signIn(local, result.member ?? null);
3994
4199
  await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
3995
4200
  }
@@ -4072,7 +4277,7 @@ export function composeRuntime(options) {
4072
4277
  streamed = progress;
4073
4278
  complete = false;
4074
4279
  recorded = false;
4075
- moveTo(transferring(status));
4280
+ moveTo(transferring());
4076
4281
  try {
4077
4282
  const local = await openLocal();
4078
4283
  if (!live())
@@ -4107,7 +4312,7 @@ export function composeRuntime(options) {
4107
4312
  if (!live() || !announced)
4108
4313
  return;
4109
4314
  const progress = { complete: true, reachable: true };
4110
- moveTo(transferring(status));
4315
+ moveTo(transferring());
4111
4316
  try {
4112
4317
  const local = await openLocal();
4113
4318
  if (!live())
@@ -4132,7 +4337,7 @@ export function composeRuntime(options) {
4132
4337
  return;
4133
4338
  countAuthorityChange();
4134
4339
  const progress = { complete: true, reachable: true };
4135
- moveTo(transferring(status));
4340
+ moveTo(transferring());
4136
4341
  try {
4137
4342
  const local = await openLocal();
4138
4343
  if (!live())
@@ -4167,6 +4372,7 @@ export function composeRuntime(options) {
4167
4372
  missedChange ||= authorityChanged;
4168
4373
  handed = false;
4169
4374
  naming = 0;
4375
+ unreadied();
4170
4376
  connectionLost();
4171
4377
  if (lost)
4172
4378
  conversations.lost();
@@ -4194,8 +4400,12 @@ export function composeRuntime(options) {
4194
4400
  await rest(null, live);
4195
4401
  });
4196
4402
  }
4197
- function cachedModels(local) {
4198
- return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4403
+ async function cachedModels(local) {
4404
+ const held = await storedRooms();
4405
+ await settleModels(listedLocators(held).flatMap((locator) => {
4406
+ const room = discovered.get(locator) ?? declarationOf(locator, held.get(locator))?.room;
4407
+ return room === undefined ? [] : [room];
4408
+ }), 'cached', local);
4199
4409
  }
4200
4410
  function caughtUpRooms(expired) {
4201
4411
  const progress = underway;
@@ -4203,8 +4413,12 @@ export function composeRuntime(options) {
4203
4413
  naming = Math.max(0, naming - 1);
4204
4414
  const cycle = progress ?? { complete: true, reachable: true };
4205
4415
  replaced(expired);
4206
- const reloading = progress === null ? [] : owedReloads(expired);
4207
- const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4416
+ const installing = new Set(installs);
4417
+ const held = progress === null ? storedRooms(expired) : storedRooms();
4418
+ const reloading = progress === null
4419
+ ? Promise.resolve([])
4420
+ : held.then((stored) => owedReloads(expired, stored));
4421
+ const replacing = Promise.all([held, reloading]).then(([stored, owed]) => reloadNow((room) => expired.includes(room.locator) || owed.includes(room.locator), cycle, stored, installing));
4208
4422
  return task(OPENING_LANE, async (live) => {
4209
4423
  if (!live())
4210
4424
  return;
@@ -4217,9 +4431,11 @@ export function composeRuntime(options) {
4217
4431
  const local = await openLocal();
4218
4432
  await replacing;
4219
4433
  if (progress.reachable) {
4220
- await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4434
+ const stored = await held;
4435
+ const owed = await reloading;
4436
+ await settleModels([...discovered.values()].filter((room) => stored.has(room.locator) &&
4221
4437
  !expired.includes(room.locator) &&
4222
- !reloading.includes(room.locator)), 'current', local);
4438
+ !owed.includes(room.locator)), 'current', local);
4223
4439
  }
4224
4440
  }
4225
4441
  catch {
@@ -4230,24 +4446,34 @@ export function composeRuntime(options) {
4230
4446
  await finishOpening(progress, live);
4231
4447
  });
4232
4448
  }
4233
- function owedReloads(expired) {
4449
+ function owedReloads(expired, held) {
4234
4450
  const reloading = [...discovered.values()]
4235
- .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4451
+ .filter((room) => !expired.includes(room.locator) && revoked(held.get(room.locator)?.record.model))
4236
4452
  .map((room) => room.locator);
4237
4453
  for (const locator of reloading)
4238
4454
  catching.add(locator);
4239
- liveQueries.recover();
4455
+ recoverReads();
4240
4456
  return reloading;
4241
4457
  }
4242
- async function reloadNow(replaced, progress) {
4243
- const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4458
+ async function reloadNow(replaced, progress, held, installing) {
4459
+ const reloaded = [...discovered.values()].filter((room) => held.has(room.locator) && !installing.has(room.locator) && replaced(room));
4460
+ const acquired = [...discovered.values()].filter((room) => (!held.has(room.locator) || installing.has(room.locator)) &&
4461
+ room.replica.admission === 'admitted' &&
4462
+ replaced(room));
4244
4463
  for (const room of reloaded)
4245
4464
  reloadsDue.add(room.locator);
4246
4465
  try {
4247
4466
  const local = await openLocal();
4467
+ const kept = acquired.length === 0 ? new Set() : (await local.held()).staged;
4248
4468
  for (const room of reloaded) {
4249
4469
  void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4250
4470
  }
4471
+ for (const room of acquired) {
4472
+ const unheld = installing.has(room.locator)
4473
+ ? undefined
4474
+ : { staged: kept.has(room.locator) };
4475
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live, unheld)).then(() => rest(progress));
4476
+ }
4251
4477
  }
4252
4478
  catch {
4253
4479
  for (const room of reloaded)
@@ -4256,29 +4482,56 @@ export function composeRuntime(options) {
4256
4482
  }
4257
4483
  }
4258
4484
  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 () => {
4485
+ const owed = (model) => model.status !== 'unavailable' && model.status !== status;
4486
+ const declaredAnew = await exclusive(async () => {
4264
4487
  if (!continuing())
4265
4488
  return [];
4266
- const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4489
+ const stored = await storedRooms(held.map(({ locator }) => locator));
4490
+ const unchanged = held.filter((room) => {
4491
+ const record = stored.get(room.locator)?.record;
4492
+ return (owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL) &&
4493
+ declaredAsHeld(record, room));
4494
+ });
4495
+ const records = unchanged.flatMap((room) => {
4496
+ const record = stored.get(room.locator)?.record;
4497
+ return record === undefined
4498
+ ? []
4499
+ : [{ ...record, model: { ...record.model, status } }];
4500
+ });
4501
+ if (records.length > 0) {
4502
+ try {
4503
+ await local.commit({
4504
+ rooms: records.map((record) => ({
4505
+ locator: record.locator,
4506
+ key: record.key,
4507
+ record: storedRecord(record),
4508
+ composed: composedOf(composed(record)),
4509
+ })),
4510
+ });
4511
+ }
4512
+ catch {
4513
+ journal.record(`model ${status} persistence failed`);
4514
+ }
4515
+ }
4267
4516
  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);
4517
+ const unheld = unheldModels.get(room.locator);
4518
+ if (!stored.has(room.locator) && unheld !== undefined) {
4519
+ unheldModels.set(room.locator, { ...unheld, status });
4520
+ }
4521
+ list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
4273
4522
  }
4274
4523
  if (unchanged.length > 0)
4275
4524
  emitRooms();
4276
- return unchanged;
4525
+ return held.filter((room) => {
4526
+ const record = stored.get(room.locator)?.record;
4527
+ return (!unchanged.includes(room) &&
4528
+ owed(record?.model ?? unheldModels.get(room.locator) ?? NO_MODEL));
4529
+ });
4277
4530
  });
4278
- for (const room of held) {
4279
- if (settled.includes(room) || !owed(room))
4280
- continue;
4281
- await retainModel(local, room, { ...modelOf(room.locator), status });
4531
+ for (const room of declaredAnew) {
4532
+ const model = await modelNow(room.locator);
4533
+ if (owed(model))
4534
+ await retainModel(local, room, { ...model, status });
4282
4535
  }
4283
4536
  }
4284
4537
  function submitLater(locator) {
@@ -4341,7 +4594,7 @@ export function composeRuntime(options) {
4341
4594
  return 'done';
4342
4595
  await rest(progress);
4343
4596
  if (!stale.has(locator)) {
4344
- if ((replicas.get(locator)?.queue.length ?? 0) > 0)
4597
+ if ((await (await openLocal()).order(locator)).length > 0)
4345
4598
  submitLater(locator);
4346
4599
  return 'done';
4347
4600
  }
@@ -4369,9 +4622,11 @@ export function composeRuntime(options) {
4369
4622
  return 'done';
4370
4623
  return reading.reachable && reading.complete ? 'done' : 'again';
4371
4624
  }
4372
- function submitAll() {
4373
- for (const [locator, replica] of replicas) {
4374
- if (replica.queue.length > 0)
4625
+ async function submitAll() {
4626
+ if (localSaves)
4627
+ return;
4628
+ for (const locator of await (await openLocal()).queued()) {
4629
+ if (!isFenced(locator))
4375
4630
  submitLater(locator);
4376
4631
  }
4377
4632
  }
@@ -4381,7 +4636,7 @@ export function composeRuntime(options) {
4381
4636
  progress.complete = false;
4382
4637
  return;
4383
4638
  }
4384
- submitAll();
4639
+ await submitAll();
4385
4640
  for (const locator of stale)
4386
4641
  if (repairable.has(locator))
4387
4642
  repairLater(locator, {});
@@ -4410,8 +4665,14 @@ export function composeRuntime(options) {
4410
4665
  async function rest(progress, live = () => !closed) {
4411
4666
  if (!live())
4412
4667
  return;
4413
- let lastCompleted = status.lastCompleted;
4414
- let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4668
+ const settled = () => complete && roomWork === 0 && naming === 0 && stale.size === 0;
4669
+ let synced = settled();
4670
+ if (synced && !recorded) {
4671
+ const queued = await queuedWork().catch(() => true);
4672
+ if (!live())
4673
+ return;
4674
+ synced = !queued && settled();
4675
+ }
4415
4676
  if (synced && !recorded) {
4416
4677
  const completedAt = new Date().toISOString();
4417
4678
  try {
@@ -4419,21 +4680,15 @@ export function composeRuntime(options) {
4419
4680
  if (!live())
4420
4681
  return;
4421
4682
  recorded = true;
4422
- lastCompleted = completedAt;
4423
4683
  }
4424
4684
  catch {
4425
4685
  synced = false;
4426
4686
  }
4427
4687
  }
4428
4688
  moveTo({
4429
- state: pending()
4430
- ? 'pending'
4431
- : synced && complete && roomWork === 0 && naming === 0
4432
- ? 'synced'
4433
- : 'syncing',
4434
- lastCompleted,
4689
+ state: synced && complete && roomWork === 0 && naming === 0 ? 'synced' : 'syncing',
4435
4690
  providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4436
- ...attention(),
4691
+ resting: true,
4437
4692
  });
4438
4693
  }
4439
4694
  function applyRoom(change) {
@@ -4449,22 +4704,26 @@ export function composeRuntime(options) {
4449
4704
  if (!live())
4450
4705
  return null;
4451
4706
  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) {
4707
+ if (room === undefined || room.replica.admission !== 'admitted')
4457
4708
  return null;
4709
+ const held = await recordAt(locator);
4710
+ if (held === undefined) {
4711
+ if (change.from !== null)
4712
+ return null;
4713
+ await installCarried(room, local, change, live, progress);
4714
+ return progress;
4458
4715
  }
4459
- const at = held.record.roomVersion;
4716
+ if (held.replica.selection !== room.replica.selection)
4717
+ return null;
4718
+ const at = held.roomVersion;
4460
4719
  if (options.adapter.reached(locator, at, change.roomVersion))
4461
4720
  return progress;
4462
- if (!options.adapter.reached(locator, at, change.from)) {
4463
- name(locator);
4721
+ if (change.from === null || !options.adapter.reached(locator, at, change.from)) {
4722
+ await name(locator);
4464
4723
  return progress;
4465
4724
  }
4466
4725
  if (underway === null)
4467
- moveTo(transferring(status));
4726
+ moveTo(transferring());
4468
4727
  const writing = () => live() && !full;
4469
4728
  const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4470
4729
  if (refreshed.kind !== 'advanced')
@@ -4476,19 +4735,52 @@ export function composeRuntime(options) {
4476
4735
  };
4477
4736
  if (await publish(local, room, refreshed.record, landing, writing)) {
4478
4737
  stale.delete(locator);
4479
- await retainModel(local, room, modelOf(locator));
4738
+ if (!declaredAsHeld(held, room))
4739
+ await retainModel(local, room, null);
4480
4740
  return progress;
4481
4741
  }
4482
4742
  }
4483
4743
  }
4484
4744
  catch {
4485
4745
  if (!stale.has(locator))
4486
- name(locator);
4746
+ void name(locator);
4487
4747
  stale.add(locator);
4488
4748
  return progress;
4489
4749
  }
4490
4750
  }).then((progress) => rest(progress), () => undefined);
4491
4751
  }
4752
+ async function installCarried(room, local, change, live, progress) {
4753
+ if (authorization === undefined)
4754
+ return;
4755
+ if (underway === null)
4756
+ moveTo(transferring());
4757
+ const prior = await recordAt(room.locator);
4758
+ const from = standingAt(room.locator, prior);
4759
+ const page = await change.read();
4760
+ const carried = {
4761
+ bootstrap: () => Promise.resolve({
4762
+ kind: 'bootstrap',
4763
+ roomVersion: page.roomVersion,
4764
+ ...(page.models === undefined ? {} : { models: page.models }),
4765
+ documents: page.documents,
4766
+ facts: page.facts,
4767
+ revisions: page.revisions,
4768
+ ...(page.fileIds === undefined ? {} : { fileIds: page.fileIds }),
4769
+ ...(page.created === undefined ? {} : { created: page.created }),
4770
+ changes: page.changes,
4771
+ binaryFiles: page.binaryFiles,
4772
+ ...(page.folders === undefined ? {} : { folders: page.folders }),
4773
+ }),
4774
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
4775
+ };
4776
+ const bootstrapped = await pullBootstrap(authorization, room, local, live, prior, carried);
4777
+ if (bootstrapped.kind !== 'staged') {
4778
+ if (live())
4779
+ await withhold(bootstrapped, room, local, progress);
4780
+ return;
4781
+ }
4782
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4783
+ }
4492
4784
  function observeStream() {
4493
4785
  const stream = options.adapter.roomStream;
4494
4786
  if (stream === undefined || withdrawStream !== undefined)
@@ -4520,8 +4812,11 @@ export function composeRuntime(options) {
4520
4812
  closedSocket(event.revoked, event.lost, event.authorityChanged);
4521
4813
  else if (event.kind === 'caught-up')
4522
4814
  void caughtUpRooms(event.expired);
4523
- else if (event.kind === 'authority-changed')
4815
+ else if (event.kind === 'authority-changed') {
4816
+ if (announced && socketOpen)
4817
+ fenceLive(event.left);
4524
4818
  void changeAuthority(event);
4819
+ }
4525
4820
  else
4526
4821
  void applyRoom(event);
4527
4822
  });
@@ -4554,10 +4849,7 @@ export function composeRuntime(options) {
4554
4849
  withdrawConversations = stream.observe((event) => {
4555
4850
  if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted')
4556
4851
  queueReadiness();
4557
- if (conversations.receive(event)) {
4558
- void ready.then(scheduleCurrency);
4559
- return;
4560
- }
4852
+ conversations.receive(event);
4561
4853
  if (event.kind !== 'gap' || conversations.current())
4562
4854
  return;
4563
4855
  show();
@@ -4581,7 +4873,7 @@ export function composeRuntime(options) {
4581
4873
  return;
4582
4874
  }
4583
4875
  const progress = { complete: false, reachable: !status.providerUnreachable };
4584
- moveTo(transferring(status));
4876
+ moveTo(transferring());
4585
4877
  try {
4586
4878
  await conversations.catchUp(progress);
4587
4879
  }
@@ -4595,6 +4887,7 @@ export function composeRuntime(options) {
4595
4887
  return;
4596
4888
  closed = true;
4597
4889
  notifyDiscovered();
4890
+ unreadied();
4598
4891
  if (probing !== null)
4599
4892
  clearInterval(probing);
4600
4893
  connectionLive();
@@ -4616,12 +4909,14 @@ export function composeRuntime(options) {
4616
4909
  memberListeners.clear();
4617
4910
  authorityChangeListeners.clear();
4618
4911
  liveQueries.close();
4912
+ figures.close();
4619
4913
  queueSubscriptions.clear();
4620
4914
  roomDraftListeners.clear();
4621
4915
  presentationListeners.clear();
4622
4916
  }
4623
4917
  async function stop() {
4624
4918
  stopping = true;
4919
+ unreadied();
4625
4920
  inFlight.abort();
4626
4921
  await recovery.retire();
4627
4922
  await Promise.all(working);
@@ -4634,16 +4929,18 @@ export function composeRuntime(options) {
4634
4929
  if (options.signal?.aborted)
4635
4930
  close();
4636
4931
  observeStream();
4637
- void openLocal().then(() => {
4638
- if (capabilities.length > 0)
4932
+ void openLocal()
4933
+ .then(async (local) => {
4934
+ if ((await local.facts()).capabilities.length > 0)
4639
4935
  readyFromStore();
4640
- }, () => {
4936
+ })
4937
+ .catch(() => {
4641
4938
  readyFromStore();
4642
4939
  });
4643
4940
  if (options.adapter.roomStream === undefined)
4644
4941
  void sync('unstreamed', null);
4645
- function planned(input, reach) {
4646
- const plan = planWithin(input, roomFacts(reach), planReach(reach));
4942
+ async function planned(input, reach) {
4943
+ const plan = planWithin(input, await roomFacts(reach), planReach(reach));
4647
4944
  const named = plan.paths !== null &&
4648
4945
  plan.rooms !== null &&
4649
4946
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
@@ -4662,10 +4959,10 @@ export function composeRuntime(options) {
4662
4959
  const watchers = seedWatchers.get(plan.key) ?? new Set();
4663
4960
  seedWatchers.set(plan.key, watchers);
4664
4961
  watchers.add(shown);
4665
- const stop = liveQueries.watch(plan, identityOf(plan), () => {
4666
- const cover = covered(plan);
4962
+ const stop = liveQueries.watch(plan, identityOf(plan), (held) => {
4963
+ const cover = covered(plan, held);
4667
4964
  awaitPreparation(cover);
4668
- return seedBehind(plan, cover);
4965
+ return seedBehind(plan, cover, held);
4669
4966
  }, (message) => {
4670
4967
  const answered = message;
4671
4968
  pending = whole(answered) && 'pending' in answered.result;
@@ -4683,6 +4980,21 @@ export function composeRuntime(options) {
4683
4980
  stop();
4684
4981
  };
4685
4982
  }
4983
+ function watchPlanned(planning, deliver, fail) {
4984
+ let stop = null;
4985
+ let stopped = false;
4986
+ planning.then((plan) => {
4987
+ if (!stopped)
4988
+ stop = watchPlan(plan, deliver, fail);
4989
+ }, (error) => {
4990
+ if (!stopped)
4991
+ fail(error);
4992
+ });
4993
+ return () => {
4994
+ stopped = true;
4995
+ stop?.();
4996
+ };
4997
+ }
4686
4998
  async function searched(input, reach) {
4687
4999
  const asked = searchPlanOf(input);
4688
5000
  await ready;
@@ -4695,7 +5007,7 @@ export function composeRuntime(options) {
4695
5007
  retryable: false,
4696
5008
  };
4697
5009
  }
4698
- if (closed || refusal !== null || connectionOf() !== 'connected')
5010
+ if (closed || refusal !== null || (await connectionNow()) !== 'connected')
4699
5011
  return OFFLINE;
4700
5012
  const reached = reach === null ? null : new Set(reach.rooms);
4701
5013
  const named = asked.rooms === null
@@ -4703,7 +5015,8 @@ export function composeRuntime(options) {
4703
5015
  ? null
4704
5016
  : [...reached]
4705
5017
  : asked.rooms.filter((room) => reached === null || reached.has(room));
4706
- const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
5018
+ const held = named === null ? null : await storedRooms(named);
5019
+ const rooms = named === null ? null : named.filter((room) => held.has(room)).sort();
4707
5020
  if (rooms !== null && rooms.length === 0)
4708
5021
  return { kind: 'found', documents: [] };
4709
5022
  try {
@@ -4741,18 +5054,15 @@ export function composeRuntime(options) {
4741
5054
  roomDraftListeners.add(listener);
4742
5055
  void exclusive(async () => {
4743
5056
  await openLocal();
4744
- }).then(() => {
5057
+ }).then(() => publication(async () => {
4745
5058
  if (closed || !roomDraftListeners.has(listener))
4746
5059
  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
- }, () => { });
5060
+ const value = await snapshotRoomDrafts();
5061
+ if (closed || !roomDraftListeners.has(listener))
5062
+ return;
5063
+ listener.ready = true;
5064
+ listener.notify(value);
5065
+ }), () => { });
4756
5066
  return () => {
4757
5067
  roomDraftListeners.delete(listener);
4758
5068
  };
@@ -4764,6 +5074,8 @@ export function composeRuntime(options) {
4764
5074
  return conversations.watchList(deliver);
4765
5075
  if (topic === 'agents')
4766
5076
  return conversations.watchAgents(deliver);
5077
+ if (topic === 'figures')
5078
+ return figures.watch(deliver, fail);
4767
5079
  if (topic === 'presentation') {
4768
5080
  const listener = {
4769
5081
  app: input[0],
@@ -4772,13 +5084,12 @@ export function composeRuntime(options) {
4772
5084
  };
4773
5085
  presentationListeners.add(listener);
4774
5086
  void exclusive(async () => {
4775
- await openLocal();
4776
- }).then(() => {
5087
+ const value = await presentationOf(listener.app);
4777
5088
  if (closed || !presentationListeners.has(listener))
4778
5089
  return;
4779
5090
  listener.ready = true;
4780
- listener.notify(presentationOf(listener.app));
4781
- }, () => { });
5091
+ listener.notify(value);
5092
+ }).catch(() => { });
4782
5093
  return () => {
4783
5094
  presentationListeners.delete(listener);
4784
5095
  };
@@ -4792,6 +5103,8 @@ export function composeRuntime(options) {
4792
5103
  return () => {
4793
5104
  unlistedRooms.delete(deliver);
4794
5105
  roomListeners.delete(deliver);
5106
+ if (roomListeners.size === 0)
5107
+ roomsShown = null;
4795
5108
  };
4796
5109
  }
4797
5110
  if (topic === 'connection-refusal') {
@@ -4824,7 +5137,12 @@ export function composeRuntime(options) {
4824
5137
  }
4825
5138
  if (topic === 'member') {
4826
5139
  memberListeners.add(deliver);
4827
- deliver(member);
5140
+ void openLocal()
5141
+ .then((local) => local.facts())
5142
+ .then(({ member }) => {
5143
+ if (memberListeners.has(deliver))
5144
+ deliver(member);
5145
+ }, () => { });
4828
5146
  return () => {
4829
5147
  memberListeners.delete(deliver);
4830
5148
  };
@@ -4839,12 +5157,18 @@ export function composeRuntime(options) {
4839
5157
  if (topic === 'queue') {
4840
5158
  const queue = { locator: input[0], notify: deliver };
4841
5159
  queueSubscriptions.add(queue);
4842
- queue.notify(entriesOf(queue.locator));
5160
+ void publication(async () => {
5161
+ if (!queueSubscriptions.has(queue))
5162
+ return;
5163
+ const entries = await entriesOf(queue.locator);
5164
+ if (queueSubscriptions.has(queue))
5165
+ queue.notify(entries);
5166
+ });
4843
5167
  return () => {
4844
5168
  queueSubscriptions.delete(queue);
4845
5169
  };
4846
5170
  }
4847
- return watchPlan(planned(input[0], null), deliver, fail);
5171
+ return watchPlanned(planned(input[0], null), deliver, fail);
4848
5172
  };
4849
5173
  const md = {
4850
5174
  loaded() {
@@ -4863,15 +5187,14 @@ export function composeRuntime(options) {
4863
5187
  emitKnownRooms();
4864
5188
  });
4865
5189
  },
4866
- query: ((input) => rowsQuery(planned(input, null))),
5190
+ query: ((input) => planned(input, null).then(rowsQuery)),
4867
5191
  search: (input) => searched(input, null),
4868
- types(locator) {
5192
+ async types(locator) {
5193
+ const held = (await storedRooms([locator])).get(locator);
5194
+ const room = isFenced(locator) ? undefined : declarationOf(locator, held)?.room;
4869
5195
  return {
4870
- collection: modelTypes(modelOf(locator)),
4871
- typedSelection: rooms.get(locator)?.typedSelection === undefined ||
4872
- rooms.get(locator)?.typedSelection === null
4873
- ? null
4874
- : modelTypes(builtIn),
5196
+ collection: modelTypes(isFenced(locator) ? NO_MODEL : modelIn(locator, held)),
5197
+ typedSelection: (room?.typedSelection ?? null) === null ? null : modelTypes(builtIn),
4875
5198
  };
4876
5199
  },
4877
5200
  roomDraft: {
@@ -4885,7 +5208,7 @@ export function composeRuntime(options) {
4885
5208
  });
4886
5209
  },
4887
5210
  async read(room, path) {
4888
- const record = roomDrafts.get(room)?.get(path);
5211
+ const record = await draftOf(room, path);
4889
5212
  return record === undefined ? null : ((await roomDraftsOf([record]))[0] ?? null);
4890
5213
  },
4891
5214
  write(room, path, revision, input, next = roomDraftRevision()) {
@@ -4903,14 +5226,15 @@ export function composeRuntime(options) {
4903
5226
  discard(room, path, revision) {
4904
5227
  return exclusive(async () => {
4905
5228
  const local = await openLocal();
4906
- const current = roomDrafts.get(room)?.get(path);
5229
+ const current = await draftOf(room, path);
4907
5230
  if (current === undefined || current.revision !== revision) {
4908
5231
  return staleWrite(current === undefined ? null : ((await roomDraftsOf([current]))[0] ?? null));
4909
5232
  }
4910
5233
  await local.discardRoomDraft(room, path);
4911
- forgetRoomDraft(room, path);
4912
- publishChangeSet(changeSet(room, { drafts: [path] }));
4913
- moveTo({ ...status, ...attention() });
5234
+ await Promise.all([
5235
+ publishChangeSet(changeSet(room, { drafts: [path] })),
5236
+ moveTo(status),
5237
+ ]);
4914
5238
  return { outcome: 'written', roomDraft: null };
4915
5239
  });
4916
5240
  },
@@ -4919,10 +5243,7 @@ export function composeRuntime(options) {
4919
5243
  chatDraft: conversations.chatDraft,
4920
5244
  presentation: {
4921
5245
  read(app) {
4922
- return exclusive(async () => {
4923
- await openLocal();
4924
- return presentationOf(app);
4925
- });
5246
+ return exclusive(() => presentationOf(app));
4926
5247
  },
4927
5248
  write(app, value) {
4928
5249
  return exclusive(() => keptPresentation(app, () => structuredClone(value)));
@@ -4955,7 +5276,8 @@ export function composeRuntime(options) {
4955
5276
  },
4956
5277
  async call(name, input) {
4957
5278
  await ready;
4958
- const room = calledRooms(name, input).find((one) => locked(one));
5279
+ const lockedNow = await lockedRooms();
5280
+ const room = (await calledRooms(name, input)).find((one) => lockedNow.includes(one));
4959
5281
  return room === undefined ? recoveredAction(name, input) : lockedRoom(room);
4960
5282
  },
4961
5283
  async reached(locator, roomVersion, path) {
@@ -5008,7 +5330,7 @@ export function composeRuntime(options) {
5008
5330
  lockedRooms,
5009
5331
  watchLocks,
5010
5332
  located,
5011
- room: (locator) => listed().find((one) => one.locator === locator),
5333
+ room: async (locator) => shownRooms(await listingNow()).find((one) => one.locator === locator),
5012
5334
  };
5013
5335
  return {
5014
5336
  md,