@agentshouse/mdruntime 0.1.0-cb122f2ba154d → 0.1.0-cb1f56e16291b

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 (121) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +68 -48
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +4 -7
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +19 -14
  7. package/dist/client.d.ts +4 -4
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +0 -1
  10. package/dist/collection.d.ts +0 -3
  11. package/dist/collection.d.ts.map +1 -1
  12. package/dist/collection.js +1 -17
  13. package/dist/compile.d.ts +1 -1
  14. package/dist/compile.d.ts.map +1 -1
  15. package/dist/compile.js +61 -43
  16. package/dist/connection.d.ts +3 -5
  17. package/dist/connection.d.ts.map +1 -1
  18. package/dist/connection.js +900 -861
  19. package/dist/context.d.ts +2 -0
  20. package/dist/context.d.ts.map +1 -0
  21. package/dist/context.js +7 -0
  22. package/dist/conversation.d.ts +15 -13
  23. package/dist/conversation.d.ts.map +1 -1
  24. package/dist/conversation.js +136 -144
  25. package/dist/durability.d.ts +0 -1
  26. package/dist/durability.d.ts.map +1 -1
  27. package/dist/entry.d.ts.map +1 -1
  28. package/dist/entry.js +1 -0
  29. package/dist/host.d.ts +3 -6
  30. package/dist/host.d.ts.map +1 -1
  31. package/dist/host.js +17 -19
  32. package/dist/index.d.ts +8 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -1
  35. package/dist/job.d.ts +14 -0
  36. package/dist/job.d.ts.map +1 -0
  37. package/dist/job.js +28 -0
  38. package/dist/live.d.ts +2 -1
  39. package/dist/live.d.ts.map +1 -1
  40. package/dist/live.js +10 -7
  41. package/dist/media.d.ts +1 -1
  42. package/dist/media.d.ts.map +1 -1
  43. package/dist/media.js +19 -9
  44. package/dist/memory.d.ts +8 -4
  45. package/dist/memory.d.ts.map +1 -1
  46. package/dist/memory.js +131 -81
  47. package/dist/messages.d.ts +2 -2
  48. package/dist/messages.d.ts.map +1 -1
  49. package/dist/messages.js +15 -16
  50. package/dist/model.d.ts +0 -1
  51. package/dist/model.d.ts.map +1 -1
  52. package/dist/model.js +0 -9
  53. package/dist/observation.d.ts +2 -2
  54. package/dist/observation.d.ts.map +1 -1
  55. package/dist/observation.js +7 -2
  56. package/dist/outcome.d.ts +0 -3
  57. package/dist/outcome.d.ts.map +1 -1
  58. package/dist/phrase.d.ts +3 -0
  59. package/dist/phrase.d.ts.map +1 -0
  60. package/dist/phrase.js +8 -0
  61. package/dist/query.d.ts +5 -1
  62. package/dist/query.d.ts.map +1 -1
  63. package/dist/query.js +20 -1
  64. package/dist/reach.d.ts +6 -5
  65. package/dist/reach.d.ts.map +1 -1
  66. package/dist/reach.js +30 -37
  67. package/dist/react.d.ts +1 -0
  68. package/dist/react.d.ts.map +1 -1
  69. package/dist/readiness.d.ts +2 -15
  70. package/dist/readiness.d.ts.map +1 -1
  71. package/dist/readiness.js +4 -10
  72. package/dist/recovery.d.ts +1 -0
  73. package/dist/recovery.d.ts.map +1 -1
  74. package/dist/recovery.js +26 -0
  75. package/dist/refresh.d.ts +7 -5
  76. package/dist/refresh.d.ts.map +1 -1
  77. package/dist/refresh.js +46 -6
  78. package/dist/replica.d.ts +0 -1
  79. package/dist/replica.d.ts.map +1 -1
  80. package/dist/replica.js +3 -4
  81. package/dist/room-draft.d.ts +2 -1
  82. package/dist/room-draft.d.ts.map +1 -1
  83. package/dist/room-draft.js +65 -52
  84. package/dist/rows.d.ts +3 -2
  85. package/dist/rows.d.ts.map +1 -1
  86. package/dist/rows.js +34 -29
  87. package/dist/search.d.ts +12 -63
  88. package/dist/search.d.ts.map +1 -1
  89. package/dist/search.js +9 -52
  90. package/dist/shared.d.ts +0 -1
  91. package/dist/shared.d.ts.map +1 -1
  92. package/dist/shared.js +0 -10
  93. package/dist/status.d.ts +0 -4
  94. package/dist/status.d.ts.map +1 -1
  95. package/dist/store.d.ts +15 -19
  96. package/dist/store.d.ts.map +1 -1
  97. package/dist/store.js +40 -39
  98. package/dist/stored.d.ts +33 -6
  99. package/dist/stored.d.ts.map +1 -1
  100. package/dist/stored.js +92 -111
  101. package/dist/tables.d.ts +34 -33
  102. package/dist/tables.d.ts.map +1 -1
  103. package/dist/tables.js +18 -54
  104. package/dist/transfer.d.ts +0 -5
  105. package/dist/transfer.d.ts.map +1 -1
  106. package/dist/validation.d.ts +3 -4
  107. package/dist/validation.d.ts.map +1 -1
  108. package/dist/validation.js +35 -96
  109. package/dist/wire.d.ts +1 -4
  110. package/dist/wire.d.ts.map +1 -1
  111. package/dist/wire.js +1 -1
  112. package/package.json +2 -2
  113. package/dist/fulltext.d.ts +0 -27
  114. package/dist/fulltext.d.ts.map +0 -1
  115. package/dist/fulltext.js +0 -370
  116. package/dist/reads.d.ts +0 -11
  117. package/dist/reads.d.ts.map +0 -1
  118. package/dist/reads.js +0 -91
  119. package/dist/words.d.ts +0 -19
  120. package/dist/words.d.ts.map +0 -1
  121. package/dist/words.js +0 -156
@@ -1,8 +1,8 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
1
+ import { parseFrontmatter, 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 { bodied, identityOf, planOf, plain, queryKey, } 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';
@@ -34,7 +33,7 @@ import { paced, ROW_WEIGHT, stepped, yielded } from './turn.js';
34
33
  import { identityTime, 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,34 @@ 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
- }
88
+ const TEXT_READS = 4;
93
89
  function pendingKey(locator, id) {
94
90
  return `${locator}\u0000${id}`;
95
91
  }
@@ -109,6 +105,7 @@ export function composeRuntime(options) {
109
105
  const usableWhenHeld = options.usableWhenHeld ?? [];
110
106
  const queuesWithoutReplica = new FencedMap(isFenced);
111
107
  const pendings = new Map();
108
+ const landedAhead = new Map();
112
109
  const seeds = new Map();
113
110
  const seedWatchers = new Map();
114
111
  const roomDrafts = new FencedMap(isFenced);
@@ -124,11 +121,19 @@ export function composeRuntime(options) {
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
  }
@@ -151,10 +156,10 @@ 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();
@@ -165,12 +170,13 @@ export function composeRuntime(options) {
165
170
  let recorded = false;
166
171
  const stale = new Set();
167
172
  let generation = 0;
168
- let discovering = null;
169
- let opened = null;
170
- const owed = new Set();
171
- const reloadsDue = new Map();
173
+ const reloadsDue = new Set();
174
+ const highest = new Map();
175
+ const beyondHorizon = new Set();
172
176
  const lanes = new Map();
173
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
177
+ const roomLanes = new Map();
178
+ let roomWork = 0;
179
+ const roomReads = { free: ROOM_READS, waiting: [] };
174
180
  const working = new Set();
175
181
  let mutations = Promise.resolve();
176
182
  let gathering = null;
@@ -186,6 +192,8 @@ export function composeRuntime(options) {
186
192
  let withdrawConversations;
187
193
  let currencyQueued = null;
188
194
  let socketOpen = false;
195
+ let announced = false;
196
+ let missedChange = false;
189
197
  const confirmed = new Set();
190
198
  let lockGrace = null;
191
199
  const lockListeners = new Set();
@@ -195,6 +203,8 @@ export function composeRuntime(options) {
195
203
  const serviceModeListeners = new Set();
196
204
  let member = null;
197
205
  const memberListeners = new Set();
206
+ let authorityChanges = 0;
207
+ const authorityChangeListeners = new Set();
198
208
  const unpreparedDemand = new Set();
199
209
  const unpreparable = new Set();
200
210
  let preparing = false;
@@ -224,12 +234,10 @@ export function composeRuntime(options) {
224
234
  const streaming = new Set();
225
235
  const underived = new Map();
226
236
  const unindexed = new Map();
227
- const unchecked = new Map();
237
+ const startedOver = new Map();
228
238
  let arrivalReported = 0;
229
- let onScreen = options.shell.onScreen;
230
- const shownWaiters = [];
231
- let streams = { generation, free: STREAMS_HELD };
232
- let queuedBytes = 0;
239
+ let streamsFree = STREAMS_HELD;
240
+ let queuedText = 0;
233
241
  const installWaiters = [];
234
242
  const stageBudget = {
235
243
  bytes: STAGE_FLOOR * 4,
@@ -243,28 +251,14 @@ export function composeRuntime(options) {
243
251
  ceiling: DERIVE_BYTES,
244
252
  aim: DERIVE_AIM_MS,
245
253
  };
246
- let installWrites = Promise.resolve();
254
+ const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
247
255
  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
256
  call: (name, input) => invokeAction(name, input),
257
+ history: (request) => readConversationHistory(request),
263
258
  durably,
264
259
  stream: () => options.adapter.conversations,
265
260
  publish: publishChangeSet,
266
261
  online: () => connectionOf() !== 'unreachable' && refusal === null,
267
- listed: () => roomsListed,
268
262
  full: () => full,
269
263
  closed: () => closed,
270
264
  unanswered: resendLater,
@@ -272,6 +266,8 @@ export function composeRuntime(options) {
272
266
  });
273
267
  const journal = createLog();
274
268
  const keptFiles = openedFiles(database, isFenced);
269
+ const holding = new Map();
270
+ const unholdable = new Map();
275
271
  let readyFromStore = () => { };
276
272
  const ready = new Promise((resolve) => {
277
273
  readyFromStore = resolve;
@@ -361,9 +357,9 @@ export function composeRuntime(options) {
361
357
  return value === undefined ? null : structuredClone(value);
362
358
  }
363
359
  async function writtenRoomDraft(room, path, revision, input, next) {
364
- const parts = readParts(input);
360
+ const parts = readParts(input, path);
365
361
  if (path === '' || normalize(path) !== path)
366
- throw runtimeError('room-draft-malformed');
362
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
367
363
  const local = await openLocal();
368
364
  const current = roomDrafts.get(room)?.get(path);
369
365
  if ((current?.revision ?? null) !== revision) {
@@ -412,7 +408,7 @@ export function composeRuntime(options) {
412
408
  const changed = new Map();
413
409
  for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
414
410
  for (const { batch, state } of entriesOf(room)) {
415
- if (state === 'accepted' || state === 'local')
411
+ if (state === 'local')
416
412
  continue;
417
413
  for (const { path } of batch.changes)
418
414
  changed.set(`${room}\u0000${path}`, { room, path });
@@ -433,16 +429,9 @@ export function composeRuntime(options) {
433
429
  returned: structuredClone(record.returned),
434
430
  });
435
431
  }
436
- for (const [room, refusal] of sourceRefusals)
437
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
438
432
  attentionRequired.sort((left, right) => {
439
433
  if (left.room !== right.room)
440
434
  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
435
  if (left.path !== right.path)
447
436
  return left.path < right.path ? -1 : 1;
448
437
  return 0;
@@ -597,9 +586,6 @@ export function composeRuntime(options) {
597
586
  if (localSaves)
598
587
  return Object.freeze({ batch, state: 'local' });
599
588
  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
589
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
604
590
  }
605
591
  function entriesOf(locator) {
@@ -673,11 +659,7 @@ export function composeRuntime(options) {
673
659
  })();
674
660
  }
675
661
  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
- ]);
662
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
681
663
  }
682
664
  function reachedRooms(plan) {
683
665
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -716,7 +698,7 @@ export function composeRuntime(options) {
716
698
  coverage.push({
717
699
  room: locator,
718
700
  generation: replica.record.transfer,
719
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
701
+ current: !(catching.has(locator) || transfers.has(locator)),
720
702
  });
721
703
  read.push({ locator, key: replica.record.key });
722
704
  for (const batch of replica.queue)
@@ -743,8 +725,6 @@ export function composeRuntime(options) {
743
725
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
744
726
  }
745
727
  function roomState(locator) {
746
- if (sourceRefusals.has(locator))
747
- return 'refused';
748
728
  const listedRoom = discovered.get(locator);
749
729
  if (roomsListed &&
750
730
  (listedRoom === undefined ||
@@ -759,7 +739,7 @@ export function composeRuntime(options) {
759
739
  }
760
740
  function conversationCoverage(reach) {
761
741
  const declared = reach === null || reach.conversations;
762
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
742
+ if (!declared || options.adapter.conversations === undefined)
763
743
  return 'undeclared';
764
744
  if (!conversations.known() || !conversations.current())
765
745
  return 'acquiring';
@@ -774,79 +754,50 @@ export function composeRuntime(options) {
774
754
  ? 'connected'
775
755
  : 'connecting';
776
756
  }
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
- });
757
+ function bytesOf(room, underway) {
758
+ const transfer = transfers.get(room);
759
+ if (transfer !== undefined) {
760
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
761
+ return {
762
+ done: transfer.done.bytes - Math.ceil(owed),
763
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
764
+ };
765
+ }
766
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
767
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
793
768
  }
794
769
  function installationOf(coverage) {
795
770
  const rooms = { done: 0, total: 0 };
796
- const parts = { done: 0, total: 0 };
797
771
  const bytes = { done: 0, total: 0 };
798
772
  let installing = 0;
799
773
  let downloading = false;
800
- let checking = 0;
801
774
  for (const { room, state } of coverage) {
802
775
  if (state === 'unadmitted')
803
776
  continue;
804
777
  rooms.total += 1;
805
778
  if (state === 'complete')
806
779
  rooms.done += 1;
807
- const transfer = transfers.get(room);
808
780
  const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
809
781
  if (underway)
810
782
  installing += 1;
811
783
  if (underway && (streaming.has(room) || !arrivals.has(room)))
812
784
  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;
785
+ const now = bytesOf(room, underway);
786
+ const earlier = underway ? startedOver.get(room) : undefined;
787
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
788
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
831
789
  }
832
790
  const step = installing === 0
833
791
  ? null
834
792
  : downloading
835
793
  ? 'downloading'
836
- : bytes.done + checking < bytes.total
794
+ : bytes.done < bytes.total
837
795
  ? 'writing'
838
796
  : 'replaying';
839
- return {
840
- step,
841
- paused: installing > 0 && !onScreen ? 'hidden' : null,
842
- rooms,
843
- parts,
844
- bytes,
845
- };
797
+ return { step, rooms, bytes };
846
798
  }
847
799
  function readinessFor(port, reach) {
848
800
  const required = requiredRooms(reach);
849
- const search = port.search === 'required';
850
801
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
851
802
  const preparing = coverage
852
803
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -859,14 +810,12 @@ export function composeRuntime(options) {
859
810
  listed: required.listed,
860
811
  rooms: coverage,
861
812
  conversations: conversationCoverage(reach),
862
- search: search ? 'required' : 'unused',
863
813
  },
864
814
  persistence,
865
815
  connection: connectionOf(),
866
816
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
867
817
  space: full ? 'full' : port.space,
868
818
  installation: installationOf(coverage),
869
- refused: refusedIn(required.rooms),
870
819
  lost: storageLoss !== null,
871
820
  established: targets[port.target]?.established === true ||
872
821
  (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
@@ -954,8 +903,12 @@ export function composeRuntime(options) {
954
903
  unpreparable.clear();
955
904
  journal.record('space freed');
956
905
  queueReadiness();
957
- if (!closed && authorization !== undefined)
958
- void sync('unstreamed');
906
+ if (closed || authorization === undefined)
907
+ return;
908
+ if (options.adapter.roomStream === undefined)
909
+ void sync('unstreamed', null);
910
+ else
911
+ void resynchronize();
959
912
  }
960
913
  function seedRoomHeld(room, version) {
961
914
  return indexed(room) && reachedBy(room, version);
@@ -995,7 +948,24 @@ export function composeRuntime(options) {
995
948
  function sameCover(left, right) {
996
949
  return JSON.stringify(left) === JSON.stringify(right);
997
950
  }
951
+ async function unheldRows(local, plan, rows, rooms) {
952
+ if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
953
+ return [];
954
+ if (!('documents' in rows) || !textOnline())
955
+ return [];
956
+ const named = new Map();
957
+ for (const { room, path, textHeld } of rows.documents) {
958
+ if (textHeld === false)
959
+ named.set(room, [...(named.get(room) ?? []), path]);
960
+ }
961
+ const unheld = [];
962
+ for (const { locator, key } of rooms) {
963
+ unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
964
+ }
965
+ return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
966
+ }
998
967
  async function coveredRows(plan, cover) {
968
+ const asked = new Set();
999
969
  for (;;) {
1000
970
  const before = cover();
1001
971
  if ('pending' in before)
@@ -1006,8 +976,14 @@ export function composeRuntime(options) {
1006
976
  const current = () => local.changes(keys) === changes;
1007
977
  const rows = await windowRows(local, plan, before.rooms, current);
1008
978
  const after = cover();
1009
- if (rows !== null && sameCover(before, after))
979
+ if (rows === null || !sameCover(before, after))
980
+ continue;
981
+ const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
982
+ if (unheld.length === 0)
1010
983
  return { rows, cover: before, current };
984
+ for (const one of unheld)
985
+ asked.add(textKey(one));
986
+ await holdTexts(unheld);
1011
987
  }
1012
988
  }
1013
989
  async function windowRows(local, plan, rooms, unchanged) {
@@ -1103,6 +1079,8 @@ export function composeRuntime(options) {
1103
1079
  }
1104
1080
  async function refuse(reason, provider = null) {
1105
1081
  discoveryPaused = true;
1082
+ inFlight.abort();
1083
+ inFlight = new AbortController();
1106
1084
  try {
1107
1085
  if (reason === 'revoked-authorization') {
1108
1086
  await exclusive(async () => {
@@ -1187,10 +1165,10 @@ export function composeRuntime(options) {
1187
1165
  providerOwnedPaths: room.providerOwnedPaths,
1188
1166
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1189
1167
  synchronizationAvailable,
1190
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1168
+ refusal: scopedRefusals.get(room.locator) ?? null,
1191
1169
  });
1192
1170
  }
1193
- async function applyDiscovery(found, started, covered) {
1171
+ async function applyDiscovery(found, started, left) {
1194
1172
  const lockedBefore = lockedRooms();
1195
1173
  for (const room of found) {
1196
1174
  const fence = fences.get(room.locator);
@@ -1204,18 +1182,11 @@ export function composeRuntime(options) {
1204
1182
  const listedLocators = new Set(listed.map((room) => room.locator));
1205
1183
  for (const locator of listedLocators)
1206
1184
  scopedRefusals.delete(locator);
1207
- if (covered === undefined)
1185
+ if (left === undefined)
1208
1186
  discovered.clear();
1209
- else
1210
- for (const locator of covered)
1211
- discovered.delete(locator);
1212
1187
  for (const room of listed)
1213
1188
  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);
1189
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1219
1190
  for (const room of listed) {
1220
1191
  const held = replicas.get(room.locator)?.record;
1221
1192
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1246,15 +1217,6 @@ export function composeRuntime(options) {
1246
1217
  ]),
1247
1218
  ].filter((locator) => !listed.has(locator));
1248
1219
  }
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
1220
  function fence(locators) {
1259
1221
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1260
1222
  const removed = fresh.map((locator) => ({
@@ -1265,24 +1227,26 @@ export function composeRuntime(options) {
1265
1227
  fences.set(locator, { at: discoveries, erased: false });
1266
1228
  lastFenced.set(locator, ++fencings);
1267
1229
  confirmed.delete(locator);
1268
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1230
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1269
1231
  pendings.delete(pendingKey(locator, batch.id));
1232
+ landedAhead.delete(pendingKey(locator, batch.id));
1233
+ }
1270
1234
  replicas.delete(locator);
1235
+ highest.delete(locator);
1236
+ beyondHorizon.delete(locator);
1271
1237
  queuesWithoutReplica.delete(locator);
1272
1238
  rooms.delete(locator);
1273
1239
  roomDrafts.delete(locator);
1274
1240
  attachedFiles.delete(locator);
1275
1241
  untransferred(locator);
1242
+ startedOver.delete(locator);
1276
1243
  catching.delete(locator);
1277
1244
  discovered.delete(locator);
1278
- announced.delete(locator);
1279
1245
  unreadable.delete(locator);
1280
1246
  stale.delete(locator);
1281
1247
  repairable.delete(locator);
1282
1248
  unpreparable.delete(locator);
1283
1249
  scopedRefusals.delete(locator);
1284
- sourceRefusals.delete(locator);
1285
- declarationRefusals.delete(locator);
1286
1250
  models.delete(locator);
1287
1251
  liveModels.delete(locator);
1288
1252
  invalidModels.delete(locator);
@@ -1373,6 +1337,7 @@ export function composeRuntime(options) {
1373
1337
  transfers.clear();
1374
1338
  arrivals.clear();
1375
1339
  streaming.clear();
1340
+ startedOver.clear();
1376
1341
  limits = held.providerLimits;
1377
1342
  capabilities = held.capabilities;
1378
1343
  member = held.member;
@@ -1382,8 +1347,9 @@ export function composeRuntime(options) {
1382
1347
  presentations.clear();
1383
1348
  for (const record of held.presentation)
1384
1349
  presentations.set(record.app, record.value);
1385
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1350
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1386
1351
  pendings.clear();
1352
+ landedAhead.clear();
1387
1353
  for (const queue of queues) {
1388
1354
  for (const { batch, state } of queue.pending) {
1389
1355
  pendings.set(pendingKey(queue.locator, batch.id), state);
@@ -1534,6 +1500,16 @@ export function composeRuntime(options) {
1534
1500
  }
1535
1501
  const REWRITE_LINKS = ['field', 'body'];
1536
1502
  const CHECKED_LINKS = ['field', 'section'];
1503
+ const BODY_LINKS = ['body'];
1504
+ async function textsNeeded(local, record, named) {
1505
+ const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
1506
+ const unheld = await unheldIn(local, record.locator, record.key, paths);
1507
+ if (unheld.length === 0)
1508
+ return null;
1509
+ await holdTexts(unheld);
1510
+ const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
1511
+ return first?.path ?? null;
1512
+ }
1537
1513
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1538
1514
  const local = await openLocal();
1539
1515
  const needed = new Set([...touchedBy(queue), ...extra]);
@@ -1634,8 +1610,6 @@ export function composeRuntime(options) {
1634
1610
  function unpairedIn(locator, batch) {
1635
1611
  if (!queueIn(locator).some((held) => held.id === batch.id))
1636
1612
  return false;
1637
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1638
- return false;
1639
1613
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1640
1614
  }
1641
1615
  async function together(local, settlements) {
@@ -1678,10 +1652,16 @@ export function composeRuntime(options) {
1678
1652
  ? read
1679
1653
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1680
1654
  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));
1655
+ const shown = queue.filter((batch) => {
1656
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1657
+ return (held !== undefined &&
1658
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1659
+ });
1660
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1661
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1662
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1683
1663
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1684
- const unqueued = [];
1664
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1685
1665
  for (const batch of withdrawn) {
1686
1666
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1687
1667
  }
@@ -1746,8 +1726,10 @@ export function composeRuntime(options) {
1746
1726
  commit,
1747
1727
  apply: (answer) => {
1748
1728
  for (const batch of previousQueue) {
1749
- if (!staying.has(batch.id))
1750
- pendings.delete(pendingKey(locator, batch.id));
1729
+ if (staying.has(batch.id))
1730
+ continue;
1731
+ pendings.delete(pendingKey(locator, batch.id));
1732
+ landedAhead.delete(pendingKey(locator, batch.id));
1751
1733
  }
1752
1734
  for (const draft of written.values())
1753
1735
  rememberRoomDraft(draft);
@@ -1804,28 +1786,34 @@ export function composeRuntime(options) {
1804
1786
  models.set(locator, state);
1805
1787
  return { collection: state };
1806
1788
  }
1807
- function publish(local, room, record, landing, live) {
1789
+ function publish(local, room, reached, landing, live) {
1808
1790
  const landed = exclusive(async () => {
1809
1791
  if (!live()) {
1810
1792
  caughtUpTo(room.locator);
1811
1793
  return true;
1812
1794
  }
1813
1795
  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);
1796
+ if (!stands(room.locator, landing.from)) {
1797
+ if (landing.kind === 'refreshed') {
1798
+ await local.unrefresh(reached.key);
1799
+ return false;
1800
+ }
1801
+ caughtUpTo(room.locator);
1802
+ await abandoned(local, room.locator);
1817
1803
  return false;
1818
1804
  }
1805
+ const record = landing.kind === 'refreshed' && prior !== undefined
1806
+ ? {
1807
+ ...prior.record,
1808
+ roomVersion: reached.roomVersion,
1809
+ ...(landing.remodelled ? { model: reached.model } : {}),
1810
+ }
1811
+ : reached;
1819
1812
  adoptModels(room.locator, record);
1820
1813
  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
1814
  let set;
1827
1815
  try {
1828
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1816
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1829
1817
  ? {
1830
1818
  room: true,
1831
1819
  view: 'refreshed',
@@ -1847,24 +1835,26 @@ export function composeRuntime(options) {
1847
1835
  return true;
1848
1836
  }
1849
1837
  if (landing.kind === 'bootstrapped') {
1838
+ highest.delete(room.locator);
1839
+ beyondHorizon.delete(room.locator);
1850
1840
  untransferred(room.locator);
1841
+ startedOver.delete(room.locator);
1851
1842
  catching.delete(room.locator);
1852
- sweep();
1843
+ if (prior !== undefined && prior.record.key !== record.key)
1844
+ sweep();
1845
+ name(room.locator);
1853
1846
  }
1854
- for (const batch of accepted)
1855
- pendings.delete(pendingKey(room.locator, batch.id));
1856
1847
  stale.delete(room.locator);
1857
1848
  unpreparable.delete(room.locator);
1858
1849
  if (unreadable.delete(room.locator))
1859
1850
  scopedRefusals.delete(room.locator);
1860
- sourceRefusals.delete(room.locator);
1861
- declarationRefusals.delete(room.locator);
1862
1851
  list(room, true);
1863
1852
  publishChangeSet(set);
1853
+ if (landing.kind === 'bootstrapped')
1854
+ void heldAgain(room.locator);
1864
1855
  await unpair(local);
1865
1856
  moveTo({ ...status, ...attention() });
1866
- options.adapter.roomStream?.published(room.locator, roomVersion);
1867
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1857
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1868
1858
  submitLater(room.locator);
1869
1859
  }
1870
1860
  return true;
@@ -1876,7 +1866,7 @@ export function composeRuntime(options) {
1876
1866
  const replica = replicas.get(locator);
1877
1867
  const room = rooms.get(locator);
1878
1868
  if (replica === undefined || room === undefined) {
1879
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1869
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1880
1870
  }
1881
1871
  return { local, replica, room };
1882
1872
  }
@@ -1938,6 +1928,19 @@ export function composeRuntime(options) {
1938
1928
  const referring = removed.length === 0
1939
1929
  ? []
1940
1930
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1931
+ const unheld = await textsNeeded(local, record, [
1932
+ ...paths,
1933
+ ...referring,
1934
+ ...(moved.length === 0
1935
+ ? []
1936
+ : await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
1937
+ ...(removed.length === 0
1938
+ ? []
1939
+ : await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
1940
+ ]);
1941
+ if (unheld !== null) {
1942
+ return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
1943
+ }
1941
1944
  const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1942
1945
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1943
1946
  const composition = composed(record);
@@ -1968,18 +1971,6 @@ export function composeRuntime(options) {
1968
1971
  return undefined;
1969
1972
  return entry.binary !== null ? null : entry.content;
1970
1973
  }
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
1974
  function lookupOf(local, key, view) {
1984
1975
  return async (paths) => {
1985
1976
  if (paths.length === 0)
@@ -1996,37 +1987,6 @@ export function composeRuntime(options) {
1996
1987
  const read = parseFrontmatter(entry.content ?? '');
1997
1988
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1998
1989
  }
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
1990
  function overriddenLookup(lookup, overrides) {
2031
1991
  return async (paths) => {
2032
1992
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2044,9 +2004,8 @@ export function composeRuntime(options) {
2044
2004
  }
2045
2005
  function validatorOf(record, confirmedLayer) {
2046
2006
  return async (resulting, touched) => {
2047
- const composition = compositionFor(record);
2048
2007
  const local = await openLocal();
2049
- const changed = [];
2008
+ const changes = [];
2050
2009
  const overrides = new Map();
2051
2010
  const pace = paced();
2052
2011
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2057,19 +2016,13 @@ export function composeRuntime(options) {
2057
2016
  continue;
2058
2017
  overrides.set(path, now);
2059
2018
  if (textOf(was) !== textOf(now))
2060
- changed.push({ path, was: textOf(was), now: textOf(now) });
2019
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2061
2020
  }
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);
2021
+ return validatedNeighbourhood(changes, compositionFor(record), {
2022
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
2023
+ texts: textsOf(local, record.key, 'confirmed'),
2024
+ referrers: referrersOf(local, record.key, 'confirmed'),
2025
+ });
2073
2026
  };
2074
2027
  }
2075
2028
  function textsOf(local, key, view) {
@@ -2146,15 +2099,19 @@ export function composeRuntime(options) {
2146
2099
  }
2147
2100
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2148
2101
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2102
+ if ('refusal' in sourceSaving)
2103
+ return { outcome: 'refused', ...sourceSaving.refusal };
2149
2104
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2150
2105
  if (changed.length > 0)
2151
2106
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2152
2107
  const target = replicas.get(destination);
2153
2108
  const listedRoom = rooms.get(destination);
2154
2109
  if (target === undefined || listedRoom === undefined) {
2155
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2110
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2156
2111
  }
2157
2112
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2113
+ if ('refusal' in arriving)
2114
+ return { outcome: 'refused', ...arriving.refusal };
2158
2115
  const built = await buildMove({
2159
2116
  source: await sideOf(locator, replica, room, sourceSaving),
2160
2117
  destination: await sideOf(destination, target, listedRoom, arriving),
@@ -2215,10 +2172,10 @@ export function composeRuntime(options) {
2215
2172
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2216
2173
  const { answer, touched } = committedBoth;
2217
2174
  if (answer.fenced !== null) {
2218
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2175
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2219
2176
  }
2220
2177
  if (fences.has(locator) || fences.has(destination)) {
2221
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2178
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2222
2179
  }
2223
2180
  pendings.set(pendingKey(locator, id), {
2224
2181
  drafts: saved,
@@ -2271,6 +2228,8 @@ export function composeRuntime(options) {
2271
2228
  ...saved.map(({ path }) => path),
2272
2229
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2273
2230
  ]);
2231
+ if ('refusal' in saving)
2232
+ return { outcome: 'refused', ...saving.refusal };
2274
2233
  const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2275
2234
  if (changed.length > 0)
2276
2235
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -2389,10 +2348,10 @@ export function composeRuntime(options) {
2389
2348
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2390
2349
  const { answer, touched } = done;
2391
2350
  if (answer.fenced !== null) {
2392
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2351
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2393
2352
  }
2394
2353
  if (fences.has(locator))
2395
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2354
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2396
2355
  for (const origin of withdrawn.keys())
2397
2356
  pendings.delete(pendingKey(locator, origin));
2398
2357
  for (const [id, drafts] of drafted) {
@@ -2441,7 +2400,7 @@ export function composeRuntime(options) {
2441
2400
  throw new Error('connection refused');
2442
2401
  await local.keepUpload(locator, identifier, upload);
2443
2402
  if (fences.has(locator))
2444
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2403
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2445
2404
  const key = pendingKey(locator, identifier);
2446
2405
  pendings.set(key, { ...pendings.get(key), upload });
2447
2406
  if (!continuing())
@@ -2578,6 +2537,83 @@ export function composeRuntime(options) {
2578
2537
  return UNCERTAIN;
2579
2538
  }
2580
2539
  }
2540
+ async function heldAgain(locator) {
2541
+ const replica = replicas.get(locator);
2542
+ if (replica === undefined || !textOnline())
2543
+ return;
2544
+ try {
2545
+ const wanted = await (await openLocal()).wanted(locator, replica.record.key);
2546
+ await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
2547
+ }
2548
+ catch {
2549
+ journal.record(`hold text ${locator} unwritten`);
2550
+ }
2551
+ }
2552
+ function textOnline() {
2553
+ return !closed && refusal === null && connectionOf() === 'connected';
2554
+ }
2555
+ function textKey({ locator, path }) {
2556
+ return documentKey(locator, path);
2557
+ }
2558
+ async function readText({ locator, path, revision }) {
2559
+ const replica = replicas.get(locator);
2560
+ if (replica === undefined || !textOnline())
2561
+ return;
2562
+ try {
2563
+ await ensureAuthorization();
2564
+ }
2565
+ catch {
2566
+ return;
2567
+ }
2568
+ if (closed || authorization === undefined || refusal !== null)
2569
+ return;
2570
+ const since = fencings;
2571
+ journal.record(`hold text ${locator} ${path}`);
2572
+ const read = await reads
2573
+ .bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
2574
+ .catch(() => null);
2575
+ journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
2576
+ if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
2577
+ return;
2578
+ const content = read.documents[path];
2579
+ const now = replicas.get(locator);
2580
+ if (content === undefined || read.revisions[path] !== revision || now === undefined) {
2581
+ unholdable.set(textKey({ locator, path, revision }), revision);
2582
+ return;
2583
+ }
2584
+ const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
2585
+ if (held.length === 0)
2586
+ unholdable.set(textKey({ locator, path, revision }), revision);
2587
+ else
2588
+ publishChangeSet(changeSet(locator, { documents: held }));
2589
+ }
2590
+ function holdText(unheld) {
2591
+ const key = textKey(unheld);
2592
+ const running = holding.get(key);
2593
+ if (running !== undefined)
2594
+ return running;
2595
+ const started = readText(unheld).finally(() => holding.delete(key));
2596
+ holding.set(key, started);
2597
+ return started;
2598
+ }
2599
+ async function holdTexts(wanted) {
2600
+ const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
2601
+ await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
2602
+ for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
2603
+ await holdText(next);
2604
+ }
2605
+ }));
2606
+ }
2607
+ async function unheldIn(local, locator, key, paths) {
2608
+ if (paths.length === 0)
2609
+ return [];
2610
+ return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
2611
+ entry.binary === null &&
2612
+ entry.outline !== null &&
2613
+ entry.revision !== null
2614
+ ? [{ locator, path: entry.path, revision: entry.revision }]
2615
+ : []);
2616
+ }
2581
2617
  function fetchBinary(request, name) {
2582
2618
  return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2583
2619
  }
@@ -2713,14 +2749,16 @@ export function composeRuntime(options) {
2713
2749
  }
2714
2750
  async function cleanUp() {
2715
2751
  const freed = await database.ask({ kind: 'opened.clean' });
2716
- return freed + ((await options.shell.cleanUp?.()) ?? 0);
2717
- }
2718
- function declaredAgents() {
2719
- for (const capability of capabilities) {
2720
- if (capability.conversationAgents !== undefined)
2721
- return capability.conversationAgents;
2752
+ const local = await openLocal();
2753
+ let texts = 0;
2754
+ for (const [locator, replica] of replicas) {
2755
+ const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
2756
+ texts += released.bytes;
2757
+ if (released.paths.length > 0) {
2758
+ publishChangeSet(changeSet(locator, { documents: released.paths }));
2759
+ }
2722
2760
  }
2723
- return undefined;
2761
+ return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
2724
2762
  }
2725
2763
  function declaredAction(name) {
2726
2764
  for (const capability of capabilities) {
@@ -2773,6 +2811,24 @@ export function composeRuntime(options) {
2773
2811
  return UNCERTAIN;
2774
2812
  }
2775
2813
  }
2814
+ async function readConversationHistory(request) {
2815
+ await ready;
2816
+ const stream = options.adapter.conversations;
2817
+ if (closed || refusal !== null || stream?.history === undefined)
2818
+ return UNCERTAIN;
2819
+ journal.record('transfer conversation history');
2820
+ try {
2821
+ const result = await stream.history(request);
2822
+ if (result.kind === 'revoked-authorization')
2823
+ await refuse('revoked-authorization');
2824
+ journal.record(`outcome conversation history ${result.kind}`);
2825
+ return calledResult(result);
2826
+ }
2827
+ catch {
2828
+ journal.record('outcome conversation history transport-uncertainty');
2829
+ return UNCERTAIN;
2830
+ }
2831
+ }
2776
2832
  function unsettled(result) {
2777
2833
  return result.kind !== 'action' && result.retryable;
2778
2834
  }
@@ -2834,6 +2890,33 @@ export function composeRuntime(options) {
2834
2890
  const held = replicas.get(locator);
2835
2891
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2836
2892
  }
2893
+ function shownAt(locator) {
2894
+ const held = replicas.get(locator)?.record.roomVersion;
2895
+ const raised = highest.get(locator);
2896
+ if (held === undefined || raised === undefined)
2897
+ return held ?? raised;
2898
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2899
+ }
2900
+ function showing(locator, roomVersion) {
2901
+ const at = shownAt(locator);
2902
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2903
+ highest.set(locator, roomVersion);
2904
+ }
2905
+ }
2906
+ function older(locator, roomVersion) {
2907
+ if (beyondHorizon.has(locator))
2908
+ return false;
2909
+ const at = shownAt(locator);
2910
+ return (at !== undefined &&
2911
+ options.adapter.reached(locator, at, roomVersion) &&
2912
+ !options.adapter.reached(locator, roomVersion, at));
2913
+ }
2914
+ function replaced(locators) {
2915
+ for (const locator of locators) {
2916
+ highest.delete(locator);
2917
+ beyondHorizon.add(locator);
2918
+ }
2919
+ }
2837
2920
  async function writtenAfter(locator, roomVersion, path) {
2838
2921
  const held = replicas.get(locator);
2839
2922
  if (held === undefined)
@@ -2844,122 +2927,27 @@ export function composeRuntime(options) {
2844
2927
  options.adapter.reached(locator, written, roomVersion) &&
2845
2928
  !options.adapter.reached(locator, roomVersion, written));
2846
2929
  }
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;
2930
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2931
+ if (paths.length === 0 || !older(locator, roomVersion))
2932
+ return new Set();
2933
+ const entries = await local.entries(key, 'confirmed', paths);
2934
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2935
+ options.adapter.reached(locator, written, roomVersion) &&
2936
+ !options.adapter.reached(locator, roomVersion, written)
2937
+ ? [path]
2938
+ : []));
2948
2939
  }
2949
- function refreshingOf(local, key, composition, liveness, replaying = null) {
2950
- const write = replaying === null ? exclusive : installWrite;
2940
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2941
+ const write = replaying ? finishingWrite : exclusive;
2951
2942
  return {
2952
- async turn() {
2953
- if (replaying === null)
2954
- return;
2955
- installing(replaying);
2956
- await screened();
2943
+ turn() {
2944
+ if (replaying)
2945
+ installing(locator);
2957
2946
  },
2958
2947
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2948
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2959
2949
  async stage(page) {
2960
2950
  for (let written = 0;;) {
2961
- if (replaying !== null)
2962
- await screened();
2963
2951
  const end = batchEnd(page.entries, written);
2964
2952
  const last = end === page.entries.length;
2965
2953
  const entries = page.entries.slice(written, end);
@@ -3004,32 +2992,71 @@ export function composeRuntime(options) {
3004
2992
  return done;
3005
2993
  }
3006
2994
  function installWrite(work) {
3007
- const next = installWrites.then(() => exclusive(work), () => exclusive(work));
3008
- installWrites = next.then(() => undefined, () => undefined);
3009
- return next;
2995
+ return installTurn(installTurns.starting, work);
2996
+ }
2997
+ function finishingWrite(work) {
2998
+ return installTurn(installTurns.finishing, work);
2999
+ }
3000
+ function installTurn(queue, work) {
3001
+ return new Promise((resolve, reject) => {
3002
+ queue.push(() => void exclusive(work)
3003
+ .then(resolve, reject)
3004
+ .finally(() => {
3005
+ installTurns.busy = false;
3006
+ nextInstallTurn();
3007
+ }));
3008
+ nextInstallTurn();
3009
+ });
3010
3010
  }
3011
- function screened() {
3012
- if (onScreen || closed)
3013
- return Promise.resolve();
3014
- queueReadiness();
3015
- return new Promise((resolve) => shownWaiters.push(resolve));
3011
+ function nextInstallTurn() {
3012
+ if (installTurns.busy || installTurns.ahead > 0)
3013
+ return;
3014
+ const turn = installTurns.finishing.shift() ?? installTurns.starting.shift();
3015
+ if (turn === undefined)
3016
+ return;
3017
+ installTurns.busy = true;
3018
+ turn();
3019
+ }
3020
+ async function ahead(work) {
3021
+ let taken = false;
3022
+ const hold = {
3023
+ take() {
3024
+ if (taken)
3025
+ return;
3026
+ taken = true;
3027
+ installTurns.ahead += 1;
3028
+ },
3029
+ release() {
3030
+ if (!taken)
3031
+ return;
3032
+ taken = false;
3033
+ installTurns.ahead -= 1;
3034
+ resumeInstall();
3035
+ },
3036
+ };
3037
+ hold.take();
3038
+ try {
3039
+ return await work(hold);
3040
+ }
3041
+ finally {
3042
+ hold.release();
3043
+ }
3044
+ }
3045
+ function resumeInstall() {
3046
+ if (installTurns.ahead > 0)
3047
+ return;
3048
+ void yielded().then(nextInstallTurn);
3016
3049
  }
3017
3050
  async function heldStream() {
3018
- for (;;) {
3019
- if (streams.generation !== generation)
3020
- streams = { generation, free: STREAMS_HELD };
3021
- const held = streams;
3022
- if (held.free > 0) {
3023
- held.free -= 1;
3024
- return once(() => {
3025
- held.free += 1;
3026
- });
3027
- }
3051
+ while (streamsFree === 0)
3028
3052
  await new Promise((admit) => installWaiters.push(admit));
3029
- }
3053
+ streamsFree -= 1;
3054
+ return once(() => {
3055
+ streamsFree += 1;
3056
+ });
3030
3057
  }
3031
3058
  async function unqueued() {
3032
- while (queuedBytes >= QUEUED_BYTES) {
3059
+ while (queuedText >= QUEUED_TEXT) {
3033
3060
  await new Promise((admit) => installWaiters.push(admit));
3034
3061
  }
3035
3062
  }
@@ -3054,14 +3081,19 @@ export function composeRuntime(options) {
3054
3081
  streaming.delete(locator);
3055
3082
  underived.delete(locator);
3056
3083
  unindexed.delete(locator);
3057
- unchecked.delete(locator);
3084
+ }
3085
+ function setBack(locator) {
3086
+ const now = bytesOf(locator, true);
3087
+ const earlier = startedOver.get(locator);
3088
+ startedOver.set(locator, {
3089
+ done: Math.max(now.done, earlier?.done ?? 0),
3090
+ total: Math.max(now.total, earlier?.total ?? 0),
3091
+ });
3058
3092
  }
3059
3093
  function owedIndexes(locator, bytes, text) {
3060
3094
  if (bytes <= 0 || text <= 0)
3061
3095
  return;
3062
- unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
3063
- const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
3064
- unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
3096
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
3065
3097
  }
3066
3098
  function built(locator, { done, text }) {
3067
3099
  const owed = unindexed.get(locator) ?? [];
@@ -3087,41 +3119,34 @@ export function composeRuntime(options) {
3087
3119
  if (installs.delete(locator))
3088
3120
  queueReadiness();
3089
3121
  }
3090
- function screen(value) {
3091
- if (value === onScreen)
3092
- return;
3093
- onScreen = value;
3094
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3095
- queueReadiness();
3096
- if (!value)
3097
- return;
3098
- for (const resolve of shownWaiters.splice(0))
3099
- resolve();
3100
- }
3101
- async function pullRefresh(authorized, held, liveness) {
3122
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3102
3123
  if (!liveness())
3103
3124
  return unconfirmed;
3104
- const turn = generation;
3105
3125
  const { record } = held;
3106
- const base = held.confirmed;
3126
+ const from = standingOf(record.locator);
3107
3127
  const local = await openLocal();
3108
3128
  try {
3109
3129
  await exclusive(() => local.unrefresh(record.key));
3110
- const result = await advancement(reads, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
3130
+ const result = await advancement(async (authorization, request) => {
3131
+ hold.release();
3132
+ try {
3133
+ return await refresh(authorization, request);
3134
+ }
3135
+ finally {
3136
+ hold.take();
3137
+ }
3138
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3111
3139
  if (result.kind !== 'advanced') {
3112
3140
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
3113
3141
  return result;
3114
3142
  }
3115
- const refused = await validatedRoom(local, result.record, 'refreshed', {
3116
- record,
3117
- changed: [...result.changed],
3118
- });
3119
- if (refused !== null) {
3120
- journal.record(`transfer refresh ${record.locator} refused`);
3121
- return refused;
3122
- }
3123
3143
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3124
- return { kind: 'advanced', record: result.record, base };
3144
+ return {
3145
+ kind: 'advanced',
3146
+ record: result.record,
3147
+ from,
3148
+ remodelled: result.record.model !== record.model,
3149
+ };
3125
3150
  }
3126
3151
  catch (error) {
3127
3152
  if (refusedForSpace(error))
@@ -3129,9 +3154,6 @@ export function composeRuntime(options) {
3129
3154
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
3130
3155
  throw error;
3131
3156
  }
3132
- finally {
3133
- reads.finished(record.locator, turn);
3134
- }
3135
3157
  }
3136
3158
  const heldContents = new Map();
3137
3159
  function heldContentOf(local, key) {
@@ -3149,14 +3171,20 @@ export function composeRuntime(options) {
3149
3171
  heldContents.set(key, made);
3150
3172
  return made;
3151
3173
  }
3174
+ async function abandoned(local, locator) {
3175
+ await local.abandon(locator);
3176
+ setBack(locator);
3177
+ untransferred(locator);
3178
+ liveQueries.recover();
3179
+ sweep();
3180
+ }
3152
3181
  function staging(local, liveness) {
3153
3182
  const derived = async (locator, key, composition) => {
3154
3183
  underived.delete(locator);
3155
3184
  for (;;) {
3156
- await screened();
3157
3185
  if (!liveness())
3158
3186
  return false;
3159
- const done = await installWrite(async () => liveness()
3187
+ const done = await finishingWrite(async () => liveness()
3160
3188
  ? timed(deriveBudget, () => local.rederive(key, composition, deriveBudget.bytes, DERIVE_LONGEST_MS, DERIVE_FLOOR))
3161
3189
  : null);
3162
3190
  if (done === null)
@@ -3170,19 +3198,13 @@ export function composeRuntime(options) {
3170
3198
  held: (locator) => transfers.get(locator) ?? null,
3171
3199
  async turn(locator, step) {
3172
3200
  installing(locator);
3173
- for (;;) {
3174
- await screened();
3175
- const release = step === 'downloading' ? await heldStream() : () => { };
3176
- if (onScreen || closed)
3177
- return release;
3178
- release();
3179
- }
3201
+ return step === 'downloading' ? heldStream() : () => { };
3180
3202
  },
3181
3203
  unqueued,
3182
- queued(bytes) {
3183
- queuedBytes += bytes;
3204
+ queued(text) {
3205
+ queuedText += text;
3184
3206
  return once(() => {
3185
- queuedBytes -= bytes;
3207
+ queuedText -= text;
3186
3208
  });
3187
3209
  },
3188
3210
  arriving(locator) {
@@ -3191,7 +3213,7 @@ export function composeRuntime(options) {
3191
3213
  at(bytes) {
3192
3214
  if (ended)
3193
3215
  return;
3194
- arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
3216
+ arrivals.set(locator, bytes);
3195
3217
  streaming.add(locator);
3196
3218
  const now = performance.now();
3197
3219
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
@@ -3199,14 +3221,6 @@ export function composeRuntime(options) {
3199
3221
  arrivalReported = now;
3200
3222
  queueReadiness();
3201
3223
  },
3202
- part() {
3203
- const arrived = arrivals.get(locator);
3204
- arrivals.set(locator, {
3205
- parts: (arrived?.parts ?? 0) + 1,
3206
- bytes: arrived?.bytes ?? 0,
3207
- });
3208
- queueReadiness();
3209
- },
3210
3224
  end() {
3211
3225
  if (ended)
3212
3226
  return;
@@ -3216,14 +3230,10 @@ export function composeRuntime(options) {
3216
3230
  },
3217
3231
  };
3218
3232
  },
3219
- announced(locator) {
3220
- const later = announced.get(locator);
3221
- return later === undefined ? [] : [later];
3222
- },
3233
+ streamed: options.adapter.roomStream !== undefined,
3223
3234
  async stage(record, page, composition) {
3224
3235
  const { locator } = record;
3225
3236
  installing(locator);
3226
- await screened();
3227
3237
  if (!liveness())
3228
3238
  return null;
3229
3239
  const key = await installWrite(async () => {
@@ -3252,19 +3262,16 @@ export function composeRuntime(options) {
3252
3262
  derive: derived,
3253
3263
  async refreshing(locator, key, composition) {
3254
3264
  await exclusive(() => local.unrefresh(key));
3255
- return refreshingOf(local, key, composition, liveness, locator);
3265
+ return refreshingOf(local, locator, key, composition, liveness, true);
3256
3266
  },
3257
3267
  abandon: (locator) => exclusive(async () => {
3258
- if (!liveness())
3259
- return;
3260
- await local.abandon(locator);
3261
- untransferred(locator);
3262
- liveQueries.recover();
3263
- sweep();
3268
+ if (liveness())
3269
+ await abandoned(local, locator);
3264
3270
  }),
3265
3271
  setAside: async (locator) => {
3266
3272
  if (!liveness())
3267
3273
  return;
3274
+ setBack(locator);
3268
3275
  untransferred(locator);
3269
3276
  liveQueries.recover();
3270
3277
  },
@@ -3273,19 +3280,6 @@ export function composeRuntime(options) {
3273
3280
  sweep();
3274
3281
  }),
3275
3282
  content: (key) => heldContentOf(local, key),
3276
- async validate(record, view) {
3277
- const { locator } = record;
3278
- const refused = await validatedRoom(local, record, view, null, null, (text) => {
3279
- const check = unchecked.get(locator);
3280
- if (check === undefined)
3281
- return;
3282
- unchecked.set(locator, { ...check, done: check.done + text });
3283
- queueReadiness();
3284
- });
3285
- unchecked.delete(locator);
3286
- queueReadiness();
3287
- return refused;
3288
- },
3289
3283
  };
3290
3284
  }
3291
3285
  function reusable(room, local) {
@@ -3297,7 +3291,6 @@ export function composeRuntime(options) {
3297
3291
  async function pullBootstrap(authorized, room, local, liveness) {
3298
3292
  if (!liveness())
3299
3293
  return unconfirmed;
3300
- const turn = generation;
3301
3294
  if (replicas.has(room.locator)) {
3302
3295
  catching.add(room.locator);
3303
3296
  liveQueries.recover();
@@ -3306,6 +3299,13 @@ export function composeRuntime(options) {
3306
3299
  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));
3307
3300
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3308
3301
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3302
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3303
+ caughtUpTo(room.locator);
3304
+ installed(room.locator);
3305
+ await abandoned(local, room.locator);
3306
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3307
+ return { kind: 'transport-uncertainty' };
3308
+ }
3309
3309
  if (result.kind !== 'staged') {
3310
3310
  caughtUpTo(room.locator);
3311
3311
  installed(room.locator);
@@ -3320,9 +3320,6 @@ export function composeRuntime(options) {
3320
3320
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3321
3321
  throw error;
3322
3322
  }
3323
- finally {
3324
- reads.finished(room.locator, turn);
3325
- }
3326
3323
  }
3327
3324
  function caughtUpTo(locator) {
3328
3325
  if (catching.delete(locator))
@@ -3330,72 +3327,83 @@ export function composeRuntime(options) {
3330
3327
  }
3331
3328
  function behind(room) {
3332
3329
  const held = replicas.get(room.locator)?.record.roomVersion;
3333
- return (held !== undefined &&
3334
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3330
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3335
3331
  }
3336
- async function advance(room, local, authorized, live) {
3337
- const carried = reads.carried(room.locator);
3338
- const withheld = await advanceOnce(room, local, authorized, live);
3339
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3340
- return withheld;
3341
- }
3342
- return advanceOnce(room, local, authorized, live);
3332
+ function standingOf(locator) {
3333
+ const held = replicas.get(locator);
3334
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3335
+ }
3336
+ function stands(locator, from) {
3337
+ const now = standingOf(locator);
3338
+ return now.key === from.key && now.confirmed === from.confirmed;
3343
3339
  }
3344
- async function advanceOnce(room, local, authorized, running) {
3340
+ async function advance(room, local, authorized, running) {
3345
3341
  const live = () => running() && !full;
3346
3342
  if (!live())
3347
3343
  return unconfirmed;
3348
3344
  const held = replicas.get(room.locator);
3349
- if (held !== undefined) {
3350
- const refreshed = await pullRefresh(authorized, held, live);
3351
- if (!live())
3352
- return unconfirmed;
3353
- if (refreshed.kind === 'still-current') {
3354
- stale.delete(room.locator);
3355
- if (sourceRefusals.has(room.locator))
3356
- return null;
3357
- if (unreadable.delete(room.locator)) {
3358
- scopedRefusals.delete(room.locator);
3359
- list(room, true);
3360
- emitRooms();
3361
- }
3362
- return null;
3363
- }
3364
- if (refreshed.kind === 'advanced') {
3365
- const landing = { kind: 'refreshed', base: refreshed.base };
3366
- if (!(await publish(local, room, refreshed.record, landing, live))) {
3367
- const now = replicas.get(room.locator)?.record.roomVersion;
3368
- const reached = now !== undefined &&
3369
- options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3370
- return reached ? null : advance(room, local, authorized, live);
3371
- }
3372
- await retainModel(local, room, modelOf(room.locator));
3373
- return null;
3374
- }
3375
- if (refreshed.kind !== 'beyond-horizon')
3345
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3346
+ stale.delete(room.locator);
3347
+ name(room.locator);
3348
+ return null;
3349
+ }
3350
+ const refresh = reads.refresh;
3351
+ if (held !== undefined && refresh !== undefined) {
3352
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3353
+ if (refreshed === 'behind')
3354
+ return advance(room, local, authorized, live);
3355
+ if (refreshed !== 'beyond-horizon')
3376
3356
  return refreshed;
3377
- if (opened !== null) {
3378
- owed.add(room.locator);
3379
- liveQueries.recover();
3380
- return unconfirmed;
3381
- }
3382
- if (reloadsDue.get(room.locator) === generation)
3357
+ if (reloadsDue.has(room.locator))
3383
3358
  return unconfirmed;
3359
+ replaced([room.locator]);
3384
3360
  }
3361
+ const from = standingOf(room.locator);
3385
3362
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3386
- if (bootstrapped.kind === 'staged') {
3387
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3363
+ if (bootstrapped.kind !== 'staged')
3364
+ return live() ? bootstrapped : unconfirmed;
3365
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3366
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3388
3367
  return live() ? null : unconfirmed;
3389
3368
  }
3390
- return live() ? bootstrapped : unconfirmed;
3369
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3370
+ ? null
3371
+ : advance(room, local, authorized, live);
3372
+ }
3373
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3374
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3375
+ if (!live())
3376
+ return unconfirmed;
3377
+ if (refreshed.kind === 'still-current') {
3378
+ stale.delete(room.locator);
3379
+ if (unreadable.delete(room.locator)) {
3380
+ scopedRefusals.delete(room.locator);
3381
+ list(room, true);
3382
+ emitRooms();
3383
+ }
3384
+ return null;
3385
+ }
3386
+ if (refreshed.kind === 'advanced') {
3387
+ const landing = {
3388
+ kind: 'refreshed',
3389
+ from: refreshed.from,
3390
+ remodelled: refreshed.remodelled,
3391
+ };
3392
+ if (!(await publish(local, room, refreshed.record, landing, live))) {
3393
+ const now = replicas.get(room.locator)?.record.roomVersion;
3394
+ const reached = now !== undefined &&
3395
+ options.adapter.reached(room.locator, now, refreshed.record.roomVersion);
3396
+ return reached ? null : 'behind';
3397
+ }
3398
+ await retainModel(local, room, modelOf(room.locator));
3399
+ return null;
3400
+ }
3401
+ return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3391
3402
  }
3392
3403
  async function withhold(withheld, room, local, progress) {
3393
- if (withheld.kind === 'unconfirmed')
3404
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3394
3405
  return;
3395
- if (withheld.kind === 'refused')
3396
- stale.delete(room.locator);
3397
- else
3398
- stale.add(room.locator);
3406
+ stale.add(room.locator);
3399
3407
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3400
3408
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3401
3409
  repairable.set(room.locator, reset);
@@ -3417,30 +3425,10 @@ export function composeRuntime(options) {
3417
3425
  list(room, false);
3418
3426
  emitRooms();
3419
3427
  }
3420
- else if (withheld.kind === 'refused') {
3421
- refusedSource(room, withheld);
3422
- }
3423
3428
  else if (withheld.kind === 'revoked-authorization') {
3424
3429
  await refuse('revoked-authorization');
3425
3430
  }
3426
3431
  }
3427
- function refusedSource(room, { refusal }) {
3428
- sourceRefusals.set(room.locator, {
3429
- code: refusal.rule,
3430
- retryable: false,
3431
- fields: {
3432
- file: refusal.file,
3433
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3434
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3435
- },
3436
- });
3437
- declarationRefusals.delete(room.locator);
3438
- unreadable.add(room.locator);
3439
- list(room, false);
3440
- emitRooms();
3441
- moveTo({ ...status, ...attention() });
3442
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3443
- }
3444
3432
  function leaving(batch, result) {
3445
3433
  if (result.kind === 'expired')
3446
3434
  return { batch, returned: returnedBy('expired') };
@@ -3462,8 +3450,8 @@ export function composeRuntime(options) {
3462
3450
  function settling(acceptance) {
3463
3451
  return acceptance !== null && 'apply' in acceptance;
3464
3452
  }
3465
- function refusalOf(acceptance) {
3466
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3453
+ function failureOf(acceptance) {
3454
+ return settling(acceptance) ? null : acceptance;
3467
3455
  }
3468
3456
  async function acceptance(room, local, submitted, accepted) {
3469
3457
  if (!continuing())
@@ -3476,7 +3464,8 @@ export function composeRuntime(options) {
3476
3464
  logAccepted(room.locator, submitted, accepted);
3477
3465
  let record = current.record;
3478
3466
  let changes;
3479
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3467
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3468
+ !older(room.locator, accepted.roomVersion)) {
3480
3469
  const paths = [...returnedPaths(submitted, accepted)];
3481
3470
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3482
3471
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3486,34 +3475,6 @@ export function composeRuntime(options) {
3486
3475
  ? accepted.roomVersion
3487
3476
  : held,
3488
3477
  };
3489
- const overrides = new Map([
3490
- ...back.set.map((entry) => [entry.path, entry]),
3491
- ...back.removed.map((path) => [path, null]),
3492
- ]);
3493
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3494
- if (refused !== null) {
3495
- const key = pendingKey(room.locator, submitted.id);
3496
- return {
3497
- locator: room.locator,
3498
- left: [],
3499
- commit: {
3500
- rooms: [],
3501
- writes: [
3502
- {
3503
- locator: room.locator,
3504
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3505
- },
3506
- ],
3507
- },
3508
- refused,
3509
- apply() {
3510
- const state = pendings.get(key);
3511
- if (state !== undefined && !fences.has(room.locator))
3512
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3513
- return changeSet(room.locator, { queue: true });
3514
- },
3515
- };
3516
- }
3517
3478
  changes = back;
3518
3479
  }
3519
3480
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3521,10 +3482,9 @@ export function composeRuntime(options) {
3521
3482
  ...settled,
3522
3483
  apply(answer) {
3523
3484
  const set = settled.apply(answer);
3485
+ showing(room.locator, accepted.roomVersion);
3524
3486
  pendings.delete(pendingKey(room.locator, submitted.id));
3525
3487
  unreadable.delete(room.locator);
3526
- sourceRefusals.delete(room.locator);
3527
- declarationRefusals.delete(room.locator);
3528
3488
  list(room, true);
3529
3489
  return set;
3530
3490
  },
@@ -3542,13 +3502,9 @@ export function composeRuntime(options) {
3542
3502
  const failed = await exclusive(async () => {
3543
3503
  const adopting = await acceptance(room, local, submitted, accepted);
3544
3504
  await adoptedTogether(local, [adopting]);
3545
- return refusalOf(adopting);
3505
+ return failureOf(adopting);
3546
3506
  });
3547
- if (failed === null)
3548
- return true;
3549
- if (failed.kind === 'refused' && continuing())
3550
- refusedSource(room, failed);
3551
- return false;
3507
+ return failed === null;
3552
3508
  }
3553
3509
  async function adoptedMove(room, partner, local, submitted, accepted) {
3554
3510
  const other = discovered.get(partner);
@@ -3562,18 +3518,10 @@ export function composeRuntime(options) {
3562
3518
  ...accepted.destination,
3563
3519
  });
3564
3520
  await adoptedTogether(local, [left, arrived]);
3565
- const refusedArrival = refusalOf(arrived);
3566
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3567
- refusedSource(other, refusedArrival);
3568
- }
3569
- return refusalOf(left);
3521
+ return failureOf(left);
3570
3522
  });
3571
3523
  submitLater(partner);
3572
- if (failed === null)
3573
- return true;
3574
- if (failed.kind === 'refused' && continuing())
3575
- refusedSource(room, failed);
3576
- return false;
3524
+ return failed === null;
3577
3525
  }
3578
3526
  function queueIn(locator) {
3579
3527
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3692,12 +3640,10 @@ export function composeRuntime(options) {
3692
3640
  async function submitRoom(room, local, authorized, progress, signal) {
3693
3641
  while (continuing() && authorization !== undefined && !signal.aborted) {
3694
3642
  const replica = replicas.get(room.locator);
3695
- const head = replica?.queue[0];
3643
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3696
3644
  if (replica === undefined || head === undefined)
3697
3645
  return 'done';
3698
3646
  const state = pendings.get(pendingKey(room.locator, head.id));
3699
- if (state?.accepted !== undefined)
3700
- return 'done';
3701
3647
  const partner = partnerOf(head, room.locator);
3702
3648
  if (partner !== null && !leavingIn(head, room.locator)) {
3703
3649
  submitLater(partner);
@@ -3811,12 +3757,12 @@ export function composeRuntime(options) {
3811
3757
  }
3812
3758
  if (result.kind === 'expired') {
3813
3759
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3814
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3760
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3815
3761
  if (other !== undefined && there !== null) {
3816
3762
  await withhold(there, other, local, progress);
3817
3763
  return 'done';
3818
3764
  }
3819
- const withheld = await advance(room, local, authorized, continuing);
3765
+ const withheld = await advanced(room, local, authorized);
3820
3766
  if (withheld !== null) {
3821
3767
  await withhold(withheld, room, local, progress);
3822
3768
  return 'done';
@@ -3838,8 +3784,8 @@ export function composeRuntime(options) {
3838
3784
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3839
3785
  const left = leaving(head, result);
3840
3786
  const refreshed = async () => ({
3841
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3842
- here: await advance(room, local, authorized, continuing),
3787
+ there: other === undefined ? null : await advanced(other, local, authorized),
3788
+ here: await advanced(room, local, authorized),
3843
3789
  });
3844
3790
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3845
3791
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3863,6 +3809,13 @@ export function composeRuntime(options) {
3863
3809
  (await landedWhole(room.locator, head, confirmedLayer))) ||
3864
3810
  (result.kind === 'conflicted' &&
3865
3811
  (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3812
+ if (held &&
3813
+ result.kind === 'conflicted' &&
3814
+ partner === null &&
3815
+ !landed(head, confirmedLayer)) {
3816
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3817
+ return;
3818
+ }
3866
3819
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3867
3820
  await unpair(local);
3868
3821
  moveTo({ ...status, ...attention() });
@@ -3919,8 +3872,10 @@ export function composeRuntime(options) {
3919
3872
  for (const { room, before, queue } of held) {
3920
3873
  const { locator } = room;
3921
3874
  lastFenced.set(locator, ++fencings);
3922
- for (const batch of queue)
3875
+ for (const batch of queue) {
3923
3876
  pendings.delete(pendingKey(locator, batch.id));
3877
+ landedAhead.delete(pendingKey(locator, batch.id));
3878
+ }
3924
3879
  queuesWithoutReplica.delete(locator);
3925
3880
  if (before !== undefined) {
3926
3881
  replicas.set(locator, {
@@ -3943,25 +3898,22 @@ export function composeRuntime(options) {
3943
3898
  if (signal.aborted)
3944
3899
  return;
3945
3900
  const progress = { complete: true, reachable: true };
3946
- await roomTask(room.locator, async (live) => {
3947
- await Promise.race([
3948
- reload(room, local, progress, () => live() && !signal.aborted),
3949
- abandonedOn(signal),
3950
- ]);
3951
- });
3901
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
3952
3902
  if (signal.aborted)
3953
3903
  return;
3954
3904
  await rest(progress);
3955
3905
  }
3956
3906
  }
3957
3907
  function awaitingAnswer(locator) {
3908
+ answering.get(locator)?.release();
3958
3909
  let release = () => { };
3959
- const awaited = new Promise((resolve) => {
3910
+ const answered = new Promise((resolve) => {
3960
3911
  release = resolve;
3961
3912
  });
3962
- answering.set(locator, { generation, answered: awaited });
3913
+ const awaited = { answered, release };
3914
+ answering.set(locator, awaited);
3963
3915
  return () => {
3964
- if (answering.get(locator)?.answered === awaited)
3916
+ if (answering.get(locator) === awaited)
3965
3917
  answering.delete(locator);
3966
3918
  release();
3967
3919
  };
@@ -3973,6 +3925,30 @@ export function composeRuntime(options) {
3973
3925
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
3974
3926
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
3975
3927
  }
3928
+ async function keepNaming(local, room) {
3929
+ const held = replicas.get(room.locator)?.record;
3930
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3931
+ return;
3932
+ await exclusive(async () => {
3933
+ if (!continuing())
3934
+ return;
3935
+ const replica = replicas.get(room.locator);
3936
+ if (replica === undefined)
3937
+ return;
3938
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3939
+ await local.commit({
3940
+ rooms: [
3941
+ {
3942
+ locator: room.locator,
3943
+ key: record.key,
3944
+ record: storedRecord(record),
3945
+ composed: composedOf(composed(record)),
3946
+ },
3947
+ ],
3948
+ });
3949
+ replicas.set(room.locator, { ...replica, record });
3950
+ });
3951
+ }
3976
3952
  async function retainModel(local, room, next) {
3977
3953
  const key = room.locator;
3978
3954
  const previous = modelOf(room.locator);
@@ -3988,26 +3964,15 @@ export function composeRuntime(options) {
3988
3964
  const held = replicas.get(room.locator);
3989
3965
  let changed = null;
3990
3966
  if (held !== undefined) {
3991
- const declared = {
3967
+ const record = {
3992
3968
  ...held.record,
3969
+ name: room.name,
3970
+ handle: room.handle,
3993
3971
  operations: room.operations,
3994
3972
  replica: room.replica,
3995
3973
  typedSelection: room.typedSelection ?? null,
3996
3974
  model: next,
3997
3975
  };
3998
- let read = 'staged';
3999
- if (!sameMembership(declared, held.record)) {
4000
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
4001
- read =
4002
- verdict.verdict !== 'refuse'
4003
- ? 'staged'
4004
- : verdict.rule === 'model-unavailable'
4005
- ? 'deferred'
4006
- : { kind: 'refused', refusal: verdict };
4007
- }
4008
- const record = read === 'staged'
4009
- ? declared
4010
- : { ...declared, typedSelection: held.record.typedSelection };
4011
3976
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
4012
3977
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
4013
3978
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -4029,16 +3994,6 @@ export function composeRuntime(options) {
4029
3994
  documents: answer.changed[room.locator] ?? [],
4030
3995
  room: true,
4031
3996
  });
4032
- if (read === 'staged' || read === 'deferred') {
4033
- if (declarationRefusals.delete(room.locator)) {
4034
- sourceRefusals.delete(room.locator);
4035
- unreadable.delete(room.locator);
4036
- }
4037
- }
4038
- else {
4039
- refusedSource(room, read);
4040
- declarationRefusals.add(room.locator);
4041
- }
4042
3997
  }
4043
3998
  heldModel(room, next);
4044
3999
  if (changed === null)
@@ -4083,6 +4038,17 @@ export function composeRuntime(options) {
4083
4038
  emitRooms();
4084
4039
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
4085
4040
  }
4041
+ function name(locator) {
4042
+ const stream = options.adapter.roomStream;
4043
+ const record = replicas.get(locator)?.record;
4044
+ const room = discovered.get(locator);
4045
+ if (stream === undefined || record === undefined || room === undefined)
4046
+ return;
4047
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
4048
+ return;
4049
+ naming += 1;
4050
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
4051
+ }
4086
4052
  function heldRooms(listed) {
4087
4053
  return listed.flatMap((room) => {
4088
4054
  const record = replicas.get(room.locator)?.record;
@@ -4100,14 +4066,19 @@ export function composeRuntime(options) {
4100
4066
  return outcome?.status === 'fulfilled' && outcome.value;
4101
4067
  });
4102
4068
  if (attempt === 'opened') {
4069
+ naming += 1;
4103
4070
  options.adapter.roomStream?.hold(heldRooms(ready));
4104
4071
  handed = true;
4105
4072
  }
4106
- discovering?.release();
4107
- const loaded = await Promise.allSettled(ready
4108
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
4109
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
4110
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
4073
+ for (const room of ready) {
4074
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4075
+ if (attempt === 'authorized')
4076
+ name(room.locator);
4077
+ continue;
4078
+ }
4079
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
4080
+ }
4081
+ const failed = prepared.find((room) => room.status === 'rejected');
4111
4082
  if (failed !== undefined)
4112
4083
  throw failed.reason;
4113
4084
  }
@@ -4126,12 +4097,16 @@ export function composeRuntime(options) {
4126
4097
  }
4127
4098
  if (prior !== undefined)
4128
4099
  await retainModel(local, room, modelOf(room.locator));
4100
+ if (prior !== undefined)
4101
+ await keepNaming(local, room);
4129
4102
  if (discoveryPaused || authorization === undefined)
4130
4103
  return false;
4131
4104
  return room.replica.admission === 'admitted';
4132
4105
  }
4133
4106
  async function caughtUp(room, local, progress, live) {
4134
- if (authorization === undefined || !live() || !behind(room))
4107
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
4108
+ return;
4109
+ if (!live() || !behind(room))
4135
4110
  return;
4136
4111
  const withheld = await advance(room, local, authorization, live);
4137
4112
  if (withheld !== null)
@@ -4144,6 +4119,8 @@ export function composeRuntime(options) {
4144
4119
  async function loadRoom(room, local, progress, live) {
4145
4120
  if (!live() || authorization === undefined)
4146
4121
  return;
4122
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
4123
+ return;
4147
4124
  if (replicas.has(room.locator) && revoked(room.locator)) {
4148
4125
  await reload(room, local, progress, live);
4149
4126
  return;
@@ -4161,20 +4138,53 @@ export function composeRuntime(options) {
4161
4138
  reloadsDue.delete(room.locator);
4162
4139
  if (!live() || authorization === undefined)
4163
4140
  return;
4141
+ const from = standingOf(room.locator);
4164
4142
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
4165
4143
  if (bootstrapped.kind !== 'staged') {
4166
4144
  if (live())
4167
4145
  await withhold(bootstrapped, room, local, progress);
4168
4146
  return;
4169
4147
  }
4170
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
4148
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4171
4149
  await caughtUp(room, local, progress, live);
4172
4150
  }
4173
4151
  finally {
4174
4152
  caughtUpTo(room.locator);
4175
4153
  }
4176
4154
  }
4177
- async function discoverRooms(local, progress, attempt, live, scope) {
4155
+ async function asked(authorized) {
4156
+ try {
4157
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
4158
+ }
4159
+ catch {
4160
+ return { kind: 'transport-uncertainty' };
4161
+ }
4162
+ }
4163
+ function malformed(listed) {
4164
+ for (const room of listed) {
4165
+ if (room.operations === undefined ||
4166
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4167
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
4168
+ room.replica === undefined ||
4169
+ typeof room.replica.selection !== 'string' ||
4170
+ room.replica.selection.length === 0 ||
4171
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4172
+ !['locked', 'open'].includes(room.replica.offline)) {
4173
+ return `invalid model or replica declaration ${room.locator}`;
4174
+ }
4175
+ if (!declaredSelection(room))
4176
+ return `invalid typed selection declaration ${room.locator}`;
4177
+ }
4178
+ return null;
4179
+ }
4180
+ async function signIn(local, signedIn) {
4181
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
4182
+ return;
4183
+ await local.fact('member', signedIn);
4184
+ member = signedIn;
4185
+ emitMember();
4186
+ }
4187
+ async function discoverRooms(local, progress, attempt, live, found) {
4178
4188
  if (closed || authorization === undefined) {
4179
4189
  notifyReady();
4180
4190
  return;
@@ -4182,13 +4192,7 @@ export function composeRuntime(options) {
4182
4192
  moveTo(transferring(status));
4183
4193
  discoveries += 1;
4184
4194
  const started = discoveries;
4185
- let result;
4186
- try {
4187
- result = await options.adapter.discover(authorization, scope);
4188
- }
4189
- catch {
4190
- result = { kind: 'transport-uncertainty' };
4191
- }
4195
+ const result = found ?? (await asked(authorization));
4192
4196
  if (result.kind === 'discovery')
4193
4197
  roomsNamed = true;
4194
4198
  if (!live()) {
@@ -4198,7 +4202,7 @@ export function composeRuntime(options) {
4198
4202
  }
4199
4203
  journal.record(`connection discover ${result.kind}`);
4200
4204
  if (result.kind === 'transport-uncertainty') {
4201
- await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4205
+ await cachedModels(local);
4202
4206
  progress.reachable = false;
4203
4207
  progress.complete = false;
4204
4208
  notifyReady();
@@ -4219,26 +4223,12 @@ export function composeRuntime(options) {
4219
4223
  notifyReady();
4220
4224
  return;
4221
4225
  }
4222
- for (const room of result.rooms) {
4223
- if (room.operations === undefined ||
4224
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4225
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
4226
- room.replica === undefined ||
4227
- typeof room.replica.selection !== 'string' ||
4228
- room.replica.selection.length === 0 ||
4229
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4230
- !['locked', 'open'].includes(room.replica.offline)) {
4231
- journal.record(`invalid model or replica declaration ${room.locator}`);
4232
- await refuse('missing-required-guarantee');
4233
- notifyReady();
4234
- return;
4235
- }
4236
- if (!declaredSelection(room)) {
4237
- journal.record(`invalid typed selection declaration ${room.locator}`);
4238
- await refuse('missing-required-guarantee');
4239
- notifyReady();
4240
- return;
4241
- }
4226
+ const invalid = malformed(result.rooms);
4227
+ if (invalid !== null) {
4228
+ journal.record(invalid);
4229
+ await refuse('missing-required-guarantee');
4230
+ notifyReady();
4231
+ return;
4242
4232
  }
4243
4233
  if (refusal !== null) {
4244
4234
  refusal = null;
@@ -4253,15 +4243,9 @@ export function composeRuntime(options) {
4253
4243
  limits = result.providerLimits;
4254
4244
  await local.fact('providerLimits', limits);
4255
4245
  await local.fact('capabilities', capabilities);
4256
- conversations.declared();
4257
- }
4258
- const signedIn = result.member ?? null;
4259
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4260
- await local.fact('member', signedIn);
4261
- member = signedIn;
4262
- emitMember();
4263
4246
  }
4264
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4247
+ await signIn(local, result.member ?? null);
4248
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4265
4249
  }
4266
4250
  async function ensureAuthorization() {
4267
4251
  if (authorization !== undefined)
@@ -4276,23 +4260,45 @@ export function composeRuntime(options) {
4276
4260
  }
4277
4261
  }
4278
4262
  async function admittedRead(work) {
4279
- if (roomReads.generation !== generation) {
4280
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4263
+ while (roomReads.free === 0) {
4264
+ await new Promise((admit) => roomReads.waiting.push(admit));
4281
4265
  }
4282
- const reads = roomReads;
4283
- while (reads.free === 0)
4284
- await new Promise((admit) => reads.waiting.push(admit));
4285
- reads.free -= 1;
4266
+ roomReads.free -= 1;
4286
4267
  try {
4287
4268
  await work();
4288
4269
  }
4289
4270
  finally {
4290
- reads.free += 1;
4291
- reads.waiting.shift()?.();
4271
+ roomReads.free += 1;
4272
+ roomReads.waiting.shift()?.();
4292
4273
  }
4293
4274
  }
4275
+ function inRoom(locator, work) {
4276
+ roomWork += 1;
4277
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4278
+ try {
4279
+ return await work(continuing);
4280
+ }
4281
+ finally {
4282
+ roomWork -= 1;
4283
+ }
4284
+ });
4285
+ const chain = ran.then(() => undefined, () => undefined);
4286
+ roomLanes.set(locator, chain);
4287
+ working.add(chain);
4288
+ void chain.finally(() => {
4289
+ working.delete(chain);
4290
+ if (roomLanes.get(locator) === chain)
4291
+ roomLanes.delete(locator);
4292
+ });
4293
+ return ran;
4294
+ }
4294
4295
  function roomTask(locator, work) {
4295
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4296
+ if (stopping)
4297
+ return Promise.resolve();
4298
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4299
+ }
4300
+ function advanced(room, local, authorized) {
4301
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4296
4302
  }
4297
4303
  function task(lane, work) {
4298
4304
  if (stopping)
@@ -4308,23 +4314,19 @@ export function composeRuntime(options) {
4308
4314
  void run.finally(() => working.delete(run));
4309
4315
  return run;
4310
4316
  }
4311
- function sync(attempt) {
4317
+ function sync(attempt, found) {
4312
4318
  return task(OPENING_LANE, async (live) => {
4313
4319
  if (!live()) {
4314
4320
  readyFromStore();
4315
4321
  return;
4316
4322
  }
4317
4323
  const progress = { complete: true, reachable: true };
4318
- const gate = discover();
4319
- if (attempt === 'opened')
4320
- opened = generation;
4321
- underway = attempt === 'opened' ? { generation, progress } : null;
4324
+ underway = attempt === 'opened' ? progress : null;
4322
4325
  if (attempt === 'opened')
4323
4326
  streamed = progress;
4324
4327
  complete = false;
4325
4328
  recorded = false;
4326
4329
  moveTo(transferring(status));
4327
- handed = false;
4328
4330
  try {
4329
4331
  const local = await openLocal();
4330
4332
  if (!live())
@@ -4332,7 +4334,7 @@ export function composeRuntime(options) {
4332
4334
  await ensureAuthorization();
4333
4335
  if (!live())
4334
4336
  return;
4335
- await discoverRooms(local, progress, attempt, live);
4337
+ await discoverRooms(local, progress, attempt, live, found);
4336
4338
  }
4337
4339
  catch {
4338
4340
  progress.complete = false;
@@ -4340,7 +4342,6 @@ export function composeRuntime(options) {
4340
4342
  progress.reachable = false;
4341
4343
  }
4342
4344
  finally {
4343
- gate.release();
4344
4345
  notifyReady();
4345
4346
  }
4346
4347
  if (!live())
@@ -4352,35 +4353,59 @@ export function composeRuntime(options) {
4352
4353
  if (handed || closed || progress.reachable)
4353
4354
  return;
4354
4355
  underway = null;
4355
- opened = null;
4356
- journal.record('connection reopen');
4357
4356
  await rest(progress, live);
4358
- options.adapter.roomStream?.reopen();
4359
4357
  });
4360
4358
  }
4361
- function rediscover(scope) {
4359
+ function resynchronize() {
4362
4360
  return task(OPENING_LANE, async (live) => {
4363
- if (!live() || !socketOpen)
4361
+ if (!live() || !announced)
4364
4362
  return;
4365
4363
  const progress = { complete: true, reachable: true };
4366
- const gate = discover();
4367
4364
  moveTo(transferring(status));
4368
4365
  try {
4369
4366
  const local = await openLocal();
4370
4367
  if (!live())
4371
4368
  return;
4372
- await ensureAuthorization();
4369
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4370
+ }
4371
+ catch {
4372
+ progress.complete = false;
4373
+ }
4374
+ if (live())
4375
+ await finishOpening(progress, live);
4376
+ });
4377
+ }
4378
+ function countAuthorityChange() {
4379
+ authorityChanges += 1;
4380
+ for (const listener of authorityChangeListeners)
4381
+ listener(authorityChanges);
4382
+ }
4383
+ function changeAuthority(change) {
4384
+ return task(OPENING_LANE, async (live) => {
4385
+ if (!live() || !announced)
4386
+ return;
4387
+ countAuthorityChange();
4388
+ const progress = { complete: true, reachable: true };
4389
+ moveTo(transferring(status));
4390
+ try {
4391
+ const local = await openLocal();
4373
4392
  if (!live())
4374
4393
  return;
4375
- await discoverRooms(local, progress, 'authorized', live, scope);
4394
+ const invalid = malformed(change.rooms);
4395
+ if (invalid !== null) {
4396
+ journal.record(invalid);
4397
+ await refuse('missing-required-guarantee');
4398
+ return;
4399
+ }
4400
+ if (change.member !== undefined)
4401
+ await signIn(local, change.member);
4402
+ discoveries += 1;
4403
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4404
+ await synchronize(listed, local, progress, 'authorized', live);
4376
4405
  }
4377
4406
  catch {
4378
4407
  progress.complete = false;
4379
4408
  }
4380
- finally {
4381
- gate.release();
4382
- notifyReady();
4383
- }
4384
4409
  if (!live())
4385
4410
  return;
4386
4411
  if (!progress.complete)
@@ -4388,15 +4413,17 @@ export function composeRuntime(options) {
4388
4413
  await rest(progress, live);
4389
4414
  });
4390
4415
  }
4391
- function closedSocket(revoked, lost) {
4392
- notifyDiscovered();
4393
- const opened = socketOpen;
4416
+ function closedSocket(revoked, lost, authorityChanged) {
4417
+ notifyReady();
4418
+ const opened = announced;
4394
4419
  socketOpen = false;
4420
+ announced = false;
4421
+ missedChange ||= authorityChanged;
4422
+ handed = false;
4423
+ naming = 0;
4395
4424
  connectionLost();
4396
- if (lost) {
4397
- reads.lost();
4425
+ if (lost)
4398
4426
  conversations.lost();
4399
- }
4400
4427
  if (revoked) {
4401
4428
  void refuse('revoked-authorization');
4402
4429
  return;
@@ -4407,18 +4434,32 @@ export function composeRuntime(options) {
4407
4434
  moveTo({
4408
4435
  ...status,
4409
4436
  state: status.state === 'synced' ? 'syncing' : status.state,
4410
- providerUnreachable: status.providerUnreachable || !opened,
4437
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4438
+ });
4439
+ void closing(opened);
4440
+ }
4441
+ function closing(opened) {
4442
+ return task(OPENING_LANE, async (live) => {
4443
+ if (!live())
4444
+ return;
4445
+ const local = await openLocal();
4446
+ if (!opened && !socketOpen)
4447
+ await cachedModels(local);
4448
+ await rest(null, live);
4411
4449
  });
4412
4450
  }
4413
- function applyOpening(expired) {
4414
- const progress = underway?.generation === generation ? underway.progress : null;
4451
+ function cachedModels(local) {
4452
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4453
+ }
4454
+ function caughtUpRooms(expired) {
4455
+ const progress = underway;
4456
+ underway = null;
4457
+ naming = Math.max(0, naming - 1);
4415
4458
  const cycle = progress ?? { complete: true, reachable: true };
4416
- if (opened === generation)
4417
- opened = null;
4459
+ replaced(expired);
4418
4460
  const reloading = progress === null ? [] : owedReloads(expired);
4419
4461
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4420
4462
  return task(OPENING_LANE, async (live) => {
4421
- underway = null;
4422
4463
  if (!live())
4423
4464
  return;
4424
4465
  if (progress === null) {
@@ -4426,20 +4467,14 @@ export function composeRuntime(options) {
4426
4467
  await rest(cycle, live);
4427
4468
  return;
4428
4469
  }
4429
- await roomsCarried([...expired, ...reloading]);
4430
- if (!live())
4431
- return;
4432
4470
  try {
4433
4471
  const local = await openLocal();
4434
- const reloaded = await replacing;
4472
+ await replacing;
4435
4473
  if (progress.reachable) {
4436
4474
  await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4437
4475
  !expired.includes(room.locator) &&
4438
4476
  !reloading.includes(room.locator)), 'current', local);
4439
4477
  }
4440
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4441
- if (failed !== undefined)
4442
- throw failed.reason;
4443
4478
  }
4444
4479
  catch {
4445
4480
  progress.complete = false;
@@ -4451,11 +4486,8 @@ export function composeRuntime(options) {
4451
4486
  }
4452
4487
  function owedReloads(expired) {
4453
4488
  const reloading = [...discovered.values()]
4454
- .filter((room) => replicas.has(room.locator) &&
4455
- !expired.includes(room.locator) &&
4456
- (owed.has(room.locator) || revoked(room.locator)))
4489
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4457
4490
  .map((room) => room.locator);
4458
- owed.clear();
4459
4491
  for (const locator of reloading)
4460
4492
  catching.add(locator);
4461
4493
  liveQueries.recover();
@@ -4464,16 +4496,17 @@ export function composeRuntime(options) {
4464
4496
  async function reloadNow(replaced, progress) {
4465
4497
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4466
4498
  for (const room of reloaded)
4467
- reloadsDue.set(room.locator, generation);
4499
+ reloadsDue.add(room.locator);
4468
4500
  try {
4469
4501
  const local = await openLocal();
4470
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4502
+ for (const room of reloaded) {
4503
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4504
+ }
4471
4505
  }
4472
4506
  catch {
4473
4507
  for (const room of reloaded)
4474
4508
  reloadsDue.delete(room.locator);
4475
4509
  progress.reachable = false;
4476
- return [];
4477
4510
  }
4478
4511
  }
4479
4512
  async function settleModels(held, status, local) {
@@ -4484,7 +4517,7 @@ export function composeRuntime(options) {
4484
4517
  const settled = await exclusive(async () => {
4485
4518
  if (!continuing())
4486
4519
  return [];
4487
- const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4520
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4488
4521
  for (const room of unchanged) {
4489
4522
  const next = { ...modelOf(room.locator), status };
4490
4523
  const replica = replicas.get(room.locator);
@@ -4544,14 +4577,10 @@ export function composeRuntime(options) {
4544
4577
  try {
4545
4578
  const local = await openLocal();
4546
4579
  await roomTask(locator, async (live) => {
4547
- const current = () => live() && !signal.aborted;
4548
- if (!current() || authorization === undefined)
4580
+ if (!live() || authorization === undefined)
4549
4581
  return;
4550
- const withheld = await Promise.race([
4551
- advance(room, local, authorization, current),
4552
- abandonedOn(signal),
4553
- ]);
4554
- if (withheld !== null && current())
4582
+ const withheld = await advance(room, local, authorization, live);
4583
+ if (withheld !== null && live())
4555
4584
  await withhold(withheld, room, local, progress);
4556
4585
  });
4557
4586
  }
@@ -4606,7 +4635,6 @@ export function composeRuntime(options) {
4606
4635
  progress.complete = false;
4607
4636
  return;
4608
4637
  }
4609
- reads.applied();
4610
4638
  submitAll();
4611
4639
  for (const locator of stale)
4612
4640
  if (repairable.has(locator))
@@ -4637,7 +4665,7 @@ export function composeRuntime(options) {
4637
4665
  if (!live())
4638
4666
  return;
4639
4667
  let lastCompleted = status.lastCompleted;
4640
- let synced = complete && stale.size === 0 && !pending();
4668
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4641
4669
  if (synced && !recorded) {
4642
4670
  const completedAt = new Date().toISOString();
4643
4671
  try {
@@ -4652,74 +4680,78 @@ export function composeRuntime(options) {
4652
4680
  }
4653
4681
  }
4654
4682
  moveTo({
4655
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4683
+ state: pending()
4684
+ ? 'pending'
4685
+ : synced && complete && roomWork === 0 && naming === 0
4686
+ ? 'synced'
4687
+ : 'syncing',
4656
4688
  lastCompleted,
4657
- providerUnreachable: !progress.reachable,
4689
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4658
4690
  ...attention(),
4659
4691
  });
4660
4692
  }
4661
- function discover() {
4662
- let release = () => { };
4663
- const done = new Promise((resolve) => {
4664
- release = resolve;
4665
- });
4666
- discovering = { generation, done, release };
4667
- return discovering;
4668
- }
4669
- function applyRoom(locator, roomVersion) {
4670
- announced.set(locator, roomVersion);
4671
- return task(roomLane(locator), async (live) => {
4672
- if (discovering?.generation === generation)
4673
- await discovering.done;
4674
- const saving = answering.get(locator);
4675
- if (saving?.generation === generation)
4676
- await saving.answered;
4677
- if (!live() || authorization === undefined)
4678
- return;
4679
- const room = discovered.get(locator);
4680
- const held = replicas.get(locator);
4681
- if (room === undefined || room.replica.admission !== 'admitted')
4682
- return;
4683
- if (held !== undefined &&
4684
- (held.record.replica.selection !== room.replica.selection ||
4685
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4686
- return;
4687
- }
4688
- const during = underway?.generation === generation ? underway.progress : null;
4689
- const progress = during ?? { complete: true, reachable: true };
4690
- if (during === null)
4691
- moveTo(transferring(status));
4693
+ function applyRoom(change) {
4694
+ const { locator } = change;
4695
+ if (stopping)
4696
+ return Promise.resolve();
4697
+ return inRoom(locator, async (live) => {
4698
+ await answering.get(locator)?.answered;
4699
+ const progress = underway ?? { complete: true, reachable: true };
4692
4700
  try {
4693
4701
  const local = await openLocal();
4694
- await admittedRead(async () => {
4695
- if (!live() || authorization === undefined)
4696
- return;
4697
- const withheld = await advance(room, local, authorization, live);
4698
- if (withheld !== null)
4699
- await withhold(withheld, room, local, progress);
4700
- });
4702
+ for (;;) {
4703
+ if (!live())
4704
+ return null;
4705
+ const room = discovered.get(locator);
4706
+ const held = replicas.get(locator);
4707
+ if (room === undefined ||
4708
+ held === undefined ||
4709
+ room.replica.admission !== 'admitted' ||
4710
+ held.record.replica.selection !== room.replica.selection) {
4711
+ return null;
4712
+ }
4713
+ const at = held.record.roomVersion;
4714
+ if (options.adapter.reached(locator, at, change.roomVersion))
4715
+ return progress;
4716
+ if (!options.adapter.reached(locator, at, change.from)) {
4717
+ name(locator);
4718
+ return progress;
4719
+ }
4720
+ if (underway === null)
4721
+ moveTo(transferring(status));
4722
+ const writing = () => live() && !full;
4723
+ let outlined = false;
4724
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
4725
+ const page = await change.read();
4726
+ const paths = Object.keys(page.outlines ?? {});
4727
+ outlined = paths.length > 0;
4728
+ await local.remember(locator, held.record.key, paths);
4729
+ return page;
4730
+ }));
4731
+ if (refreshed.kind !== 'advanced')
4732
+ return progress;
4733
+ const landing = {
4734
+ kind: 'refreshed',
4735
+ from: refreshed.from,
4736
+ remodelled: refreshed.remodelled,
4737
+ };
4738
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4739
+ stale.delete(locator);
4740
+ await retainModel(local, room, modelOf(locator));
4741
+ if (outlined)
4742
+ void heldAgain(locator);
4743
+ return progress;
4744
+ }
4745
+ }
4701
4746
  }
4702
4747
  catch {
4748
+ if (!stale.has(locator))
4749
+ name(locator);
4703
4750
  stale.add(locator);
4751
+ return progress;
4704
4752
  }
4705
- finally {
4706
- if (during === null)
4707
- await rest(progress, live);
4708
- }
4709
- });
4753
+ }).then((progress) => rest(progress), () => undefined);
4710
4754
  }
4711
- function roomsCarried(replacing) {
4712
- const replaced = new Set(replacing.map(roomLane));
4713
- return Promise.all([...lanes]
4714
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4715
- .map(([, held]) => held.chain));
4716
- }
4717
- const receipts = {
4718
- heads: async () => (await openLocal()).receivedHeads(),
4719
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4720
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4721
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4722
- };
4723
4755
  function observeStream() {
4724
4756
  const stream = options.adapter.roomStream;
4725
4757
  if (stream === undefined || withdrawStream !== undefined)
@@ -4729,31 +4761,35 @@ export function composeRuntime(options) {
4729
4761
  return;
4730
4762
  if (event.kind === 'opened') {
4731
4763
  socketOpen = true;
4764
+ announced = false;
4765
+ handed = false;
4766
+ naming = 0;
4732
4767
  connectionLive();
4733
4768
  generation += 1;
4734
- for (const admit of installWaiters.splice(0))
4735
- admit();
4736
- reads.opened();
4737
4769
  conversations.reopened();
4738
4770
  recovery.resume();
4739
- void sync('opened');
4771
+ }
4772
+ else if (event.kind === 'discovered') {
4773
+ if (!socketOpen)
4774
+ return;
4775
+ announced = true;
4776
+ if (missedChange) {
4777
+ missedChange = false;
4778
+ countAuthorityChange();
4779
+ }
4780
+ void sync('opened', event.discovery);
4781
+ for (const locator of replicas.keys())
4782
+ void heldAgain(locator);
4740
4783
  }
4741
4784
  else if (event.kind === 'closed')
4742
- closedSocket(event.revoked, event.lost);
4743
- else if (event.kind === 'opening')
4744
- void applyOpening(event.expired);
4785
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4786
+ else if (event.kind === 'caught-up')
4787
+ void caughtUpRooms(event.expired);
4745
4788
  else if (event.kind === 'authority-changed')
4746
- void rediscover(event.scope);
4789
+ void changeAuthority(event);
4747
4790
  else
4748
- void applyRoom(event.locator, event.roomVersion);
4749
- }, receipts, heldRevisions);
4750
- }
4751
- async function heldRevisions(locator, paths) {
4752
- const held = replicas.get(locator);
4753
- if (held === undefined || paths.length === 0)
4754
- return new Map();
4755
- const entries = await (await openLocal()).entries(held.record.key, 'confirmed', paths);
4756
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4791
+ void applyRoom(event);
4792
+ });
4757
4793
  }
4758
4794
  function writesHeld() {
4759
4795
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4826,8 +4862,6 @@ export function composeRuntime(options) {
4826
4862
  notifyDiscovered();
4827
4863
  if (probing !== null)
4828
4864
  clearInterval(probing);
4829
- for (const resolve of shownWaiters.splice(0))
4830
- resolve();
4831
4865
  connectionLive();
4832
4866
  lockListeners.clear();
4833
4867
  readinessWatches.clear();
@@ -4845,6 +4879,7 @@ export function composeRuntime(options) {
4845
4879
  statusListeners.clear();
4846
4880
  serviceModeListeners.clear();
4847
4881
  memberListeners.clear();
4882
+ authorityChangeListeners.clear();
4848
4883
  liveQueries.close();
4849
4884
  queueSubscriptions.clear();
4850
4885
  roomDraftListeners.clear();
@@ -4852,6 +4887,7 @@ export function composeRuntime(options) {
4852
4887
  }
4853
4888
  async function stop() {
4854
4889
  stopping = true;
4890
+ inFlight.abort();
4855
4891
  await recovery.retire();
4856
4892
  await Promise.all(working);
4857
4893
  await mutations;
@@ -4870,7 +4906,7 @@ export function composeRuntime(options) {
4870
4906
  readyFromStore();
4871
4907
  });
4872
4908
  if (options.adapter.roomStream === undefined)
4873
- void sync('unstreamed');
4909
+ void sync('unstreamed', null);
4874
4910
  function planned(input, reach) {
4875
4911
  const plan = planOf(input, roomFacts(reach), planReach(reach));
4876
4912
  const named = plan.paths !== null &&
@@ -4878,7 +4914,7 @@ export function composeRuntime(options) {
4878
4914
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4879
4915
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4880
4916
  }
4881
- function watchPlan(plan, deliver) {
4917
+ function watchPlan(plan, deliver, fail) {
4882
4918
  let seeded = false;
4883
4919
  let pending = true;
4884
4920
  const shown = () => {
@@ -4904,7 +4940,7 @@ export function composeRuntime(options) {
4904
4940
  }
4905
4941
  seeded = false;
4906
4942
  deliver(message);
4907
- });
4943
+ }, fail);
4908
4944
  return () => {
4909
4945
  watchers.delete(shown);
4910
4946
  if (watchers.size === 0)
@@ -4912,61 +4948,59 @@ export function composeRuntime(options) {
4912
4948
  stop();
4913
4949
  };
4914
4950
  }
4915
- async function searched(input, reach, locked) {
4951
+ async function searched(input, reach) {
4916
4952
  const asked = searchPlanOf(input);
4917
- const filter = {
4918
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4919
- locked,
4920
- };
4921
- const unlocking = (filter.rooms ?? [])
4922
- .filter((room) => locked.includes(room))
4923
- .map((room) => ({ room, awaits: 'unlock' }));
4924
- for (;;) {
4925
- const cover = covered(filter);
4926
- awaitPreparation(cover);
4927
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4928
- const complete = pending.length === 0 && !('pending' in cover);
4929
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
4930
- const answering = complete
4931
- ? filter
4932
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
4933
- const answered = covered(answering);
4934
- if ('pending' in answered)
4935
- continue;
4936
- const listed = conversations.searchable();
4937
- const named = reach !== null && !reach.conversations
4938
- ? []
4939
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
4940
- const hits = await (await openLocal()).search({
4941
- text: asked.text,
4942
- limit: capped(asked.limit),
4943
- after: asked.after,
4944
- total: asked.total,
4945
- documents: {
4946
- plan: answering,
4947
- rooms: answered.rooms,
4948
- beneath: asked.collection
4949
- ? answered.rooms.map(({ locator, key }) => ({
4950
- key,
4951
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
4952
- }))
4953
- : [],
4954
- },
4955
- conversations: named,
4956
- });
4957
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
4958
- continue;
4959
- const coverage = {
4960
- rooms: answered.coverage.rooms,
4961
- conversations: named,
4962
- overlay: answered.coverage.overlay,
4953
+ await ready;
4954
+ const search = options.adapter.search;
4955
+ if (search === undefined) {
4956
+ return {
4957
+ kind: 'undeclared',
4958
+ code: 'action-undeclared',
4959
+ fields: { name: 'search' },
4960
+ retryable: false,
4961
+ };
4962
+ }
4963
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4964
+ return OFFLINE;
4965
+ const reached = reach === null ? null : new Set(reach.rooms);
4966
+ const named = asked.rooms === null
4967
+ ? reached === null
4968
+ ? null
4969
+ : [...reached]
4970
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4971
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4972
+ if (rooms !== null && rooms.length === 0)
4973
+ return { kind: 'found', documents: [] };
4974
+ try {
4975
+ await ensureAuthorization();
4976
+ }
4977
+ catch {
4978
+ return UNCERTAIN;
4979
+ }
4980
+ if (closed || authorization === undefined || refusal !== null)
4981
+ return UNCERTAIN;
4982
+ journal.record('transfer search');
4983
+ try {
4984
+ const result = await search(authorization, { text: asked.text, rooms });
4985
+ if (result.kind === 'revoked-authorization')
4986
+ await refuse('revoked-authorization');
4987
+ journal.record(`outcome search ${result.kind}`);
4988
+ if (result.kind === 'rejected')
4989
+ return result;
4990
+ if (result.kind !== 'found')
4991
+ return failure(result);
4992
+ const kept = rooms === null ? null : new Set(rooms);
4993
+ return {
4994
+ kind: 'found',
4995
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
4963
4996
  };
4964
- return complete
4965
- ? { ...hits, coverage }
4966
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4997
+ }
4998
+ catch {
4999
+ journal.record('outcome search transport-uncertainty');
5000
+ return UNCERTAIN;
4967
5001
  }
4968
5002
  }
4969
- const watch = (topic, input, deliver) => {
5003
+ const watch = (topic, input, deliver, fail) => {
4970
5004
  if (topic === 'room-drafts') {
4971
5005
  const listener = { notify: deliver, ready: false };
4972
5006
  roomDraftListeners.add(listener);
@@ -5015,7 +5049,7 @@ export function composeRuntime(options) {
5015
5049
  };
5016
5050
  }
5017
5051
  if (topic === 'readiness') {
5018
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
5052
+ return watchReadiness({ target: '', code: 'stored', space: null }, null, deliver);
5019
5053
  }
5020
5054
  if (topic === 'rooms') {
5021
5055
  unlistedRooms.add(deliver);
@@ -5060,6 +5094,13 @@ export function composeRuntime(options) {
5060
5094
  memberListeners.delete(deliver);
5061
5095
  };
5062
5096
  }
5097
+ if (topic === 'authority-changes') {
5098
+ authorityChangeListeners.add(deliver);
5099
+ deliver(authorityChanges);
5100
+ return () => {
5101
+ authorityChangeListeners.delete(deliver);
5102
+ };
5103
+ }
5063
5104
  if (topic === 'queue') {
5064
5105
  const queue = { locator: input[0], notify: deliver };
5065
5106
  queueSubscriptions.add(queue);
@@ -5068,7 +5109,7 @@ export function composeRuntime(options) {
5068
5109
  queueSubscriptions.delete(queue);
5069
5110
  };
5070
5111
  }
5071
- return watchPlan(planned(input[0], null), deliver);
5112
+ return watchPlan(planned(input[0], null), deliver, fail);
5072
5113
  };
5073
5114
  const md = {
5074
5115
  loaded() {
@@ -5088,7 +5129,7 @@ export function composeRuntime(options) {
5088
5129
  });
5089
5130
  },
5090
5131
  query: ((input) => rowsQuery(planned(input, null))),
5091
- search: (input) => searched(input, null, []),
5132
+ search: (input) => searched(input, null),
5092
5133
  types(locator) {
5093
5134
  return {
5094
5135
  collection: modelTypes(modelOf(locator)),
@@ -5213,7 +5254,7 @@ export function composeRuntime(options) {
5213
5254
  authorization = undefined;
5214
5255
  discoveryPaused = false;
5215
5256
  if (options.adapter.roomStream === undefined)
5216
- await sync('unstreamed');
5257
+ await sync('unstreamed', null);
5217
5258
  else
5218
5259
  await task(OPENING_LANE, ensureAuthorization);
5219
5260
  },
@@ -5236,7 +5277,6 @@ export function composeRuntime(options) {
5236
5277
  md,
5237
5278
  seed,
5238
5279
  stop,
5239
- lose: loseRooms,
5240
5280
  watch,
5241
5281
  persisted(answer) {
5242
5282
  if (answer === persistence)
@@ -5245,7 +5285,6 @@ export function composeRuntime(options) {
5245
5285
  journal.record(`persistence ${answer}`);
5246
5286
  queueReadiness();
5247
5287
  },
5248
- shown: screen,
5249
5288
  within: (reach, port) => {
5250
5289
  if (full)
5251
5290
  void probed();