@agentshouse/mdruntime 0.1.0-cefbc8bb8a1c0 → 0.1.0-cf1e7fdb9c600

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 (118) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter.d.ts +69 -49
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +5 -8
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +20 -21
  7. package/dist/chain.d.ts +2 -1
  8. package/dist/chain.d.ts.map +1 -1
  9. package/dist/client.d.ts +6 -5
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +0 -1
  12. package/dist/collection.d.ts +2 -4
  13. package/dist/collection.d.ts.map +1 -1
  14. package/dist/collection.js +1 -17
  15. package/dist/compile.d.ts +1 -6
  16. package/dist/compile.d.ts.map +1 -1
  17. package/dist/compile.js +97 -281
  18. package/dist/connection.d.ts +5 -7
  19. package/dist/connection.d.ts.map +1 -1
  20. package/dist/connection.js +816 -846
  21. package/dist/context.d.ts +2 -0
  22. package/dist/context.d.ts.map +1 -0
  23. package/dist/context.js +7 -0
  24. package/dist/conversation.d.ts +15 -13
  25. package/dist/conversation.d.ts.map +1 -1
  26. package/dist/conversation.js +133 -142
  27. package/dist/durability.d.ts +0 -1
  28. package/dist/durability.d.ts.map +1 -1
  29. package/dist/entry.d.ts +2 -2
  30. package/dist/entry.d.ts.map +1 -1
  31. package/dist/entry.js +14 -5
  32. package/dist/host.d.ts +3 -7
  33. package/dist/host.d.ts.map +1 -1
  34. package/dist/host.js +12 -26
  35. package/dist/index.d.ts +9 -6
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +3 -1
  38. package/dist/job.d.ts +14 -0
  39. package/dist/job.d.ts.map +1 -0
  40. package/dist/job.js +28 -0
  41. package/dist/live.d.ts +2 -1
  42. package/dist/live.d.ts.map +1 -1
  43. package/dist/live.js +10 -7
  44. package/dist/memory.d.ts +9 -4
  45. package/dist/memory.d.ts.map +1 -1
  46. package/dist/memory.js +132 -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 +1 -3
  51. package/dist/model.d.ts.map +1 -1
  52. package/dist/model.js +0 -12
  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/query.d.ts +5 -209
  59. package/dist/query.d.ts.map +1 -1
  60. package/dist/query.js +3 -505
  61. package/dist/reach.d.ts +7 -7
  62. package/dist/reach.d.ts.map +1 -1
  63. package/dist/reach.js +31 -44
  64. package/dist/react.d.ts +3 -1
  65. package/dist/react.d.ts.map +1 -1
  66. package/dist/readiness.d.ts +2 -15
  67. package/dist/readiness.d.ts.map +1 -1
  68. package/dist/readiness.js +4 -10
  69. package/dist/refresh.d.ts +7 -5
  70. package/dist/refresh.d.ts.map +1 -1
  71. package/dist/refresh.js +46 -6
  72. package/dist/replica.d.ts +2 -2
  73. package/dist/replica.d.ts.map +1 -1
  74. package/dist/replica.js +5 -5
  75. package/dist/room-draft.d.ts +2 -1
  76. package/dist/room-draft.d.ts.map +1 -1
  77. package/dist/room-draft.js +65 -52
  78. package/dist/rows.d.ts +3 -19
  79. package/dist/rows.d.ts.map +1 -1
  80. package/dist/rows.js +36 -164
  81. package/dist/search.d.ts +12 -63
  82. package/dist/search.d.ts.map +1 -1
  83. package/dist/search.js +9 -52
  84. package/dist/shared.d.ts +0 -1
  85. package/dist/shared.d.ts.map +1 -1
  86. package/dist/shared.js +0 -10
  87. package/dist/status.d.ts +0 -4
  88. package/dist/status.d.ts.map +1 -1
  89. package/dist/store.d.ts +15 -19
  90. package/dist/store.d.ts.map +1 -1
  91. package/dist/store.js +40 -39
  92. package/dist/stored.d.ts +33 -6
  93. package/dist/stored.d.ts.map +1 -1
  94. package/dist/stored.js +87 -124
  95. package/dist/tables.d.ts +34 -33
  96. package/dist/tables.d.ts.map +1 -1
  97. package/dist/tables.js +21 -54
  98. package/dist/transfer.d.ts +0 -5
  99. package/dist/transfer.d.ts.map +1 -1
  100. package/dist/validation.d.ts +3 -4
  101. package/dist/validation.d.ts.map +1 -1
  102. package/dist/validation.js +39 -97
  103. package/dist/windows.d.ts +1 -12
  104. package/dist/windows.d.ts.map +1 -1
  105. package/dist/windows.js +2 -75
  106. package/dist/wire.d.ts +1 -4
  107. package/dist/wire.d.ts.map +1 -1
  108. package/dist/wire.js +1 -1
  109. package/package.json +2 -2
  110. package/dist/fulltext.d.ts +0 -27
  111. package/dist/fulltext.d.ts.map +0 -1
  112. package/dist/fulltext.js +0 -370
  113. package/dist/reads.d.ts +0 -11
  114. package/dist/reads.d.ts.map +0 -1
  115. package/dist/reads.js +0 -91
  116. package/dist/words.d.ts +0 -19
  117. package/dist/words.d.ts.map +0 -1
  118. package/dist/words.js +0 -156
@@ -1,8 +1,8 @@
1
- import { escapePointer, parseFrontmatter, roomTarget, target } from '@agentshouse/mdmodel';
1
+ import { bodied, identityOf, parseFrontmatter, plain, queryKey, roomTarget, target, } from '@agentshouse/mdmodel';
2
2
  import { attachedModel, builtInModel, roomTypesOf } from './attached-model.js';
3
3
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
4
4
  import { observing, whole } from './observation.js';
5
- import { filterOf, identityOf, planOf, plain, queryKey, } from './query.js';
5
+ import { planWithin, } from './query.js';
6
6
  import { createLiveQueries } from './live.js';
7
7
  import { seededResult } from './seed.js';
8
8
  import { documentKey, roomOfKey } from './rows.js';
@@ -12,7 +12,7 @@ import { bootstrapRoom } from './bootstrap.js';
12
12
  import { changeSet, mergeChangeSets } from './change-set.js';
13
13
  import { createConversationStore, } from './conversation.js';
14
14
  import { leaving as leavingIn, partnerOf, queuedFile, } from './change.js';
15
- import { available, collectionOf, composedOf, unreadable as unreadableIn, validRoot, } from './collection.js';
15
+ import { available, collectionOf, composedOf, validRoot, } from './collection.js';
16
16
  import { runtimeError } from './failure.js';
17
17
  import { createLog } from './log.js';
18
18
  import { binaryRecord } from './binary.js';
@@ -20,10 +20,9 @@ import { attachedFile } from './file.js';
20
20
  import { openedFiles } from './opened.js';
21
21
  import { ABSENT_TARGET, absentBinary, attachAnswer, binaryAnswer, binaryLinkAnswer, calledResult, failure, historyAnswer, LOCKED_UNREADABLE, lockedRoom, OFFLINE, undeclared, UNCERTAIN, UNREADABLE, } from './outcome.js';
22
22
  import { createRecovery } from './recovery.js';
23
- import { doorReads } from './reads.js';
24
23
  import { advancement } from './refresh.js';
25
24
  import { buildBatch, buildMove, joined, limiting, queuedDocuments, requeuing, } from './save.js';
26
- import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, sameMembership, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
25
+ import { compositionOf, entryAt, landed, movedBy, overlayOf, replaying, returning, returnedPaths, slicing, slicingWith, standing, storedRecord, touchedBy, } from './replica.js';
27
26
  import { binaryBase, changedParts, changesOf, gatheredParts, held, heldFile, joinedDraft, moveTarget, readParts, readSavedParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
28
27
  import { same, saving, STARTING, transferring, } from './status.js';
29
28
  import { detectStorageLoss, nextGeneration, refusedForSpace, } from './durability.js';
@@ -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, its full-text writes included, 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 };
69
+ const NOTHING_HELD = { bytes: 0 };
72
70
  const QUEUED_TEXT = 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.
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);
@@ -120,15 +117,24 @@ export function composeRuntime(options) {
120
117
  const counted = new Map();
121
118
  const unreadable = new Set();
122
119
  const builtIn = builtInModel(roomTypes);
120
+ const typedPaths = options.adapter.typedPaths ?? [];
123
121
  const models = new FencedMap(isFenced);
124
122
  const liveModels = new FencedMap(isFenced);
125
123
  const invalidModels = new Set();
126
124
  const discovered = new Map();
127
- const reads = doorReads(options.adapter, (locator) => discovered.get(locator), () => generation);
125
+ let inFlight = new AbortController();
126
+ const refreshed = options.adapter.refresh?.bind(options.adapter);
127
+ const reads = {
128
+ bootstrap: (authorized, request) => readUntil(options.adapter.bootstrap(authorized, request), inFlight.signal),
129
+ ...(refreshed === undefined
130
+ ? {}
131
+ : {
132
+ refresh: (authorized, request) => readUntil(refreshed(authorized, request), inFlight.signal),
133
+ }),
134
+ reached: (locator, roomVersion, target) => options.adapter.reached(locator, roomVersion, target),
135
+ };
128
136
  let roomsListed = false;
129
137
  const scopedRefusals = new Map();
130
- const sourceRefusals = new Map();
131
- const declarationRefusals = new Set();
132
138
  function modelOf(locator) {
133
139
  return models.get(locator) ?? NO_MODEL;
134
140
  }
@@ -138,7 +144,7 @@ export function composeRuntime(options) {
138
144
  : modelOf(locator);
139
145
  }
140
146
  function composed(record) {
141
- return compositionOf(record, builtIn);
147
+ return compositionOf(record, builtIn, typedPaths);
142
148
  }
143
149
  function compositionFor(record) {
144
150
  return composed({ ...record, model: savedModel(record.locator) });
@@ -151,10 +157,10 @@ export function composeRuntime(options) {
151
157
  let closed = false;
152
158
  let stopping = false;
153
159
  let withdrawStream;
154
- const announced = new Map();
155
160
  let underway = null;
156
161
  let streamed = null;
157
162
  let handed = false;
163
+ let naming = 0;
158
164
  const recovery = (options.recovery ?? createRecovery()).scope(ROOM_ATTEMPTS);
159
165
  const repairing = new Set();
160
166
  const calling = new Set();
@@ -165,12 +171,13 @@ export function composeRuntime(options) {
165
171
  let recorded = false;
166
172
  const stale = new Set();
167
173
  let generation = 0;
168
- let discovering = null;
169
- let opened = null;
170
- const owed = new Set();
171
- const reloadsDue = new Map();
174
+ const reloadsDue = new Set();
175
+ const highest = new Map();
176
+ const beyondHorizon = new Set();
172
177
  const lanes = new Map();
173
- let roomReads = { generation, free: ROOM_READS, waiting: [] };
178
+ const roomLanes = new Map();
179
+ let roomWork = 0;
180
+ const roomReads = { free: ROOM_READS, waiting: [] };
174
181
  const working = new Set();
175
182
  let mutations = Promise.resolve();
176
183
  let gathering = null;
@@ -186,6 +193,8 @@ export function composeRuntime(options) {
186
193
  let withdrawConversations;
187
194
  let currencyQueued = null;
188
195
  let socketOpen = false;
196
+ let announced = false;
197
+ let missedChange = false;
189
198
  const confirmed = new Set();
190
199
  let lockGrace = null;
191
200
  const lockListeners = new Set();
@@ -195,6 +204,8 @@ export function composeRuntime(options) {
195
204
  const serviceModeListeners = new Set();
196
205
  let member = null;
197
206
  const memberListeners = new Set();
207
+ let authorityChanges = 0;
208
+ const authorityChangeListeners = new Set();
198
209
  const unpreparedDemand = new Set();
199
210
  const unpreparable = new Set();
200
211
  let preparing = false;
@@ -224,11 +235,9 @@ export function composeRuntime(options) {
224
235
  const streaming = new Set();
225
236
  const underived = new Map();
226
237
  const unindexed = new Map();
227
- const unchecked = new Map();
238
+ const startedOver = new Map();
228
239
  let arrivalReported = 0;
229
- let onScreen = options.shell.onScreen;
230
- const shownWaiters = [];
231
- let streams = { generation, free: STREAMS_HELD };
240
+ let streamsFree = STREAMS_HELD;
232
241
  let queuedText = 0;
233
242
  const installWaiters = [];
234
243
  const stageBudget = {
@@ -245,26 +254,12 @@ export function composeRuntime(options) {
245
254
  };
246
255
  const installTurns = { busy: false, ahead: 0, finishing: [], starting: [] };
247
256
  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
257
  call: (name, input) => invokeAction(name, input),
258
+ history: (request) => readConversationHistory(request),
263
259
  durably,
264
260
  stream: () => options.adapter.conversations,
265
261
  publish: publishChangeSet,
266
262
  online: () => connectionOf() !== 'unreachable' && refusal === null,
267
- listed: () => roomsListed,
268
263
  full: () => full,
269
264
  closed: () => closed,
270
265
  unanswered: resendLater,
@@ -272,6 +267,8 @@ export function composeRuntime(options) {
272
267
  });
273
268
  const journal = createLog();
274
269
  const keptFiles = openedFiles(database, isFenced);
270
+ const holding = new Map();
271
+ const unholdable = new Map();
275
272
  let readyFromStore = () => { };
276
273
  const ready = new Promise((resolve) => {
277
274
  readyFromStore = resolve;
@@ -361,9 +358,9 @@ export function composeRuntime(options) {
361
358
  return value === undefined ? null : structuredClone(value);
362
359
  }
363
360
  async function writtenRoomDraft(room, path, revision, input, next) {
364
- const parts = readParts(input);
361
+ const parts = readParts(input, path);
365
362
  if (path === '' || normalize(path) !== path)
366
- throw runtimeError('room-draft-malformed');
363
+ throw runtimeError('room-draft-malformed', `room-draft-malformed at ${path}: a path inside the Room, such as notes/a.md`);
367
364
  const local = await openLocal();
368
365
  const current = roomDrafts.get(room)?.get(path);
369
366
  if ((current?.revision ?? null) !== revision) {
@@ -412,7 +409,7 @@ export function composeRuntime(options) {
412
409
  const changed = new Map();
413
410
  for (const room of [...replicas.keys(), ...queuesWithoutReplica.keys()]) {
414
411
  for (const { batch, state } of entriesOf(room)) {
415
- if (state === 'accepted' || state === 'local')
412
+ if (state === 'local')
416
413
  continue;
417
414
  for (const { path } of batch.changes)
418
415
  changed.set(`${room}\u0000${path}`, { room, path });
@@ -433,16 +430,9 @@ export function composeRuntime(options) {
433
430
  returned: structuredClone(record.returned),
434
431
  });
435
432
  }
436
- for (const [room, refusal] of sourceRefusals)
437
- attentionRequired.push({ room, refusal: structuredClone(refusal) });
438
433
  attentionRequired.sort((left, right) => {
439
434
  if (left.room !== right.room)
440
435
  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
436
  if (left.path !== right.path)
447
437
  return left.path < right.path ? -1 : 1;
448
438
  return 0;
@@ -597,9 +587,6 @@ export function composeRuntime(options) {
597
587
  if (localSaves)
598
588
  return Object.freeze({ batch, state: 'local' });
599
589
  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
590
  return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
604
591
  }
605
592
  function entriesOf(locator) {
@@ -673,11 +660,7 @@ export function composeRuntime(options) {
673
660
  })();
674
661
  }
675
662
  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
- ]);
663
+ return new Set([...reach.rooms, ...reach.documents.map(roomOfKey)]);
681
664
  }
682
665
  function reachedRooms(plan) {
683
666
  const reached = plan.reach === null ? null : roomsOfReach(plan.reach);
@@ -716,7 +699,7 @@ export function composeRuntime(options) {
716
699
  coverage.push({
717
700
  room: locator,
718
701
  generation: replica.record.transfer,
719
- current: !(catching.has(locator) || owed.has(locator) || transfers.has(locator)),
702
+ current: !(catching.has(locator) || transfers.has(locator)),
720
703
  });
721
704
  read.push({ locator, key: replica.record.key });
722
705
  for (const batch of replica.queue)
@@ -743,8 +726,6 @@ export function composeRuntime(options) {
743
726
  return { listed: named.every((room) => replicas.has(room)), rooms: named };
744
727
  }
745
728
  function roomState(locator) {
746
- if (sourceRefusals.has(locator))
747
- return 'refused';
748
729
  const listedRoom = discovered.get(locator);
749
730
  if (roomsListed &&
750
731
  (listedRoom === undefined ||
@@ -759,7 +740,7 @@ export function composeRuntime(options) {
759
740
  }
760
741
  function conversationCoverage(reach) {
761
742
  const declared = reach === null || reach.conversations;
762
- if (!declared || options.adapter.conversations === undefined || declaredAgents() === undefined)
743
+ if (!declared || options.adapter.conversations === undefined)
763
744
  return 'undeclared';
764
745
  if (!conversations.known() || !conversations.current())
765
746
  return 'acquiring';
@@ -774,79 +755,50 @@ export function composeRuntime(options) {
774
755
  ? 'connected'
775
756
  : 'connecting';
776
757
  }
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
- });
758
+ function bytesOf(room, underway) {
759
+ const transfer = transfers.get(room);
760
+ if (transfer !== undefined) {
761
+ const owed = (unindexed.get(room) ?? []).reduce((sum, part) => sum + part.bytes, 0);
762
+ return {
763
+ done: transfer.done.bytes - Math.ceil(owed),
764
+ total: Math.max(arrivals.get(room) ?? 0, transfer.done.bytes),
765
+ };
766
+ }
767
+ const held = underway ? NOTHING_HELD : (replicas.get(room)?.record.inventory ?? NOTHING_HELD);
768
+ return { done: held.bytes, total: arrivals.get(room) ?? held.bytes };
793
769
  }
794
770
  function installationOf(coverage) {
795
771
  const rooms = { done: 0, total: 0 };
796
- const parts = { done: 0, total: 0 };
797
772
  const bytes = { done: 0, total: 0 };
798
773
  let installing = 0;
799
774
  let downloading = false;
800
- let checking = 0;
801
775
  for (const { room, state } of coverage) {
802
776
  if (state === 'unadmitted')
803
777
  continue;
804
778
  rooms.total += 1;
805
779
  if (state === 'complete')
806
780
  rooms.done += 1;
807
- const transfer = transfers.get(room);
808
781
  const underway = state === 'acquiring' || (installs.has(room) && state !== 'complete');
809
782
  if (underway)
810
783
  installing += 1;
811
784
  if (underway && (streaming.has(room) || !arrivals.has(room)))
812
785
  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;
786
+ const now = bytesOf(room, underway);
787
+ const earlier = underway ? startedOver.get(room) : undefined;
788
+ bytes.done += Math.max(now.done, earlier?.done ?? 0);
789
+ bytes.total += Math.max(now.total, earlier?.total ?? 0);
831
790
  }
832
791
  const step = installing === 0
833
792
  ? null
834
793
  : downloading
835
794
  ? 'downloading'
836
- : bytes.done + checking < bytes.total
795
+ : bytes.done < bytes.total
837
796
  ? 'writing'
838
797
  : 'replaying';
839
- return {
840
- step,
841
- paused: installing > 0 && !onScreen ? 'hidden' : null,
842
- rooms,
843
- parts,
844
- bytes,
845
- };
798
+ return { step, rooms, bytes };
846
799
  }
847
800
  function readinessFor(port, reach) {
848
801
  const required = requiredRooms(reach);
849
- const search = port.search === 'required';
850
802
  const coverage = required.rooms.map((room) => ({ room, state: roomState(room) }));
851
803
  const preparing = coverage
852
804
  .filter((room) => room.state === 'preparing' && !unpreparable.has(room.room))
@@ -859,14 +811,12 @@ export function composeRuntime(options) {
859
811
  listed: required.listed,
860
812
  rooms: coverage,
861
813
  conversations: conversationCoverage(reach),
862
- search: search ? 'required' : 'unused',
863
814
  },
864
815
  persistence,
865
816
  connection: connectionOf(),
866
817
  house: refusal !== null ? 'refused' : writesHeld() ? 'write-halted' : 'open',
867
- space: full ? 'full' : port.space,
818
+ space: full ? 'full' : null,
868
819
  installation: installationOf(coverage),
869
- refused: refusedIn(required.rooms),
870
820
  lost: storageLoss !== null,
871
821
  established: targets[port.target]?.established === true ||
872
822
  (usableWhenHeld.includes(port.target) && required.rooms.some((room) => reopened.has(room))),
@@ -954,8 +904,12 @@ export function composeRuntime(options) {
954
904
  unpreparable.clear();
955
905
  journal.record('space freed');
956
906
  queueReadiness();
957
- if (!closed && authorization !== undefined)
958
- void sync('unstreamed');
907
+ if (closed || authorization === undefined)
908
+ return;
909
+ if (options.adapter.roomStream === undefined)
910
+ void sync('unstreamed', null);
911
+ else
912
+ void resynchronize();
959
913
  }
960
914
  function seedRoomHeld(room, version) {
961
915
  return indexed(room) && reachedBy(room, version);
@@ -995,7 +949,24 @@ export function composeRuntime(options) {
995
949
  function sameCover(left, right) {
996
950
  return JSON.stringify(left) === JSON.stringify(right);
997
951
  }
952
+ async function unheldRows(local, plan, rows, rooms) {
953
+ if (plan.shape.kind !== 'documents' || !bodied(plan.shape))
954
+ return [];
955
+ if (!('documents' in rows) || !textOnline())
956
+ return [];
957
+ const named = new Map();
958
+ for (const { room, path, textHeld } of rows.documents) {
959
+ if (textHeld === false)
960
+ named.set(room, [...(named.get(room) ?? []), path]);
961
+ }
962
+ const unheld = [];
963
+ for (const { locator, key } of rooms) {
964
+ unheld.push(...(await unheldIn(local, locator, key, named.get(locator) ?? [])));
965
+ }
966
+ return unheld.filter((one) => unholdable.get(textKey(one)) !== one.revision);
967
+ }
998
968
  async function coveredRows(plan, cover) {
969
+ const asked = new Set();
999
970
  for (;;) {
1000
971
  const before = cover();
1001
972
  if ('pending' in before)
@@ -1006,8 +977,14 @@ export function composeRuntime(options) {
1006
977
  const current = () => local.changes(keys) === changes;
1007
978
  const rows = await windowRows(local, plan, before.rooms, current);
1008
979
  const after = cover();
1009
- if (rows !== null && sameCover(before, after))
980
+ if (rows === null || !sameCover(before, after))
981
+ continue;
982
+ const unheld = (await unheldRows(local, plan, rows, before.rooms)).filter((one) => !asked.has(textKey(one)));
983
+ if (unheld.length === 0)
1010
984
  return { rows, cover: before, current };
985
+ for (const one of unheld)
986
+ asked.add(textKey(one));
987
+ await holdTexts(unheld);
1011
988
  }
1012
989
  }
1013
990
  async function windowRows(local, plan, rooms, unchanged) {
@@ -1103,6 +1080,8 @@ export function composeRuntime(options) {
1103
1080
  }
1104
1081
  async function refuse(reason, provider = null) {
1105
1082
  discoveryPaused = true;
1083
+ inFlight.abort();
1084
+ inFlight = new AbortController();
1106
1085
  try {
1107
1086
  if (reason === 'revoked-authorization') {
1108
1087
  await exclusive(async () => {
@@ -1187,10 +1166,10 @@ export function composeRuntime(options) {
1187
1166
  providerOwnedPaths: room.providerOwnedPaths,
1188
1167
  roomVersion: replicas.get(room.locator)?.record.roomVersion ?? null,
1189
1168
  synchronizationAvailable,
1190
- refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
1169
+ refusal: scopedRefusals.get(room.locator) ?? null,
1191
1170
  });
1192
1171
  }
1193
- async function applyDiscovery(found, started, covered) {
1172
+ async function applyDiscovery(found, started, left) {
1194
1173
  const lockedBefore = lockedRooms();
1195
1174
  for (const room of found) {
1196
1175
  const fence = fences.get(room.locator);
@@ -1204,18 +1183,11 @@ export function composeRuntime(options) {
1204
1183
  const listedLocators = new Set(listed.map((room) => room.locator));
1205
1184
  for (const locator of listedLocators)
1206
1185
  scopedRefusals.delete(locator);
1207
- if (covered === undefined)
1186
+ if (left === undefined)
1208
1187
  discovered.clear();
1209
- else
1210
- for (const locator of covered)
1211
- discovered.delete(locator);
1212
1188
  for (const room of listed)
1213
1189
  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);
1190
+ await loseRooms(left ?? unlistedHeld(new Set(found.map((room) => room.locator))));
1219
1191
  for (const room of listed) {
1220
1192
  const held = replicas.get(room.locator)?.record;
1221
1193
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -1246,15 +1218,6 @@ export function composeRuntime(options) {
1246
1218
  ]),
1247
1219
  ].filter((locator) => !listed.has(locator));
1248
1220
  }
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
1221
  function fence(locators) {
1259
1222
  const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
1260
1223
  const removed = fresh.map((locator) => ({
@@ -1265,24 +1228,26 @@ export function composeRuntime(options) {
1265
1228
  fences.set(locator, { at: discoveries, erased: false });
1266
1229
  lastFenced.set(locator, ++fencings);
1267
1230
  confirmed.delete(locator);
1268
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
1231
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []) {
1269
1232
  pendings.delete(pendingKey(locator, batch.id));
1233
+ landedAhead.delete(pendingKey(locator, batch.id));
1234
+ }
1270
1235
  replicas.delete(locator);
1236
+ highest.delete(locator);
1237
+ beyondHorizon.delete(locator);
1271
1238
  queuesWithoutReplica.delete(locator);
1272
1239
  rooms.delete(locator);
1273
1240
  roomDrafts.delete(locator);
1274
1241
  attachedFiles.delete(locator);
1275
1242
  untransferred(locator);
1243
+ startedOver.delete(locator);
1276
1244
  catching.delete(locator);
1277
1245
  discovered.delete(locator);
1278
- announced.delete(locator);
1279
1246
  unreadable.delete(locator);
1280
1247
  stale.delete(locator);
1281
1248
  repairable.delete(locator);
1282
1249
  unpreparable.delete(locator);
1283
1250
  scopedRefusals.delete(locator);
1284
- sourceRefusals.delete(locator);
1285
- declarationRefusals.delete(locator);
1286
1251
  models.delete(locator);
1287
1252
  liveModels.delete(locator);
1288
1253
  invalidModels.delete(locator);
@@ -1373,6 +1338,7 @@ export function composeRuntime(options) {
1373
1338
  transfers.clear();
1374
1339
  arrivals.clear();
1375
1340
  streaming.clear();
1341
+ startedOver.clear();
1376
1342
  limits = held.providerLimits;
1377
1343
  capabilities = held.capabilities;
1378
1344
  member = held.member;
@@ -1382,8 +1348,9 @@ export function composeRuntime(options) {
1382
1348
  presentations.clear();
1383
1349
  for (const record of held.presentation)
1384
1350
  presentations.set(record.app, record.value);
1385
- conversations.hydrate(heldConversations, chatDrafts, held.conversationList);
1351
+ conversations.hydrate(heldConversations, chatDrafts, held.conversationList, held.conversationCursor);
1386
1352
  pendings.clear();
1353
+ landedAhead.clear();
1387
1354
  for (const queue of queues) {
1388
1355
  for (const { batch, state } of queue.pending) {
1389
1356
  pendings.set(pendingKey(queue.locator, batch.id), state);
@@ -1534,6 +1501,16 @@ export function composeRuntime(options) {
1534
1501
  }
1535
1502
  const REWRITE_LINKS = ['field', 'body'];
1536
1503
  const CHECKED_LINKS = ['field', 'section'];
1504
+ const BODY_LINKS = ['body'];
1505
+ async function textsNeeded(local, record, named) {
1506
+ const paths = [...new Set(named.map((one) => (typeof one === 'string' ? one : one.path)))];
1507
+ const unheld = await unheldIn(local, record.locator, record.key, paths);
1508
+ if (unheld.length === 0)
1509
+ return null;
1510
+ await holdTexts(unheld);
1511
+ const [first] = await unheldIn(local, record.locator, record.key, unheld.map(({ path }) => path));
1512
+ return first?.path ?? null;
1513
+ }
1537
1514
  async function sliceAt(record, view, queue, extra = [], moved = []) {
1538
1515
  const local = await openLocal();
1539
1516
  const needed = new Set([...touchedBy(queue), ...extra]);
@@ -1634,8 +1611,6 @@ export function composeRuntime(options) {
1634
1611
  function unpairedIn(locator, batch) {
1635
1612
  if (!queueIn(locator).some((held) => held.id === batch.id))
1636
1613
  return false;
1637
- if (pendings.get(pendingKey(locator, batch.id))?.accepted !== undefined)
1638
- return false;
1639
1614
  return !leavingIn(batch, locator) || withdrawable(locator)(batch);
1640
1615
  }
1641
1616
  async function together(local, settlements) {
@@ -1678,10 +1653,16 @@ export function composeRuntime(options) {
1678
1653
  ? read
1679
1654
  : await stepped(slicingWith(read, overrides.set, overrides.removed));
1680
1655
  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));
1656
+ const shown = queue.filter((batch) => {
1657
+ const held = landedAhead.get(pendingKey(locator, batch.id));
1658
+ return (held !== undefined &&
1659
+ Object.entries(held).some(([path, revision]) => confirmedLayer.revisions[path] !== revision));
1660
+ });
1661
+ const unshown = queue.filter((batch) => !shown.includes(batch));
1662
+ const { withdrawn, kept } = await stepped(standing(confirmedLayer, unshown, withdrawable(locator), localSaves, locator, composition));
1663
+ const founded = kept.filter((batch) => !unshown.includes(batch));
1683
1664
  const queueChanged = kept.length !== previousQueue.length || founded.length > 0;
1684
- const unqueued = [];
1665
+ const unqueued = shown.map((batch) => ({ batch, landed: true }));
1685
1666
  for (const batch of withdrawn) {
1686
1667
  unqueued.push({ batch, landed: await landedWhole(locator, batch, confirmedLayer) });
1687
1668
  }
@@ -1746,8 +1727,10 @@ export function composeRuntime(options) {
1746
1727
  commit,
1747
1728
  apply: (answer) => {
1748
1729
  for (const batch of previousQueue) {
1749
- if (!staying.has(batch.id))
1750
- pendings.delete(pendingKey(locator, batch.id));
1730
+ if (staying.has(batch.id))
1731
+ continue;
1732
+ pendings.delete(pendingKey(locator, batch.id));
1733
+ landedAhead.delete(pendingKey(locator, batch.id));
1751
1734
  }
1752
1735
  for (const draft of written.values())
1753
1736
  rememberRoomDraft(draft);
@@ -1804,28 +1787,34 @@ export function composeRuntime(options) {
1804
1787
  models.set(locator, state);
1805
1788
  return { collection: state };
1806
1789
  }
1807
- function publish(local, room, record, landing, live) {
1790
+ function publish(local, room, reached, landing, live) {
1808
1791
  const landed = exclusive(async () => {
1809
1792
  if (!live()) {
1810
1793
  caughtUpTo(room.locator);
1811
1794
  return true;
1812
1795
  }
1813
1796
  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);
1797
+ if (!stands(room.locator, landing.from)) {
1798
+ if (landing.kind === 'refreshed') {
1799
+ await local.unrefresh(reached.key);
1800
+ return false;
1801
+ }
1802
+ caughtUpTo(room.locator);
1803
+ await abandoned(local, room.locator);
1817
1804
  return false;
1818
1805
  }
1806
+ const record = landing.kind === 'refreshed' && prior !== undefined
1807
+ ? {
1808
+ ...prior.record,
1809
+ roomVersion: reached.roomVersion,
1810
+ ...(landing.remodelled ? { model: reached.model } : {}),
1811
+ }
1812
+ : reached;
1819
1813
  adoptModels(room.locator, record);
1820
1814
  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
1815
  let set;
1827
1816
  try {
1828
- set = await settle(local, room.locator, prior, record, queue.filter((batch) => !accepted.includes(batch)), landing.kind === 'refreshed'
1817
+ set = await settle(local, room.locator, prior, record, queue, landing.kind === 'refreshed'
1829
1818
  ? {
1830
1819
  room: true,
1831
1820
  view: 'refreshed',
@@ -1847,25 +1836,26 @@ export function composeRuntime(options) {
1847
1836
  return true;
1848
1837
  }
1849
1838
  if (landing.kind === 'bootstrapped') {
1839
+ highest.delete(room.locator);
1840
+ beyondHorizon.delete(room.locator);
1850
1841
  untransferred(room.locator);
1842
+ startedOver.delete(room.locator);
1851
1843
  catching.delete(room.locator);
1852
1844
  if (prior !== undefined && prior.record.key !== record.key)
1853
1845
  sweep();
1846
+ name(room.locator);
1854
1847
  }
1855
- for (const batch of accepted)
1856
- pendings.delete(pendingKey(room.locator, batch.id));
1857
1848
  stale.delete(room.locator);
1858
1849
  unpreparable.delete(room.locator);
1859
1850
  if (unreadable.delete(room.locator))
1860
1851
  scopedRefusals.delete(room.locator);
1861
- sourceRefusals.delete(room.locator);
1862
- declarationRefusals.delete(room.locator);
1863
1852
  list(room, true);
1864
1853
  publishChangeSet(set);
1854
+ if (landing.kind === 'bootstrapped')
1855
+ void heldAgain(room.locator);
1865
1856
  await unpair(local);
1866
1857
  moveTo({ ...status, ...attention() });
1867
- options.adapter.roomStream?.published(room.locator, roomVersion);
1868
- if (accepted.length > 0 && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1858
+ if (landing.kind === 'bootstrapped' && (replicas.get(room.locator)?.queue.length ?? 0) > 0) {
1869
1859
  submitLater(room.locator);
1870
1860
  }
1871
1861
  return true;
@@ -1877,7 +1867,7 @@ export function composeRuntime(options) {
1877
1867
  const replica = replicas.get(locator);
1878
1868
  const room = rooms.get(locator);
1879
1869
  if (replica === undefined || room === undefined) {
1880
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1870
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
1881
1871
  }
1882
1872
  return { local, replica, room };
1883
1873
  }
@@ -1939,6 +1929,19 @@ export function composeRuntime(options) {
1939
1929
  const referring = removed.length === 0
1940
1930
  ? []
1941
1931
  : (await local.referrers(record.key, 'confirmed', removed, CHECKED_LINKS)).map(({ path }) => path);
1932
+ const unheld = await textsNeeded(local, record, [
1933
+ ...paths,
1934
+ ...referring,
1935
+ ...(moved.length === 0
1936
+ ? []
1937
+ : await local.referrers(record.key, 'confirmed', moved, REWRITE_LINKS)),
1938
+ ...(removed.length === 0
1939
+ ? []
1940
+ : await local.referrers(record.key, 'confirmed', removed, BODY_LINKS)),
1941
+ ]);
1942
+ if (unheld !== null) {
1943
+ return { refusal: { code: 'transport-uncertainty', retryable: true, file: unheld } };
1944
+ }
1942
1945
  const confirmedLayer = await sliceAt(record, 'confirmed', replica.queue, [...paths, ...paths.flatMap(ancestors), ...referring], moved);
1943
1946
  const { visible } = await replayed(record, confirmedLayer, replica.queue);
1944
1947
  const composition = composed(record);
@@ -1969,18 +1972,6 @@ export function composeRuntime(options) {
1969
1972
  return undefined;
1970
1973
  return entry.binary !== null ? null : entry.content;
1971
1974
  }
1972
- function walkOf(local, key, view) {
1973
- return async (after) => {
1974
- const page = await local.walk(key, view, after, WALK_BYTES);
1975
- return {
1976
- entries: page.entries.map(({ path, content, binary }) => ({
1977
- path,
1978
- content: binary !== null ? null : content,
1979
- })),
1980
- next: page.next,
1981
- };
1982
- };
1983
- }
1984
1975
  function lookupOf(local, key, view) {
1985
1976
  return async (paths) => {
1986
1977
  if (paths.length === 0)
@@ -1997,37 +1988,6 @@ export function composeRuntime(options) {
1997
1988
  const read = parseFrontmatter(entry.content ?? '');
1998
1989
  return { type: read.ok ? (read.frontmatter.type ?? 'any') : undefined, binary: false };
1999
1990
  }
2000
- function overriddenWalk(walk, overrides) {
2001
- const sorted = [...overrides.keys()].sort();
2002
- const beyond = (path) => {
2003
- if (path === null)
2004
- return 0;
2005
- let low = 0;
2006
- let high = sorted.length;
2007
- while (low < high) {
2008
- const middle = (low + high) >>> 1;
2009
- if (sorted[middle] <= path)
2010
- low = middle + 1;
2011
- else
2012
- high = middle;
2013
- }
2014
- return low;
2015
- };
2016
- return async (after) => {
2017
- const page = await walk(after);
2018
- const end = page.next;
2019
- const added = sorted
2020
- .slice(beyond(after), end === null ? sorted.length : beyond(end))
2021
- .flatMap((path) => {
2022
- const entry = overrides.get(path);
2023
- return entry === null || entry === undefined
2024
- ? []
2025
- : [{ path, content: entry.binary !== null ? null : entry.content }];
2026
- });
2027
- const entries = [...page.entries.filter(({ path }) => !overrides.has(path)), ...added].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
2028
- return { entries, next: end };
2029
- };
2030
- }
2031
1991
  function overriddenLookup(lookup, overrides) {
2032
1992
  return async (paths) => {
2033
1993
  const known = new Map(await lookup(paths.filter((path) => !overrides.has(path))));
@@ -2045,9 +2005,8 @@ export function composeRuntime(options) {
2045
2005
  }
2046
2006
  function validatorOf(record, confirmedLayer) {
2047
2007
  return async (resulting, touched) => {
2048
- const composition = compositionFor(record);
2049
2008
  const local = await openLocal();
2050
- const changed = [];
2009
+ const changes = [];
2051
2010
  const overrides = new Map();
2052
2011
  const pace = paced();
2053
2012
  for (const path of new Set([...confirmedLayer.held, ...touched])) {
@@ -2058,19 +2017,13 @@ export function composeRuntime(options) {
2058
2017
  continue;
2059
2018
  overrides.set(path, now);
2060
2019
  if (textOf(was) !== textOf(now))
2061
- changed.push({ path, was: textOf(was), now: textOf(now) });
2020
+ changes.push({ path, was: textOf(was), now: textOf(now) });
2062
2021
  }
2063
- const lookup = overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides);
2064
- const clean = available(composition) &&
2065
- changed.length <= CLEAN_CHANGES &&
2066
- (await changedCleanly(changed, composed(record), composition, {
2067
- lookup,
2068
- texts: textsOf(local, record.key, 'confirmed'),
2069
- referrers: referrersOf(local, record.key, 'confirmed'),
2070
- }));
2071
- if (clean)
2072
- return { verdict: 'accept' };
2073
- return validatedCollection(overriddenWalk(walkOf(local, record.key, 'confirmed'), overrides), lookup, composition);
2022
+ return validatedNeighbourhood(changes, compositionFor(record), {
2023
+ lookup: overriddenLookup(lookupOf(local, record.key, 'confirmed'), overrides),
2024
+ texts: textsOf(local, record.key, 'confirmed'),
2025
+ referrers: referrersOf(local, record.key, 'confirmed'),
2026
+ });
2074
2027
  };
2075
2028
  }
2076
2029
  function textsOf(local, key, view) {
@@ -2147,15 +2100,19 @@ export function composeRuntime(options) {
2147
2100
  }
2148
2101
  const moves = saved.map(({ path, parts }) => ({ path, to: moveTarget(parts.move) }));
2149
2102
  const sourceSaving = await savingIn(replica, moves.map(({ path }) => ({ kind: 'remove', path })), saved.map(({ path }) => path), moves.map(({ path }) => path));
2103
+ if ('refusal' in sourceSaving)
2104
+ return { outcome: 'refused', ...sourceSaving.refusal };
2150
2105
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, parts, sourceSaving.visible));
2151
2106
  if (changed.length > 0)
2152
2107
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
2153
2108
  const target = replicas.get(destination);
2154
2109
  const listedRoom = rooms.get(destination);
2155
2110
  if (target === undefined || listedRoom === undefined) {
2156
- throw runtimeError('room-not-held', `No Local Replica for ${destination}`);
2111
+ throw runtimeError('unknown-room', `Unknown Room ${destination}`);
2157
2112
  }
2158
2113
  const arriving = await savingIn(target, [], moves.map(({ to }) => to));
2114
+ if ('refusal' in arriving)
2115
+ return { outcome: 'refused', ...arriving.refusal };
2159
2116
  const built = await buildMove({
2160
2117
  source: await sideOf(locator, replica, room, sourceSaving),
2161
2118
  destination: await sideOf(destination, target, listedRoom, arriving),
@@ -2216,10 +2173,10 @@ export function composeRuntime(options) {
2216
2173
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2217
2174
  const { answer, touched } = committedBoth;
2218
2175
  if (answer.fenced !== null) {
2219
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2176
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2220
2177
  }
2221
2178
  if (fences.has(locator) || fences.has(destination)) {
2222
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2179
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2223
2180
  }
2224
2181
  pendings.set(pendingKey(locator, id), {
2225
2182
  drafts: saved,
@@ -2272,6 +2229,8 @@ export function composeRuntime(options) {
2272
2229
  ...saved.map(({ path }) => path),
2273
2230
  ...parts.flatMap(({ bases }) => bases.map(({ path }) => path)),
2274
2231
  ]);
2232
+ if ('refusal' in saving)
2233
+ return { outcome: 'refused', ...saving.refusal };
2275
2234
  const changed = saved.flatMap(({ path, parts: held }) => changedParts(path, held, saving.visible));
2276
2235
  if (changed.length > 0)
2277
2236
  return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
@@ -2390,10 +2349,10 @@ export function composeRuntime(options) {
2390
2349
  return { outcome: 'refused', code: 'durable-write', retryable: true };
2391
2350
  const { answer, touched } = done;
2392
2351
  if (answer.fenced !== null) {
2393
- throw runtimeError('room-not-held', `No Local Replica for ${answer.fenced}`);
2352
+ throw runtimeError('unknown-room', `Unknown Room ${answer.fenced}`);
2394
2353
  }
2395
2354
  if (fences.has(locator))
2396
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2355
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2397
2356
  for (const origin of withdrawn.keys())
2398
2357
  pendings.delete(pendingKey(locator, origin));
2399
2358
  for (const [id, drafts] of drafted) {
@@ -2442,7 +2401,7 @@ export function composeRuntime(options) {
2442
2401
  throw new Error('connection refused');
2443
2402
  await local.keepUpload(locator, identifier, upload);
2444
2403
  if (fences.has(locator))
2445
- throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
2404
+ throw runtimeError('unknown-room', `Unknown Room ${locator}`);
2446
2405
  const key = pendingKey(locator, identifier);
2447
2406
  pendings.set(key, { ...pendings.get(key), upload });
2448
2407
  if (!continuing())
@@ -2579,6 +2538,83 @@ export function composeRuntime(options) {
2579
2538
  return UNCERTAIN;
2580
2539
  }
2581
2540
  }
2541
+ async function heldAgain(locator) {
2542
+ const replica = replicas.get(locator);
2543
+ if (replica === undefined || !textOnline())
2544
+ return;
2545
+ try {
2546
+ const wanted = await (await openLocal()).wanted(locator, replica.record.key);
2547
+ await holdTexts(wanted.map(({ path, revision }) => ({ locator, path, revision })));
2548
+ }
2549
+ catch {
2550
+ journal.record(`hold text ${locator} unwritten`);
2551
+ }
2552
+ }
2553
+ function textOnline() {
2554
+ return !closed && refusal === null && connectionOf() === 'connected';
2555
+ }
2556
+ function textKey({ locator, path }) {
2557
+ return documentKey(locator, path);
2558
+ }
2559
+ async function readText({ locator, path, revision }) {
2560
+ const replica = replicas.get(locator);
2561
+ if (replica === undefined || !textOnline())
2562
+ return;
2563
+ try {
2564
+ await ensureAuthorization();
2565
+ }
2566
+ catch {
2567
+ return;
2568
+ }
2569
+ if (closed || authorization === undefined || refusal !== null)
2570
+ return;
2571
+ const since = fencings;
2572
+ journal.record(`hold text ${locator} ${path}`);
2573
+ const read = await reads
2574
+ .bootstrap(authorization, { locator, selection: replica.record.replica.selection, path })
2575
+ .catch(() => null);
2576
+ journal.record(`outcome text ${read?.kind ?? 'transport-uncertainty'}`);
2577
+ if (read === null || read.kind !== 'bootstrap' || fencedSince(locator, since))
2578
+ return;
2579
+ const content = read.documents[path];
2580
+ const now = replicas.get(locator);
2581
+ if (content === undefined || read.revisions[path] !== revision || now === undefined) {
2582
+ unholdable.set(textKey({ locator, path, revision }), revision);
2583
+ return;
2584
+ }
2585
+ const held = await (await openLocal()).hold(locator, now.record.key, compositionFor(now.record), [{ path, revision, content }]);
2586
+ if (held.length === 0)
2587
+ unholdable.set(textKey({ locator, path, revision }), revision);
2588
+ else
2589
+ publishChangeSet(changeSet(locator, { documents: held }));
2590
+ }
2591
+ function holdText(unheld) {
2592
+ const key = textKey(unheld);
2593
+ const running = holding.get(key);
2594
+ if (running !== undefined)
2595
+ return running;
2596
+ const started = readText(unheld).finally(() => holding.delete(key));
2597
+ holding.set(key, started);
2598
+ return started;
2599
+ }
2600
+ async function holdTexts(wanted) {
2601
+ const queue = wanted.filter((one) => unholdable.get(textKey(one)) !== one.revision);
2602
+ await Promise.all(Array.from({ length: Math.min(TEXT_READS, queue.length) }, async () => {
2603
+ for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
2604
+ await holdText(next);
2605
+ }
2606
+ }));
2607
+ }
2608
+ async function unheldIn(local, locator, key, paths) {
2609
+ if (paths.length === 0)
2610
+ return [];
2611
+ return (await local.entries(key, 'confirmed', paths)).flatMap((entry) => entry.content === null &&
2612
+ entry.binary === null &&
2613
+ entry.outline !== null &&
2614
+ entry.revision !== null
2615
+ ? [{ locator, path: entry.path, revision: entry.revision }]
2616
+ : []);
2617
+ }
2582
2618
  function fetchBinary(request, name) {
2583
2619
  return transferBinary(request, (authorization) => keptFiles.fetch(name, () => options.adapter.binary(authorization, request)), binaryAnswer);
2584
2620
  }
@@ -2714,14 +2750,16 @@ export function composeRuntime(options) {
2714
2750
  }
2715
2751
  async function cleanUp() {
2716
2752
  const freed = await database.ask({ kind: 'opened.clean' });
2717
- return freed + ((await options.shell.cleanUp?.()) ?? 0);
2718
- }
2719
- function declaredAgents() {
2720
- for (const capability of capabilities) {
2721
- if (capability.conversationAgents !== undefined)
2722
- return capability.conversationAgents;
2753
+ const local = await openLocal();
2754
+ let texts = 0;
2755
+ for (const [locator, replica] of replicas) {
2756
+ const released = await local.unhold(locator, replica.record.key, compositionFor(replica.record));
2757
+ texts += released.bytes;
2758
+ if (released.paths.length > 0) {
2759
+ publishChangeSet(changeSet(locator, { documents: released.paths }));
2760
+ }
2723
2761
  }
2724
- return undefined;
2762
+ return freed + texts + ((await options.shell.cleanUp?.()) ?? 0);
2725
2763
  }
2726
2764
  function declaredAction(name) {
2727
2765
  for (const capability of capabilities) {
@@ -2774,6 +2812,24 @@ export function composeRuntime(options) {
2774
2812
  return UNCERTAIN;
2775
2813
  }
2776
2814
  }
2815
+ async function readConversationHistory(request) {
2816
+ await ready;
2817
+ const stream = options.adapter.conversations;
2818
+ if (closed || refusal !== null || stream?.history === undefined)
2819
+ return UNCERTAIN;
2820
+ journal.record('transfer conversation history');
2821
+ try {
2822
+ const result = await stream.history(request);
2823
+ if (result.kind === 'revoked-authorization')
2824
+ await refuse('revoked-authorization');
2825
+ journal.record(`outcome conversation history ${result.kind}`);
2826
+ return calledResult(result);
2827
+ }
2828
+ catch {
2829
+ journal.record('outcome conversation history transport-uncertainty');
2830
+ return UNCERTAIN;
2831
+ }
2832
+ }
2777
2833
  function unsettled(result) {
2778
2834
  return result.kind !== 'action' && result.retryable;
2779
2835
  }
@@ -2835,6 +2891,33 @@ export function composeRuntime(options) {
2835
2891
  const held = replicas.get(locator);
2836
2892
  return (held !== undefined && options.adapter.reached(locator, held.record.roomVersion, roomVersion));
2837
2893
  }
2894
+ function shownAt(locator) {
2895
+ const held = replicas.get(locator)?.record.roomVersion;
2896
+ const raised = highest.get(locator);
2897
+ if (held === undefined || raised === undefined)
2898
+ return held ?? raised;
2899
+ return options.adapter.reached(locator, held, raised) ? held : raised;
2900
+ }
2901
+ function showing(locator, roomVersion) {
2902
+ const at = shownAt(locator);
2903
+ if (at === undefined || !options.adapter.reached(locator, at, roomVersion)) {
2904
+ highest.set(locator, roomVersion);
2905
+ }
2906
+ }
2907
+ function older(locator, roomVersion) {
2908
+ if (beyondHorizon.has(locator))
2909
+ return false;
2910
+ const at = shownAt(locator);
2911
+ return (at !== undefined &&
2912
+ options.adapter.reached(locator, at, roomVersion) &&
2913
+ !options.adapter.reached(locator, roomVersion, at));
2914
+ }
2915
+ function replaced(locators) {
2916
+ for (const locator of locators) {
2917
+ highest.delete(locator);
2918
+ beyondHorizon.add(locator);
2919
+ }
2920
+ }
2838
2921
  async function writtenAfter(locator, roomVersion, path) {
2839
2922
  const held = replicas.get(locator);
2840
2923
  if (held === undefined)
@@ -2845,122 +2928,27 @@ export function composeRuntime(options) {
2845
2928
  options.adapter.reached(locator, written, roomVersion) &&
2846
2929
  !options.adapter.reached(locator, roomVersion, written));
2847
2930
  }
2848
- function overriddenTexts(texts, overrides) {
2849
- return async (paths) => {
2850
- const known = new Map(await texts(paths.filter((path) => !overrides.has(path))));
2851
- for (const path of paths) {
2852
- const entry = overrides.get(path);
2853
- if (entry === undefined)
2854
- continue;
2855
- if (entry === null)
2856
- known.delete(path);
2857
- else
2858
- known.set(path, entry.binary !== null ? null : entry.content);
2859
- }
2860
- return known;
2861
- };
2862
- }
2863
- async function unreadableRoom(texts, walk, paths) {
2864
- if (paths !== null) {
2865
- const text = {};
2866
- for (const [path, content] of await texts(paths))
2867
- if (content !== null)
2868
- text[path] = content;
2869
- return stepped(unreadableIn(text, Object.keys(text)));
2870
- }
2871
- for (let after = null;;) {
2872
- const page = await walk(after);
2873
- const text = {};
2874
- for (const { path, content } of page.entries)
2875
- if (content !== null)
2876
- text[path] = content;
2877
- const refused = await stepped(unreadableIn(text, Object.keys(text)));
2878
- if (refused !== null)
2879
- return refused;
2880
- if (page.next === null)
2881
- return null;
2882
- after = page.next;
2883
- }
2884
- }
2885
- async function validatedRoom(local, record, view, previous, overrides = null, checked = null) {
2886
- const composition = composed(record);
2887
- const modeled = record.model.status !== 'unavailable' || record.typedSelection !== null;
2888
- const lookup = overrides === null
2889
- ? lookupOf(local, record.key, view)
2890
- : overriddenLookup(lookupOf(local, record.key, view), overrides);
2891
- const texts = overrides === null
2892
- ? textsOf(local, record.key, view)
2893
- : overriddenTexts(textsOf(local, record.key, view), overrides);
2894
- const walking = overrides === null
2895
- ? walkOf(local, record.key, view)
2896
- : overriddenWalk(walkOf(local, record.key, view), overrides);
2897
- const walk = checked === null
2898
- ? walking
2899
- : async (after) => {
2900
- const page = await walking(after);
2901
- checked(page.entries.reduce((sum, entry) => sum + (entry.content?.length ?? 0), 0));
2902
- return page;
2903
- };
2904
- let changes = null;
2905
- let reshaped = false;
2906
- if (previous !== null && previous.changed.length <= CLEAN_CHANGES) {
2907
- const [was, now] = await Promise.all([
2908
- local.entries(record.key, 'confirmed', previous.changed),
2909
- overrides === null
2910
- ? local.entries(record.key, view, previous.changed)
2911
- : Promise.resolve([]),
2912
- ]);
2913
- const before = new Map(was.map((entry) => [entry.path, entry]));
2914
- const after = overrides === null
2915
- ? new Map(now.map((entry) => [entry.path, entry]))
2916
- : overrides;
2917
- changes = previous.changed.flatMap((path) => {
2918
- const one = before.get(path) ?? null;
2919
- const other = after.get(path) ?? null;
2920
- reshaped ||= textDiffers(one, other);
2921
- const held = textOf(one);
2922
- const next = textOf(other);
2923
- return held === next ? [] : [{ path, was: held, now: next }];
2924
- });
2925
- }
2926
- const named = changes === null ? null : changes.map(({ path }) => path);
2927
- const textChanged = changes === null || changes.length > 0 || reshaped;
2928
- const modelsChanged = previous === null || !sameComposition(composed(previous.record), composition);
2929
- if (modeled && (textChanged || modelsChanged)) {
2930
- const clean = previous !== null &&
2931
- changes !== null &&
2932
- (await changedCleanly(changes, composed(previous.record), composition, {
2933
- lookup,
2934
- texts,
2935
- referrers: referrersOf(local, record.key, 'confirmed'),
2936
- }));
2937
- if (clean)
2938
- return null;
2939
- const verdict = await validatedCollection(walk, lookup, composition);
2940
- if (verdict.verdict === 'accept')
2941
- return null;
2942
- if (verdict.rule !== 'model-unavailable')
2943
- return { kind: 'refused', refusal: verdict };
2944
- return unreadableRoom(texts, walk, named);
2945
- }
2946
- if (textChanged)
2947
- return unreadableRoom(texts, walk, named);
2948
- return null;
2931
+ async function writtenLater(local, locator, key, paths, roomVersion) {
2932
+ if (paths.length === 0 || !older(locator, roomVersion))
2933
+ return new Set();
2934
+ const entries = await local.entries(key, 'confirmed', paths);
2935
+ return new Set(entries.flatMap(({ path, written }) => written !== null &&
2936
+ options.adapter.reached(locator, written, roomVersion) &&
2937
+ !options.adapter.reached(locator, roomVersion, written)
2938
+ ? [path]
2939
+ : []));
2949
2940
  }
2950
- function refreshingOf(local, key, composition, liveness, replaying = null) {
2951
- const write = replaying === null ? exclusive : finishingWrite;
2941
+ function refreshingOf(local, locator, key, composition, liveness, replaying = false) {
2942
+ const write = replaying ? finishingWrite : exclusive;
2952
2943
  return {
2953
- async turn() {
2954
- if (replaying === null)
2955
- return;
2956
- installing(replaying);
2957
- await screened();
2944
+ turn() {
2945
+ if (replaying)
2946
+ installing(locator);
2958
2947
  },
2959
2948
  prior: async (paths) => new Map((await local.entries(key, 'refreshed', paths)).map((entry) => [entry.path, entry])),
2949
+ later: (paths, roomVersion) => writtenLater(local, locator, key, paths, roomVersion),
2960
2950
  async stage(page) {
2961
2951
  for (let written = 0;;) {
2962
- if (replaying !== null)
2963
- await screened();
2964
2952
  const end = batchEnd(page.entries, written);
2965
2953
  const last = end === page.entries.length;
2966
2954
  const entries = page.entries.slice(written, end);
@@ -3060,25 +3048,13 @@ export function composeRuntime(options) {
3060
3048
  return;
3061
3049
  void yielded().then(nextInstallTurn);
3062
3050
  }
3063
- function screened() {
3064
- if (onScreen || closed)
3065
- return Promise.resolve();
3066
- queueReadiness();
3067
- return new Promise((resolve) => shownWaiters.push(resolve));
3068
- }
3069
3051
  async function heldStream() {
3070
- for (;;) {
3071
- if (streams.generation !== generation)
3072
- streams = { generation, free: STREAMS_HELD };
3073
- const held = streams;
3074
- if (held.free > 0) {
3075
- held.free -= 1;
3076
- return once(() => {
3077
- held.free += 1;
3078
- });
3079
- }
3052
+ while (streamsFree === 0)
3080
3053
  await new Promise((admit) => installWaiters.push(admit));
3081
- }
3054
+ streamsFree -= 1;
3055
+ return once(() => {
3056
+ streamsFree += 1;
3057
+ });
3082
3058
  }
3083
3059
  async function unqueued() {
3084
3060
  while (queuedText >= QUEUED_TEXT) {
@@ -3106,14 +3082,19 @@ export function composeRuntime(options) {
3106
3082
  streaming.delete(locator);
3107
3083
  underived.delete(locator);
3108
3084
  unindexed.delete(locator);
3109
- unchecked.delete(locator);
3085
+ }
3086
+ function setBack(locator) {
3087
+ const now = bytesOf(locator, true);
3088
+ const earlier = startedOver.get(locator);
3089
+ startedOver.set(locator, {
3090
+ done: Math.max(now.done, earlier?.done ?? 0),
3091
+ total: Math.max(now.total, earlier?.total ?? 0),
3092
+ });
3110
3093
  }
3111
3094
  function owedIndexes(locator, bytes, text) {
3112
3095
  if (bytes <= 0 || text <= 0)
3113
3096
  return;
3114
- unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes: bytes / 2, text }]);
3115
- const check = unchecked.get(locator) ?? { bytes: 0, text: 0, done: 0 };
3116
- unchecked.set(locator, { ...check, bytes: check.bytes + bytes / 2, text: check.text + text });
3097
+ unindexed.set(locator, [...(unindexed.get(locator) ?? []), { bytes, text }]);
3117
3098
  }
3118
3099
  function built(locator, { done, text }) {
3119
3100
  const owed = unindexed.get(locator) ?? [];
@@ -3139,52 +3120,34 @@ export function composeRuntime(options) {
3139
3120
  if (installs.delete(locator))
3140
3121
  queueReadiness();
3141
3122
  }
3142
- function screen(value) {
3143
- if (value === onScreen)
3144
- return;
3145
- onScreen = value;
3146
- journal.record(`installation ${value ? 'shown' : 'hidden'}`);
3147
- queueReadiness();
3148
- if (!value)
3149
- return;
3150
- for (const resolve of shownWaiters.splice(0))
3151
- resolve();
3152
- }
3153
- async function pullRefresh(authorized, held, liveness, hold) {
3123
+ async function pullRefresh(authorized, held, liveness, hold, refresh) {
3154
3124
  if (!liveness())
3155
3125
  return unconfirmed;
3156
- const turn = generation;
3157
3126
  const { record } = held;
3158
- const base = held.confirmed;
3127
+ const from = standingOf(record.locator);
3159
3128
  const local = await openLocal();
3160
3129
  try {
3161
3130
  await exclusive(() => local.unrefresh(record.key));
3162
- const result = await advancement({
3163
- ...reads,
3164
- async refresh(authorization, request) {
3165
- hold.release();
3166
- try {
3167
- return await reads.refresh(authorization, request);
3168
- }
3169
- finally {
3170
- hold.take();
3171
- }
3172
- },
3173
- }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.key, composed(record), liveness));
3131
+ const result = await advancement(async (authorization, request) => {
3132
+ hold.release();
3133
+ try {
3134
+ return await refresh(authorization, request);
3135
+ }
3136
+ finally {
3137
+ hold.take();
3138
+ }
3139
+ }, authorized, record, (models, kept) => (liveness() ? remodel(record.locator, models, kept) : kept), liveness, refreshingOf(local, record.locator, record.key, composed(record), liveness));
3174
3140
  if (result.kind !== 'advanced') {
3175
3141
  journal.record(`transfer refresh ${record.locator} ${result.kind}`);
3176
3142
  return result;
3177
3143
  }
3178
- const refused = await validatedRoom(local, result.record, 'refreshed', {
3179
- record,
3180
- changed: [...result.changed],
3181
- });
3182
- if (refused !== null) {
3183
- journal.record(`transfer refresh ${record.locator} refused`);
3184
- return refused;
3185
- }
3186
3144
  journal.record(`transfer refresh ${record.locator} ${result.record.roomVersion}`);
3187
- return { kind: 'advanced', record: result.record, base };
3145
+ return {
3146
+ kind: 'advanced',
3147
+ record: result.record,
3148
+ from,
3149
+ remodelled: result.record.model !== record.model,
3150
+ };
3188
3151
  }
3189
3152
  catch (error) {
3190
3153
  if (refusedForSpace(error))
@@ -3192,9 +3155,6 @@ export function composeRuntime(options) {
3192
3155
  journal.record(`transfer refresh ${record.locator} transport-uncertainty`);
3193
3156
  throw error;
3194
3157
  }
3195
- finally {
3196
- reads.finished(record.locator, turn);
3197
- }
3198
3158
  }
3199
3159
  const heldContents = new Map();
3200
3160
  function heldContentOf(local, key) {
@@ -3212,11 +3172,17 @@ export function composeRuntime(options) {
3212
3172
  heldContents.set(key, made);
3213
3173
  return made;
3214
3174
  }
3175
+ async function abandoned(local, locator) {
3176
+ await local.abandon(locator);
3177
+ setBack(locator);
3178
+ untransferred(locator);
3179
+ liveQueries.recover();
3180
+ sweep();
3181
+ }
3215
3182
  function staging(local, liveness) {
3216
3183
  const derived = async (locator, key, composition) => {
3217
3184
  underived.delete(locator);
3218
3185
  for (;;) {
3219
- await screened();
3220
3186
  if (!liveness())
3221
3187
  return false;
3222
3188
  const done = await finishingWrite(async () => liveness()
@@ -3233,13 +3199,7 @@ export function composeRuntime(options) {
3233
3199
  held: (locator) => transfers.get(locator) ?? null,
3234
3200
  async turn(locator, step) {
3235
3201
  installing(locator);
3236
- for (;;) {
3237
- await screened();
3238
- const release = step === 'downloading' ? await heldStream() : () => { };
3239
- if (onScreen || closed)
3240
- return release;
3241
- release();
3242
- }
3202
+ return step === 'downloading' ? heldStream() : () => { };
3243
3203
  },
3244
3204
  unqueued,
3245
3205
  queued(text) {
@@ -3254,7 +3214,7 @@ export function composeRuntime(options) {
3254
3214
  at(bytes) {
3255
3215
  if (ended)
3256
3216
  return;
3257
- arrivals.set(locator, { parts: arrivals.get(locator)?.parts ?? 0, bytes });
3217
+ arrivals.set(locator, bytes);
3258
3218
  streaming.add(locator);
3259
3219
  const now = performance.now();
3260
3220
  if (now - arrivalReported < ARRIVAL_REPORT_MS)
@@ -3262,14 +3222,6 @@ export function composeRuntime(options) {
3262
3222
  arrivalReported = now;
3263
3223
  queueReadiness();
3264
3224
  },
3265
- part() {
3266
- const arrived = arrivals.get(locator);
3267
- arrivals.set(locator, {
3268
- parts: (arrived?.parts ?? 0) + 1,
3269
- bytes: arrived?.bytes ?? 0,
3270
- });
3271
- queueReadiness();
3272
- },
3273
3225
  end() {
3274
3226
  if (ended)
3275
3227
  return;
@@ -3279,14 +3231,10 @@ export function composeRuntime(options) {
3279
3231
  },
3280
3232
  };
3281
3233
  },
3282
- announced(locator) {
3283
- const later = announced.get(locator);
3284
- return later === undefined ? [] : [later];
3285
- },
3234
+ streamed: options.adapter.roomStream !== undefined,
3286
3235
  async stage(record, page, composition) {
3287
3236
  const { locator } = record;
3288
3237
  installing(locator);
3289
- await screened();
3290
3238
  if (!liveness())
3291
3239
  return null;
3292
3240
  const key = await installWrite(async () => {
@@ -3315,19 +3263,16 @@ export function composeRuntime(options) {
3315
3263
  derive: derived,
3316
3264
  async refreshing(locator, key, composition) {
3317
3265
  await exclusive(() => local.unrefresh(key));
3318
- return refreshingOf(local, key, composition, liveness, locator);
3266
+ return refreshingOf(local, locator, key, composition, liveness, true);
3319
3267
  },
3320
3268
  abandon: (locator) => exclusive(async () => {
3321
- if (!liveness())
3322
- return;
3323
- await local.abandon(locator);
3324
- untransferred(locator);
3325
- liveQueries.recover();
3326
- sweep();
3269
+ if (liveness())
3270
+ await abandoned(local, locator);
3327
3271
  }),
3328
3272
  setAside: async (locator) => {
3329
3273
  if (!liveness())
3330
3274
  return;
3275
+ setBack(locator);
3331
3276
  untransferred(locator);
3332
3277
  liveQueries.recover();
3333
3278
  },
@@ -3336,19 +3281,6 @@ export function composeRuntime(options) {
3336
3281
  sweep();
3337
3282
  }),
3338
3283
  content: (key) => heldContentOf(local, key),
3339
- async validate(record, view) {
3340
- const { locator } = record;
3341
- const refused = await validatedRoom(local, record, view, null, null, (text) => {
3342
- const check = unchecked.get(locator);
3343
- if (check === undefined)
3344
- return;
3345
- unchecked.set(locator, { ...check, done: check.done + text });
3346
- queueReadiness();
3347
- });
3348
- unchecked.delete(locator);
3349
- queueReadiness();
3350
- return refused;
3351
- },
3352
3284
  };
3353
3285
  }
3354
3286
  function reusable(room, local) {
@@ -3360,15 +3292,21 @@ export function composeRuntime(options) {
3360
3292
  async function pullBootstrap(authorized, room, local, liveness) {
3361
3293
  if (!liveness())
3362
3294
  return unconfirmed;
3363
- const turn = generation;
3364
3295
  if (replicas.has(room.locator)) {
3365
3296
  catching.add(room.locator);
3366
3297
  liveQueries.recover();
3367
3298
  }
3368
3299
  try {
3369
- 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));
3300
+ const result = await bootstrapRoom(reads, authorized, room, { collection: modelOf(room.locator) }, builtIn, typedPaths, (models, held) => (liveness() ? remodel(room.locator, models, held) : held), liveness, staging(local, liveness), reusable(room, local));
3370
3301
  const detail = result.kind === 'staged' ? result.record.roomVersion : result.kind;
3371
3302
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
3303
+ if (result.kind === 'staged' && older(room.locator, result.record.roomVersion)) {
3304
+ caughtUpTo(room.locator);
3305
+ installed(room.locator);
3306
+ await abandoned(local, room.locator);
3307
+ journal.record(`transfer bootstrap ${room.locator} older than shown`);
3308
+ return { kind: 'transport-uncertainty' };
3309
+ }
3372
3310
  if (result.kind !== 'staged') {
3373
3311
  caughtUpTo(room.locator);
3374
3312
  installed(room.locator);
@@ -3383,9 +3321,6 @@ export function composeRuntime(options) {
3383
3321
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
3384
3322
  throw error;
3385
3323
  }
3386
- finally {
3387
- reads.finished(room.locator, turn);
3388
- }
3389
3324
  }
3390
3325
  function caughtUpTo(locator) {
3391
3326
  if (catching.delete(locator))
@@ -3393,51 +3328,55 @@ export function composeRuntime(options) {
3393
3328
  }
3394
3329
  function behind(room) {
3395
3330
  const held = replicas.get(room.locator)?.record.roomVersion;
3396
- return (held !== undefined &&
3397
- [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later)));
3331
+ return held !== undefined && !options.adapter.reached(room.locator, held, room.roomVersion);
3398
3332
  }
3399
- async function advance(room, local, authorized, live) {
3400
- const carried = reads.carried(room.locator);
3401
- const withheld = await advanceOnce(room, local, authorized, live);
3402
- if (withheld !== null || reads.carried(room.locator) === carried || !behind(room)) {
3403
- return withheld;
3404
- }
3405
- return advanceOnce(room, local, authorized, live);
3333
+ function standingOf(locator) {
3334
+ const held = replicas.get(locator);
3335
+ return { key: held?.record.key ?? null, confirmed: held?.confirmed ?? 0 };
3406
3336
  }
3407
- async function advanceOnce(room, local, authorized, running) {
3337
+ function stands(locator, from) {
3338
+ const now = standingOf(locator);
3339
+ return now.key === from.key && now.confirmed === from.confirmed;
3340
+ }
3341
+ async function advance(room, local, authorized, running) {
3408
3342
  const live = () => running() && !full;
3409
3343
  if (!live())
3410
3344
  return unconfirmed;
3411
3345
  const held = replicas.get(room.locator);
3412
- if (held !== undefined) {
3413
- const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold));
3346
+ if (held !== undefined && options.adapter.roomStream !== undefined) {
3347
+ stale.delete(room.locator);
3348
+ name(room.locator);
3349
+ return null;
3350
+ }
3351
+ const refresh = reads.refresh;
3352
+ if (held !== undefined && refresh !== undefined) {
3353
+ const refreshed = await ahead((hold) => refreshedRoom(room, local, authorized, held, live, hold, refresh));
3414
3354
  if (refreshed === 'behind')
3415
3355
  return advance(room, local, authorized, live);
3416
3356
  if (refreshed !== 'beyond-horizon')
3417
3357
  return refreshed;
3418
- if (opened !== null) {
3419
- owed.add(room.locator);
3420
- liveQueries.recover();
3421
- return unconfirmed;
3422
- }
3423
- if (reloadsDue.get(room.locator) === generation)
3358
+ if (reloadsDue.has(room.locator))
3424
3359
  return unconfirmed;
3360
+ replaced([room.locator]);
3425
3361
  }
3362
+ const from = standingOf(room.locator);
3426
3363
  const bootstrapped = await pullBootstrap(authorized, room, local, live);
3427
- if (bootstrapped.kind === 'staged') {
3428
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
3364
+ if (bootstrapped.kind !== 'staged')
3365
+ return live() ? bootstrapped : unconfirmed;
3366
+ const landing = { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from };
3367
+ if (await publish(local, room, bootstrapped.record, landing, live)) {
3429
3368
  return live() ? null : unconfirmed;
3430
3369
  }
3431
- return live() ? bootstrapped : unconfirmed;
3370
+ return reachedBy(room.locator, bootstrapped.record.roomVersion)
3371
+ ? null
3372
+ : advance(room, local, authorized, live);
3432
3373
  }
3433
- async function refreshedRoom(room, local, authorized, held, live, hold) {
3434
- const refreshed = await pullRefresh(authorized, held, live, hold);
3374
+ async function refreshedRoom(room, local, authorized, held, live, hold, refresh) {
3375
+ const refreshed = await pullRefresh(authorized, held, live, hold, refresh);
3435
3376
  if (!live())
3436
3377
  return unconfirmed;
3437
3378
  if (refreshed.kind === 'still-current') {
3438
3379
  stale.delete(room.locator);
3439
- if (sourceRefusals.has(room.locator))
3440
- return null;
3441
3380
  if (unreadable.delete(room.locator)) {
3442
3381
  scopedRefusals.delete(room.locator);
3443
3382
  list(room, true);
@@ -3446,7 +3385,11 @@ export function composeRuntime(options) {
3446
3385
  return null;
3447
3386
  }
3448
3387
  if (refreshed.kind === 'advanced') {
3449
- const landing = { kind: 'refreshed', base: refreshed.base };
3388
+ const landing = {
3389
+ kind: 'refreshed',
3390
+ from: refreshed.from,
3391
+ remodelled: refreshed.remodelled,
3392
+ };
3450
3393
  if (!(await publish(local, room, refreshed.record, landing, live))) {
3451
3394
  const now = replicas.get(room.locator)?.record.roomVersion;
3452
3395
  const reached = now !== undefined &&
@@ -3459,12 +3402,9 @@ export function composeRuntime(options) {
3459
3402
  return refreshed.kind === 'beyond-horizon' ? 'beyond-horizon' : refreshed;
3460
3403
  }
3461
3404
  async function withhold(withheld, room, local, progress) {
3462
- if (withheld.kind === 'unconfirmed')
3405
+ if (withheld.kind === 'unconfirmed' || isFenced(room.locator))
3463
3406
  return;
3464
- if (withheld.kind === 'refused')
3465
- stale.delete(room.locator);
3466
- else
3467
- stale.add(room.locator);
3407
+ stale.add(room.locator);
3468
3408
  if (withheld.kind === 'transport-uncertainty' || withheld.kind === 'retryable-refusal') {
3469
3409
  const reset = withheld.kind === 'retryable-refusal' ? withheld.reset : undefined;
3470
3410
  repairable.set(room.locator, reset);
@@ -3486,30 +3426,10 @@ export function composeRuntime(options) {
3486
3426
  list(room, false);
3487
3427
  emitRooms();
3488
3428
  }
3489
- else if (withheld.kind === 'refused') {
3490
- refusedSource(room, withheld);
3491
- }
3492
3429
  else if (withheld.kind === 'revoked-authorization') {
3493
3430
  await refuse('revoked-authorization');
3494
3431
  }
3495
3432
  }
3496
- function refusedSource(room, { refusal }) {
3497
- sourceRefusals.set(room.locator, {
3498
- code: refusal.rule,
3499
- retryable: false,
3500
- fields: {
3501
- file: refusal.file,
3502
- ...(refusal.location === undefined ? {} : { location: refusal.location }),
3503
- ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
3504
- },
3505
- });
3506
- declarationRefusals.delete(room.locator);
3507
- unreadable.add(room.locator);
3508
- list(room, false);
3509
- emitRooms();
3510
- moveTo({ ...status, ...attention() });
3511
- journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
3512
- }
3513
3433
  function leaving(batch, result) {
3514
3434
  if (result.kind === 'expired')
3515
3435
  return { batch, returned: returnedBy('expired') };
@@ -3531,8 +3451,8 @@ export function composeRuntime(options) {
3531
3451
  function settling(acceptance) {
3532
3452
  return acceptance !== null && 'apply' in acceptance;
3533
3453
  }
3534
- function refusalOf(acceptance) {
3535
- return settling(acceptance) ? (acceptance.refused ?? null) : acceptance;
3454
+ function failureOf(acceptance) {
3455
+ return settling(acceptance) ? null : acceptance;
3536
3456
  }
3537
3457
  async function acceptance(room, local, submitted, accepted) {
3538
3458
  if (!continuing())
@@ -3545,7 +3465,8 @@ export function composeRuntime(options) {
3545
3465
  logAccepted(room.locator, submitted, accepted);
3546
3466
  let record = current.record;
3547
3467
  let changes;
3548
- if (!options.adapter.reached(room.locator, held, accepted.roomVersion)) {
3468
+ if (!options.adapter.reached(room.locator, held, accepted.roomVersion) &&
3469
+ !older(room.locator, accepted.roomVersion)) {
3549
3470
  const paths = [...returnedPaths(submitted, accepted)];
3550
3471
  const confirmedLayer = await stepped(slicing(await local.entries(record.key, 'confirmed', paths), paths));
3551
3472
  const back = await stepped(returning(confirmedLayer, submitted, accepted, accepted.roomVersion));
@@ -3555,34 +3476,6 @@ export function composeRuntime(options) {
3555
3476
  ? accepted.roomVersion
3556
3477
  : held,
3557
3478
  };
3558
- const overrides = new Map([
3559
- ...back.set.map((entry) => [entry.path, entry]),
3560
- ...back.removed.map((path) => [path, null]),
3561
- ]);
3562
- const refused = await validatedRoom(local, record, 'confirmed', { record: current.record, changed: [...overrides.keys()] }, overrides);
3563
- if (refused !== null) {
3564
- const key = pendingKey(room.locator, submitted.id);
3565
- return {
3566
- locator: room.locator,
3567
- left: [],
3568
- commit: {
3569
- rooms: [],
3570
- writes: [
3571
- {
3572
- locator: room.locator,
3573
- accepted: [{ id: submitted.id, roomVersion: accepted.roomVersion }],
3574
- },
3575
- ],
3576
- },
3577
- refused,
3578
- apply() {
3579
- const state = pendings.get(key);
3580
- if (state !== undefined && !fences.has(room.locator))
3581
- pendings.set(key, { ...state, accepted: accepted.roomVersion });
3582
- return changeSet(room.locator, { queue: true });
3583
- },
3584
- };
3585
- }
3586
3479
  changes = back;
3587
3480
  }
3588
3481
  const settled = await settlement(local, room.locator, current, record, current.queue.filter((batch) => batch.id !== submitted.id), { room: true, ...(changes === undefined ? {} : { changes }) });
@@ -3590,10 +3483,9 @@ export function composeRuntime(options) {
3590
3483
  ...settled,
3591
3484
  apply(answer) {
3592
3485
  const set = settled.apply(answer);
3486
+ showing(room.locator, accepted.roomVersion);
3593
3487
  pendings.delete(pendingKey(room.locator, submitted.id));
3594
3488
  unreadable.delete(room.locator);
3595
- sourceRefusals.delete(room.locator);
3596
- declarationRefusals.delete(room.locator);
3597
3489
  list(room, true);
3598
3490
  return set;
3599
3491
  },
@@ -3611,13 +3503,9 @@ export function composeRuntime(options) {
3611
3503
  const failed = await exclusive(async () => {
3612
3504
  const adopting = await acceptance(room, local, submitted, accepted);
3613
3505
  await adoptedTogether(local, [adopting]);
3614
- return refusalOf(adopting);
3506
+ return failureOf(adopting);
3615
3507
  });
3616
- if (failed === null)
3617
- return true;
3618
- if (failed.kind === 'refused' && continuing())
3619
- refusedSource(room, failed);
3620
- return false;
3508
+ return failed === null;
3621
3509
  }
3622
3510
  async function adoptedMove(room, partner, local, submitted, accepted) {
3623
3511
  const other = discovered.get(partner);
@@ -3631,18 +3519,10 @@ export function composeRuntime(options) {
3631
3519
  ...accepted.destination,
3632
3520
  });
3633
3521
  await adoptedTogether(local, [left, arrived]);
3634
- const refusedArrival = refusalOf(arrived);
3635
- if (other !== undefined && refusedArrival?.kind === 'refused' && continuing()) {
3636
- refusedSource(other, refusedArrival);
3637
- }
3638
- return refusalOf(left);
3522
+ return failureOf(left);
3639
3523
  });
3640
3524
  submitLater(partner);
3641
- if (failed === null)
3642
- return true;
3643
- if (failed.kind === 'refused' && continuing())
3644
- refusedSource(room, failed);
3645
- return false;
3525
+ return failed === null;
3646
3526
  }
3647
3527
  function queueIn(locator) {
3648
3528
  return replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [];
@@ -3761,12 +3641,10 @@ export function composeRuntime(options) {
3761
3641
  async function submitRoom(room, local, authorized, progress, signal) {
3762
3642
  while (continuing() && authorization !== undefined && !signal.aborted) {
3763
3643
  const replica = replicas.get(room.locator);
3764
- const head = replica?.queue[0];
3644
+ const head = replica?.queue.find((batch) => !landedAhead.has(pendingKey(room.locator, batch.id)));
3765
3645
  if (replica === undefined || head === undefined)
3766
3646
  return 'done';
3767
3647
  const state = pendings.get(pendingKey(room.locator, head.id));
3768
- if (state?.accepted !== undefined)
3769
- return 'done';
3770
3648
  const partner = partnerOf(head, room.locator);
3771
3649
  if (partner !== null && !leavingIn(head, room.locator)) {
3772
3650
  submitLater(partner);
@@ -3880,12 +3758,12 @@ export function composeRuntime(options) {
3880
3758
  }
3881
3759
  if (result.kind === 'expired') {
3882
3760
  await exclusive(() => mark(local, room.locator, head, { state: 'waiting' }, partner !== null && lost));
3883
- const there = other === undefined ? null : await advance(other, local, authorized, continuing);
3761
+ const there = other === undefined ? null : await advanced(other, local, authorized);
3884
3762
  if (other !== undefined && there !== null) {
3885
3763
  await withhold(there, other, local, progress);
3886
3764
  return 'done';
3887
3765
  }
3888
- const withheld = await advance(room, local, authorized, continuing);
3766
+ const withheld = await advanced(room, local, authorized);
3889
3767
  if (withheld !== null) {
3890
3768
  await withhold(withheld, room, local, progress);
3891
3769
  return 'done';
@@ -3907,8 +3785,8 @@ export function composeRuntime(options) {
3907
3785
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
3908
3786
  const left = leaving(head, result);
3909
3787
  const refreshed = async () => ({
3910
- there: other === undefined ? null : await advance(other, local, authorized, continuing),
3911
- here: await advance(room, local, authorized, continuing),
3788
+ there: other === undefined ? null : await advanced(other, local, authorized),
3789
+ here: await advanced(room, local, authorized),
3912
3790
  });
3913
3791
  const judged = result.kind === 'conflicted' && other !== undefined ? await refreshed() : null;
3914
3792
  if (judged !== null && (judged.there !== null || judged.here !== null)) {
@@ -3932,6 +3810,13 @@ export function composeRuntime(options) {
3932
3810
  (await landedWhole(room.locator, head, confirmedLayer))) ||
3933
3811
  (result.kind === 'conflicted' &&
3934
3812
  (await landedWhole(room.locator, head, confirmedLayer, result.affected)));
3813
+ if (held &&
3814
+ result.kind === 'conflicted' &&
3815
+ partner === null &&
3816
+ !landed(head, confirmedLayer)) {
3817
+ landedAhead.set(pendingKey(room.locator, head.id), Object.fromEntries([...touchedBy([head])].map((path) => [path, confirmedLayer.revisions[path]])));
3818
+ return;
3819
+ }
3935
3820
  publishChangeSet(await settle(local, room.locator, current, current.record, current.queue.filter((batch) => batch.id !== head.id), { leaving: [held ? { ...left, returned: null } : left] }));
3936
3821
  await unpair(local);
3937
3822
  moveTo({ ...status, ...attention() });
@@ -3988,8 +3873,10 @@ export function composeRuntime(options) {
3988
3873
  for (const { room, before, queue } of held) {
3989
3874
  const { locator } = room;
3990
3875
  lastFenced.set(locator, ++fencings);
3991
- for (const batch of queue)
3876
+ for (const batch of queue) {
3992
3877
  pendings.delete(pendingKey(locator, batch.id));
3878
+ landedAhead.delete(pendingKey(locator, batch.id));
3879
+ }
3993
3880
  queuesWithoutReplica.delete(locator);
3994
3881
  if (before !== undefined) {
3995
3882
  replicas.set(locator, {
@@ -4012,25 +3899,22 @@ export function composeRuntime(options) {
4012
3899
  if (signal.aborted)
4013
3900
  return;
4014
3901
  const progress = { complete: true, reachable: true };
4015
- await roomTask(room.locator, async (live) => {
4016
- await Promise.race([
4017
- reload(room, local, progress, () => live() && !signal.aborted),
4018
- abandonedOn(signal),
4019
- ]);
4020
- });
3902
+ await roomTask(room.locator, (live) => reload(room, local, progress, live));
4021
3903
  if (signal.aborted)
4022
3904
  return;
4023
3905
  await rest(progress);
4024
3906
  }
4025
3907
  }
4026
3908
  function awaitingAnswer(locator) {
3909
+ answering.get(locator)?.release();
4027
3910
  let release = () => { };
4028
- const awaited = new Promise((resolve) => {
3911
+ const answered = new Promise((resolve) => {
4029
3912
  release = resolve;
4030
3913
  });
4031
- answering.set(locator, { generation, answered: awaited });
3914
+ const awaited = { answered, release };
3915
+ answering.set(locator, awaited);
4032
3916
  return () => {
4033
- if (answering.get(locator)?.answered === awaited)
3917
+ if (answering.get(locator) === awaited)
4034
3918
  answering.delete(locator);
4035
3919
  release();
4036
3920
  };
@@ -4042,6 +3926,30 @@ export function composeRuntime(options) {
4042
3926
  JSON.stringify(held.replica) === JSON.stringify(room.replica) &&
4043
3927
  JSON.stringify(held.typedSelection) === JSON.stringify(room.typedSelection ?? null)));
4044
3928
  }
3929
+ async function keepNaming(local, room) {
3930
+ const held = replicas.get(room.locator)?.record;
3931
+ if (held === undefined || (held.name === room.name && held.handle === room.handle))
3932
+ return;
3933
+ await exclusive(async () => {
3934
+ if (!continuing())
3935
+ return;
3936
+ const replica = replicas.get(room.locator);
3937
+ if (replica === undefined)
3938
+ return;
3939
+ const record = { ...replica.record, name: room.name, handle: room.handle };
3940
+ await local.commit({
3941
+ rooms: [
3942
+ {
3943
+ locator: room.locator,
3944
+ key: record.key,
3945
+ record: storedRecord(record),
3946
+ composed: composedOf(composed(record)),
3947
+ },
3948
+ ],
3949
+ });
3950
+ replicas.set(room.locator, { ...replica, record });
3951
+ });
3952
+ }
4045
3953
  async function retainModel(local, room, next) {
4046
3954
  const key = room.locator;
4047
3955
  const previous = modelOf(room.locator);
@@ -4057,26 +3965,15 @@ export function composeRuntime(options) {
4057
3965
  const held = replicas.get(room.locator);
4058
3966
  let changed = null;
4059
3967
  if (held !== undefined) {
4060
- const declared = {
3968
+ const record = {
4061
3969
  ...held.record,
3970
+ name: room.name,
3971
+ handle: room.handle,
4062
3972
  operations: room.operations,
4063
3973
  replica: room.replica,
4064
3974
  typedSelection: room.typedSelection ?? null,
4065
3975
  model: next,
4066
3976
  };
4067
- let read = 'staged';
4068
- if (!sameMembership(declared, held.record)) {
4069
- const verdict = await validatedCollection(walkOf(local, held.record.key, 'confirmed'), lookupOf(local, held.record.key, 'confirmed'), composed(declared));
4070
- read =
4071
- verdict.verdict !== 'refuse'
4072
- ? 'staged'
4073
- : verdict.rule === 'model-unavailable'
4074
- ? 'deferred'
4075
- : { kind: 'refused', refusal: verdict };
4076
- }
4077
- const record = read === 'staged'
4078
- ? declared
4079
- : { ...declared, typedSelection: held.record.typedSelection };
4080
3977
  const confirmedLayer = await sliceAt(record, 'confirmed', held.queue, held.overlaid);
4081
3978
  const replayedLayer = await replayed(record, confirmedLayer, held.queue);
4082
3979
  const diff = await overlayDiff(record, held, replayedLayer.visible, replayedLayer.touched);
@@ -4098,16 +3995,6 @@ export function composeRuntime(options) {
4098
3995
  documents: answer.changed[room.locator] ?? [],
4099
3996
  room: true,
4100
3997
  });
4101
- if (read === 'staged' || read === 'deferred') {
4102
- if (declarationRefusals.delete(room.locator)) {
4103
- sourceRefusals.delete(room.locator);
4104
- unreadable.delete(room.locator);
4105
- }
4106
- }
4107
- else {
4108
- refusedSource(room, read);
4109
- declarationRefusals.add(room.locator);
4110
- }
4111
3998
  }
4112
3999
  heldModel(room, next);
4113
4000
  if (changed === null)
@@ -4152,6 +4039,17 @@ export function composeRuntime(options) {
4152
4039
  emitRooms();
4153
4040
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
4154
4041
  }
4042
+ function name(locator) {
4043
+ const stream = options.adapter.roomStream;
4044
+ const record = replicas.get(locator)?.record;
4045
+ const room = discovered.get(locator);
4046
+ if (stream === undefined || record === undefined || room === undefined)
4047
+ return;
4048
+ if (!socketOpen || !handed || record.replica.selection !== room.replica.selection)
4049
+ return;
4050
+ naming += 1;
4051
+ stream.hold([{ locator, roomVersion: record.roomVersion }]);
4052
+ }
4155
4053
  function heldRooms(listed) {
4156
4054
  return listed.flatMap((room) => {
4157
4055
  const record = replicas.get(room.locator)?.record;
@@ -4169,14 +4067,19 @@ export function composeRuntime(options) {
4169
4067
  return outcome?.status === 'fulfilled' && outcome.value;
4170
4068
  });
4171
4069
  if (attempt === 'opened') {
4070
+ naming += 1;
4172
4071
  options.adapter.roomStream?.hold(heldRooms(ready));
4173
4072
  handed = true;
4174
4073
  }
4175
- discovering?.release();
4176
- const loaded = await Promise.allSettled(ready
4177
- .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
4178
- .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
4179
- const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
4074
+ for (const room of ready) {
4075
+ if (attempt !== 'unstreamed' && replicas.has(room.locator)) {
4076
+ if (attempt === 'authorized')
4077
+ name(room.locator);
4078
+ continue;
4079
+ }
4080
+ void roomTask(room.locator, (live) => loadRoom(room, local, progress, live)).then(() => rest(progress));
4081
+ }
4082
+ const failed = prepared.find((room) => room.status === 'rejected');
4180
4083
  if (failed !== undefined)
4181
4084
  throw failed.reason;
4182
4085
  }
@@ -4195,12 +4098,16 @@ export function composeRuntime(options) {
4195
4098
  }
4196
4099
  if (prior !== undefined)
4197
4100
  await retainModel(local, room, modelOf(room.locator));
4101
+ if (prior !== undefined)
4102
+ await keepNaming(local, room);
4198
4103
  if (discoveryPaused || authorization === undefined)
4199
4104
  return false;
4200
4105
  return room.replica.admission === 'admitted';
4201
4106
  }
4202
4107
  async function caughtUp(room, local, progress, live) {
4203
- if (authorization === undefined || !live() || !behind(room))
4108
+ if (options.adapter.roomStream !== undefined || authorization === undefined)
4109
+ return;
4110
+ if (!live() || !behind(room))
4204
4111
  return;
4205
4112
  const withheld = await advance(room, local, authorization, live);
4206
4113
  if (withheld !== null)
@@ -4213,6 +4120,8 @@ export function composeRuntime(options) {
4213
4120
  async function loadRoom(room, local, progress, live) {
4214
4121
  if (!live() || authorization === undefined)
4215
4122
  return;
4123
+ if (replicas.has(room.locator) && !(await prepareRoom(room, local, progress)))
4124
+ return;
4216
4125
  if (replicas.has(room.locator) && revoked(room.locator)) {
4217
4126
  await reload(room, local, progress, live);
4218
4127
  return;
@@ -4230,20 +4139,53 @@ export function composeRuntime(options) {
4230
4139
  reloadsDue.delete(room.locator);
4231
4140
  if (!live() || authorization === undefined)
4232
4141
  return;
4142
+ const from = standingOf(room.locator);
4233
4143
  const bootstrapped = await pullBootstrap(authorization, room, local, live);
4234
4144
  if (bootstrapped.kind !== 'staged') {
4235
4145
  if (live())
4236
4146
  await withhold(bootstrapped, room, local, progress);
4237
4147
  return;
4238
4148
  }
4239
- await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed }, live);
4149
+ await publish(local, room, bootstrapped.record, { kind: 'bootstrapped', refreshed: bootstrapped.refreshed, from }, live);
4240
4150
  await caughtUp(room, local, progress, live);
4241
4151
  }
4242
4152
  finally {
4243
4153
  caughtUpTo(room.locator);
4244
4154
  }
4245
4155
  }
4246
- async function discoverRooms(local, progress, attempt, live, scope) {
4156
+ async function asked(authorized) {
4157
+ try {
4158
+ return (await options.adapter.discover?.(authorized)) ?? { kind: 'transport-uncertainty' };
4159
+ }
4160
+ catch {
4161
+ return { kind: 'transport-uncertainty' };
4162
+ }
4163
+ }
4164
+ function malformed(listed) {
4165
+ for (const room of listed) {
4166
+ if (room.operations === undefined ||
4167
+ !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4168
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
4169
+ room.replica === undefined ||
4170
+ typeof room.replica.selection !== 'string' ||
4171
+ room.replica.selection.length === 0 ||
4172
+ !['admitted', 'unavailable'].includes(room.replica.admission) ||
4173
+ !['locked', 'open'].includes(room.replica.offline)) {
4174
+ return `invalid model or replica declaration ${room.locator}`;
4175
+ }
4176
+ if (!declaredSelection(room))
4177
+ return `invalid typed selection declaration ${room.locator}`;
4178
+ }
4179
+ return null;
4180
+ }
4181
+ async function signIn(local, signedIn) {
4182
+ if (JSON.stringify(signedIn) === JSON.stringify(member))
4183
+ return;
4184
+ await local.fact('member', signedIn);
4185
+ member = signedIn;
4186
+ emitMember();
4187
+ }
4188
+ async function discoverRooms(local, progress, attempt, live, found) {
4247
4189
  if (closed || authorization === undefined) {
4248
4190
  notifyReady();
4249
4191
  return;
@@ -4251,13 +4193,7 @@ export function composeRuntime(options) {
4251
4193
  moveTo(transferring(status));
4252
4194
  discoveries += 1;
4253
4195
  const started = discoveries;
4254
- let result;
4255
- try {
4256
- result = await options.adapter.discover(authorization, scope);
4257
- }
4258
- catch {
4259
- result = { kind: 'transport-uncertainty' };
4260
- }
4196
+ const result = found ?? (await asked(authorization));
4261
4197
  if (result.kind === 'discovery')
4262
4198
  roomsNamed = true;
4263
4199
  if (!live()) {
@@ -4267,7 +4203,7 @@ export function composeRuntime(options) {
4267
4203
  }
4268
4204
  journal.record(`connection discover ${result.kind}`);
4269
4205
  if (result.kind === 'transport-uncertainty') {
4270
- await settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4206
+ await cachedModels(local);
4271
4207
  progress.reachable = false;
4272
4208
  progress.complete = false;
4273
4209
  notifyReady();
@@ -4288,26 +4224,12 @@ export function composeRuntime(options) {
4288
4224
  notifyReady();
4289
4225
  return;
4290
4226
  }
4291
- for (const room of result.rooms) {
4292
- if (room.operations === undefined ||
4293
- !['write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
4294
- room.operations[operation].every((scope) => typeof scope === 'string')) ||
4295
- room.replica === undefined ||
4296
- typeof room.replica.selection !== 'string' ||
4297
- room.replica.selection.length === 0 ||
4298
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
4299
- !['locked', 'open'].includes(room.replica.offline)) {
4300
- journal.record(`invalid model or replica declaration ${room.locator}`);
4301
- await refuse('missing-required-guarantee');
4302
- notifyReady();
4303
- return;
4304
- }
4305
- if (!declaredSelection(room)) {
4306
- journal.record(`invalid typed selection declaration ${room.locator}`);
4307
- await refuse('missing-required-guarantee');
4308
- notifyReady();
4309
- return;
4310
- }
4227
+ const invalid = malformed(result.rooms);
4228
+ if (invalid !== null) {
4229
+ journal.record(invalid);
4230
+ await refuse('missing-required-guarantee');
4231
+ notifyReady();
4232
+ return;
4311
4233
  }
4312
4234
  if (refusal !== null) {
4313
4235
  refusal = null;
@@ -4322,15 +4244,9 @@ export function composeRuntime(options) {
4322
4244
  limits = result.providerLimits;
4323
4245
  await local.fact('providerLimits', limits);
4324
4246
  await local.fact('capabilities', capabilities);
4325
- conversations.declared();
4326
- }
4327
- const signedIn = result.member ?? null;
4328
- if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
4329
- await local.fact('member', signedIn);
4330
- member = signedIn;
4331
- emitMember();
4332
4247
  }
4333
- await synchronize(await applyDiscovery(result.rooms, started, result.covered), local, progress, attempt, live);
4248
+ await signIn(local, result.member ?? null);
4249
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
4334
4250
  }
4335
4251
  async function ensureAuthorization() {
4336
4252
  if (authorization !== undefined)
@@ -4345,23 +4261,45 @@ export function composeRuntime(options) {
4345
4261
  }
4346
4262
  }
4347
4263
  async function admittedRead(work) {
4348
- if (roomReads.generation !== generation) {
4349
- roomReads = { generation, free: ROOM_READS, waiting: [] };
4264
+ while (roomReads.free === 0) {
4265
+ await new Promise((admit) => roomReads.waiting.push(admit));
4350
4266
  }
4351
- const reads = roomReads;
4352
- while (reads.free === 0)
4353
- await new Promise((admit) => reads.waiting.push(admit));
4354
- reads.free -= 1;
4267
+ roomReads.free -= 1;
4355
4268
  try {
4356
4269
  await work();
4357
4270
  }
4358
4271
  finally {
4359
- reads.free += 1;
4360
- reads.waiting.shift()?.();
4272
+ roomReads.free += 1;
4273
+ roomReads.waiting.shift()?.();
4361
4274
  }
4362
4275
  }
4276
+ function inRoom(locator, work) {
4277
+ roomWork += 1;
4278
+ const ran = (roomLanes.get(locator) ?? Promise.resolve()).then(async () => {
4279
+ try {
4280
+ return await work(continuing);
4281
+ }
4282
+ finally {
4283
+ roomWork -= 1;
4284
+ }
4285
+ });
4286
+ const chain = ran.then(() => undefined, () => undefined);
4287
+ roomLanes.set(locator, chain);
4288
+ working.add(chain);
4289
+ void chain.finally(() => {
4290
+ working.delete(chain);
4291
+ if (roomLanes.get(locator) === chain)
4292
+ roomLanes.delete(locator);
4293
+ });
4294
+ return ran;
4295
+ }
4363
4296
  function roomTask(locator, work) {
4364
- return task(roomLane(locator), (live) => admittedRead(() => work(live)));
4297
+ if (stopping)
4298
+ return Promise.resolve();
4299
+ return inRoom(locator, (live) => admittedRead(() => work(live))).catch(() => undefined);
4300
+ }
4301
+ function advanced(room, local, authorized) {
4302
+ return inRoom(room.locator, (live) => advance(room, local, authorized, live));
4365
4303
  }
4366
4304
  function task(lane, work) {
4367
4305
  if (stopping)
@@ -4377,23 +4315,19 @@ export function composeRuntime(options) {
4377
4315
  void run.finally(() => working.delete(run));
4378
4316
  return run;
4379
4317
  }
4380
- function sync(attempt) {
4318
+ function sync(attempt, found) {
4381
4319
  return task(OPENING_LANE, async (live) => {
4382
4320
  if (!live()) {
4383
4321
  readyFromStore();
4384
4322
  return;
4385
4323
  }
4386
4324
  const progress = { complete: true, reachable: true };
4387
- const gate = discover();
4388
- if (attempt === 'opened')
4389
- opened = generation;
4390
- underway = attempt === 'opened' ? { generation, progress } : null;
4325
+ underway = attempt === 'opened' ? progress : null;
4391
4326
  if (attempt === 'opened')
4392
4327
  streamed = progress;
4393
4328
  complete = false;
4394
4329
  recorded = false;
4395
4330
  moveTo(transferring(status));
4396
- handed = false;
4397
4331
  try {
4398
4332
  const local = await openLocal();
4399
4333
  if (!live())
@@ -4401,7 +4335,7 @@ export function composeRuntime(options) {
4401
4335
  await ensureAuthorization();
4402
4336
  if (!live())
4403
4337
  return;
4404
- await discoverRooms(local, progress, attempt, live);
4338
+ await discoverRooms(local, progress, attempt, live, found);
4405
4339
  }
4406
4340
  catch {
4407
4341
  progress.complete = false;
@@ -4409,7 +4343,6 @@ export function composeRuntime(options) {
4409
4343
  progress.reachable = false;
4410
4344
  }
4411
4345
  finally {
4412
- gate.release();
4413
4346
  notifyReady();
4414
4347
  }
4415
4348
  if (!live())
@@ -4421,35 +4354,59 @@ export function composeRuntime(options) {
4421
4354
  if (handed || closed || progress.reachable)
4422
4355
  return;
4423
4356
  underway = null;
4424
- opened = null;
4425
- journal.record('connection reopen');
4426
4357
  await rest(progress, live);
4427
- options.adapter.roomStream?.reopen();
4428
4358
  });
4429
4359
  }
4430
- function rediscover(scope) {
4360
+ function resynchronize() {
4431
4361
  return task(OPENING_LANE, async (live) => {
4432
- if (!live() || !socketOpen)
4362
+ if (!live() || !announced)
4433
4363
  return;
4434
4364
  const progress = { complete: true, reachable: true };
4435
- const gate = discover();
4436
4365
  moveTo(transferring(status));
4437
4366
  try {
4438
4367
  const local = await openLocal();
4439
4368
  if (!live())
4440
4369
  return;
4441
- await ensureAuthorization();
4370
+ await synchronize([...discovered.values()], local, progress, 'unstreamed', live);
4371
+ }
4372
+ catch {
4373
+ progress.complete = false;
4374
+ }
4375
+ if (live())
4376
+ await finishOpening(progress, live);
4377
+ });
4378
+ }
4379
+ function countAuthorityChange() {
4380
+ authorityChanges += 1;
4381
+ for (const listener of authorityChangeListeners)
4382
+ listener(authorityChanges);
4383
+ }
4384
+ function changeAuthority(change) {
4385
+ return task(OPENING_LANE, async (live) => {
4386
+ if (!live() || !announced)
4387
+ return;
4388
+ countAuthorityChange();
4389
+ const progress = { complete: true, reachable: true };
4390
+ moveTo(transferring(status));
4391
+ try {
4392
+ const local = await openLocal();
4442
4393
  if (!live())
4443
4394
  return;
4444
- await discoverRooms(local, progress, 'authorized', live, scope);
4395
+ const invalid = malformed(change.rooms);
4396
+ if (invalid !== null) {
4397
+ journal.record(invalid);
4398
+ await refuse('missing-required-guarantee');
4399
+ return;
4400
+ }
4401
+ if (change.member !== undefined)
4402
+ await signIn(local, change.member);
4403
+ discoveries += 1;
4404
+ const listed = await applyDiscovery(change.rooms, discoveries, change.left);
4405
+ await synchronize(listed, local, progress, 'authorized', live);
4445
4406
  }
4446
4407
  catch {
4447
4408
  progress.complete = false;
4448
4409
  }
4449
- finally {
4450
- gate.release();
4451
- notifyReady();
4452
- }
4453
4410
  if (!live())
4454
4411
  return;
4455
4412
  if (!progress.complete)
@@ -4457,15 +4414,17 @@ export function composeRuntime(options) {
4457
4414
  await rest(progress, live);
4458
4415
  });
4459
4416
  }
4460
- function closedSocket(revoked, lost) {
4461
- notifyDiscovered();
4462
- const opened = socketOpen;
4417
+ function closedSocket(revoked, lost, authorityChanged) {
4418
+ notifyReady();
4419
+ const opened = announced;
4463
4420
  socketOpen = false;
4421
+ announced = false;
4422
+ missedChange ||= authorityChanged;
4423
+ handed = false;
4424
+ naming = 0;
4464
4425
  connectionLost();
4465
- if (lost) {
4466
- reads.lost();
4426
+ if (lost)
4467
4427
  conversations.lost();
4468
- }
4469
4428
  if (revoked) {
4470
4429
  void refuse('revoked-authorization');
4471
4430
  return;
@@ -4476,18 +4435,32 @@ export function composeRuntime(options) {
4476
4435
  moveTo({
4477
4436
  ...status,
4478
4437
  state: status.state === 'synced' ? 'syncing' : status.state,
4479
- providerUnreachable: status.providerUnreachable || !opened,
4438
+ providerUnreachable: status.providerUnreachable || (!opened && lost),
4480
4439
  });
4440
+ void closing(opened);
4481
4441
  }
4482
- function applyOpening(expired) {
4483
- const progress = underway?.generation === generation ? underway.progress : null;
4442
+ function closing(opened) {
4443
+ return task(OPENING_LANE, async (live) => {
4444
+ if (!live())
4445
+ return;
4446
+ const local = await openLocal();
4447
+ if (!opened && !socketOpen)
4448
+ await cachedModels(local);
4449
+ await rest(null, live);
4450
+ });
4451
+ }
4452
+ function cachedModels(local) {
4453
+ return settleModels([...rooms.values()].map((room) => discovered.get(room.locator) ?? room), 'cached', local);
4454
+ }
4455
+ function caughtUpRooms(expired) {
4456
+ const progress = underway;
4457
+ underway = null;
4458
+ naming = Math.max(0, naming - 1);
4484
4459
  const cycle = progress ?? { complete: true, reachable: true };
4485
- if (opened === generation)
4486
- opened = null;
4460
+ replaced(expired);
4487
4461
  const reloading = progress === null ? [] : owedReloads(expired);
4488
4462
  const replacing = reloadNow((room) => expired.includes(room.locator) || reloading.includes(room.locator), cycle);
4489
4463
  return task(OPENING_LANE, async (live) => {
4490
- underway = null;
4491
4464
  if (!live())
4492
4465
  return;
4493
4466
  if (progress === null) {
@@ -4495,20 +4468,14 @@ export function composeRuntime(options) {
4495
4468
  await rest(cycle, live);
4496
4469
  return;
4497
4470
  }
4498
- await roomsCarried([...expired, ...reloading]);
4499
- if (!live())
4500
- return;
4501
4471
  try {
4502
4472
  const local = await openLocal();
4503
- const reloaded = await replacing;
4473
+ await replacing;
4504
4474
  if (progress.reachable) {
4505
4475
  await settleModels([...discovered.values()].filter((room) => replicas.has(room.locator) &&
4506
4476
  !expired.includes(room.locator) &&
4507
4477
  !reloading.includes(room.locator)), 'current', local);
4508
4478
  }
4509
- const failed = reloaded.find((outcome) => outcome.status === 'rejected');
4510
- if (failed !== undefined)
4511
- throw failed.reason;
4512
4479
  }
4513
4480
  catch {
4514
4481
  progress.complete = false;
@@ -4520,11 +4487,8 @@ export function composeRuntime(options) {
4520
4487
  }
4521
4488
  function owedReloads(expired) {
4522
4489
  const reloading = [...discovered.values()]
4523
- .filter((room) => replicas.has(room.locator) &&
4524
- !expired.includes(room.locator) &&
4525
- (owed.has(room.locator) || revoked(room.locator)))
4490
+ .filter((room) => replicas.has(room.locator) && !expired.includes(room.locator) && revoked(room.locator))
4526
4491
  .map((room) => room.locator);
4527
- owed.clear();
4528
4492
  for (const locator of reloading)
4529
4493
  catching.add(locator);
4530
4494
  liveQueries.recover();
@@ -4533,16 +4497,17 @@ export function composeRuntime(options) {
4533
4497
  async function reloadNow(replaced, progress) {
4534
4498
  const reloaded = [...discovered.values()].filter((room) => replicas.has(room.locator) && replaced(room));
4535
4499
  for (const room of reloaded)
4536
- reloadsDue.set(room.locator, generation);
4500
+ reloadsDue.add(room.locator);
4537
4501
  try {
4538
4502
  const local = await openLocal();
4539
- return await Promise.allSettled(reloaded.map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
4503
+ for (const room of reloaded) {
4504
+ void roomTask(room.locator, (live) => reload(room, local, progress, live)).then(() => rest(progress));
4505
+ }
4540
4506
  }
4541
4507
  catch {
4542
4508
  for (const room of reloaded)
4543
4509
  reloadsDue.delete(room.locator);
4544
4510
  progress.reachable = false;
4545
- return [];
4546
4511
  }
4547
4512
  }
4548
4513
  async function settleModels(held, status, local) {
@@ -4553,7 +4518,7 @@ export function composeRuntime(options) {
4553
4518
  const settled = await exclusive(async () => {
4554
4519
  if (!continuing())
4555
4520
  return [];
4556
- const unchanged = held.filter((room) => owed(room) && !declarationRefusals.has(room.locator) && declaredAsHeld(room));
4521
+ const unchanged = held.filter((room) => owed(room) && declaredAsHeld(room));
4557
4522
  for (const room of unchanged) {
4558
4523
  const next = { ...modelOf(room.locator), status };
4559
4524
  const replica = replicas.get(room.locator);
@@ -4613,14 +4578,10 @@ export function composeRuntime(options) {
4613
4578
  try {
4614
4579
  const local = await openLocal();
4615
4580
  await roomTask(locator, async (live) => {
4616
- const current = () => live() && !signal.aborted;
4617
- if (!current() || authorization === undefined)
4581
+ if (!live() || authorization === undefined)
4618
4582
  return;
4619
- const withheld = await Promise.race([
4620
- advance(room, local, authorization, current),
4621
- abandonedOn(signal),
4622
- ]);
4623
- if (withheld !== null && current())
4583
+ const withheld = await advance(room, local, authorization, live);
4584
+ if (withheld !== null && live())
4624
4585
  await withhold(withheld, room, local, progress);
4625
4586
  });
4626
4587
  }
@@ -4675,7 +4636,6 @@ export function composeRuntime(options) {
4675
4636
  progress.complete = false;
4676
4637
  return;
4677
4638
  }
4678
- reads.applied();
4679
4639
  submitAll();
4680
4640
  for (const locator of stale)
4681
4641
  if (repairable.has(locator))
@@ -4706,7 +4666,7 @@ export function composeRuntime(options) {
4706
4666
  if (!live())
4707
4667
  return;
4708
4668
  let lastCompleted = status.lastCompleted;
4709
- let synced = complete && stale.size === 0 && !pending();
4669
+ let synced = complete && roomWork === 0 && naming === 0 && stale.size === 0 && !pending();
4710
4670
  if (synced && !recorded) {
4711
4671
  const completedAt = new Date().toISOString();
4712
4672
  try {
@@ -4721,75 +4681,78 @@ export function composeRuntime(options) {
4721
4681
  }
4722
4682
  }
4723
4683
  moveTo({
4724
- state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
4684
+ state: pending()
4685
+ ? 'pending'
4686
+ : synced && complete && roomWork === 0 && naming === 0
4687
+ ? 'synced'
4688
+ : 'syncing',
4725
4689
  lastCompleted,
4726
- providerUnreachable: !progress.reachable,
4690
+ providerUnreachable: progress === null ? status.providerUnreachable : !progress.reachable,
4727
4691
  ...attention(),
4728
4692
  });
4729
4693
  }
4730
- function discover() {
4731
- let release = () => { };
4732
- const done = new Promise((resolve) => {
4733
- release = resolve;
4734
- });
4735
- discovering = { generation, done, release };
4736
- return discovering;
4737
- }
4738
- function applyRoom(locator, roomVersion) {
4739
- announced.set(locator, roomVersion);
4740
- return task(roomLane(locator), async (live) => {
4741
- if (discovering?.generation === generation)
4742
- await discovering.done;
4743
- const saving = answering.get(locator);
4744
- if (saving?.generation === generation)
4745
- await saving.answered;
4746
- if (!live() || authorization === undefined)
4747
- return;
4748
- const room = discovered.get(locator);
4749
- const held = replicas.get(locator);
4750
- if (room === undefined || room.replica.admission !== 'admitted')
4751
- return;
4752
- if (held !== undefined &&
4753
- (held.record.replica.selection !== room.replica.selection ||
4754
- options.adapter.reached(locator, held.record.roomVersion, roomVersion))) {
4755
- return;
4756
- }
4757
- const during = underway?.generation === generation ? underway.progress : null;
4758
- const progress = during ?? { complete: true, reachable: true };
4759
- if (during === null)
4760
- moveTo(transferring(status));
4694
+ function applyRoom(change) {
4695
+ const { locator } = change;
4696
+ if (stopping)
4697
+ return Promise.resolve();
4698
+ return inRoom(locator, async (live) => {
4699
+ await answering.get(locator)?.answered;
4700
+ const progress = underway ?? { complete: true, reachable: true };
4761
4701
  try {
4762
4702
  const local = await openLocal();
4763
- await admittedRead(async () => {
4764
- if (!live() || authorization === undefined)
4765
- return;
4766
- const withheld = await advance(room, local, authorization, live);
4767
- if (withheld !== null)
4768
- await withhold(withheld, room, local, progress);
4769
- });
4703
+ for (;;) {
4704
+ if (!live())
4705
+ return null;
4706
+ const room = discovered.get(locator);
4707
+ const held = replicas.get(locator);
4708
+ if (room === undefined ||
4709
+ held === undefined ||
4710
+ room.replica.admission !== 'admitted' ||
4711
+ held.record.replica.selection !== room.replica.selection) {
4712
+ return null;
4713
+ }
4714
+ const at = held.record.roomVersion;
4715
+ if (options.adapter.reached(locator, at, change.roomVersion))
4716
+ return progress;
4717
+ if (!options.adapter.reached(locator, at, change.from)) {
4718
+ name(locator);
4719
+ return progress;
4720
+ }
4721
+ if (underway === null)
4722
+ moveTo(transferring(status));
4723
+ const writing = () => live() && !full;
4724
+ let outlined = false;
4725
+ const refreshed = await ahead((hold) => pullRefresh(authorization ?? {}, held, writing, hold, async () => {
4726
+ const page = await change.read();
4727
+ const paths = Object.keys(page.outlines ?? {});
4728
+ outlined = paths.length > 0;
4729
+ await local.remember(locator, held.record.key, paths);
4730
+ return page;
4731
+ }));
4732
+ if (refreshed.kind !== 'advanced')
4733
+ return progress;
4734
+ const landing = {
4735
+ kind: 'refreshed',
4736
+ from: refreshed.from,
4737
+ remodelled: refreshed.remodelled,
4738
+ };
4739
+ if (await publish(local, room, refreshed.record, landing, writing)) {
4740
+ stale.delete(locator);
4741
+ await retainModel(local, room, modelOf(locator));
4742
+ if (outlined)
4743
+ void heldAgain(locator);
4744
+ return progress;
4745
+ }
4746
+ }
4770
4747
  }
4771
4748
  catch {
4749
+ if (!stale.has(locator))
4750
+ name(locator);
4772
4751
  stale.add(locator);
4752
+ return progress;
4773
4753
  }
4774
- finally {
4775
- if (during === null)
4776
- await rest(progress, live);
4777
- }
4778
- });
4754
+ }).then((progress) => rest(progress), () => undefined);
4779
4755
  }
4780
- function roomsCarried(replacing) {
4781
- const replaced = new Set(replacing.map(roomLane));
4782
- return Promise.all([...lanes]
4783
- .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
4784
- .map(([, held]) => held.chain));
4785
- }
4786
- const receipts = {
4787
- heads: async () => (await openLocal()).receivedHeads(),
4788
- parts: async (key, count) => (await openLocal()).receivedParts(key, count),
4789
- keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
4790
- drop: async (keys) => (await openLocal()).dropReceived(keys),
4791
- ahead,
4792
- };
4793
4756
  function observeStream() {
4794
4757
  const stream = options.adapter.roomStream;
4795
4758
  if (stream === undefined || withdrawStream !== undefined)
@@ -4799,31 +4762,35 @@ export function composeRuntime(options) {
4799
4762
  return;
4800
4763
  if (event.kind === 'opened') {
4801
4764
  socketOpen = true;
4765
+ announced = false;
4766
+ handed = false;
4767
+ naming = 0;
4802
4768
  connectionLive();
4803
4769
  generation += 1;
4804
- for (const admit of installWaiters.splice(0))
4805
- admit();
4806
- reads.opened();
4807
4770
  conversations.reopened();
4808
4771
  recovery.resume();
4809
- void sync('opened');
4772
+ }
4773
+ else if (event.kind === 'discovered') {
4774
+ if (!socketOpen)
4775
+ return;
4776
+ announced = true;
4777
+ if (missedChange) {
4778
+ missedChange = false;
4779
+ countAuthorityChange();
4780
+ }
4781
+ void sync('opened', event.discovery);
4782
+ for (const locator of replicas.keys())
4783
+ void heldAgain(locator);
4810
4784
  }
4811
4785
  else if (event.kind === 'closed')
4812
- closedSocket(event.revoked, event.lost);
4813
- else if (event.kind === 'opening')
4814
- void applyOpening(event.expired);
4786
+ closedSocket(event.revoked, event.lost, event.authorityChanged);
4787
+ else if (event.kind === 'caught-up')
4788
+ void caughtUpRooms(event.expired);
4815
4789
  else if (event.kind === 'authority-changed')
4816
- void rediscover(event.scope);
4790
+ void changeAuthority(event);
4817
4791
  else
4818
- void applyRoom(event.locator, event.roomVersion);
4819
- }, receipts, heldRevisions);
4820
- }
4821
- async function heldRevisions(locator, paths) {
4822
- const held = replicas.get(locator);
4823
- if (held === undefined || paths.length === 0)
4824
- return new Map();
4825
- const entries = await ahead(async () => (await openLocal()).entries(held.record.key, 'confirmed', paths));
4826
- return new Map(entries.flatMap(({ path, revision }) => revision === null ? [] : [[path, revision]]));
4792
+ void applyRoom(event);
4793
+ });
4827
4794
  }
4828
4795
  function writesHeld() {
4829
4796
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -4896,8 +4863,6 @@ export function composeRuntime(options) {
4896
4863
  notifyDiscovered();
4897
4864
  if (probing !== null)
4898
4865
  clearInterval(probing);
4899
- for (const resolve of shownWaiters.splice(0))
4900
- resolve();
4901
4866
  connectionLive();
4902
4867
  lockListeners.clear();
4903
4868
  readinessWatches.clear();
@@ -4915,6 +4880,7 @@ export function composeRuntime(options) {
4915
4880
  statusListeners.clear();
4916
4881
  serviceModeListeners.clear();
4917
4882
  memberListeners.clear();
4883
+ authorityChangeListeners.clear();
4918
4884
  liveQueries.close();
4919
4885
  queueSubscriptions.clear();
4920
4886
  roomDraftListeners.clear();
@@ -4922,6 +4888,7 @@ export function composeRuntime(options) {
4922
4888
  }
4923
4889
  async function stop() {
4924
4890
  stopping = true;
4891
+ inFlight.abort();
4925
4892
  await recovery.retire();
4926
4893
  await Promise.all(working);
4927
4894
  await mutations;
@@ -4940,15 +4907,15 @@ export function composeRuntime(options) {
4940
4907
  readyFromStore();
4941
4908
  });
4942
4909
  if (options.adapter.roomStream === undefined)
4943
- void sync('unstreamed');
4910
+ void sync('unstreamed', null);
4944
4911
  function planned(input, reach) {
4945
- const plan = planOf(input, roomFacts(reach), planReach(reach));
4912
+ const plan = planWithin(input, roomFacts(reach), planReach(reach));
4946
4913
  const named = plan.paths !== null &&
4947
4914
  plan.rooms !== null &&
4948
4915
  plan.paths.length * plan.rooms.length <= MAXIMUM_WINDOW;
4949
4916
  return plan.limit === null && named ? plan : { ...plan, limit: capped(plan.limit) };
4950
4917
  }
4951
- function watchPlan(plan, deliver) {
4918
+ function watchPlan(plan, deliver, fail) {
4952
4919
  let seeded = false;
4953
4920
  let pending = true;
4954
4921
  const shown = () => {
@@ -4974,7 +4941,7 @@ export function composeRuntime(options) {
4974
4941
  }
4975
4942
  seeded = false;
4976
4943
  deliver(message);
4977
- });
4944
+ }, fail);
4978
4945
  return () => {
4979
4946
  watchers.delete(shown);
4980
4947
  if (watchers.size === 0)
@@ -4982,61 +4949,59 @@ export function composeRuntime(options) {
4982
4949
  stop();
4983
4950
  };
4984
4951
  }
4985
- async function searched(input, reach, locked) {
4952
+ async function searched(input, reach) {
4986
4953
  const asked = searchPlanOf(input);
4987
- const filter = {
4988
- ...filterOf(asked.documents ?? {}, roomFacts(reach), planReach(reach)),
4989
- locked,
4990
- };
4991
- const unlocking = (filter.rooms ?? [])
4992
- .filter((room) => locked.includes(room))
4993
- .map((room) => ({ room, awaits: 'unlock' }));
4994
- for (;;) {
4995
- const cover = covered(filter);
4996
- awaitPreparation(cover);
4997
- const pending = [...unlocking, ...('pending' in cover ? cover.pending : [])];
4998
- const complete = pending.length === 0 && !('pending' in cover);
4999
- const awaited = new Set('pending' in cover ? cover.pending.map(({ room }) => room) : []);
5000
- const answering = complete
5001
- ? filter
5002
- : { ...filter, rooms: reachedRooms(filter).filter((room) => !awaited.has(room)) };
5003
- const answered = covered(answering);
5004
- if ('pending' in answered)
5005
- continue;
5006
- const listed = conversations.searchable();
5007
- const named = reach !== null && !reach.conversations
5008
- ? []
5009
- : (asked.conversations ?? listed).filter((one) => listed.includes(one) && conversations.acquired(one));
5010
- const hits = await (await openLocal()).search({
5011
- text: asked.text,
5012
- limit: capped(asked.limit),
5013
- after: asked.after,
5014
- total: asked.total,
5015
- documents: {
5016
- plan: answering,
5017
- rooms: answered.rooms,
5018
- beneath: asked.collection
5019
- ? answered.rooms.map(({ locator, key }) => ({
5020
- key,
5021
- roots: replicas.get(locator)?.record.typedSelection?.roots ?? [],
5022
- }))
5023
- : [],
5024
- },
5025
- conversations: named,
5026
- });
5027
- if (JSON.stringify(covered(answering)) !== JSON.stringify(answered))
5028
- continue;
5029
- const coverage = {
5030
- rooms: answered.coverage.rooms,
5031
- conversations: named,
5032
- overlay: answered.coverage.overlay,
4954
+ await ready;
4955
+ const search = options.adapter.search;
4956
+ if (search === undefined) {
4957
+ return {
4958
+ kind: 'undeclared',
4959
+ code: 'action-undeclared',
4960
+ fields: { name: 'search' },
4961
+ retryable: false,
4962
+ };
4963
+ }
4964
+ if (closed || refusal !== null || connectionOf() !== 'connected')
4965
+ return OFFLINE;
4966
+ const reached = reach === null ? null : new Set(reach.rooms);
4967
+ const named = asked.rooms === null
4968
+ ? reached === null
4969
+ ? null
4970
+ : [...reached]
4971
+ : asked.rooms.filter((room) => reached === null || reached.has(room));
4972
+ const rooms = named === null ? null : named.filter((room) => replicas.has(room)).sort();
4973
+ if (rooms !== null && rooms.length === 0)
4974
+ return { kind: 'found', documents: [] };
4975
+ try {
4976
+ await ensureAuthorization();
4977
+ }
4978
+ catch {
4979
+ return UNCERTAIN;
4980
+ }
4981
+ if (closed || authorization === undefined || refusal !== null)
4982
+ return UNCERTAIN;
4983
+ journal.record('transfer search');
4984
+ try {
4985
+ const result = await search(authorization, { text: asked.text, rooms });
4986
+ if (result.kind === 'revoked-authorization')
4987
+ await refuse('revoked-authorization');
4988
+ journal.record(`outcome search ${result.kind}`);
4989
+ if (result.kind === 'rejected')
4990
+ return result;
4991
+ if (result.kind !== 'found')
4992
+ return failure(result);
4993
+ const kept = rooms === null ? null : new Set(rooms);
4994
+ return {
4995
+ kind: 'found',
4996
+ documents: result.documents.filter((hit) => kept === null || kept.has(hit.room)),
5033
4997
  };
5034
- return complete
5035
- ? { ...hits, coverage }
5036
- : { pending, documents: hits.documents, messages: hits.messages, coverage };
4998
+ }
4999
+ catch {
5000
+ journal.record('outcome search transport-uncertainty');
5001
+ return UNCERTAIN;
5037
5002
  }
5038
5003
  }
5039
- const watch = (topic, input, deliver) => {
5004
+ const watch = (topic, input, deliver, fail) => {
5040
5005
  if (topic === 'room-drafts') {
5041
5006
  const listener = { notify: deliver, ready: false };
5042
5007
  roomDraftListeners.add(listener);
@@ -5085,7 +5050,7 @@ export function composeRuntime(options) {
5085
5050
  };
5086
5051
  }
5087
5052
  if (topic === 'readiness') {
5088
- return watchReadiness({ target: '', code: 'stored', space: null, search: 'unused' }, null, deliver);
5053
+ return watchReadiness({ target: '', code: 'stored' }, null, deliver);
5089
5054
  }
5090
5055
  if (topic === 'rooms') {
5091
5056
  unlistedRooms.add(deliver);
@@ -5130,6 +5095,13 @@ export function composeRuntime(options) {
5130
5095
  memberListeners.delete(deliver);
5131
5096
  };
5132
5097
  }
5098
+ if (topic === 'authority-changes') {
5099
+ authorityChangeListeners.add(deliver);
5100
+ deliver(authorityChanges);
5101
+ return () => {
5102
+ authorityChangeListeners.delete(deliver);
5103
+ };
5104
+ }
5133
5105
  if (topic === 'queue') {
5134
5106
  const queue = { locator: input[0], notify: deliver };
5135
5107
  queueSubscriptions.add(queue);
@@ -5138,7 +5110,7 @@ export function composeRuntime(options) {
5138
5110
  queueSubscriptions.delete(queue);
5139
5111
  };
5140
5112
  }
5141
- return watchPlan(planned(input[0], null), deliver);
5113
+ return watchPlan(planned(input[0], null), deliver, fail);
5142
5114
  };
5143
5115
  const md = {
5144
5116
  loaded() {
@@ -5158,7 +5130,7 @@ export function composeRuntime(options) {
5158
5130
  });
5159
5131
  },
5160
5132
  query: ((input) => rowsQuery(planned(input, null))),
5161
- search: (input) => searched(input, null, []),
5133
+ search: (input) => searched(input, null),
5162
5134
  types(locator) {
5163
5135
  return {
5164
5136
  collection: modelTypes(modelOf(locator)),
@@ -5283,7 +5255,7 @@ export function composeRuntime(options) {
5283
5255
  authorization = undefined;
5284
5256
  discoveryPaused = false;
5285
5257
  if (options.adapter.roomStream === undefined)
5286
- await sync('unstreamed');
5258
+ await sync('unstreamed', null);
5287
5259
  else
5288
5260
  await task(OPENING_LANE, ensureAuthorization);
5289
5261
  },
@@ -5306,7 +5278,6 @@ export function composeRuntime(options) {
5306
5278
  md,
5307
5279
  seed,
5308
5280
  stop,
5309
- lose: loseRooms,
5310
5281
  watch,
5311
5282
  persisted(answer) {
5312
5283
  if (answer === persistence)
@@ -5315,7 +5286,6 @@ export function composeRuntime(options) {
5315
5286
  journal.record(`persistence ${answer}`);
5316
5287
  queueReadiness();
5317
5288
  },
5318
- shown: screen,
5319
5289
  within: (reach, port) => {
5320
5290
  if (full)
5321
5291
  void probed();