@agentshouse/mdruntime 0.1.0-cba6f2439bb25 → 0.1.0-cbe358f60d1d3

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 (127) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +66 -52
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +6 -9
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +28 -22
  7. package/dist/chain.d.ts +2 -1
  8. package/dist/chain.d.ts.map +1 -1
  9. package/dist/client.d.ts +9 -5
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +2 -1
  12. package/dist/collection.d.ts +2 -4
  13. package/dist/collection.d.ts.map +1 -1
  14. package/dist/collection.js +1 -17
  15. package/dist/compile.d.ts +1 -6
  16. package/dist/compile.d.ts.map +1 -1
  17. package/dist/compile.js +91 -276
  18. package/dist/connection.d.ts +8 -8
  19. package/dist/connection.d.ts.map +1 -1
  20. package/dist/connection.js +738 -968
  21. package/dist/context.d.ts +2 -0
  22. package/dist/context.d.ts.map +1 -0
  23. package/dist/context.js +7 -0
  24. package/dist/conversation.d.ts +15 -14
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +141 -167
  27. package/dist/durability.d.ts +6 -1
  28. package/dist/durability.d.ts.map +1 -1
  29. package/dist/entry.d.ts +2 -2
  30. package/dist/entry.d.ts.map +1 -1
  31. package/dist/entry.js +13 -5
  32. package/dist/host.d.ts +3 -7
  33. package/dist/host.d.ts.map +1 -1
  34. package/dist/host.js +22 -27
  35. package/dist/identity.d.ts +0 -1
  36. package/dist/identity.d.ts.map +1 -1
  37. package/dist/identity.js +0 -3
  38. package/dist/index.d.ts +11 -7
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -1
  41. package/dist/job.d.ts +14 -0
  42. package/dist/job.d.ts.map +1 -0
  43. package/dist/job.js +28 -0
  44. package/dist/live.d.ts +2 -1
  45. package/dist/live.d.ts.map +1 -1
  46. package/dist/live.js +10 -7
  47. package/dist/media.d.ts +1 -1
  48. package/dist/media.d.ts.map +1 -1
  49. package/dist/media.js +19 -9
  50. package/dist/memory.d.ts +9 -4
  51. package/dist/memory.d.ts.map +1 -1
  52. package/dist/memory.js +132 -82
  53. package/dist/messages.d.ts +2 -2
  54. package/dist/messages.d.ts.map +1 -1
  55. package/dist/messages.js +15 -16
  56. package/dist/model.d.ts +1 -3
  57. package/dist/model.d.ts.map +1 -1
  58. package/dist/model.js +0 -12
  59. package/dist/observation.d.ts +2 -2
  60. package/dist/observation.d.ts.map +1 -1
  61. package/dist/observation.js +7 -2
  62. package/dist/outcome.d.ts +0 -3
  63. package/dist/outcome.d.ts.map +1 -1
  64. package/dist/query.d.ts +4 -209
  65. package/dist/query.d.ts.map +1 -1
  66. package/dist/query.js +3 -505
  67. package/dist/reach.d.ts +7 -7
  68. package/dist/reach.d.ts.map +1 -1
  69. package/dist/reach.js +34 -45
  70. package/dist/react.d.ts +3 -1
  71. package/dist/react.d.ts.map +1 -1
  72. package/dist/readiness.d.ts +2 -15
  73. package/dist/readiness.d.ts.map +1 -1
  74. package/dist/readiness.js +4 -10
  75. package/dist/recovery.d.ts +1 -0
  76. package/dist/recovery.d.ts.map +1 -1
  77. package/dist/recovery.js +26 -0
  78. package/dist/refresh.d.ts +6 -4
  79. package/dist/refresh.d.ts.map +1 -1
  80. package/dist/refresh.js +26 -5
  81. package/dist/replica.d.ts +2 -2
  82. package/dist/replica.d.ts.map +1 -1
  83. package/dist/replica.js +2 -5
  84. package/dist/room-draft.d.ts +4 -3
  85. package/dist/room-draft.d.ts.map +1 -1
  86. package/dist/room-draft.js +65 -52
  87. package/dist/rows.d.ts +1 -17
  88. package/dist/rows.d.ts.map +1 -1
  89. package/dist/rows.js +4 -137
  90. package/dist/search.d.ts +12 -63
  91. package/dist/search.d.ts.map +1 -1
  92. package/dist/search.js +9 -52
  93. package/dist/shared.d.ts +0 -1
  94. package/dist/shared.d.ts.map +1 -1
  95. package/dist/shared.js +0 -10
  96. package/dist/status.d.ts +0 -4
  97. package/dist/status.d.ts.map +1 -1
  98. package/dist/store.d.ts +5 -20
  99. package/dist/store.d.ts.map +1 -1
  100. package/dist/store.js +10 -44
  101. package/dist/stored.d.ts +0 -6
  102. package/dist/stored.d.ts.map +1 -1
  103. package/dist/stored.js +42 -115
  104. package/dist/tables.d.ts +10 -34
  105. package/dist/tables.d.ts.map +1 -1
  106. package/dist/tables.js +23 -57
  107. package/dist/transfer.d.ts +0 -5
  108. package/dist/transfer.d.ts.map +1 -1
  109. package/dist/validation.d.ts +3 -4
  110. package/dist/validation.d.ts.map +1 -1
  111. package/dist/validation.js +39 -97
  112. package/dist/windows.d.ts +1 -12
  113. package/dist/windows.d.ts.map +1 -1
  114. package/dist/windows.js +2 -75
  115. package/dist/wire.d.ts +9 -4
  116. package/dist/wire.d.ts.map +1 -1
  117. package/dist/wire.js +3 -1
  118. package/package.json +2 -2
  119. package/dist/fulltext.d.ts +0 -27
  120. package/dist/fulltext.d.ts.map +0 -1
  121. package/dist/fulltext.js +0 -370
  122. package/dist/reads.d.ts +0 -11
  123. package/dist/reads.d.ts.map +0 -1
  124. package/dist/reads.js +0 -91
  125. package/dist/words.d.ts +0 -19
  126. package/dist/words.d.ts.map +0 -1
  127. package/dist/words.js +0 -156
@@ -1,8 +1,8 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
1
+ import { identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
2
2
  import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { 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';
@@ -12,7 +12,7 @@ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, composedOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
@@ -20,10 +20,9 @@ import { attachedFile } from './file.js';
20
20
  import { openedFiles } from './opened.js';
21
21
  import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
22
22
  import { createRecovery } from './recovery.js';
23
- import { doorReads } from './reads.js';
24
23
  import { advancement } from './refresh.js';
25
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
26
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
27
26
  import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
28
27
  import { same, saving, STARTING, transferring, } from './status.js';
29
28
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
@@ -31,10 +30,10 @@ 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';
33
+ import { roomDraftRevision } from './identity.js';
35
34
  import { ancestors, beneath, normalize } from './paths.js';
36
35
  import { openRuntimeStore, } from './store.js';
37
- import { changedCleanly, sameComposition, validatedCollection, } from './validation.js';
36
+ import { validatedNeighbourhood } from './validation.js';
38
37
  import { unconfirmed } from './transfer.js';
39
38
  import { governs, holds, narrowed, planReach, } from './reach.js';
40
39
  function within(root, path) {
@@ -59,37 +58,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
- // A build past its floor of text stops after a second and a half of work, so a Room whose text costs ten times the usual still reports progress within the 2 s bound.
61
+ // A build past its floor of text stops after a second and a half of work, its full-text writes included, so a Room whose text costs ten times the usual holds a Save, a query or the next part's progress behind no longer build.
63
62
  const DERIVE_LONGEST_MS = 1_500;
64
- const WALK_BYTES = 1 << 20;
65
63
  const QUERY_BYTES = 1 << 20;
66
64
  const STAGE_BYTES = 1 << 20;
67
65
  const STAGE_FLOOR = 1 << 16;
68
66
  // A batch aims at this much database work, so a Save or a query waits behind at most one short write.
69
67
  const STAGE_AIM_MS = 50;
70
68
  const STREAMS_HELD = 4;
71
- const NOTHING_HELD = { parts: 0, bytes: 0 };
72
- const QUEUED_BYTES = 1 << 23;
73
- // Bytes still arriving are reported at most this often, well inside the 2 s progress bound, so readiness is not rebuilt for every chunk the network hands over.
69
+ const NOTHING_HELD = { bytes: 0 };
70
+ const QUEUED_TEXT = 1 << 23;
71
+ // Bytes still arriving are reported at most this often, so readiness is not rebuilt for every chunk the network hands over while a slow link still moves the figure.
74
72
  const ARRIVAL_REPORT_MS = 250;
75
- const CLEAN_CHANGES = 2_000;
76
73
  // A Room that locks offline stays readable this long after the connection drops, so a laptop waking or changing network keeps working while a disconnected device locks soon after.
77
74
  const LOCK_GRACE_MS = 120_000;
78
75
  function operationOf(input) {
79
76
  const operation = input?.operation_id;
80
77
  return typeof operation === 'string' ? operation : null;
81
78
  }
82
- function abandonedOn(signal) {
83
- return new Promise((resolve) => {
84
- if (signal.aborted)
85
- resolve(null);
86
- else
87
- signal.addEventListener('abort', () => resolve(null), { once: true });
79
+ function readUntil(read, signal) {
80
+ if (signal.aborted)
81
+ return Promise.resolve({ kind: 'transport-uncertainty' });
82
+ return new Promise((resolve, reject) => {
83
+ const stop = () => resolve({ kind: 'transport-uncertainty' });
84
+ signal.addEventListener('abort', stop, { once: true });
85
+ read.then(resolve, reject).finally(() => signal.removeEventListener('abort', stop));
88
86
  });
89
87
  }
90
- function roomLane(locator) {
91
- return `room ${locator}`;
92
- }
93
88
  function pendingKey(locator, id) {
94
89
  return `${locator}\u0000${id}`;
95
90
  }
@@ -109,6 +104,7 @@ export function composeRuntime(options) {
109
104
  const usableWhenHeld = options.usableWhenHeld ?? [];
110
105
  const queuesWithoutReplica = new FencedMap(isFenced);
111
106
  const pendings = new Map();
107
+ const landedAhead = new Map();
112
108
  const seeds = new Map();
113
109
  const seedWatchers = new Map();
114
110
  const roomDrafts = new FencedMap(isFenced);
@@ -120,15 +116,24 @@ export function composeRuntime(options) {
120
116
  const counted = new Map();
121
117
  const unreadable = new Set();
122
118
  const builtIn = builtInModel(roomTypes);
119
+ const typedPaths = options.adapter.typedPaths ?? [];
123
120
  const models = new FencedMap(isFenced);
124
121
  const liveModels = new FencedMap(isFenced);
125
122
  const invalidModels = new Set();
126
123
  const discovered = new Map();
127
- 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
+ };
128
135
  let roomsListed = false;
129
136
  const scopedRefusals = new Map();
130
- const sourceRefusals = new Map();
131
- const declarationRefusals = new Set();
132
137
  function modelOf(locator) {
133
138
  return models.get(locator) ?? NO_MODEL;
134
139
  }
@@ -138,7 +143,7 @@ export function composeRuntime(options) {
138
143
  : modelOf(locator);
139
144
  }
140
145
  function composed(record) {
141
- return compositionOf(record, builtIn);
146
+ return compositionOf(record, builtIn, typedPaths);
142
147
  }
143
148
  function compositionFor(record) {
144
149
  return composed({ ...record, model: savedModel(record.locator) });
@@ -151,26 +156,26 @@ export function composeRuntime(options) {
151
156
  let closed = false;
152
157
  let stopping = false;
153
158
  let withdrawStream;
154
- const announced = new Map();
155
159
  let underway = null;
156
160
  let streamed = null;
157
161
  let handed = false;
162
+ let naming = 0;
158
163
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
159
164
  const repairing = new Set();
160
165
  const calling = new Set();
161
166
  const repairable = new Map();
162
- let outcomeRetention = null;
163
167
  const answering = new Map();
164
168
  let complete = false;
165
169
  let recorded = false;
166
170
  const stale = new Set();
167
171
  let generation = 0;
168
- let discovering = null;
169
- let opened = null;
170
- const owed = new Set();
171
- const reloadsDue = new Map();
172
+ const reloadsDue = new Set();
173
+ const highest = new Map();
174
+ const beyondHorizon = new Set();
172
175
  const lanes = new Map();
173
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
176
+ const roomLanes = new Map();
177
+ let roomWork = 0;
178
+ const roomReads = { free: ROOM_READS, waiting: [] };
174
179
  const working = new Set();
175
180
  let mutations = Promise.resolve();
176
181
  let gathering = null;
@@ -186,6 +191,8 @@ export function composeRuntime(options) {
186
191
  let withdrawConversations;
187
192
  let currencyQueued = null;
188
193
  let socketOpen = false;
194
+ let announced = false;
195
+ let missedChange = false;
189
196
  const confirmed = new Set();
190
197
  let lockGrace = null;
191
198
  const lockListeners = new Set();
@@ -195,6 +202,8 @@ export function composeRuntime(options) {
195
202
  const serviceModeListeners = new Set();
196
203
  let member = null;
197
204
  const memberListeners = new Set();
205
+ let authorityChanges = 0;
206
+ const authorityChangeListeners = new Set();
198
207
  const unpreparedDemand = new Set();
199
208
  const unpreparable = new Set();
200
209
  let preparing = false;
@@ -224,12 +233,10 @@ export function composeRuntime(options) {
224
233
  const streaming = new Set();
225
234
  const underived = new Map();
226
235
  const unindexed = new Map();
227
- const unchecked = new Map();
236
+ const startedOver = new Map();
228
237
  let arrivalReported = 0;
229
- let onScreen = options.shell.onScreen;
230
- const shownWaiters = [];
231
- let streams = { generation, free: STREAMS_HELD };
232
- let queuedBytes = 0;
238
+ let streamsFree = STREAMS_HELD;
239
+ let queuedText = 0;
233
240
  const installWaiters = [];
234
241
  const stageBudget = {
235
242
  bytes: STAGE_FLOOR * 4,
@@ -243,32 +250,17 @@ export function composeRuntime(options) {
243
250
  ceiling: DERIVE_BYTES,
244
251
  aim: DERIVE_AIM_MS,
245
252
  };
246
- const installTurns = { busy: false, finishing: [], starting: [] };
253
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
247
254
  const conversations = createConversationStore({
248
- agents: declaredAgents,
249
- documents: (declaration, after) => rowsQuery(planOf({
250
- rooms: [declaration.locator],
251
- types: [declaration.type],
252
- select: {
253
- fields: [
254
- declaration.identity,
255
- declaration.name,
256
- ...(declaration.retired === undefined ? [] : [declaration.retired]),
257
- ].map((field) => `/${escapePointer(field)}`),
258
- },
259
- limit: MAXIMUM_WINDOW,
260
- ...(after === undefined ? {} : { after }),
261
- }, roomFacts())),
262
255
  call: (name, input) => invokeAction(name, input),
256
+ history: (request) => readConversationHistory(request),
263
257
  durably,
264
258
  stream: () => options.adapter.conversations,
265
259
  publish: publishChangeSet,
266
260
  online: () => connectionOf() !== 'unreachable' && refusal === null,
267
- listed: () => roomsListed,
268
261
  full: () => full,
269
262
  closed: () => closed,
270
263
  unanswered: resendLater,
271
- expired: (operation) => pastHorizon(operation),
272
264
  });
273
265
  const journal = createLog();
274
266
  const keptFiles = openedFiles(database, isFenced);
@@ -361,9 +353,9 @@ export function composeRuntime(options) {
361
353
  return value === undefined ? null : structuredClone(value);
362
354
  }
363
355
  async function writtenRoomDraft(room, path, revision, input, next) {
364
- const parts = readParts(input);
356
+ const parts = readParts(input, path);
365
357
  if (path === '' || normalize(path) !== path)
366
- 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`);
367
359
  const local = await openLocal();
368
360
  const current = roomDrafts.get(room)?.get(path);
369
361
  if ((current?.revision ?? null) !== revision) {
@@ -412,7 +404,7 @@ export function composeRuntime(options) {
412
404
  const changed = new Map();
413
405
  for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
414
406
  for (const { batch, state } of entriesOf(room)) {
415
- if (state === 'accepted' || state === 'local')
407
+ if (state === 'local')
416
408
  continue;
417
409
  for (const { path } of batch.changes)
418
410
  changed.set(`${room}\u0000${path}`, { room, path });
@@ -433,16 +425,9 @@ export function composeRuntime(options) {
433
425
  returned: structuredClone(record.returned),
434
426
  });
435
427
  }
436
- for (const [room, refusal] of sourceRefusals)
437
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
438
428
  attentionRequired.sort((left, right) => {
439
429
  if (left.room !== right.room)
440
430
  return left.room < right.room ? -1 : 1;
441
- if ('refusal' in left || 'refusal' in right) {
442
- if ('refusal' in left && 'refusal' in right)
443
- return 0;
444
- return 'refusal' in left ? -1 : 1;
445
- }
446
431
  if (left.path !== right.path)
447
432
  return left.path < right.path ? -1 : 1;
448
433
  return 0;
@@ -597,9 +582,6 @@ export function composeRuntime(options) {
597
582
  if (localSaves)
598
583
  return Object.freeze({ batch, state: 'local' });
599
584
  const state = pendings.get(pendingKey(locator, batch.id));
600
- if (state?.accepted !== undefined) {
601
- return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
602
- }
603
585
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
604
586
  }
605
587
  function entriesOf(locator) {
@@ -673,11 +655,7 @@ export function composeRuntime(options) {
673
655
  })();
674
656
  }
675
657
  function roomsOfReach(reach) {
676
- return new Set([
677
- ...reach.rooms,
678
- ...reach.documents.map(roomOfKey),
679
- ...reach.areas.flatMap((area) => (area.types.length === 0 ? [] : [area.room])),
680
- ]);
658
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
681
659
  }
682
660
  function reachedRooms(plan) {
683
661
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -716,7 +694,7 @@ export function composeRuntime(options) {
716
694
  coverage.push({
717
695
  room: locator,
718
696
  generation: replica.record.transfer,
719
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
697
+ current: !(catching.has(locator) || transfers.has(locator)),
720
698
  });
721
699
  read.push({ locator, key: replica.record.key });
722
700
  for (const batch of replica.queue)
@@ -743,8 +721,6 @@ export function composeRuntime(options) {
743
721
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
744
722
  }
745
723
  function roomState(locator) {
746
- if (sourceRefusals.has(locator))
747
- return 'refused';
748
724
  const listedRoom = discovered.get(locator);
749
725
  if (roomsListed &&
750
726
  (listedRoom === undefined ||
@@ -759,7 +735,7 @@ export function composeRuntime(options) {
759
735
  }
760
736
  function conversationCoverage(reach) {
761
737
  const declared = reach === null || reach.conversations;
762
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
738
+ if (!declared || options.adapter.conversations === undefined)
763
739
  return 'undeclared';
764
740
  if (!conversations.known() || !conversations.current())
765
741
  return 'acquiring';
@@ -774,79 +750,50 @@ export function composeRuntime(options) {
774
750
  ? 'connected'
775
751
  : 'connecting';
776
752
  }
777
- function refusedIn(locators) {
778
- return locators.flatMap((room) => {
779
- const held = sourceRefusals.get(room);
780
- if (held === undefined)
781
- return [];
782
- const { file, location } = held.fields;
783
- return [
784
- {
785
- room,
786
- name: discovered.get(room)?.name ?? room,
787
- file: typeof file === 'string' ? file : '',
788
- rule: held.code,
789
- location: typeof location === 'string' ? location : null,
790
- },
791
- ];
792
- });
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 };
793
764
  }
794
765
  function installationOf(coverage) {
795
766
  const rooms = { done: 0, total: 0 };
796
- const parts = { done: 0, total: 0 };
797
767
  const bytes = { done: 0, total: 0 };
798
768
  let installing = 0;
799
769
  let downloading = false;
800
- let checking = 0;
801
770
  for (const { room, state } of coverage) {
802
771
  if (state === 'unadmitted')
803
772
  continue;
804
773
  rooms.total += 1;
805
774
  if (state === 'complete')
806
775
  rooms.done += 1;
807
- const transfer = transfers.get(room);
808
776
  const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
809
777
  if (underway)
810
778
  installing += 1;
811
779
  if (underway && (streaming.has(room) || !arrivals.has(room)))
812
780
  downloading = true;
813
- if (transfer !== undefined) {
814
- const check = unchecked.get(room);
815
- const unchecking = check === undefined ? 0 : check.bytes * (1 - Math.min(1, check.done / check.text));
816
- const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
817
- const arrived = arrivals.get(room);
818
- parts.done += transfer.done.parts;
819
- parts.total += Math.max(arrived?.parts ?? 0, transfer.done.parts);
820
- bytes.done += transfer.done.bytes - Math.ceil(owed + unchecking);
821
- checking += Math.ceil(owed + unchecking) - Math.ceil(owed);
822
- bytes.total += Math.max(arrived?.bytes ?? 0, transfer.done.bytes);
823
- continue;
824
- }
825
- const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
826
- const arrived = arrivals.get(room) ?? held;
827
- parts.done += held.parts;
828
- parts.total += arrived.parts;
829
- bytes.done += held.bytes;
830
- bytes.total += arrived.bytes;
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);
831
785
  }
832
786
  const step = installing === 0
833
787
  ? null
834
788
  : downloading
835
789
  ? 'downloading'
836
- : bytes.done + checking < bytes.total
790
+ : bytes.done < bytes.total
837
791
  ? 'writing'
838
792
  : 'replaying';
839
- return {
840
- step,
841
- paused: installing > 0 && !onScreen ? 'hidden' : null,
842
- rooms,
843
- parts,
844
- bytes,
845
- };
793
+ return { step, rooms, bytes };
846
794
  }
847
795
  function readinessFor(port, reach) {
848
796
  const required = requiredRooms(reach);
849
- const search = port.search === 'required';
850
797
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
851
798
  const preparing = coverage
852
799
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -859,14 +806,12 @@ export function composeRuntime(options) {
859
806
  listed: required.listed,
860
807
  rooms: coverage,
861
808
  conversations: conversationCoverage(reach),
862
- search: search ? 'required' : 'unused',
863
809
  },
864
810
  persistence,
865
811
  connection: connectionOf(),
866
812
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
867
- space: full ? 'full' : port.space,
813
+ space: full ? 'full' : null,
868
814
  installation: installationOf(coverage),
869
- refused: refusedIn(required.rooms),
870
815
  lost: storageLoss !== null,
871
816
  established: targets[port.target]?.established === true ||
872
817
  (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
@@ -954,8 +899,12 @@ export function composeRuntime(options) {
954
899
  unpreparable.clear();
955
900
  journal.record('space freed');
956
901
  queueReadiness();
957
- if (!closed && authorization !== undefined)
958
- 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();
959
908
  }
960
909
  function seedRoomHeld(room, version) {
961
910
  return indexed(room) && reachedBy(room, version);
@@ -1103,6 +1052,8 @@ export function composeRuntime(options) {
1103
1052
  }
1104
1053
  async function refuse(reason, provider = null) {
1105
1054
  discoveryPaused = true;
1055
+ inFlight.abort();
1056
+ inFlight = new AbortController();
1106
1057
  try {
1107
1058
  if (reason === 'revoked-authorization') {
1108
1059
  await exclusive(async () => {
@@ -1187,10 +1138,10 @@ export function composeRuntime(options) {
1187
1138
  providerOwnedPaths: room.providerOwnedPaths,
1188
1139
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1189
1140
  synchronizationAvailable,
1190
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1141
+ refusal: scopedRefusals.get(room.locator) ?? null,
1191
1142
  });
1192
1143
  }
1193
- async function applyDiscovery(found, started, covered) {
1144
+ async function applyDiscovery(found, started, left) {
1194
1145
  const lockedBefore = lockedRooms();
1195
1146
  for (const room of found) {
1196
1147
  const fence = fences.get(room.locator);
@@ -1204,18 +1155,11 @@ export function composeRuntime(options) {
1204
1155
  const listedLocators = new Set(listed.map((room) => room.locator));
1205
1156
  for (const locator of listedLocators)
1206
1157
  scopedRefusals.delete(locator);
1207
- if (covered === undefined)
1158
+ if (left === undefined)
1208
1159
  discovered.clear();
1209
- else
1210
- for (const locator of covered)
1211
- discovered.delete(locator);
1212
1160
  for (const room of listed)
1213
1161
  discovered.set(room.locator, room);
1214
- const unlisted = unlistedHeld(new Set(found.map((room) => room.locator))).filter((locator) => covered === undefined || covered.includes(locator));
1215
- if (options.adapter.exhaustiveDiscovery === true)
1216
- await loseRooms(unlisted);
1217
- else
1218
- unlistedKept(unlisted);
1162
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1219
1163
  for (const room of listed) {
1220
1164
  const held = replicas.get(room.locator)?.record;
1221
1165
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1246,15 +1190,6 @@ export function composeRuntime(options) {
1246
1190
  ]),
1247
1191
  ].filter((locator) => !listed.has(locator));
1248
1192
  }
1249
- function unlistedKept(unlisted) {
1250
- for (const locator of unlisted) {
1251
- const held = replicas.get(locator)?.record;
1252
- if (held !== undefined)
1253
- list(held, false);
1254
- else
1255
- rooms.delete(locator);
1256
- }
1257
- }
1258
1193
  function fence(locators) {
1259
1194
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1260
1195
  const removed = fresh.map((locator) => ({
@@ -1265,24 +1200,26 @@ export function composeRuntime(options) {
1265
1200
  fences.set(locator, { at: discoveries, erased: false });
1266
1201
  lastFenced.set(locator, ++fencings);
1267
1202
  confirmed.delete(locator);
1268
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1203
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1269
1204
  pendings.delete(pendingKey(locator, batch.id));
1205
+ landedAhead.delete(pendingKey(locator, batch.id));
1206
+ }
1270
1207
  replicas.delete(locator);
1208
+ highest.delete(locator);
1209
+ beyondHorizon.delete(locator);
1271
1210
  queuesWithoutReplica.delete(locator);
1272
1211
  rooms.delete(locator);
1273
1212
  roomDrafts.delete(locator);
1274
1213
  attachedFiles.delete(locator);
1275
1214
  untransferred(locator);
1215
+ startedOver.delete(locator);
1276
1216
  catching.delete(locator);
1277
1217
  discovered.delete(locator);
1278
- announced.delete(locator);
1279
1218
  unreadable.delete(locator);
1280
1219
  stale.delete(locator);
1281
1220
  repairable.delete(locator);
1282
1221
  unpreparable.delete(locator);
1283
1222
  scopedRefusals.delete(locator);
1284
- sourceRefusals.delete(locator);
1285
- declarationRefusals.delete(locator);
1286
1223
  models.delete(locator);
1287
1224
  liveModels.delete(locator);
1288
1225
  invalidModels.delete(locator);
@@ -1373,6 +1310,7 @@ export function composeRuntime(options) {
1373
1310
  transfers.clear();
1374
1311
  arrivals.clear();
1375
1312
  streaming.clear();
1313
+ startedOver.clear();
1376
1314
  limits = held.providerLimits;
1377
1315
  capabilities = held.capabilities;
1378
1316
  member = held.member;
@@ -1382,8 +1320,9 @@ export function composeRuntime(options) {
1382
1320
  presentations.clear();
1383
1321
  for (const record of held.presentation)
1384
1322
  presentations.set(record.app, record.value);
1385
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1323
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1386
1324
  pendings.clear();
1325
+ landedAhead.clear();
1387
1326
  for (const queue of queues) {
1388
1327
  for (const { batch, state } of queue.pending) {
1389
1328
  pendings.set(pendingKey(queue.locator, batch.id), state);
@@ -1634,8 +1573,6 @@ export function composeRuntime(options) {
1634
1573
  function unpairedIn(locator, batch) {
1635
1574
  if (!queueIn(locator).some((held) => held.id === batch.id))
1636
1575
  return false;
1637
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1638
- return false;
1639
1576
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1640
1577
  }
1641
1578
  async function together(local, settlements) {
@@ -1678,10 +1615,16 @@ export function composeRuntime(options) {
1678
1615
  ? read
1679
1616
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1680
1617
  const composition = composed(record);
1681
- const { withdrawn, kept } = await stepped(standing(confirmedLayer, queue, withdrawable(locator), localSaves, locator, composition));
1682
- const founded = kept.filter((batch) => !queue.includes(batch));
1618
+ const shown = queue.filter((batch) => {
1619
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1620
+ return (held !== undefined &&
1621
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1622
+ });
1623
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1624
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1625
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1683
1626
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1684
- const unqueued = [];
1627
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1685
1628
  for (const batch of withdrawn) {
1686
1629
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1687
1630
  }
@@ -1746,8 +1689,10 @@ export function composeRuntime(options) {
1746
1689
  commit,
1747
1690
  apply: (answer) => {
1748
1691
  for (const batch of previousQueue) {
1749
- if (!staying.has(batch.id))
1750
- pendings.delete(pendingKey(locator, batch.id));
1692
+ if (staying.has(batch.id))
1693
+ continue;
1694
+ pendings.delete(pendingKey(locator, batch.id));
1695
+ landedAhead.delete(pendingKey(locator, batch.id));
1751
1696
  }
1752
1697
  for (const draft of written.values())
1753
1698
  rememberRoomDraft(draft);
@@ -1804,28 +1749,34 @@ export function composeRuntime(options) {
1804
1749
  models.set(locator, state);
1805
1750
  return { collection: state };
1806
1751
  }
1807
- function publish(local, room, record, landing, live) {
1752
+ function publish(local, room, reached, landing, live) {
1808
1753
  const landed = exclusive(async () => {
1809
1754
  if (!live()) {
1810
1755
  caughtUpTo(room.locator);
1811
1756
  return true;
1812
1757
  }
1813
1758
  const prior = replicas.get(room.locator);
1814
- if (landing.kind === 'refreshed' &&
1815
- (prior === undefined || prior.record.key !== record.key || prior.confirmed !== landing.base)) {
1816
- await local.unrefresh(record.key);
1759
+ if (!stands(room.locator, landing.from)) {
1760
+ if (landing.kind === 'refreshed') {
1761
+ await local.unrefresh(reached.key);
1762
+ return false;
1763
+ }
1764
+ caughtUpTo(room.locator);
1765
+ await abandoned(local, room.locator);
1817
1766
  return false;
1818
1767
  }
1768
+ const record = landing.kind === 'refreshed' && prior !== undefined
1769
+ ? {
1770
+ ...prior.record,
1771
+ roomVersion: reached.roomVersion,
1772
+ ...(landing.remodelled ? { model: reached.model } : {}),
1773
+ }
1774
+ : reached;
1819
1775
  adoptModels(room.locator, record);
1820
1776
  const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
1821
- const roomVersion = record.roomVersion;
1822
- const accepted = queue.filter((batch) => {
1823
- const at = pendings.get(pendingKey(room.locator, batch.id))?.accepted;
1824
- return at !== undefined && options.adapter.reached(room.locator, roomVersion, at);
1825
- });
1826
1777
  let set;
1827
1778
  try {
1828
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1779
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1829
1780
  ? {
1830
1781
  room: true,
1831
1782
  view: 'refreshed',
@@ -1847,24 +1798,24 @@ export function composeRuntime(options) {
1847
1798
  return true;
1848
1799
  }
1849
1800
  if (landing.kind === 'bootstrapped') {
1801
+ highest.delete(room.locator);
1802
+ beyondHorizon.delete(room.locator);
1850
1803
  untransferred(room.locator);
1804
+ startedOver.delete(room.locator);
1851
1805
  catching.delete(room.locator);
1852
- sweep();
1806
+ if (prior !== undefined && prior.record.key !== record.key)
1807
+ sweep();
1808
+ name(room.locator);
1853
1809
  }
1854
- for (const batch of accepted)
1855
- pendings.delete(pendingKey(room.locator, batch.id));
1856
1810
  stale.delete(room.locator);
1857
1811
  unpreparable.delete(room.locator);
1858
1812
  if (unreadable.delete(room.locator))
1859
1813
  scopedRefusals.delete(room.locator);
1860
- sourceRefusals.delete(room.locator);
1861
- declarationRefusals.delete(room.locator);
1862
1814
  list(room, true);
1863
1815
  publishChangeSet(set);
1864
1816
  await unpair(local);
1865
1817
  moveTo({ ...status, ...attention() });
1866
- options.adapter.roomStream?.published(room.locator, roomVersion);
1867
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1818
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1868
1819
  submitLater(room.locator);
1869
1820
  }
1870
1821
  return true;
@@ -1876,7 +1827,7 @@ export function composeRuntime(options) {
1876
1827
  const replica = replicas.get(locator);
1877
1828
  const room = rooms.get(locator);
1878
1829
  if (replica === undefined || room === undefined) {
1879
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1830
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1880
1831
  }
1881
1832
  return { local, replica, room };
1882
1833
  }
@@ -1968,18 +1919,6 @@ export function composeRuntime(options) {
1968
1919
  return undefined;
1969
1920
  return entry.binary !== null ? null : entry.content;
1970
1921
  }
1971
- function walkOf(local, key, view) {
1972
- return async (after) => {
1973
- const page = await local.walk(key, view, after, WALK_BYTES);
1974
- return {
1975
- entries: page.entries.map(({ path, content, binary }) => ({
1976
- path,
1977
- content: binary !== null ? null : content,
1978
- })),
1979
- next: page.next,
1980
- };
1981
- };
1982
- }
1983
1922
  function lookupOf(local, key, view) {
1984
1923
  return async (paths) => {
1985
1924
  if (paths.length === 0)
@@ -1996,37 +1935,6 @@ export function composeRuntime(options) {
1996
1935
  const read = parseFrontmatter(entry.content ?? '');
1997
1936
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1998
1937
  }
1999
- function overriddenWalk(walk, overrides) {
2000
- const sorted = [...overrides.keys()].sort();
2001
- const beyond = (path) => {
2002
- if (path === null)
2003
- return 0;
2004
- let low = 0;
2005
- let high = sorted.length;
2006
- while (low < high) {
2007
- const middle = (low + high) >>> 1;
2008
- if (sorted[middle] <= path)
2009
- low = middle + 1;
2010
- else
2011
- high = middle;
2012
- }
2013
- return low;
2014
- };
2015
- return async (after) => {
2016
- const page = await walk(after);
2017
- const end = page.next;
2018
- const added = sorted
2019
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
2020
- .flatMap((path) => {
2021
- const entry = overrides.get(path);
2022
- return entry === null || entry === undefined
2023
- ? []
2024
- : [{ path, content: entry.binary !== null ? null : entry.content }];
2025
- });
2026
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
2027
- return { entries, next: end };
2028
- };
2029
- }
2030
1938
  function overriddenLookup(lookup, overrides) {
2031
1939
  return async (paths) => {
2032
1940
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2044,9 +1952,8 @@ export function composeRuntime(options) {
2044
1952
  }
2045
1953
  function validatorOf(record, confirmedLayer) {
2046
1954
  return async (resulting, touched) => {
2047
- const composition = compositionFor(record);
2048
1955
  const local = await openLocal();
2049
- const changed = [];
1956
+ const changes = [];
2050
1957
  const overrides = new Map();
2051
1958
  const pace = paced();
2052
1959
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2057,19 +1964,13 @@ export function composeRuntime(options) {
2057
1964
  continue;
2058
1965
  overrides.set(path, now);
2059
1966
  if (textOf(was) !== textOf(now))
2060
- changed.push({ path, was: textOf(was), now: textOf(now) });
1967
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2061
1968
  }
2062
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2063
- const clean = available(composition) &&
2064
- changed.length <= CLEAN_CHANGES &&
2065
- (await changedCleanly(changed, composed(record), composition, {
2066
- lookup,
2067
- texts: textsOf(local, record.key, 'confirmed'),
2068
- referrers: referrersOf(local, record.key, 'confirmed'),
2069
- }));
2070
- if (clean)
2071
- return { verdict: 'accept' };
2072
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
1969
+ return validatedNeighbourhood(changes, compositionFor(record), {
1970
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
1971
+ texts: textsOf(local, record.key, 'confirmed'),
1972
+ referrers: referrersOf(local, record.key, 'confirmed'),
1973
+ });
2073
1974
  };
2074
1975
  }
2075
1976
  function textsOf(local, key, view) {
@@ -2152,7 +2053,7 @@ export function composeRuntime(options) {
2152
2053
  const target = replicas.get(destination);
2153
2054
  const listedRoom = rooms.get(destination);
2154
2055
  if (target === undefined || listedRoom === undefined) {
2155
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2056
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2156
2057
  }
2157
2058
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2158
2059
  const built = await buildMove({
@@ -2215,10 +2116,10 @@ export function composeRuntime(options) {
2215
2116
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2216
2117
  const { answer, touched } = committedBoth;
2217
2118
  if (answer.fenced !== null) {
2218
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2119
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2219
2120
  }
2220
2121
  if (fences.has(locator) || fences.has(destination)) {
2221
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2122
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2222
2123
  }
2223
2124
  pendings.set(pendingKey(locator, id), {
2224
2125
  drafts: saved,
@@ -2389,10 +2290,10 @@ export function composeRuntime(options) {
2389
2290
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2390
2291
  const { answer, touched } = done;
2391
2292
  if (answer.fenced !== null) {
2392
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2293
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2393
2294
  }
2394
2295
  if (fences.has(locator))
2395
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2296
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2396
2297
  for (const origin of withdrawn.keys())
2397
2298
  pendings.delete(pendingKey(locator, origin));
2398
2299
  for (const [id, drafts] of drafted) {
@@ -2441,7 +2342,7 @@ export function composeRuntime(options) {
2441
2342
  throw new Error('connection refused');
2442
2343
  await local.keepUpload(locator, identifier, upload);
2443
2344
  if (fences.has(locator))
2444
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2345
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2445
2346
  const key = pendingKey(locator, identifier);
2446
2347
  pendings.set(key, { ...pendings.get(key), upload });
2447
2348
  if (!continuing())
@@ -2715,13 +2616,6 @@ export function composeRuntime(options) {
2715
2616
  const freed = await database.ask({ kind: 'opened.clean' });
2716
2617
  return freed + ((await options.shell.cleanUp?.()) ?? 0);
2717
2618
  }
2718
- function declaredAgents() {
2719
- for (const capability of capabilities) {
2720
- if (capability.conversationAgents !== undefined)
2721
- return capability.conversationAgents;
2722
- }
2723
- return undefined;
2724
- }
2725
2619
  function declaredAction(name) {
2726
2620
  for (const capability of capabilities) {
2727
2621
  for (const action of capability.actions ?? []) {
@@ -2773,6 +2667,24 @@ export function composeRuntime(options) {
2773
2667
  return UNCERTAIN;
2774
2668
  }
2775
2669
  }
2670
+ async function readConversationHistory(request) {
2671
+ await ready;
2672
+ const stream = options.adapter.conversations;
2673
+ if (closed || refusal !== null || stream?.history === undefined)
2674
+ return UNCERTAIN;
2675
+ journal.record('transfer conversation history');
2676
+ try {
2677
+ const result = await stream.history(request);
2678
+ if (result.kind === 'revoked-authorization')
2679
+ await refuse('revoked-authorization');
2680
+ journal.record(`outcome conversation history ${result.kind}`);
2681
+ return calledResult(result);
2682
+ }
2683
+ catch {
2684
+ journal.record('outcome conversation history transport-uncertainty');
2685
+ return UNCERTAIN;
2686
+ }
2687
+ }
2776
2688
  function unsettled(result) {
2777
2689
  return result.kind !== 'action' && result.retryable;
2778
2690
  }
@@ -2811,10 +2723,6 @@ export function composeRuntime(options) {
2811
2723
  continue;
2812
2724
  if (closed || signal.aborted)
2813
2725
  return 'done';
2814
- if (pastHorizon(held.operation)) {
2815
- await local.dropCall(held.operation);
2816
- continue;
2817
- }
2818
2726
  calling.add(held.operation);
2819
2727
  try {
2820
2728
  if (unsettled(await invokeAction(held.name, held.input)))
@@ -2834,6 +2742,33 @@ export function composeRuntime(options) {
2834
2742
  const held = replicas.get(locator);
2835
2743
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2836
2744
  }
2745
+ function shownAt(locator) {
2746
+ const held = replicas.get(locator)?.record.roomVersion;
2747
+ const raised = highest.get(locator);
2748
+ if (held === undefined || raised === undefined)
2749
+ return held ?? raised;
2750
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2751
+ }
2752
+ function showing(locator, roomVersion) {
2753
+ const at = shownAt(locator);
2754
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2755
+ highest.set(locator, roomVersion);
2756
+ }
2757
+ }
2758
+ function older(locator, roomVersion) {
2759
+ if (beyondHorizon.has(locator))
2760
+ return false;
2761
+ const at = shownAt(locator);
2762
+ return (at !== undefined &&
2763
+ options.adapter.reached(locator, at, roomVersion) &&
2764
+ !options.adapter.reached(locator, roomVersion, at));
2765
+ }
2766
+ function replaced(locators) {
2767
+ for (const locator of locators) {
2768
+ highest.delete(locator);
2769
+ beyondHorizon.add(locator);
2770
+ }
2771
+ }
2837
2772
  async function writtenAfter(locator, roomVersion, path) {
2838
2773
  const held = replicas.get(locator);
2839
2774
  if (held === undefined)
@@ -2844,122 +2779,27 @@ export function composeRuntime(options) {
2844
2779
  options.adapter.reached(locator, written, roomVersion) &&
2845
2780
  !options.adapter.reached(locator, roomVersion, written));
2846
2781
  }
2847
- function overriddenTexts(texts, overrides) {
2848
- return async (paths) => {
2849
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2850
- for (const path of paths) {
2851
- const entry = overrides.get(path);
2852
- if (entry === undefined)
2853
- continue;
2854
- if (entry === null)
2855
- known.delete(path);
2856
- else
2857
- known.set(path, entry.binary !== null ? null : entry.content);
2858
- }
2859
- return known;
2860
- };
2861
- }
2862
- async function unreadableRoom(texts, walk, paths) {
2863
- if (paths !== null) {
2864
- const text = {};
2865
- for (const [path, content] of await texts(paths))
2866
- if (content !== null)
2867
- text[path] = content;
2868
- return stepped(unreadableIn(text, Object.keys(text)));
2869
- }
2870
- for (let after = null;;) {
2871
- const page = await walk(after);
2872
- const text = {};
2873
- for (const { path, content } of page.entries)
2874
- if (content !== null)
2875
- text[path] = content;
2876
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2877
- if (refused !== null)
2878
- return refused;
2879
- if (page.next === null)
2880
- return null;
2881
- after = page.next;
2882
- }
2883
- }
2884
- async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
2885
- const composition = composed(record);
2886
- const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2887
- const lookup = overrides === null
2888
- ? lookupOf(local, record.key, view)
2889
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2890
- const texts = overrides === null
2891
- ? textsOf(local, record.key, view)
2892
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2893
- const walking = overrides === null
2894
- ? walkOf(local, record.key, view)
2895
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2896
- const walk = checked === null
2897
- ? walking
2898
- : async (after) => {
2899
- const page = await walking(after);
2900
- checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
2901
- return page;
2902
- };
2903
- let changes = null;
2904
- let reshaped = false;
2905
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2906
- const [was, now] = await Promise.all([
2907
- local.entries(record.key, 'confirmed', previous.changed),
2908
- overrides === null
2909
- ? local.entries(record.key, view, previous.changed)
2910
- : Promise.resolve([]),
2911
- ]);
2912
- const before = new Map(was.map((entry) => [entry.path, entry]));
2913
- const after = overrides === null
2914
- ? new Map(now.map((entry) => [entry.path, entry]))
2915
- : overrides;
2916
- changes = previous.changed.flatMap((path) => {
2917
- const one = before.get(path) ?? null;
2918
- const other = after.get(path) ?? null;
2919
- reshaped ||= textDiffers(one, other);
2920
- const held = textOf(one);
2921
- const next = textOf(other);
2922
- return held === next ? [] : [{ path, was: held, now: next }];
2923
- });
2924
- }
2925
- const named = changes === null ? null : changes.map(({ path }) => path);
2926
- const textChanged = changes === null || changes.length > 0 || reshaped;
2927
- const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2928
- if (modeled && (textChanged || modelsChanged)) {
2929
- const clean = previous !== null &&
2930
- changes !== null &&
2931
- (await changedCleanly(changes, composed(previous.record), composition, {
2932
- lookup,
2933
- texts,
2934
- referrers: referrersOf(local, record.key, 'confirmed'),
2935
- }));
2936
- if (clean)
2937
- return null;
2938
- const verdict = await validatedCollection(walk, lookup, composition);
2939
- if (verdict.verdict === 'accept')
2940
- return null;
2941
- if (verdict.rule !== 'model-unavailable')
2942
- return { kind: 'refused', refusal: verdict };
2943
- return unreadableRoom(texts, walk, named);
2944
- }
2945
- if (textChanged)
2946
- return unreadableRoom(texts, walk, named);
2947
- return null;
2782
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2783
+ if (paths.length === 0 || !older(locator, roomVersion))
2784
+ return new Set();
2785
+ const entries = await local.entries(key, 'confirmed', paths);
2786
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2787
+ options.adapter.reached(locator, written, roomVersion) &&
2788
+ !options.adapter.reached(locator, roomVersion, written)
2789
+ ? [path]
2790
+ : []));
2948
2791
  }
2949
- function refreshingOf(local, key, composition, liveness, replaying = null) {
2950
- const write = replaying === null ? exclusive : finishingWrite;
2792
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2793
+ const write = replaying ? finishingWrite : exclusive;
2951
2794
  return {
2952
- async turn() {
2953
- if (replaying === null)
2954
- return;
2955
- installing(replaying);
2956
- await screened();
2795
+ turn() {
2796
+ if (replaying)
2797
+ installing(locator);
2957
2798
  },
2958
2799
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2800
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2959
2801
  async stage(page) {
2960
2802
  for (let written = 0;;) {
2961
- if (replaying !== null)
2962
- await screened();
2963
2803
  const end = batchEnd(page.entries, written);
2964
2804
  const last = end === page.entries.length;
2965
2805
  const entries = page.entries.slice(written, end);
@@ -3011,38 +2851,64 @@ export function composeRuntime(options) {
3011
2851
  }
3012
2852
  function installTurn(queue, work) {
3013
2853
  return new Promise((resolve, reject) => {
3014
- queue.push(() => void exclusive(work).then(resolve, reject).finally(nextInstallTurn));
3015
- if (!installTurns.busy)
2854
+ queue.push(() => void exclusive(work)
2855
+ .then(resolve, reject)
2856
+ .finally(() => {
2857
+ installTurns.busy = false;
3016
2858
  nextInstallTurn();
2859
+ }));
2860
+ nextInstallTurn();
3017
2861
  });
3018
2862
  }
3019
2863
  function nextInstallTurn() {
2864
+ if (installTurns.busy || installTurns.ahead > 0)
2865
+ return;
3020
2866
  const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3021
- installTurns.busy = turn !== undefined;
3022
- turn?.();
2867
+ if (turn === undefined)
2868
+ return;
2869
+ installTurns.busy = true;
2870
+ turn();
2871
+ }
2872
+ async function ahead(work) {
2873
+ let taken = false;
2874
+ const hold = {
2875
+ take() {
2876
+ if (taken)
2877
+ return;
2878
+ taken = true;
2879
+ installTurns.ahead += 1;
2880
+ },
2881
+ release() {
2882
+ if (!taken)
2883
+ return;
2884
+ taken = false;
2885
+ installTurns.ahead -= 1;
2886
+ resumeInstall();
2887
+ },
2888
+ };
2889
+ hold.take();
2890
+ try {
2891
+ return await work(hold);
2892
+ }
2893
+ finally {
2894
+ hold.release();
2895
+ }
3023
2896
  }
3024
- function screened() {
3025
- if (onScreen || closed)
3026
- return Promise.resolve();
3027
- queueReadiness();
3028
- return new Promise((resolve) => shownWaiters.push(resolve));
2897
+ function resumeInstall() {
2898
+ if (installTurns.ahead > 0)
2899
+ return;
2900
+ void yielded().then(nextInstallTurn);
3029
2901
  }
3030
2902
  async function heldStream() {
3031
- for (;;) {
3032
- if (streams.generation !== generation)
3033
- streams = { generation, free: STREAMS_HELD };
3034
- const held = streams;
3035
- if (held.free > 0) {
3036
- held.free -= 1;
3037
- return once(() => {
3038
- held.free += 1;
3039
- });
3040
- }
2903
+ while (streamsFree === 0)
3041
2904
  await new Promise((admit) => installWaiters.push(admit));
3042
- }
2905
+ streamsFree -= 1;
2906
+ return once(() => {
2907
+ streamsFree += 1;
2908
+ });
3043
2909
  }
3044
2910
  async function unqueued() {
3045
- while (queuedBytes >= QUEUED_BYTES) {
2911
+ while (queuedText >= QUEUED_TEXT) {
3046
2912
  await new Promise((admit) => installWaiters.push(admit));
3047
2913
  }
3048
2914
  }
@@ -3067,14 +2933,19 @@ export function composeRuntime(options) {
3067
2933
  streaming.delete(locator);
3068
2934
  underived.delete(locator);
3069
2935
  unindexed.delete(locator);
3070
- unchecked.delete(locator);
2936
+ }
2937
+ function setBack(locator) {
2938
+ const now = bytesOf(locator, true);
2939
+ const earlier = startedOver.get(locator);
2940
+ startedOver.set(locator, {
2941
+ done: Math.max(now.done, earlier?.done ?? 0),
2942
+ total: Math.max(now.total, earlier?.total ?? 0),
2943
+ });
3071
2944
  }
3072
2945
  function owedIndexes(locator, bytes, text) {
3073
2946
  if (bytes <= 0 || text <= 0)
3074
2947
  return;
3075
- unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
3076
- const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
3077
- unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
2948
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
3078
2949
  }
3079
2950
  function built(locator, { done, text }) {
3080
2951
  const owed = unindexed.get(locator) ?? [];
@@ -3100,41 +2971,34 @@ export function composeRuntime(options) {
3100
2971
  if (installs.delete(locator))
3101
2972
  queueReadiness();
3102
2973
  }
3103
- function screen(value) {
3104
- if (value === onScreen)
3105
- return;
3106
- onScreen = value;
3107
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3108
- queueReadiness();
3109
- if (!value)
3110
- return;
3111
- for (const resolve of shownWaiters.splice(0))
3112
- resolve();
3113
- }
3114
- async function pullRefresh(authorized, held, liveness) {
2974
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3115
2975
  if (!liveness())
3116
2976
  return unconfirmed;
3117
- const turn = generation;
3118
2977
  const { record } = held;
3119
- const base = held.confirmed;
2978
+ const from = standingOf(record.locator);
3120
2979
  const local = await openLocal();
3121
2980
  try {
3122
2981
  await exclusive(() => local.unrefresh(record.key));
3123
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
2982
+ const result = await advancement(async (authorization, request) => {
2983
+ hold.release();
2984
+ try {
2985
+ return await refresh(authorization, request);
2986
+ }
2987
+ finally {
2988
+ hold.take();
2989
+ }
2990
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3124
2991
  if (result.kind !== 'advanced') {
3125
2992
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
3126
2993
  return result;
3127
2994
  }
3128
- const refused = await validatedRoom(local, result.record, 'refreshed', {
3129
- record,
3130
- changed: [...result.changed],
3131
- });
3132
- if (refused !== null) {
3133
- journal.record(`transfer refresh ${record.locator} refused`);
3134
- return refused;
3135
- }
3136
2995
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3137
- return { kind: 'advanced', record: result.record, base };
2996
+ return {
2997
+ kind: 'advanced',
2998
+ record: result.record,
2999
+ from,
3000
+ remodelled: result.record.model !== record.model,
3001
+ };
3138
3002
  }
3139
3003
  catch (error) {
3140
3004
  if (refusedForSpace(error))
@@ -3142,9 +3006,6 @@ export function composeRuntime(options) {
3142
3006
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
3143
3007
  throw error;
3144
3008
  }
3145
- finally {
3146
- reads.finished(record.locator, turn);
3147
- }
3148
3009
  }
3149
3010
  const heldContents = new Map();
3150
3011
  function heldContentOf(local, key) {
@@ -3162,11 +3023,17 @@ export function composeRuntime(options) {
3162
3023
  heldContents.set(key, made);
3163
3024
  return made;
3164
3025
  }
3026
+ async function abandoned(local, locator) {
3027
+ await local.abandon(locator);
3028
+ setBack(locator);
3029
+ untransferred(locator);
3030
+ liveQueries.recover();
3031
+ sweep();
3032
+ }
3165
3033
  function staging(local, liveness) {
3166
3034
  const derived = async (locator, key, composition) => {
3167
3035
  underived.delete(locator);
3168
3036
  for (;;) {
3169
- await screened();
3170
3037
  if (!liveness())
3171
3038
  return false;
3172
3039
  const done = await finishingWrite(async () => liveness()
@@ -3183,19 +3050,13 @@ export function composeRuntime(options) {
3183
3050
  held: (locator) => transfers.get(locator) ?? null,
3184
3051
  async turn(locator, step) {
3185
3052
  installing(locator);
3186
- for (;;) {
3187
- await screened();
3188
- const release = step === 'downloading' ? await heldStream() : () => { };
3189
- if (onScreen || closed)
3190
- return release;
3191
- release();
3192
- }
3053
+ return step === 'downloading' ? heldStream() : () => { };
3193
3054
  },
3194
3055
  unqueued,
3195
- queued(bytes) {
3196
- queuedBytes += bytes;
3056
+ queued(text) {
3057
+ queuedText += text;
3197
3058
  return once(() => {
3198
- queuedBytes -= bytes;
3059
+ queuedText -= text;
3199
3060
  });
3200
3061
  },
3201
3062
  arriving(locator) {
@@ -3204,7 +3065,7 @@ export function composeRuntime(options) {
3204
3065
  at(bytes) {
3205
3066
  if (ended)
3206
3067
  return;
3207
- arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
3068
+ arrivals.set(locator, bytes);
3208
3069
  streaming.add(locator);
3209
3070
  const now = performance.now();
3210
3071
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
@@ -3212,14 +3073,6 @@ export function composeRuntime(options) {
3212
3073
  arrivalReported = now;
3213
3074
  queueReadiness();
3214
3075
  },
3215
- part() {
3216
- const arrived = arrivals.get(locator);
3217
- arrivals.set(locator, {
3218
- parts: (arrived?.parts ?? 0) + 1,
3219
- bytes: arrived?.bytes ?? 0,
3220
- });
3221
- queueReadiness();
3222
- },
3223
3076
  end() {
3224
3077
  if (ended)
3225
3078
  return;
@@ -3229,14 +3082,10 @@ export function composeRuntime(options) {
3229
3082
  },
3230
3083
  };
3231
3084
  },
3232
- announced(locator) {
3233
- const later = announced.get(locator);
3234
- return later === undefined ? [] : [later];
3235
- },
3085
+ streamed: options.adapter.roomStream !== undefined,
3236
3086
  async stage(record, page, composition) {
3237
3087
  const { locator } = record;
3238
3088
  installing(locator);
3239
- await screened();
3240
3089
  if (!liveness())
3241
3090
  return null;
3242
3091
  const key = await installWrite(async () => {
@@ -3265,19 +3114,16 @@ export function composeRuntime(options) {
3265
3114
  derive: derived,
3266
3115
  async refreshing(locator, key, composition) {
3267
3116
  await exclusive(() => local.unrefresh(key));
3268
- return refreshingOf(local, key, composition, liveness, locator);
3117
+ return refreshingOf(local, locator, key, composition, liveness, true);
3269
3118
  },
3270
3119
  abandon: (locator) => exclusive(async () => {
3271
- if (!liveness())
3272
- return;
3273
- await local.abandon(locator);
3274
- untransferred(locator);
3275
- liveQueries.recover();
3276
- sweep();
3120
+ if (liveness())
3121
+ await abandoned(local, locator);
3277
3122
  }),
3278
3123
  setAside: async (locator) => {
3279
3124
  if (!liveness())
3280
3125
  return;
3126
+ setBack(locator);
3281
3127
  untransferred(locator);
3282
3128
  liveQueries.recover();
3283
3129
  },
@@ -3286,19 +3132,6 @@ export function composeRuntime(options) {
3286
3132
  sweep();
3287
3133
  }),
3288
3134
  content: (key) => heldContentOf(local, key),
3289
- async validate(record, view) {
3290
- const { locator } = record;
3291
- const refused = await validatedRoom(local, record, view, null, null, (text) => {
3292
- const check = unchecked.get(locator);
3293
- if (check === undefined)
3294
- return;
3295
- unchecked.set(locator, { ...check, done: check.done + text });
3296
- queueReadiness();
3297
- });
3298
- unchecked.delete(locator);
3299
- queueReadiness();
3300
- return refused;
3301
- },
3302
3135
  };
3303
3136
  }
3304
3137
  function reusable(room, local) {
@@ -3310,15 +3143,21 @@ export function composeRuntime(options) {
3310
3143
  async function pullBootstrap(authorized, room, local, liveness) {
3311
3144
  if (!liveness())
3312
3145
  return unconfirmed;
3313
- const turn = generation;
3314
3146
  if (replicas.has(room.locator)) {
3315
3147
  catching.add(room.locator);
3316
3148
  liveQueries.recover();
3317
3149
  }
3318
3150
  try {
3319
- 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));
3151
+ 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));
3320
3152
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3321
3153
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3154
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3155
+ caughtUpTo(room.locator);
3156
+ installed(room.locator);
3157
+ await abandoned(local, room.locator);
3158
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3159
+ return { kind: 'transport-uncertainty' };
3160
+ }
3322
3161
  if (result.kind !== 'staged') {
3323
3162
  caughtUpTo(room.locator);
3324
3163
  installed(room.locator);
@@ -3333,9 +3172,6 @@ export function composeRuntime(options) {
3333
3172
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3334
3173
  throw error;
3335
3174
  }
3336
- finally {
3337
- reads.finished(room.locator, turn);
3338
- }
3339
3175
  }
3340
3176
  function caughtUpTo(locator) {
3341
3177
  if (catching.delete(locator))
@@ -3343,72 +3179,83 @@ export function composeRuntime(options) {
3343
3179
  }
3344
3180
  function behind(room) {
3345
3181
  const held = replicas.get(room.locator)?.record.roomVersion;
3346
- return (held !== undefined &&
3347
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3182
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3348
3183
  }
3349
- async function advance(room, local, authorized, live) {
3350
- const carried = reads.carried(room.locator);
3351
- const withheld = await advanceOnce(room, local, authorized, live);
3352
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3353
- return withheld;
3354
- }
3355
- return advanceOnce(room, local, authorized, live);
3184
+ function standingOf(locator) {
3185
+ const held = replicas.get(locator);
3186
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3356
3187
  }
3357
- async function advanceOnce(room, local, authorized, running) {
3188
+ function stands(locator, from) {
3189
+ const now = standingOf(locator);
3190
+ return now.key === from.key && now.confirmed === from.confirmed;
3191
+ }
3192
+ async function advance(room, local, authorized, running) {
3358
3193
  const live = () => running() && !full;
3359
3194
  if (!live())
3360
3195
  return unconfirmed;
3361
3196
  const held = replicas.get(room.locator);
3362
- if (held !== undefined) {
3363
- const refreshed = await pullRefresh(authorized, held, live);
3364
- if (!live())
3365
- return unconfirmed;
3366
- if (refreshed.kind === 'still-current') {
3367
- stale.delete(room.locator);
3368
- if (sourceRefusals.has(room.locator))
3369
- return null;
3370
- if (unreadable.delete(room.locator)) {
3371
- scopedRefusals.delete(room.locator);
3372
- list(room, true);
3373
- emitRooms();
3374
- }
3375
- return null;
3376
- }
3377
- if (refreshed.kind === 'advanced') {
3378
- const landing = { kind: 'refreshed', base: refreshed.base };
3379
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3380
- const now = replicas.get(room.locator)?.record.roomVersion;
3381
- const reached = now !== undefined &&
3382
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3383
- return reached ? null : advance(room, local, authorized, live);
3384
- }
3385
- await retainModel(local, room, modelOf(room.locator));
3386
- return null;
3387
- }
3388
- if (refreshed.kind !== 'beyond-horizon')
3197
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3198
+ stale.delete(room.locator);
3199
+ name(room.locator);
3200
+ return null;
3201
+ }
3202
+ const refresh = reads.refresh;
3203
+ if (held !== undefined && refresh !== undefined) {
3204
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3205
+ if (refreshed === 'behind')
3206
+ return advance(room, local, authorized, live);
3207
+ if (refreshed !== 'beyond-horizon')
3389
3208
  return refreshed;
3390
- if (opened !== null) {
3391
- owed.add(room.locator);
3392
- liveQueries.recover();
3393
- return unconfirmed;
3394
- }
3395
- if (reloadsDue.get(room.locator) === generation)
3209
+ if (reloadsDue.has(room.locator))
3396
3210
  return unconfirmed;
3211
+ replaced([room.locator]);
3397
3212
  }
3213
+ const from = standingOf(room.locator);
3398
3214
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3399
- if (bootstrapped.kind === 'staged') {
3400
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3215
+ if (bootstrapped.kind !== 'staged')
3216
+ return live() ? bootstrapped : unconfirmed;
3217
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3218
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3401
3219
  return live() ? null : unconfirmed;
3402
3220
  }
3403
- return live() ? bootstrapped : unconfirmed;
3221
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3222
+ ? null
3223
+ : advance(room, local, authorized, live);
3224
+ }
3225
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3226
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3227
+ if (!live())
3228
+ return unconfirmed;
3229
+ if (refreshed.kind === 'still-current') {
3230
+ stale.delete(room.locator);
3231
+ if (unreadable.delete(room.locator)) {
3232
+ scopedRefusals.delete(room.locator);
3233
+ list(room, true);
3234
+ emitRooms();
3235
+ }
3236
+ return null;
3237
+ }
3238
+ if (refreshed.kind === 'advanced') {
3239
+ const landing = {
3240
+ kind: 'refreshed',
3241
+ from: refreshed.from,
3242
+ remodelled: refreshed.remodelled,
3243
+ };
3244
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3245
+ const now = replicas.get(room.locator)?.record.roomVersion;
3246
+ const reached = now !== undefined &&
3247
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3248
+ return reached ? null : 'behind';
3249
+ }
3250
+ await retainModel(local, room, modelOf(room.locator));
3251
+ return null;
3252
+ }
3253
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3404
3254
  }
3405
3255
  async function withhold(withheld, room, local, progress) {
3406
- if (withheld.kind === 'unconfirmed')
3256
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3407
3257
  return;
3408
- if (withheld.kind === 'refused')
3409
- stale.delete(room.locator);
3410
- else
3411
- stale.add(room.locator);
3258
+ stale.add(room.locator);
3412
3259
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3413
3260
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3414
3261
  repairable.set(room.locator, reset);
@@ -3430,33 +3277,11 @@ export function composeRuntime(options) {
3430
3277
  list(room, false);
3431
3278
  emitRooms();
3432
3279
  }
3433
- else if (withheld.kind === 'refused') {
3434
- refusedSource(room, withheld);
3435
- }
3436
3280
  else if (withheld.kind === 'revoked-authorization') {
3437
3281
  await refuse('revoked-authorization');
3438
3282
  }
3439
3283
  }
3440
- function refusedSource(room, { refusal }) {
3441
- sourceRefusals.set(room.locator, {
3442
- code: refusal.rule,
3443
- retryable: false,
3444
- fields: {
3445
- file: refusal.file,
3446
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3447
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3448
- },
3449
- });
3450
- declarationRefusals.delete(room.locator);
3451
- unreadable.add(room.locator);
3452
- list(room, false);
3453
- emitRooms();
3454
- moveTo({ ...status, ...attention() });
3455
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3456
- }
3457
3284
  function leaving(batch, result) {
3458
- if (result.kind === 'expired')
3459
- return { batch, returned: returnedBy('expired') };
3460
3285
  return {
3461
3286
  batch,
3462
3287
  returned: {
@@ -3475,8 +3300,8 @@ export function composeRuntime(options) {
3475
3300
  function settling(acceptance) {
3476
3301
  return acceptance !== null && 'apply' in acceptance;
3477
3302
  }
3478
- function refusalOf(acceptance) {
3479
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3303
+ function failureOf(acceptance) {
3304
+ return settling(acceptance) ? null : acceptance;
3480
3305
  }
3481
3306
  async function acceptance(room, local, submitted, accepted) {
3482
3307
  if (!continuing())
@@ -3489,7 +3314,8 @@ export function composeRuntime(options) {
3489
3314
  logAccepted(room.locator, submitted, accepted);
3490
3315
  let record = current.record;
3491
3316
  let changes;
3492
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3317
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3318
+ !older(room.locator, accepted.roomVersion)) {
3493
3319
  const paths = [...returnedPaths(submitted, accepted)];
3494
3320
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3495
3321
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3499,34 +3325,6 @@ export function composeRuntime(options) {
3499
3325
  ? accepted.roomVersion
3500
3326
  : held,
3501
3327
  };
3502
- const overrides = new Map([
3503
- ...back.set.map((entry) => [entry.path, entry]),
3504
- ...back.removed.map((path) => [path, null]),
3505
- ]);
3506
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3507
- if (refused !== null) {
3508
- const key = pendingKey(room.locator, submitted.id);
3509
- return {
3510
- locator: room.locator,
3511
- left: [],
3512
- commit: {
3513
- rooms: [],
3514
- writes: [
3515
- {
3516
- locator: room.locator,
3517
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3518
- },
3519
- ],
3520
- },
3521
- refused,
3522
- apply() {
3523
- const state = pendings.get(key);
3524
- if (state !== undefined && !fences.has(room.locator))
3525
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3526
- return changeSet(room.locator, { queue: true });
3527
- },
3528
- };
3529
- }
3530
3328
  changes = back;
3531
3329
  }
3532
3330
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3534,10 +3332,9 @@ export function composeRuntime(options) {
3534
3332
  ...settled,
3535
3333
  apply(answer) {
3536
3334
  const set = settled.apply(answer);
3335
+ showing(room.locator, accepted.roomVersion);
3537
3336
  pendings.delete(pendingKey(room.locator, submitted.id));
3538
3337
  unreadable.delete(room.locator);
3539
- sourceRefusals.delete(room.locator);
3540
- declarationRefusals.delete(room.locator);
3541
3338
  list(room, true);
3542
3339
  return set;
3543
3340
  },
@@ -3555,13 +3352,9 @@ export function composeRuntime(options) {
3555
3352
  const failed = await exclusive(async () => {
3556
3353
  const adopting = await acceptance(room, local, submitted, accepted);
3557
3354
  await adoptedTogether(local, [adopting]);
3558
- return refusalOf(adopting);
3355
+ return failureOf(adopting);
3559
3356
  });
3560
- if (failed === null)
3561
- return true;
3562
- if (failed.kind === 'refused' && continuing())
3563
- refusedSource(room, failed);
3564
- return false;
3357
+ return failed === null;
3565
3358
  }
3566
3359
  async function adoptedMove(room, partner, local, submitted, accepted) {
3567
3360
  const other = discovered.get(partner);
@@ -3575,18 +3368,10 @@ export function composeRuntime(options) {
3575
3368
  ...accepted.destination,
3576
3369
  });
3577
3370
  await adoptedTogether(local, [left, arrived]);
3578
- const refusedArrival = refusalOf(arrived);
3579
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3580
- refusedSource(other, refusedArrival);
3581
- }
3582
- return refusalOf(left);
3371
+ return failureOf(left);
3583
3372
  });
3584
3373
  submitLater(partner);
3585
- if (failed === null)
3586
- return true;
3587
- if (failed.kind === 'refused' && continuing())
3588
- refusedSource(room, failed);
3589
- return false;
3374
+ return failed === null;
3590
3375
  }
3591
3376
  function queueIn(locator) {
3592
3377
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3697,20 +3482,13 @@ export function composeRuntime(options) {
3697
3482
  return;
3698
3483
  moveTo({ ...status, ...attention() });
3699
3484
  }
3700
- async function discardHeld(head, room, local, signal) {
3701
- const partner = partnerOf(head, room.locator);
3702
- const other = partner === null ? undefined : discovered.get(partner);
3703
- await discardPending(other === undefined ? [room] : [room, other], local, signal);
3704
- }
3705
3485
  async function submitRoom(room, local, authorized, progress, signal) {
3706
3486
  while (continuing() && authorization !== undefined && !signal.aborted) {
3707
3487
  const replica = replicas.get(room.locator);
3708
- const head = replica?.queue[0];
3488
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3709
3489
  if (replica === undefined || head === undefined)
3710
3490
  return 'done';
3711
3491
  const state = pendings.get(pendingKey(room.locator, head.id));
3712
- if (state?.accepted !== undefined)
3713
- return 'done';
3714
3492
  const partner = partnerOf(head, room.locator);
3715
3493
  if (partner !== null && !leavingIn(head, room.locator)) {
3716
3494
  submitLater(partner);
@@ -3734,10 +3512,6 @@ export function composeRuntime(options) {
3734
3512
  return 'done';
3735
3513
  }
3736
3514
  const lost = earlier || submission.state === 'submitted';
3737
- if (lost && pastHorizon(head.id)) {
3738
- await discardHeld(head, room, local, signal);
3739
- return 'done';
3740
- }
3741
3515
  let result;
3742
3516
  journal.record(`submission ${room.locator} ${head.id}`);
3743
3517
  await exclusive(() => mark(local, room.locator, head, { state: 'submitted' }, lost));
@@ -3815,32 +3589,12 @@ export function composeRuntime(options) {
3815
3589
  const other = partner === null ? undefined : discovered.get(partner);
3816
3590
  if (result.kind === 'conflicted' ||
3817
3591
  result.kind === 'rejected' ||
3818
- result.kind === 'expired' ||
3819
3592
  result.kind === 'retryable-refusal') {
3820
- if (result.kind === 'expired' && lost && result.restored !== true) {
3821
- journal.record(`outcome ${room.locator} ${head.id} expired`);
3822
- await discardHeld(head, room, local, signal);
3823
- return 'done';
3824
- }
3825
- if (result.kind === 'expired') {
3826
- await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3827
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3828
- if (other !== undefined && there !== null) {
3829
- await withhold(there, other, local, progress);
3830
- return 'done';
3831
- }
3832
- const withheld = await advance(room, local, authorized, continuing);
3833
- if (withheld !== null) {
3834
- await withhold(withheld, room, local, progress);
3835
- return 'done';
3836
- }
3837
- }
3838
3593
  journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
3839
3594
  (result.kind === 'conflicted' && result.roomVersion !== undefined
3840
3595
  ? ` ${result.roomVersion}`
3841
3596
  : ''));
3842
- if (result.kind !== 'expired')
3843
- journal.record(`refusal ${result.kind} ${result.code}`);
3597
+ journal.record(`refusal ${result.kind} ${result.code}`);
3844
3598
  }
3845
3599
  if (result.kind === 'retryable-refusal') {
3846
3600
  const reset = result.reset;
@@ -3848,11 +3602,11 @@ export function composeRuntime(options) {
3848
3602
  await exclusive(() => mark(local, room.locator, head, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
3849
3603
  return reset === undefined ? 'again' : { at: Date.parse(reset) };
3850
3604
  }
3851
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3605
+ if (result.kind === 'conflicted' || result.kind === 'rejected') {
3852
3606
  const left = leaving(head, result);
3853
3607
  const refreshed = async () => ({
3854
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3855
- here: await advance(room, local, authorized, continuing),
3608
+ there: other === undefined ? null : await advanced(other, local, authorized),
3609
+ here: await advanced(room, local, authorized),
3856
3610
  });
3857
3611
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3858
3612
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3872,16 +3626,19 @@ export function composeRuntime(options) {
3872
3626
  return;
3873
3627
  }
3874
3628
  const confirmedLayer = await sliceAt(current.record, 'confirmed', [head]);
3875
- const held = (result.kind === 'expired' &&
3876
- (await landedWhole(room.locator, head, confirmedLayer))) ||
3877
- (result.kind === 'conflicted' &&
3878
- (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3629
+ const held = result.kind === 'conflicted' &&
3630
+ (await landedWhole(room.locator, head, confirmedLayer, result.affected));
3631
+ if (held &&
3632
+ result.kind === 'conflicted' &&
3633
+ partner === null &&
3634
+ !landed(head, confirmedLayer)) {
3635
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3636
+ return;
3637
+ }
3879
3638
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3880
3639
  await unpair(local);
3881
3640
  moveTo({ ...status, ...attention() });
3882
3641
  });
3883
- if (result.kind === 'expired')
3884
- continue;
3885
3642
  const { there, here } = judged ?? (await refreshed());
3886
3643
  if (other !== undefined && there !== null)
3887
3644
  await withhold(there, other, local, progress);
@@ -3901,80 +3658,16 @@ export function composeRuntime(options) {
3901
3658
  }
3902
3659
  return 'done';
3903
3660
  }
3904
- function pastHorizon(identity) {
3905
- return (outcomeRetention !== null && identityTime(identity) + outcomeRetention * 1000 <= Date.now());
3906
- }
3907
- async function discardPending(rooms, local, signal) {
3908
- const discarded = await exclusive(async () => {
3909
- const held = rooms
3910
- .filter(({ locator }) => !fences.has(locator))
3911
- .map((room) => ({
3912
- room,
3913
- before: replicas.get(room.locator),
3914
- queue: queueIn(room.locator),
3915
- }));
3916
- if (held.length === 0)
3917
- return [];
3918
- const answer = await local.commit({
3919
- rooms: held.flatMap(({ room, before }) => before === undefined
3920
- ? []
3921
- : [
3922
- {
3923
- locator: room.locator,
3924
- key: before.record.key,
3925
- roots: before.record.typedSelection?.roots ?? [],
3926
- composed: composedOf(composed(before.record)),
3927
- overlay: { set: [], cleared: [...before.overlaid] },
3928
- },
3929
- ]),
3930
- discards: held.map(({ room }) => room.locator),
3931
- });
3932
- for (const { room, before, queue } of held) {
3933
- const { locator } = room;
3934
- lastFenced.set(locator, ++fencings);
3935
- for (const batch of queue)
3936
- pendings.delete(pendingKey(locator, batch.id));
3937
- queuesWithoutReplica.delete(locator);
3938
- if (before !== undefined) {
3939
- replicas.set(locator, {
3940
- ...before,
3941
- queue: [],
3942
- overlaid: new Set(),
3943
- count: answer.counts[locator] ?? before.count,
3944
- });
3945
- }
3946
- journal.record(`room discarded ${locator}`);
3947
- }
3948
- publishChangeSet(mergeChangeSets(held.map(({ room }) => changeSet(room.locator, {
3949
- documents: answer.changed[room.locator] ?? [],
3950
- queue: true,
3951
- }))));
3952
- moveTo({ ...status, ...attention() });
3953
- return held.map(({ room }) => room);
3954
- });
3955
- for (const room of discarded) {
3956
- if (signal.aborted)
3957
- return;
3958
- const progress = { complete: true, reachable: true };
3959
- await roomTask(room.locator, async (live) => {
3960
- await Promise.race([
3961
- reload(room, local, progress, () => live() && !signal.aborted),
3962
- abandonedOn(signal),
3963
- ]);
3964
- });
3965
- if (signal.aborted)
3966
- return;
3967
- await rest(progress);
3968
- }
3969
- }
3970
3661
  function awaitingAnswer(locator) {
3662
+ answering.get(locator)?.release();
3971
3663
  let release = () => { };
3972
- const awaited = new Promise((resolve) => {
3664
+ const answered = new Promise((resolve) => {
3973
3665
  release = resolve;
3974
3666
  });
3975
- answering.set(locator, { generation, answered: awaited });
3667
+ const awaited = { answered, release };
3668
+ answering.set(locator, awaited);
3976
3669
  return () => {
3977
- if (answering.get(locator)?.answered === awaited)
3670
+ if (answering.get(locator) === awaited)
3978
3671
  answering.delete(locator);
3979
3672
  release();
3980
3673
  };
@@ -3986,6 +3679,30 @@ export function composeRuntime(options) {
3986
3679
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3987
3680
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3988
3681
  }
3682
+ async function keepNaming(local, room) {
3683
+ const held = replicas.get(room.locator)?.record;
3684
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3685
+ return;
3686
+ await exclusive(async () => {
3687
+ if (!continuing())
3688
+ return;
3689
+ const replica = replicas.get(room.locator);
3690
+ if (replica === undefined)
3691
+ return;
3692
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3693
+ await local.commit({
3694
+ rooms: [
3695
+ {
3696
+ locator: room.locator,
3697
+ key: record.key,
3698
+ record: storedRecord(record),
3699
+ composed: composedOf(composed(record)),
3700
+ },
3701
+ ],
3702
+ });
3703
+ replicas.set(room.locator, { ...replica, record });
3704
+ });
3705
+ }
3989
3706
  async function retainModel(local, room, next) {
3990
3707
  const key = room.locator;
3991
3708
  const previous = modelOf(room.locator);
@@ -4001,26 +3718,15 @@ export function composeRuntime(options) {
4001
3718
  const held = replicas.get(room.locator);
4002
3719
  let changed = null;
4003
3720
  if (held !== undefined) {
4004
- const declared = {
3721
+ const record = {
4005
3722
  ...held.record,
3723
+ name: room.name,
3724
+ handle: room.handle,
4006
3725
  operations: room.operations,
4007
3726
  replica: room.replica,
4008
3727
  typedSelection: room.typedSelection ?? null,
4009
3728
  model: next,
4010
3729
  };
4011
- let read = 'staged';
4012
- if (!sameMembership(declared, held.record)) {
4013
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
4014
- read =
4015
- verdict.verdict !== 'refuse'
4016
- ? 'staged'
4017
- : verdict.rule === 'model-unavailable'
4018
- ? 'deferred'
4019
- : { kind: 'refused', refusal: verdict };
4020
- }
4021
- const record = read === 'staged'
4022
- ? declared
4023
- : { ...declared, typedSelection: held.record.typedSelection };
4024
3730
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
4025
3731
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
4026
3732
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -4042,16 +3748,6 @@ export function composeRuntime(options) {
4042
3748
  documents: answer.changed[room.locator] ?? [],
4043
3749
  room: true,
4044
3750
  });
4045
- if (read === 'staged' || read === 'deferred') {
4046
- if (declarationRefusals.delete(room.locator)) {
4047
- sourceRefusals.delete(room.locator);
4048
- unreadable.delete(room.locator);
4049
- }
4050
- }
4051
- else {
4052
- refusedSource(room, read);
4053
- declarationRefusals.add(room.locator);
4054
- }
4055
3751
  }
4056
3752
  heldModel(room, next);
4057
3753
  if (changed === null)
@@ -4096,6 +3792,17 @@ export function composeRuntime(options) {
4096
3792
  emitRooms();
4097
3793
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
4098
3794
  }
3795
+ function name(locator) {
3796
+ const stream = options.adapter.roomStream;
3797
+ const record = replicas.get(locator)?.record;
3798
+ const room = discovered.get(locator);
3799
+ if (stream === undefined || record === undefined || room === undefined)
3800
+ return;
3801
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
3802
+ return;
3803
+ naming += 1;
3804
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
3805
+ }
4099
3806
  function heldRooms(listed) {
4100
3807
  return listed.flatMap((room) => {
4101
3808
  const record = replicas.get(room.locator)?.record;
@@ -4113,14 +3820,19 @@ export function composeRuntime(options) {
4113
3820
  return outcome?.status === 'fulfilled' && outcome.value;
4114
3821
  });
4115
3822
  if (attempt === 'opened') {
3823
+ naming += 1;
4116
3824
  options.adapter.roomStream?.hold(heldRooms(ready));
4117
3825
  handed = true;
4118
3826
  }
4119
- discovering?.release();
4120
- const loaded = await Promise.allSettled(ready
4121
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
4122
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
4123
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
3827
+ for (const room of ready) {
3828
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
3829
+ if (attempt === 'authorized')
3830
+ name(room.locator);
3831
+ continue;
3832
+ }
3833
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
3834
+ }
3835
+ const failed = prepared.find((room) => room.status === 'rejected');
4124
3836
  if (failed !== undefined)
4125
3837
  throw failed.reason;
4126
3838
  }
@@ -4139,12 +3851,16 @@ export function composeRuntime(options) {
4139
3851
  }
4140
3852
  if (prior !== undefined)
4141
3853
  await retainModel(local, room, modelOf(room.locator));
3854
+ if (prior !== undefined)
3855
+ await keepNaming(local, room);
4142
3856
  if (discoveryPaused || authorization === undefined)
4143
3857
  return false;
4144
3858
  return room.replica.admission === 'admitted';
4145
3859
  }
4146
3860
  async function caughtUp(room, local, progress, live) {
4147
- if (authorization === undefined || !live() || !behind(room))
3861
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
3862
+ return;
3863
+ if (!live() || !behind(room))
4148
3864
  return;
4149
3865
  const withheld = await advance(room, local, authorization, live);
4150
3866
  if (withheld !== null)
@@ -4157,6 +3873,8 @@ export function composeRuntime(options) {
4157
3873
  async function loadRoom(room, local, progress, live) {
4158
3874
  if (!live() || authorization === undefined)
4159
3875
  return;
3876
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
3877
+ return;
4160
3878
  if (replicas.has(room.locator) && revoked(room.locator)) {
4161
3879
  await reload(room, local, progress, live);
4162
3880
  return;
@@ -4174,20 +3892,53 @@ export function composeRuntime(options) {
4174
3892
  reloadsDue.delete(room.locator);
4175
3893
  if (!live() || authorization === undefined)
4176
3894
  return;
3895
+ const from = standingOf(room.locator);
4177
3896
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
4178
3897
  if (bootstrapped.kind !== 'staged') {
4179
3898
  if (live())
4180
3899
  await withhold(bootstrapped, room, local, progress);
4181
3900
  return;
4182
3901
  }
4183
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3902
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4184
3903
  await caughtUp(room, local, progress, live);
4185
3904
  }
4186
3905
  finally {
4187
3906
  caughtUpTo(room.locator);
4188
3907
  }
4189
3908
  }
4190
- async function discoverRooms(local, progress, attempt, live, scope) {
3909
+ async function asked(authorized) {
3910
+ try {
3911
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
3912
+ }
3913
+ catch {
3914
+ return { kind: 'transport-uncertainty' };
3915
+ }
3916
+ }
3917
+ function malformed(listed) {
3918
+ for (const room of listed) {
3919
+ if (room.operations === undefined ||
3920
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
3921
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
3922
+ room.replica === undefined ||
3923
+ typeof room.replica.selection !== 'string' ||
3924
+ room.replica.selection.length === 0 ||
3925
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
3926
+ !['locked', 'open'].includes(room.replica.offline)) {
3927
+ return `invalid model or replica declaration ${room.locator}`;
3928
+ }
3929
+ if (!declaredSelection(room))
3930
+ return `invalid typed selection declaration ${room.locator}`;
3931
+ }
3932
+ return null;
3933
+ }
3934
+ async function signIn(local, signedIn) {
3935
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
3936
+ return;
3937
+ await local.fact('member', signedIn);
3938
+ member = signedIn;
3939
+ emitMember();
3940
+ }
3941
+ async function discoverRooms(local, progress, attempt, live, found) {
4191
3942
  if (closed || authorization === undefined) {
4192
3943
  notifyReady();
4193
3944
  return;
@@ -4195,13 +3946,7 @@ export function composeRuntime(options) {
4195
3946
  moveTo(transferring(status));
4196
3947
  discoveries += 1;
4197
3948
  const started = discoveries;
4198
- let result;
4199
- try {
4200
- result = await options.adapter.discover(authorization, scope);
4201
- }
4202
- catch {
4203
- result = { kind: 'transport-uncertainty' };
4204
- }
3949
+ const result = found ?? (await asked(authorization));
4205
3950
  if (result.kind === 'discovery')
4206
3951
  roomsNamed = true;
4207
3952
  if (!live()) {
@@ -4211,7 +3956,7 @@ export function composeRuntime(options) {
4211
3956
  }
4212
3957
  journal.record(`connection discover ${result.kind}`);
4213
3958
  if (result.kind === 'transport-uncertainty') {
4214
- await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
3959
+ await cachedModels(local);
4215
3960
  progress.reachable = false;
4216
3961
  progress.complete = false;
4217
3962
  notifyReady();
@@ -4232,26 +3977,12 @@ export function composeRuntime(options) {
4232
3977
  notifyReady();
4233
3978
  return;
4234
3979
  }
4235
- for (const room of result.rooms) {
4236
- if (room.operations === undefined ||
4237
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4238
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
4239
- room.replica === undefined ||
4240
- typeof room.replica.selection !== 'string' ||
4241
- room.replica.selection.length === 0 ||
4242
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4243
- !['locked', 'open'].includes(room.replica.offline)) {
4244
- journal.record(`invalid model or replica declaration ${room.locator}`);
4245
- await refuse('missing-required-guarantee');
4246
- notifyReady();
4247
- return;
4248
- }
4249
- if (!declaredSelection(room)) {
4250
- journal.record(`invalid typed selection declaration ${room.locator}`);
4251
- await refuse('missing-required-guarantee');
4252
- notifyReady();
4253
- return;
4254
- }
3980
+ const invalid = malformed(result.rooms);
3981
+ if (invalid !== null) {
3982
+ journal.record(invalid);
3983
+ await refuse('missing-required-guarantee');
3984
+ notifyReady();
3985
+ return;
4255
3986
  }
4256
3987
  if (refusal !== null) {
4257
3988
  refusal = null;
@@ -4259,22 +3990,15 @@ export function composeRuntime(options) {
4259
3990
  }
4260
3991
  const previousCapabilities = capabilities;
4261
3992
  capabilities = result.capabilities;
4262
- outcomeRetention = result.retention.outcome;
4263
3993
  notifyReady();
4264
3994
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
4265
3995
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
4266
3996
  limits = result.providerLimits;
4267
3997
  await local.fact('providerLimits', limits);
4268
3998
  await local.fact('capabilities', capabilities);
4269
- conversations.declared();
4270
3999
  }
4271
- const signedIn = result.member ?? null;
4272
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4273
- await local.fact('member', signedIn);
4274
- member = signedIn;
4275
- emitMember();
4276
- }
4277
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4000
+ await signIn(local, result.member ?? null);
4001
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4278
4002
  }
4279
4003
  async function ensureAuthorization() {
4280
4004
  if (authorization !== undefined)
@@ -4289,23 +4013,45 @@ export function composeRuntime(options) {
4289
4013
  }
4290
4014
  }
4291
4015
  async function admittedRead(work) {
4292
- if (roomReads.generation !== generation) {
4293
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4016
+ while (roomReads.free === 0) {
4017
+ await new Promise((admit) => roomReads.waiting.push(admit));
4294
4018
  }
4295
- const reads = roomReads;
4296
- while (reads.free === 0)
4297
- await new Promise((admit) => reads.waiting.push(admit));
4298
- reads.free -= 1;
4019
+ roomReads.free -= 1;
4299
4020
  try {
4300
4021
  await work();
4301
4022
  }
4302
4023
  finally {
4303
- reads.free += 1;
4304
- reads.waiting.shift()?.();
4024
+ roomReads.free += 1;
4025
+ roomReads.waiting.shift()?.();
4305
4026
  }
4306
4027
  }
4028
+ function inRoom(locator, work) {
4029
+ roomWork += 1;
4030
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4031
+ try {
4032
+ return await work(continuing);
4033
+ }
4034
+ finally {
4035
+ roomWork -= 1;
4036
+ }
4037
+ });
4038
+ const chain = ran.then(() => undefined, () => undefined);
4039
+ roomLanes.set(locator, chain);
4040
+ working.add(chain);
4041
+ void chain.finally(() => {
4042
+ working.delete(chain);
4043
+ if (roomLanes.get(locator) === chain)
4044
+ roomLanes.delete(locator);
4045
+ });
4046
+ return ran;
4047
+ }
4307
4048
  function roomTask(locator, work) {
4308
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4049
+ if (stopping)
4050
+ return Promise.resolve();
4051
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4052
+ }
4053
+ function advanced(room, local, authorized) {
4054
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4309
4055
  }
4310
4056
  function task(lane, work) {
4311
4057
  if (stopping)
@@ -4321,23 +4067,19 @@ export function composeRuntime(options) {
4321
4067
  void run.finally(() => working.delete(run));
4322
4068
  return run;
4323
4069
  }
4324
- function sync(attempt) {
4070
+ function sync(attempt, found) {
4325
4071
  return task(OPENING_LANE, async (live) => {
4326
4072
  if (!live()) {
4327
4073
  readyFromStore();
4328
4074
  return;
4329
4075
  }
4330
4076
  const progress = { complete: true, reachable: true };
4331
- const gate = discover();
4332
- if (attempt === 'opened')
4333
- opened = generation;
4334
- underway = attempt === 'opened' ? { generation, progress } : null;
4077
+ underway = attempt === 'opened' ? progress : null;
4335
4078
  if (attempt === 'opened')
4336
4079
  streamed = progress;
4337
4080
  complete = false;
4338
4081
  recorded = false;
4339
4082
  moveTo(transferring(status));
4340
- handed = false;
4341
4083
  try {
4342
4084
  const local = await openLocal();
4343
4085
  if (!live())
@@ -4345,7 +4087,7 @@ export function composeRuntime(options) {
4345
4087
  await ensureAuthorization();
4346
4088
  if (!live())
4347
4089
  return;
4348
- await discoverRooms(local, progress, attempt, live);
4090
+ await discoverRooms(local, progress, attempt, live, found);
4349
4091
  }
4350
4092
  catch {
4351
4093
  progress.complete = false;
@@ -4353,7 +4095,6 @@ export function composeRuntime(options) {
4353
4095
  progress.reachable = false;
4354
4096
  }
4355
4097
  finally {
4356
- gate.release();
4357
4098
  notifyReady();
4358
4099
  }
4359
4100
  if (!live())
@@ -4365,35 +4106,59 @@ export function composeRuntime(options) {
4365
4106
  if (handed || closed || progress.reachable)
4366
4107
  return;
4367
4108
  underway = null;
4368
- opened = null;
4369
- journal.record('connection reopen');
4370
4109
  await rest(progress, live);
4371
- options.adapter.roomStream?.reopen();
4372
4110
  });
4373
4111
  }
4374
- function rediscover(scope) {
4112
+ function resynchronize() {
4375
4113
  return task(OPENING_LANE, async (live) => {
4376
- if (!live() || !socketOpen)
4114
+ if (!live() || !announced)
4377
4115
  return;
4378
4116
  const progress = { complete: true, reachable: true };
4379
- const gate = discover();
4380
4117
  moveTo(transferring(status));
4381
4118
  try {
4382
4119
  const local = await openLocal();
4383
4120
  if (!live())
4384
4121
  return;
4385
- await ensureAuthorization();
4122
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4123
+ }
4124
+ catch {
4125
+ progress.complete = false;
4126
+ }
4127
+ if (live())
4128
+ await finishOpening(progress, live);
4129
+ });
4130
+ }
4131
+ function countAuthorityChange() {
4132
+ authorityChanges += 1;
4133
+ for (const listener of authorityChangeListeners)
4134
+ listener(authorityChanges);
4135
+ }
4136
+ function changeAuthority(change) {
4137
+ return task(OPENING_LANE, async (live) => {
4138
+ if (!live() || !announced)
4139
+ return;
4140
+ countAuthorityChange();
4141
+ const progress = { complete: true, reachable: true };
4142
+ moveTo(transferring(status));
4143
+ try {
4144
+ const local = await openLocal();
4386
4145
  if (!live())
4387
4146
  return;
4388
- await discoverRooms(local, progress, 'authorized', live, scope);
4147
+ const invalid = malformed(change.rooms);
4148
+ if (invalid !== null) {
4149
+ journal.record(invalid);
4150
+ await refuse('missing-required-guarantee');
4151
+ return;
4152
+ }
4153
+ if (change.member !== undefined)
4154
+ await signIn(local, change.member);
4155
+ discoveries += 1;
4156
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4157
+ await synchronize(listed, local, progress, 'authorized', live);
4389
4158
  }
4390
4159
  catch {
4391
4160
  progress.complete = false;
4392
4161
  }
4393
- finally {
4394
- gate.release();
4395
- notifyReady();
4396
- }
4397
4162
  if (!live())
4398
4163
  return;
4399
4164
  if (!progress.complete)
@@ -4401,15 +4166,17 @@ export function composeRuntime(options) {
4401
4166
  await rest(progress, live);
4402
4167
  });
4403
4168
  }
4404
- function closedSocket(revoked, lost) {
4405
- notifyDiscovered();
4406
- const opened = socketOpen;
4169
+ function closedSocket(revoked, lost, authorityChanged) {
4170
+ notifyReady();
4171
+ const opened = announced;
4407
4172
  socketOpen = false;
4173
+ announced = false;
4174
+ missedChange ||= authorityChanged;
4175
+ handed = false;
4176
+ naming = 0;
4408
4177
  connectionLost();
4409
- if (lost) {
4410
- reads.lost();
4178
+ if (lost)
4411
4179
  conversations.lost();
4412
- }
4413
4180
  if (revoked) {
4414
4181
  void refuse('revoked-authorization');
4415
4182
  return;
@@ -4420,18 +4187,32 @@ export function composeRuntime(options) {
4420
4187
  moveTo({
4421
4188
  ...status,
4422
4189
  state: status.state === 'synced' ? 'syncing' : status.state,
4423
- providerUnreachable: status.providerUnreachable || !opened,
4190
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4191
+ });
4192
+ void closing(opened);
4193
+ }
4194
+ function closing(opened) {
4195
+ return task(OPENING_LANE, async (live) => {
4196
+ if (!live())
4197
+ return;
4198
+ const local = await openLocal();
4199
+ if (!opened && !socketOpen)
4200
+ await cachedModels(local);
4201
+ await rest(null, live);
4424
4202
  });
4425
4203
  }
4426
- function applyOpening(expired) {
4427
- const progress = underway?.generation === generation ? underway.progress : null;
4204
+ function cachedModels(local) {
4205
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4206
+ }
4207
+ function caughtUpRooms(expired) {
4208
+ const progress = underway;
4209
+ underway = null;
4210
+ naming = Math.max(0, naming - 1);
4428
4211
  const cycle = progress ?? { complete: true, reachable: true };
4429
- if (opened === generation)
4430
- opened = null;
4212
+ replaced(expired);
4431
4213
  const reloading = progress === null ? [] : owedReloads(expired);
4432
4214
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4433
4215
  return task(OPENING_LANE, async (live) => {
4434
- underway = null;
4435
4216
  if (!live())
4436
4217
  return;
4437
4218
  if (progress === null) {
@@ -4439,20 +4220,14 @@ export function composeRuntime(options) {
4439
4220
  await rest(cycle, live);
4440
4221
  return;
4441
4222
  }
4442
- await roomsCarried([...expired, ...reloading]);
4443
- if (!live())
4444
- return;
4445
4223
  try {
4446
4224
  const local = await openLocal();
4447
- const reloaded = await replacing;
4225
+ await replacing;
4448
4226
  if (progress.reachable) {
4449
4227
  await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4450
4228
  !expired.includes(room.locator) &&
4451
4229
  !reloading.includes(room.locator)), 'current', local);
4452
4230
  }
4453
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4454
- if (failed !== undefined)
4455
- throw failed.reason;
4456
4231
  }
4457
4232
  catch {
4458
4233
  progress.complete = false;
@@ -4464,11 +4239,8 @@ export function composeRuntime(options) {
4464
4239
  }
4465
4240
  function owedReloads(expired) {
4466
4241
  const reloading = [...discovered.values()]
4467
- .filter((room) => replicas.has(room.locator) &&
4468
- !expired.includes(room.locator) &&
4469
- (owed.has(room.locator) || revoked(room.locator)))
4242
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4470
4243
  .map((room) => room.locator);
4471
- owed.clear();
4472
4244
  for (const locator of reloading)
4473
4245
  catching.add(locator);
4474
4246
  liveQueries.recover();
@@ -4477,16 +4249,17 @@ export function composeRuntime(options) {
4477
4249
  async function reloadNow(replaced, progress) {
4478
4250
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4479
4251
  for (const room of reloaded)
4480
- reloadsDue.set(room.locator, generation);
4252
+ reloadsDue.add(room.locator);
4481
4253
  try {
4482
4254
  const local = await openLocal();
4483
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4255
+ for (const room of reloaded) {
4256
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4257
+ }
4484
4258
  }
4485
4259
  catch {
4486
4260
  for (const room of reloaded)
4487
4261
  reloadsDue.delete(room.locator);
4488
4262
  progress.reachable = false;
4489
- return [];
4490
4263
  }
4491
4264
  }
4492
4265
  async function settleModels(held, status, local) {
@@ -4497,7 +4270,7 @@ export function composeRuntime(options) {
4497
4270
  const settled = await exclusive(async () => {
4498
4271
  if (!continuing())
4499
4272
  return [];
4500
- const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4273
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4501
4274
  for (const room of unchanged) {
4502
4275
  const next = { ...modelOf(room.locator), status };
4503
4276
  const replica = replicas.get(room.locator);
@@ -4557,14 +4330,10 @@ export function composeRuntime(options) {
4557
4330
  try {
4558
4331
  const local = await openLocal();
4559
4332
  await roomTask(locator, async (live) => {
4560
- const current = () => live() && !signal.aborted;
4561
- if (!current() || authorization === undefined)
4333
+ if (!live() || authorization === undefined)
4562
4334
  return;
4563
- const withheld = await Promise.race([
4564
- advance(room, local, authorization, current),
4565
- abandonedOn(signal),
4566
- ]);
4567
- if (withheld !== null && current())
4335
+ const withheld = await advance(room, local, authorization, live);
4336
+ if (withheld !== null && live())
4568
4337
  await withhold(withheld, room, local, progress);
4569
4338
  });
4570
4339
  }
@@ -4619,7 +4388,6 @@ export function composeRuntime(options) {
4619
4388
  progress.complete = false;
4620
4389
  return;
4621
4390
  }
4622
- reads.applied();
4623
4391
  submitAll();
4624
4392
  for (const locator of stale)
4625
4393
  if (repairable.has(locator))
@@ -4650,7 +4418,7 @@ export function composeRuntime(options) {
4650
4418
  if (!live())
4651
4419
  return;
4652
4420
  let lastCompleted = status.lastCompleted;
4653
- let synced = complete && stale.size === 0 && !pending();
4421
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4654
4422
  if (synced && !recorded) {
4655
4423
  const completedAt = new Date().toISOString();
4656
4424
  try {
@@ -4665,74 +4433,69 @@ export function composeRuntime(options) {
4665
4433
  }
4666
4434
  }
4667
4435
  moveTo({
4668
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4436
+ state: pending()
4437
+ ? 'pending'
4438
+ : synced && complete && roomWork === 0 && naming === 0
4439
+ ? 'synced'
4440
+ : 'syncing',
4669
4441
  lastCompleted,
4670
- providerUnreachable: !progress.reachable,
4442
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4671
4443
  ...attention(),
4672
4444
  });
4673
4445
  }
4674
- function discover() {
4675
- let release = () => { };
4676
- const done = new Promise((resolve) => {
4677
- release = resolve;
4678
- });
4679
- discovering = { generation, done, release };
4680
- return discovering;
4681
- }
4682
- function applyRoom(locator, roomVersion) {
4683
- announced.set(locator, roomVersion);
4684
- return task(roomLane(locator), async (live) => {
4685
- if (discovering?.generation === generation)
4686
- await discovering.done;
4687
- const saving = answering.get(locator);
4688
- if (saving?.generation === generation)
4689
- await saving.answered;
4690
- if (!live() || authorization === undefined)
4691
- return;
4692
- const room = discovered.get(locator);
4693
- const held = replicas.get(locator);
4694
- if (room === undefined || room.replica.admission !== 'admitted')
4695
- return;
4696
- if (held !== undefined &&
4697
- (held.record.replica.selection !== room.replica.selection ||
4698
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4699
- return;
4700
- }
4701
- const during = underway?.generation === generation ? underway.progress : null;
4702
- const progress = during ?? { complete: true, reachable: true };
4703
- if (during === null)
4704
- moveTo(transferring(status));
4446
+ function applyRoom(change) {
4447
+ const { locator } = change;
4448
+ if (stopping)
4449
+ return Promise.resolve();
4450
+ return inRoom(locator, async (live) => {
4451
+ await answering.get(locator)?.answered;
4452
+ const progress = underway ?? { complete: true, reachable: true };
4705
4453
  try {
4706
4454
  const local = await openLocal();
4707
- await admittedRead(async () => {
4708
- if (!live() || authorization === undefined)
4709
- return;
4710
- const withheld = await advance(room, local, authorization, live);
4711
- if (withheld !== null)
4712
- await withhold(withheld, room, local, progress);
4713
- });
4455
+ for (;;) {
4456
+ if (!live())
4457
+ return null;
4458
+ const room = discovered.get(locator);
4459
+ const held = replicas.get(locator);
4460
+ if (room === undefined ||
4461
+ held === undefined ||
4462
+ room.replica.admission !== 'admitted' ||
4463
+ held.record.replica.selection !== room.replica.selection) {
4464
+ return null;
4465
+ }
4466
+ const at = held.record.roomVersion;
4467
+ if (options.adapter.reached(locator, at, change.roomVersion))
4468
+ return progress;
4469
+ if (!options.adapter.reached(locator, at, change.from)) {
4470
+ name(locator);
4471
+ return progress;
4472
+ }
4473
+ if (underway === null)
4474
+ moveTo(transferring(status));
4475
+ const writing = () => live() && !full;
4476
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, () => change.read()));
4477
+ if (refreshed.kind !== 'advanced')
4478
+ return progress;
4479
+ const landing = {
4480
+ kind: 'refreshed',
4481
+ from: refreshed.from,
4482
+ remodelled: refreshed.remodelled,
4483
+ };
4484
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4485
+ stale.delete(locator);
4486
+ await retainModel(local, room, modelOf(locator));
4487
+ return progress;
4488
+ }
4489
+ }
4714
4490
  }
4715
4491
  catch {
4492
+ if (!stale.has(locator))
4493
+ name(locator);
4716
4494
  stale.add(locator);
4495
+ return progress;
4717
4496
  }
4718
- finally {
4719
- if (during === null)
4720
- await rest(progress, live);
4721
- }
4722
- });
4497
+ }).then((progress) => rest(progress), () => undefined);
4723
4498
  }
4724
- function roomsCarried(replacing) {
4725
- const replaced = new Set(replacing.map(roomLane));
4726
- return Promise.all([...lanes]
4727
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4728
- .map(([, held]) => held.chain));
4729
- }
4730
- const receipts = {
4731
- heads: async () => (await openLocal()).receivedHeads(),
4732
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4733
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4734
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4735
- };
4736
4499
  function observeStream() {
4737
4500
  const stream = options.adapter.roomStream;
4738
4501
  if (stream === undefined || withdrawStream !== undefined)
@@ -4742,31 +4505,33 @@ export function composeRuntime(options) {
4742
4505
  return;
4743
4506
  if (event.kind === 'opened') {
4744
4507
  socketOpen = true;
4508
+ announced = false;
4509
+ handed = false;
4510
+ naming = 0;
4745
4511
  connectionLive();
4746
4512
  generation += 1;
4747
- for (const admit of installWaiters.splice(0))
4748
- admit();
4749
- reads.opened();
4750
4513
  conversations.reopened();
4751
4514
  recovery.resume();
4752
- void sync('opened');
4515
+ }
4516
+ else if (event.kind === 'discovered') {
4517
+ if (!socketOpen)
4518
+ return;
4519
+ announced = true;
4520
+ if (missedChange) {
4521
+ missedChange = false;
4522
+ countAuthorityChange();
4523
+ }
4524
+ void sync('opened', event.discovery);
4753
4525
  }
4754
4526
  else if (event.kind === 'closed')
4755
- closedSocket(event.revoked, event.lost);
4756
- else if (event.kind === 'opening')
4757
- void applyOpening(event.expired);
4527
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4528
+ else if (event.kind === 'caught-up')
4529
+ void caughtUpRooms(event.expired);
4758
4530
  else if (event.kind === 'authority-changed')
4759
- void rediscover(event.scope);
4531
+ void changeAuthority(event);
4760
4532
  else
4761
- void applyRoom(event.locator, event.roomVersion);
4762
- }, receipts, heldRevisions);
4763
- }
4764
- async function heldRevisions(locator, paths) {
4765
- const held = replicas.get(locator);
4766
- if (held === undefined || paths.length === 0)
4767
- return new Map();
4768
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4769
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4533
+ void applyRoom(event);
4534
+ });
4770
4535
  }
4771
4536
  function writesHeld() {
4772
4537
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4839,8 +4604,6 @@ export function composeRuntime(options) {
4839
4604
  notifyDiscovered();
4840
4605
  if (probing !== null)
4841
4606
  clearInterval(probing);
4842
- for (const resolve of shownWaiters.splice(0))
4843
- resolve();
4844
4607
  connectionLive();
4845
4608
  lockListeners.clear();
4846
4609
  readinessWatches.clear();
@@ -4858,6 +4621,7 @@ export function composeRuntime(options) {
4858
4621
  statusListeners.clear();
4859
4622
  serviceModeListeners.clear();
4860
4623
  memberListeners.clear();
4624
+ authorityChangeListeners.clear();
4861
4625
  liveQueries.close();
4862
4626
  queueSubscriptions.clear();
4863
4627
  roomDraftListeners.clear();
@@ -4865,6 +4629,7 @@ export function composeRuntime(options) {
4865
4629
  }
4866
4630
  async function stop() {
4867
4631
  stopping = true;
4632
+ inFlight.abort();
4868
4633
  await recovery.retire();
4869
4634
  await Promise.all(working);
4870
4635
  await mutations;
@@ -4883,15 +4648,15 @@ export function composeRuntime(options) {
4883
4648
  readyFromStore();
4884
4649
  });
4885
4650
  if (options.adapter.roomStream === undefined)
4886
- void sync('unstreamed');
4651
+ void sync('unstreamed', null);
4887
4652
  function planned(input, reach) {
4888
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4653
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4889
4654
  const named = plan.paths !== null &&
4890
4655
  plan.rooms !== null &&
4891
4656
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4892
4657
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4893
4658
  }
4894
- function watchPlan(plan, deliver) {
4659
+ function watchPlan(plan, deliver, fail) {
4895
4660
  let seeded = false;
4896
4661
  let pending = true;
4897
4662
  const shown = () => {
@@ -4917,7 +4682,7 @@ export function composeRuntime(options) {
4917
4682
  }
4918
4683
  seeded = false;
4919
4684
  deliver(message);
4920
- });
4685
+ }, fail);
4921
4686
  return () => {
4922
4687
  watchers.delete(shown);
4923
4688
  if (watchers.size === 0)
@@ -4925,61 +4690,59 @@ export function composeRuntime(options) {
4925
4690
  stop();
4926
4691
  };
4927
4692
  }
4928
- async function searched(input, reach, locked) {
4693
+ async function searched(input, reach) {
4929
4694
  const asked = searchPlanOf(input);
4930
- const filter = {
4931
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4932
- locked,
4933
- };
4934
- const unlocking = (filter.rooms ?? [])
4935
- .filter((room) => locked.includes(room))
4936
- .map((room) => ({ room, awaits: 'unlock' }));
4937
- for (;;) {
4938
- const cover = covered(filter);
4939
- awaitPreparation(cover);
4940
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4941
- const complete = pending.length === 0 && !('pending' in cover);
4942
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4943
- const answering = complete
4944
- ? filter
4945
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4946
- const answered = covered(answering);
4947
- if ('pending' in answered)
4948
- continue;
4949
- const listed = conversations.searchable();
4950
- const named = reach !== null && !reach.conversations
4951
- ? []
4952
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4953
- const hits = await (await openLocal()).search({
4954
- text: asked.text,
4955
- limit: capped(asked.limit),
4956
- after: asked.after,
4957
- total: asked.total,
4958
- documents: {
4959
- plan: answering,
4960
- rooms: answered.rooms,
4961
- beneath: asked.collection
4962
- ? answered.rooms.map(({ locator, key }) => ({
4963
- key,
4964
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4965
- }))
4966
- : [],
4967
- },
4968
- conversations: named,
4969
- });
4970
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4971
- continue;
4972
- const coverage = {
4973
- rooms: answered.coverage.rooms,
4974
- conversations: named,
4975
- overlay: answered.coverage.overlay,
4695
+ await ready;
4696
+ const search = options.adapter.search;
4697
+ if (search === undefined) {
4698
+ return {
4699
+ kind: 'undeclared',
4700
+ code: 'action-undeclared',
4701
+ fields: { name: 'search' },
4702
+ retryable: false,
4976
4703
  };
4977
- return complete
4978
- ? { ...hits, coverage }
4979
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4704
+ }
4705
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4706
+ return OFFLINE;
4707
+ const reached = reach === null ? null : new Set(reach.rooms);
4708
+ const named = asked.rooms === null
4709
+ ? reached === null
4710
+ ? null
4711
+ : [...reached]
4712
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4713
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4714
+ if (rooms !== null && rooms.length === 0)
4715
+ return { kind: 'found', documents: [] };
4716
+ try {
4717
+ await ensureAuthorization();
4718
+ }
4719
+ catch {
4720
+ return UNCERTAIN;
4721
+ }
4722
+ if (closed || authorization === undefined || refusal !== null)
4723
+ return UNCERTAIN;
4724
+ journal.record('transfer search');
4725
+ try {
4726
+ const result = await search(authorization, { text: asked.text, rooms });
4727
+ if (result.kind === 'revoked-authorization')
4728
+ await refuse('revoked-authorization');
4729
+ journal.record(`outcome search ${result.kind}`);
4730
+ if (result.kind === 'rejected')
4731
+ return result;
4732
+ if (result.kind !== 'found')
4733
+ return failure(result);
4734
+ const kept = rooms === null ? null : new Set(rooms);
4735
+ return {
4736
+ kind: 'found',
4737
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4738
+ };
4739
+ }
4740
+ catch {
4741
+ journal.record('outcome search transport-uncertainty');
4742
+ return UNCERTAIN;
4980
4743
  }
4981
4744
  }
4982
- const watch = (topic, input, deliver) => {
4745
+ const watch = (topic, input, deliver, fail) => {
4983
4746
  if (topic === 'room-drafts') {
4984
4747
  const listener = { notify: deliver, ready: false };
4985
4748
  roomDraftListeners.add(listener);
@@ -5028,7 +4791,7 @@ export function composeRuntime(options) {
5028
4791
  };
5029
4792
  }
5030
4793
  if (topic === 'readiness') {
5031
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
4794
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
5032
4795
  }
5033
4796
  if (topic === 'rooms') {
5034
4797
  unlistedRooms.add(deliver);
@@ -5073,6 +4836,13 @@ export function composeRuntime(options) {
5073
4836
  memberListeners.delete(deliver);
5074
4837
  };
5075
4838
  }
4839
+ if (topic === 'authority-changes') {
4840
+ authorityChangeListeners.add(deliver);
4841
+ deliver(authorityChanges);
4842
+ return () => {
4843
+ authorityChangeListeners.delete(deliver);
4844
+ };
4845
+ }
5076
4846
  if (topic === 'queue') {
5077
4847
  const queue = { locator: input[0], notify: deliver };
5078
4848
  queueSubscriptions.add(queue);
@@ -5081,7 +4851,7 @@ export function composeRuntime(options) {
5081
4851
  queueSubscriptions.delete(queue);
5082
4852
  };
5083
4853
  }
5084
- return watchPlan(planned(input[0], null), deliver);
4854
+ return watchPlan(planned(input[0], null), deliver, fail);
5085
4855
  };
5086
4856
  const md = {
5087
4857
  loaded() {
@@ -5101,7 +4871,7 @@ export function composeRuntime(options) {
5101
4871
  });
5102
4872
  },
5103
4873
  query: ((input) => rowsQuery(planned(input, null))),
5104
- search: (input) => searched(input, null, []),
4874
+ search: (input) => searched(input, null),
5105
4875
  types(locator) {
5106
4876
  return {
5107
4877
  collection: modelTypes(modelOf(locator)),
@@ -5204,6 +4974,8 @@ export function composeRuntime(options) {
5204
4974
  return journal.export();
5205
4975
  },
5206
4976
  cleanUp,
4977
+ unsent: () => database.ask({ kind: 'authored.unsent' }),
4978
+ reset: () => options.shell.reset(),
5207
4979
  persistence() {
5208
4980
  return persistence === 'granted';
5209
4981
  },
@@ -5226,7 +4998,7 @@ export function composeRuntime(options) {
5226
4998
  authorization = undefined;
5227
4999
  discoveryPaused = false;
5228
5000
  if (options.adapter.roomStream === undefined)
5229
- await sync('unstreamed');
5001
+ await sync('unstreamed', null);
5230
5002
  else
5231
5003
  await task(OPENING_LANE, ensureAuthorization);
5232
5004
  },
@@ -5249,7 +5021,6 @@ export function composeRuntime(options) {
5249
5021
  md,
5250
5022
  seed,
5251
5023
  stop,
5252
- lose: loseRooms,
5253
5024
  watch,
5254
5025
  persisted(answer) {
5255
5026
  if (answer === persistence)
@@ -5258,7 +5029,6 @@ export function composeRuntime(options) {
5258
5029
  journal.record(`persistence ${answer}`);
5259
5030
  queueReadiness();
5260
5031
  },
5261
- shown: screen,
5262
5032
  within: (reach, port) => {
5263
5033
  if (full)
5264
5034
  void probed();