@agentshouse/mdruntime 0.1.0-ccbb89b0ca151 → 0.1.0-ccc0e3407f107

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