@agentshouse/mdruntime 0.1.0-c8c4b020a3ec7 → 0.1.0-c99e5271060cd

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 (97) hide show
  1. package/AGENTS.md +94 -33
  2. package/README.md +10 -10
  3. package/dist/adapter.d.ts +92 -46
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +47 -0
  9. package/dist/bootstrap.d.ts +20 -6
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +89 -35
  12. package/dist/change-set.d.ts.map +1 -1
  13. package/dist/change-set.js +13 -7
  14. package/dist/change.d.ts +4 -0
  15. package/dist/change.d.ts.map +1 -1
  16. package/dist/client.d.ts +8 -5
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +57 -24
  19. package/dist/collection.d.ts +6 -11
  20. package/dist/collection.d.ts.map +1 -1
  21. package/dist/collection.js +56 -19
  22. package/dist/connection.d.ts +20 -16
  23. package/dist/connection.d.ts.map +1 -1
  24. package/dist/connection.js +699 -578
  25. package/dist/conversation.d.ts +23 -11
  26. package/dist/conversation.d.ts.map +1 -1
  27. package/dist/conversation.js +252 -106
  28. package/dist/failure.d.ts +13 -0
  29. package/dist/failure.d.ts.map +1 -0
  30. package/dist/failure.js +19 -0
  31. package/dist/host.d.ts +8 -1
  32. package/dist/host.d.ts.map +1 -1
  33. package/dist/host.js +38 -38
  34. package/dist/identity.d.ts +1 -0
  35. package/dist/identity.d.ts.map +1 -1
  36. package/dist/identity.js +7 -1
  37. package/dist/index.d.ts +5 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/memory.d.ts +12 -5
  40. package/dist/memory.d.ts.map +1 -1
  41. package/dist/memory.js +108 -36
  42. package/dist/model.d.ts +13 -12
  43. package/dist/model.d.ts.map +1 -1
  44. package/dist/model.js +6 -3
  45. package/dist/observation.d.ts +2 -0
  46. package/dist/observation.d.ts.map +1 -1
  47. package/dist/observation.js +24 -14
  48. package/dist/open.d.ts +2 -0
  49. package/dist/open.d.ts.map +1 -0
  50. package/dist/open.js +1 -0
  51. package/dist/outcome.d.ts +22 -3
  52. package/dist/outcome.d.ts.map +1 -1
  53. package/dist/outcome.js +35 -1
  54. package/dist/overlay.d.ts +1 -1
  55. package/dist/overlay.d.ts.map +1 -1
  56. package/dist/overlay.js +19 -15
  57. package/dist/paths.d.ts +0 -1
  58. package/dist/paths.d.ts.map +1 -1
  59. package/dist/paths.js +0 -16
  60. package/dist/query.d.ts +10 -3
  61. package/dist/query.d.ts.map +1 -1
  62. package/dist/query.js +78 -36
  63. package/dist/reach.d.ts +40 -0
  64. package/dist/reach.d.ts.map +1 -0
  65. package/dist/reach.js +272 -0
  66. package/dist/react.d.ts +40 -0
  67. package/dist/react.d.ts.map +1 -0
  68. package/dist/react.js +64 -0
  69. package/dist/read-side.d.ts +4 -0
  70. package/dist/read-side.d.ts.map +1 -1
  71. package/dist/read-side.js +116 -54
  72. package/dist/refresh.d.ts +12 -1
  73. package/dist/refresh.d.ts.map +1 -1
  74. package/dist/refresh.js +35 -22
  75. package/dist/replica.d.ts +6 -6
  76. package/dist/replica.d.ts.map +1 -1
  77. package/dist/replica.js +53 -39
  78. package/dist/room-draft.d.ts +11 -8
  79. package/dist/room-draft.d.ts.map +1 -1
  80. package/dist/room-draft.js +19 -26
  81. package/dist/rows.d.ts +4 -1
  82. package/dist/rows.d.ts.map +1 -1
  83. package/dist/rows.js +82 -39
  84. package/dist/save.d.ts +4 -3
  85. package/dist/save.d.ts.map +1 -1
  86. package/dist/save.js +127 -68
  87. package/dist/search.d.ts +3 -2
  88. package/dist/search.d.ts.map +1 -1
  89. package/dist/status.d.ts +2 -1
  90. package/dist/status.d.ts.map +1 -1
  91. package/dist/store.d.ts +25 -2
  92. package/dist/store.d.ts.map +1 -1
  93. package/dist/store.js +206 -90
  94. package/dist/wire.d.ts +55 -19
  95. package/dist/wire.d.ts.map +1 -1
  96. package/dist/wire.js +62 -61
  97. package/package.json +14 -2
@@ -1,56 +1,43 @@
1
- import { verify } from '@agentshouse/mdmodel';
1
+ import { attachedModel } from './attached-model.js';
2
2
  import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
- import { opened } from './observation.js';
3
+ import { observing } from './observation.js';
4
4
  import { filterOf, identityOf, planOf, } from './query.js';
5
5
  import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
6
7
  import { createSearchIndex, searchPlanOf, } from './search.js';
7
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES, } from './adapter.js';
8
8
  import { bootstrapRoom } from './bootstrap.js';
9
9
  import { changeSet, mergeChangeSets } from './change-set.js';
10
10
  import { createConversationStore, } from './conversation.js';
11
- import { MD_MODEL, parseCollection, validRoot } from './collection.js';
11
+ import { validRoot } from './collection.js';
12
+ import { runtimeError } from './failure.js';
12
13
  import { createLog } from './log.js';
14
+ import { calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
13
15
  import { refreshRoom } from './refresh.js';
14
16
  import { buildBatch } from './save.js';
15
- import { applyReturned, compositionOf, landed, readConfirmed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
16
- import { changedParts, changesOf, held, readParts, returnedInto, underneath, } from './room-draft.js';
17
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
18
+ import { changedParts, changesOf, held, readParts, returnedBy, returnedInto, staleWrite, underneath, } from './room-draft.js';
17
19
  import { settled, same, saving, STARTING, transferring, } from './status.js';
18
20
  import { detectStorageLoss, nextGeneration } from './durability.js';
19
21
  import { roomDraftRevision } from './identity.js';
20
22
  import { beneath, normalize } from './paths.js';
21
23
  import { openRuntimeStore, } from './store.js';
22
24
  import { unconfirmed } from './transfer.js';
23
- // Discovery repeats each minute so a Room the stream stopped serving stays unrefreshed no longer.
24
- const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
25
- const EMPTY_SCHEMA = { verdict: 'accept', model: { types: {} } };
26
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
25
+ import { governs, holds, narrowed, planReach, } from './reach.js';
27
26
  function within(root, path) {
28
27
  return root === '' || beneath(root, path);
29
28
  }
30
- function declaredIdentity(model) {
31
- if (model === undefined)
32
- return false;
33
- if (model === null)
34
- return true;
35
- return (typeof model.identity === 'string' &&
36
- model.identity.length > 0 &&
37
- typeof model.version === 'string' &&
38
- model.version.length > 0);
39
- }
40
29
  function declaredSelection(room) {
41
30
  const selection = room.typedSelection;
42
31
  if (selection === undefined)
43
32
  return true;
44
33
  if (!Array.isArray(selection.roots) || selection.roots.length === 0)
45
34
  return false;
46
- if (!declaredIdentity(selection.attachedModel))
47
- return false;
48
35
  return selection.roots.every((root, index) => validRoot(root) &&
49
36
  root !== '' &&
50
37
  selection.roots.every((other, at) => at === index || !within(other, root)));
51
38
  }
52
39
  export function composeRuntime(options) {
53
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
40
+ const localSaves = options.adapter.localSaves === true;
54
41
  let authorization = options.authorization;
55
42
  let opening;
56
43
  let store;
@@ -58,20 +45,14 @@ export function composeRuntime(options) {
58
45
  const pendings = new Map();
59
46
  const roomDrafts = new Map();
60
47
  const presentations = new Map();
61
- const wanted = new Map();
62
48
  let limits = [];
63
49
  let capabilities = [];
64
- function rewriting() {
65
- return capabilities.some((capability) => capability.name === 'move-reference-rewrites')
66
- ? 'provider'
67
- : 'runtime';
68
- }
69
50
  const rooms = new Map();
51
+ const counted = new Map();
70
52
  const unreadable = new Set();
71
53
  const models = new Map();
72
54
  const liveModels = new Map();
73
55
  const invalidModels = new Set();
74
- const modelEpoch = new Map();
75
56
  const discovered = new Map();
76
57
  let roomsListed = false;
77
58
  const scopedRefusals = new Map();
@@ -100,16 +81,20 @@ export function composeRuntime(options) {
100
81
  let status = STARTING;
101
82
  let shown = STARTING;
102
83
  let discoveryPaused = false;
103
- let timer;
84
+ let resetTimer;
104
85
  let closed = false;
105
86
  let stopping = false;
106
- let withdrawNotifications;
107
- let handed;
108
- const notices = new Map();
87
+ let withdrawStream;
88
+ const announced = new Map();
89
+ let underway = null;
90
+ let handed = false;
91
+ const submitting = new Map();
92
+ const answering = new Map();
93
+ let complete = false;
94
+ let recorded = false;
95
+ const stale = new Set();
109
96
  let chain = Promise.resolve();
110
97
  let mutations = Promise.resolve();
111
- let inflight = false;
112
- let queued = false;
113
98
  const roomListeners = new Set();
114
99
  const refusalListeners = new Set();
115
100
  const storageLossListeners = new Set();
@@ -120,13 +105,19 @@ export function composeRuntime(options) {
120
105
  const readSide = createReadSide();
121
106
  let withdrawConversations;
122
107
  let currencyQueued = false;
108
+ let socketOpen = false;
123
109
  let serviceMode = 'open';
124
110
  let haltedRefusal = false;
125
111
  let openSignals = 0;
126
112
  const serviceModeListeners = new Set();
113
+ let member = null;
114
+ const memberListeners = new Set();
115
+ const materialized = new Set();
116
+ let everyRoom = false;
117
+ const transfers = new Map();
127
118
  const conversations = createConversationStore({
128
- cards: declaredCards,
129
- documents: (locator) => readSide.query(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
119
+ agents: declaredAgents,
120
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
130
121
  call: (name, input) => invokeAction(name, input),
131
122
  durably,
132
123
  stream: () => options.adapter.conversations,
@@ -202,7 +193,7 @@ export function composeRuntime(options) {
202
193
  }
203
194
  }
204
195
  function pending() {
205
- return [...replicas.values()].some((replica) => replica.queue.length > 0);
196
+ return !localSaves && [...replicas.values()].some((replica) => replica.queue.length > 0);
206
197
  }
207
198
  function attention() {
208
199
  const attentionRequired = [];
@@ -231,14 +222,33 @@ export function composeRuntime(options) {
231
222
  }
232
223
  return { attentionRequired, reset };
233
224
  }
225
+ function documentsIn(locator) {
226
+ const replica = replicas.get(locator);
227
+ if (replica === undefined)
228
+ return 0;
229
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
230
+ let count = 0;
231
+ for (const path of Object.keys(replica.visible.documents)) {
232
+ if (markdownPath(path) && !roots.some((root) => beneath(root, path)))
233
+ count += 1;
234
+ }
235
+ return count;
236
+ }
234
237
  function listed() {
235
- return structuredClone([...rooms.values()]);
238
+ return structuredClone([...rooms.values()].map((room) => {
239
+ const documents = documentsIn(room.locator);
240
+ counted.set(room.locator, documents);
241
+ return { ...room, documents };
242
+ }));
236
243
  }
237
- function roomFacts() {
238
- const held = [...rooms.values()].map((room) => {
244
+ function roomFacts(reach = null) {
245
+ const held = [...rooms.values()]
246
+ .filter((room) => holds(reach, room.locator))
247
+ .map((room) => {
248
+ const governed = governs(reach, room);
239
249
  const attached = [
240
- modelOf(room.locator),
241
- ...(room.typedSelection === null ? [] : [modelOf(room.locator, 'typed-selection')]),
250
+ ...(governed.collection ? [modelOf(room.locator)] : []),
251
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
242
252
  ];
243
253
  return {
244
254
  locator: room.locator,
@@ -261,6 +271,10 @@ export function composeRuntime(options) {
261
271
  for (const listener of storageLossListeners)
262
272
  listener(storageLoss);
263
273
  }
274
+ function emitMember() {
275
+ for (const listener of memberListeners)
276
+ listener(member);
277
+ }
264
278
  function moveTo(next) {
265
279
  if (closed)
266
280
  return;
@@ -280,6 +294,8 @@ export function composeRuntime(options) {
280
294
  listener(shown);
281
295
  }
282
296
  function entryOf(batch) {
297
+ if (localSaves)
298
+ return Object.freeze({ batch, state: 'local' });
283
299
  const state = pendings.get(batch.id);
284
300
  if (state?.accepted !== undefined) {
285
301
  return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
@@ -300,12 +316,47 @@ export function composeRuntime(options) {
300
316
  }
301
317
  }
302
318
  function applyChangeSet(set) {
319
+ const held = everyRoom
320
+ ? set
321
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
322
+ readSide.apply(held);
323
+ searchIndex.changed(held);
324
+ }
325
+ function materialize(locators) {
326
+ if (everyRoom)
327
+ return;
328
+ if (locators === null)
329
+ everyRoom = true;
330
+ const sets = [];
331
+ for (const locator of locators ?? [...replicas.keys()]) {
332
+ if (materialized.has(locator))
333
+ continue;
334
+ materialized.add(locator);
335
+ const replica = replicas.get(locator);
336
+ if (replica !== undefined)
337
+ sets.push(changeSet(locator, null, replica));
338
+ }
339
+ if (sets.length === 0)
340
+ return;
341
+ const set = mergeChangeSets(sets);
303
342
  readSide.apply(set);
304
343
  searchIndex.changed(set);
305
344
  }
345
+ function reachedRooms(plan) {
346
+ if (plan.rooms !== null)
347
+ return plan.rooms;
348
+ if (plan.reach === null)
349
+ return null;
350
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
351
+ }
352
+ function rowsQuery(plan) {
353
+ materialize(reachedRooms(plan));
354
+ return readSide.query(plan);
355
+ }
306
356
  function publishChangeSet(set) {
307
357
  applyChangeSet(set);
308
- if (set.rooms.length > 0)
358
+ if (set.rooms.length > 0 ||
359
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
309
360
  emitRooms();
310
361
  for (const locator of set.queues)
311
362
  emitQueue(locator);
@@ -316,23 +367,32 @@ export function composeRuntime(options) {
316
367
  for (const app of set.presentations)
317
368
  emitPresentation(app);
318
369
  conversations.changed(set);
319
- const cards = declaredCards();
320
- if (cards !== undefined && set.documents.some((change) => change.locator === cards.locator)) {
321
- scheduleCurrency();
322
- }
323
370
  }
324
371
  function stopTimer() {
325
- if (timer === undefined)
372
+ if (resetTimer === undefined)
326
373
  return;
327
- clearInterval(timer);
328
- timer = undefined;
374
+ clearTimeout(resetTimer);
375
+ resetTimer = undefined;
329
376
  }
330
- function startTimer() {
331
- if (timer !== undefined)
377
+ function armTimer() {
378
+ stopTimer();
379
+ if (closed || stopping)
332
380
  return;
333
- timer = setInterval(() => {
334
- void enqueue();
335
- }, period);
381
+ let earliest = null;
382
+ for (const { submission } of pendings.values()) {
383
+ if (submission.state !== 'reset')
384
+ continue;
385
+ const at = Date.parse(submission.reset);
386
+ if (earliest === null || at < earliest)
387
+ earliest = at;
388
+ }
389
+ if (earliest === null)
390
+ return;
391
+ // A Save House held back goes again only once the reset its answer named has passed.
392
+ resetTimer = setTimeout(() => {
393
+ resetTimer = undefined;
394
+ submitAll();
395
+ }, Math.max(0, earliest - Date.now()));
336
396
  }
337
397
  async function refuse(reason, provider = null) {
338
398
  stopTimer();
@@ -358,7 +418,6 @@ export function composeRuntime(options) {
358
418
  }
359
419
  for (const key of keys) {
360
420
  invalidModels.add(key);
361
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
362
421
  models.set(key, revoked);
363
422
  liveModels.delete(key);
364
423
  }
@@ -371,10 +430,7 @@ export function composeRuntime(options) {
371
430
  model: revoked,
372
431
  selectionModel: revoked,
373
432
  };
374
- const read = readConfirmed(record, before.confirmed);
375
- replicas.set(locator, read.kind === 'staged'
376
- ? replicaOf(read.confirmed, before.queue, rewriting(), before)
377
- : { ...before, confirmed: { ...before.confirmed, record } });
433
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
378
434
  }
379
435
  rooms.set(locator, {
380
436
  ...room,
@@ -391,7 +447,8 @@ export function composeRuntime(options) {
391
447
  }
392
448
  finally {
393
449
  refusal = {
394
- reason,
450
+ code: reason,
451
+ retryable: false,
395
452
  reauthorizationOffered: reason === 'revoked-authorization',
396
453
  refusal: provider,
397
454
  };
@@ -404,15 +461,14 @@ export function composeRuntime(options) {
404
461
  rooms.set(room.locator, {
405
462
  name: room.name,
406
463
  locator: room.locator,
464
+ handle: room.handle,
407
465
  operations: room.operations,
408
466
  replica: room.replica,
409
- attachedModel: room.attachedModel,
410
467
  model: modelTypes(modelOf(room.locator)),
411
468
  typedSelection: selection === null
412
469
  ? null
413
470
  : {
414
471
  roots: selection.roots,
415
- attachedModel: selection.attachedModel,
416
472
  model: modelTypes(modelOf(room.locator, 'typed-selection')),
417
473
  },
418
474
  providerOwnedPaths: room.providerOwnedPaths,
@@ -429,9 +485,6 @@ export function composeRuntime(options) {
429
485
  discovered.clear();
430
486
  for (const room of listed)
431
487
  discovered.set(room.locator, room);
432
- for (const locator of wanted.keys())
433
- if (!listedLocators.has(locator))
434
- wanted.delete(locator);
435
488
  await dropUnlisted(listedLocators, local);
436
489
  for (const room of listed) {
437
490
  const held = replicas.get(room.locator)?.confirmed.record;
@@ -445,7 +498,7 @@ export function composeRuntime(options) {
445
498
  emitRooms();
446
499
  }
447
500
  function unlistedHeld(listed) {
448
- return [...new Set([...rooms.keys(), ...roomDrafts.keys()])].filter((locator) => !listed.has(locator));
501
+ return [...new Set([...rooms.keys(), ...roomDrafts.keys(), ...transfers.keys()])].filter((locator) => !listed.has(locator));
449
502
  }
450
503
  async function dropUnlisted(listed, local) {
451
504
  if (unlistedHeld(listed).length === 0)
@@ -464,7 +517,9 @@ export function composeRuntime(options) {
464
517
  replicas.delete(locator);
465
518
  rooms.delete(locator);
466
519
  roomDrafts.delete(locator);
520
+ transfers.delete(locator);
467
521
  unreadable.delete(locator);
522
+ stale.delete(locator);
468
523
  scopedRefusals.delete(locator);
469
524
  sourceRefusals.delete(locator);
470
525
  declarationRefusals.delete(locator);
@@ -473,7 +528,6 @@ export function composeRuntime(options) {
473
528
  models.delete(key);
474
529
  liveModels.delete(key);
475
530
  invalidModels.delete(key);
476
- modelEpoch.delete(key);
477
531
  }
478
532
  journal.record(`room unlisted ${locator}`);
479
533
  }
@@ -486,7 +540,6 @@ export function composeRuntime(options) {
486
540
  if (prior !== undefined &&
487
541
  prior.status !== 'unavailable' &&
488
542
  (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
489
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
490
543
  invalidModels.add(key);
491
544
  }
492
545
  const model = persisted.status === 'unavailable' ||
@@ -501,7 +554,7 @@ export function composeRuntime(options) {
501
554
  return model;
502
555
  }
503
556
  async function hydrate(opened) {
504
- const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named] = await Promise.all([
557
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
505
558
  opened.all(),
506
559
  opened.queues(),
507
560
  opened.roomDrafts(),
@@ -511,9 +564,15 @@ export function composeRuntime(options) {
511
564
  opened.lastCompleted(),
512
565
  opened.providerLimits(),
513
566
  opened.capabilities(),
567
+ opened.member(),
568
+ opened.transfers(),
514
569
  ]);
570
+ transfers.clear();
571
+ for (const record of staged)
572
+ transfers.set(record.locator, record);
515
573
  limits = declared;
516
574
  capabilities = named;
575
+ member = known;
517
576
  roomDrafts.clear();
518
577
  for (const record of kept)
519
578
  rememberRoomDraft(record);
@@ -533,15 +592,8 @@ export function composeRuntime(options) {
533
592
  const model = restored(modelKey(record.locator, 'collection'), record.model);
534
593
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
535
594
  const prior = previousReplicas.get(record.locator);
536
- const retyped = { ...record, model, selectionModel };
537
- const read = readConfirmed(retyped, prior?.confirmed);
538
- const confirmed = read.kind === 'staged'
539
- ? read.confirmed
540
- : {
541
- record: retyped,
542
- collection: parseCollection(retyped.documents, compositionOf(retyped)),
543
- };
544
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], rewriting(), prior));
595
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
596
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
545
597
  const existing = rooms.get(record.locator);
546
598
  if (existing === undefined)
547
599
  list(record, false);
@@ -552,21 +604,15 @@ export function composeRuntime(options) {
552
604
  attachments: record.attachments,
553
605
  operations: record.operations,
554
606
  replica: record.replica,
555
- attachedModel: record.attachedModel,
556
607
  model: modelTypes(model),
557
608
  typedSelection: record.typedSelection === null
558
609
  ? null
559
610
  : {
560
611
  roots: record.typedSelection.roots,
561
- attachedModel: record.typedSelection.attachedModel,
562
612
  model: modelTypes(selectionModel),
563
613
  },
564
614
  });
565
615
  }
566
- if (read.kind === 'refused') {
567
- refusedSource(record, read);
568
- declarationRefusals.add(record.locator);
569
- }
570
616
  }
571
617
  return {
572
618
  lastCompleted,
@@ -579,6 +625,8 @@ export function composeRuntime(options) {
579
625
  applyChangeSet(read);
580
626
  if (replicas.size > 0)
581
627
  emitRooms();
628
+ if (member !== null)
629
+ emitMember();
582
630
  for (const locator of locators)
583
631
  emitQueue(locator);
584
632
  publishRoomDrafts(false);
@@ -626,10 +674,14 @@ export function composeRuntime(options) {
626
674
  return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
627
675
  }
628
676
  async function settle(local, locator, before, confirmed, queue, settling = {}) {
629
- const withdrawn = withdrawals(confirmed, queue, withdrawable, rewriting());
677
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
630
678
  const kept = queue.filter((batch) => !withdrawn.includes(batch));
679
+ const unqueued = withdrawn.map((batch) => ({
680
+ batch,
681
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
682
+ }));
631
683
  const returning = [
632
- ...withdrawn.map((batch) => ({ batch, returned: { kind: 'withdrawn' } })).reverse(),
684
+ ...[...unqueued].reverse(),
633
685
  ...(settling.leaving === undefined ? [] : [settling.leaving]),
634
686
  ];
635
687
  const written = new Map();
@@ -650,6 +702,7 @@ export function composeRuntime(options) {
650
702
  await local.write({
651
703
  next: confirmed.record,
652
704
  previous: before?.confirmed.record ?? null,
705
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
653
706
  queue: kept,
654
707
  previousQueue: before?.queue ?? [],
655
708
  roomDrafts: [...written.values()],
@@ -660,44 +713,80 @@ export function composeRuntime(options) {
660
713
  }
661
714
  for (const record of written.values())
662
715
  rememberRoomDraft(record);
663
- for (const batch of withdrawn)
664
- journal.record(`outcome ${locator} ${batch.id} withdrawn`);
665
- replicas.set(locator, replicaOf(confirmed, kept, rewriting(), before));
716
+ for (const { batch, returned } of unqueued) {
717
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
718
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
719
+ }
720
+ replicas.set(locator, replicaOf(confirmed, kept, before));
666
721
  return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
667
722
  room: settling.room,
668
723
  queue: kept.length !== (before?.queue.length ?? 0),
669
724
  drafts: [...written.keys()],
670
725
  });
671
726
  }
672
- function publish(local, room, confirmed) {
727
+ function adoptModels(locator, record) {
728
+ for (const [attached, model] of [
729
+ ['collection', record.model],
730
+ ['typed-selection', record.selectionModel],
731
+ ]) {
732
+ const key = modelKey(locator, attached);
733
+ models.set(key, model);
734
+ invalidModels.delete(key);
735
+ if (model.status === 'current')
736
+ liveModels.set(key, JSON.stringify(model.snapshot));
737
+ else if (model.status === 'unavailable')
738
+ liveModels.delete(key);
739
+ }
740
+ }
741
+ function remodel(locator, answers, held) {
742
+ const taken = (answer, previous, attached) => {
743
+ if (answer === undefined)
744
+ return previous;
745
+ const { state, refused } = attachedModel(answer, previous);
746
+ if (refused !== null) {
747
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
748
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
749
+ }
750
+ models.set(modelKey(locator, attached), state);
751
+ return state;
752
+ };
753
+ return {
754
+ collection: taken(answers?.collection, held.collection, 'collection'),
755
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
756
+ };
757
+ }
758
+ function publish(local, room, confirmed, landing = {}) {
759
+ const { base, stored } = landing;
673
760
  return exclusive(async () => {
674
761
  if (!continuing())
675
- return;
762
+ return true;
676
763
  const prior = replicas.get(room.locator);
677
- const retained = readConfirmed({
678
- ...confirmed.record,
679
- model: modelOf(room.locator),
680
- selectionModel: modelOf(room.locator, 'typed-selection'),
681
- }, prior?.confirmed);
682
- if (retained.kind !== 'staged')
683
- throw new Error(`Cannot retype staged Room ${room.locator}`);
764
+ if (base !== undefined &&
765
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
766
+ prior.confirmed.record.revisions !== base.record.revisions)) {
767
+ return false;
768
+ }
769
+ adoptModels(room.locator, confirmed.record);
684
770
  const queue = prior?.queue ?? [];
685
- const roomVersion = retained.confirmed.record.roomVersion;
771
+ const roomVersion = confirmed.record.roomVersion;
686
772
  const accepted = queue.filter((batch) => {
687
773
  const accepted = pendings.get(batch.id)?.accepted;
688
774
  return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
689
775
  });
690
- const set = await settle(local, room.locator, prior, retained.confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true });
691
- const target = wanted.get(room.locator);
692
- if (target !== undefined && options.adapter.reached(room.locator, roomVersion, target)) {
693
- wanted.delete(room.locator);
694
- }
695
- unreadable.delete(room.locator);
776
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
777
+ if (stored !== undefined)
778
+ transfers.delete(room.locator);
779
+ for (const batch of accepted)
780
+ pendings.delete(batch.id);
781
+ stale.delete(room.locator);
782
+ if (unreadable.delete(room.locator))
783
+ scopedRefusals.delete(room.locator);
696
784
  sourceRefusals.delete(room.locator);
697
785
  declarationRefusals.delete(room.locator);
698
786
  list(room, true);
699
787
  publishChangeSet(set);
700
788
  moveTo({ ...status, ...attention() });
789
+ return true;
701
790
  });
702
791
  }
703
792
  async function saveRoomDrafts(locator, named, writtenBy) {
@@ -705,28 +794,29 @@ export function composeRuntime(options) {
705
794
  const replica = replicas.get(locator);
706
795
  const room = rooms.get(locator);
707
796
  if (replica === undefined || room === undefined) {
708
- throw new Error(`No Local Replica for ${locator}`);
797
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
709
798
  }
710
799
  const saved = [];
711
800
  for (const { path, revision } of named) {
712
801
  const record = roomDrafts.get(locator)?.get(path);
713
802
  if (record === undefined || record.revision !== revision) {
714
- return { outcome: 'refused', rule: 'stale', file: path };
803
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
715
804
  }
716
805
  if (!saved.some((draft) => draft.path === path))
717
806
  saved.push({ path, parts: record.parts });
718
807
  }
719
808
  const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
720
809
  if (changed.length > 0)
721
- return { outcome: 'refused', rule: 'changed-underneath', parts: changed };
810
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
722
811
  const parts = saved.map(({ path, parts }) => changesOf(path, parts));
723
812
  const built = buildBatch({
813
+ local: localSaves,
724
814
  baseRoomVersion: replica.confirmed.record.roomVersion,
725
815
  composition: compositionFor(replica.confirmed.record),
726
816
  operations: room.operations,
727
817
  providerOwnedPaths: room.providerOwnedPaths,
728
818
  providerLimits: limits,
729
- rewriting: rewriting(),
819
+ handle: room.handle,
730
820
  visible: replica.visible,
731
821
  changes: parts.flatMap(({ changes }) => changes),
732
822
  bases: parts.flatMap(({ bases }) => bases),
@@ -747,18 +837,20 @@ export function composeRuntime(options) {
747
837
  });
748
838
  }
749
839
  catch {
750
- return { outcome: 'refused', rule: 'durable-write' };
840
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
751
841
  }
752
842
  pendings.set(built.batch.id, { drafts: saved, submission: { state: 'waiting' }, lost: false });
843
+ if (!localSaves)
844
+ recorded = false;
753
845
  for (const path of consumed)
754
846
  forgetRoomDraft(locator, path);
755
- replicas.set(locator, replicaOf(replica.confirmed, batches, rewriting(), replica));
847
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
756
848
  publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
757
849
  queue: true,
758
850
  drafts: consumed,
759
851
  }));
760
- moveTo({ ...saving(status), ...attention() });
761
- void enqueue();
852
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
853
+ submitLater(locator);
762
854
  return { outcome: 'saved', identifier: built.batch.id };
763
855
  }
764
856
  async function mark(local, locator, identifier, submission, lost) {
@@ -785,16 +877,15 @@ export function composeRuntime(options) {
785
877
  }
786
878
  async function readHistory(locator, path, query = {}) {
787
879
  if (closed || refusal !== null)
788
- return { kind: 'transport-uncertainty' };
880
+ return UNCERTAIN;
789
881
  try {
790
882
  await ensureAuthorization();
791
883
  }
792
884
  catch {
793
- return { kind: 'transport-uncertainty' };
794
- }
795
- if (closed || authorization === undefined || refusal !== null) {
796
- return { kind: 'transport-uncertainty' };
885
+ return UNCERTAIN;
797
886
  }
887
+ if (closed || authorization === undefined || refusal !== null)
888
+ return UNCERTAIN;
798
889
  const transfer = ['transfer', 'history', locator, path];
799
890
  if (query.revision !== undefined)
800
891
  transfer.push(query.revision);
@@ -816,17 +907,17 @@ export function composeRuntime(options) {
816
907
  }
817
908
  if (result.kind === 'revoked-authorization')
818
909
  await refuse('revoked-authorization');
819
- return result;
910
+ return historyAnswer(result);
820
911
  }
821
912
  catch {
822
913
  journal.record('outcome history transport-uncertainty');
823
- return { kind: 'transport-uncertainty' };
914
+ return UNCERTAIN;
824
915
  }
825
916
  }
826
- function declaredCards() {
917
+ function declaredAgents() {
827
918
  for (const capability of capabilities) {
828
- if (capability.conversationCards !== undefined)
829
- return capability.conversationCards;
919
+ if (capability.conversationAgents !== undefined)
920
+ return capability.conversationAgents;
830
921
  }
831
922
  return undefined;
832
923
  }
@@ -842,23 +933,20 @@ export function composeRuntime(options) {
842
933
  async function invokeAction(name, input) {
843
934
  await ready;
844
935
  if (closed || refusal !== null)
845
- return { kind: 'transport-uncertainty' };
936
+ return UNCERTAIN;
846
937
  if (declaredAction(name) === undefined)
847
- return { kind: 'undeclared' };
938
+ return undeclared(name);
848
939
  try {
849
940
  await ensureAuthorization();
850
941
  }
851
942
  catch {
852
- return { kind: 'transport-uncertainty' };
853
- }
854
- if (closed || authorization === undefined || refusal !== null) {
855
- return { kind: 'transport-uncertainty' };
943
+ return UNCERTAIN;
856
944
  }
945
+ if (closed || authorization === undefined || refusal !== null)
946
+ return UNCERTAIN;
857
947
  journal.record(`transfer action ${name}`);
858
948
  try {
859
949
  const result = await options.adapter.action(authorization, { name, input });
860
- if (result.kind === 'action' && result.at !== undefined)
861
- want(result.at);
862
950
  if (result.kind === 'scoped-refusal') {
863
951
  const room = replicas.get(result.locator)?.confirmed.record;
864
952
  if (room !== undefined)
@@ -869,11 +957,11 @@ export function composeRuntime(options) {
869
957
  journal.record(`outcome action ${result.kind}`);
870
958
  if (result.kind === 'rejected')
871
959
  journal.record(`refusal rejected ${result.code}`);
872
- return result;
960
+ return calledResult(result);
873
961
  }
874
962
  catch {
875
963
  journal.record('outcome action transport-uncertainty');
876
- return { kind: 'transport-uncertainty' };
964
+ return UNCERTAIN;
877
965
  }
878
966
  }
879
967
  function continuing() {
@@ -890,32 +978,11 @@ export function composeRuntime(options) {
890
978
  options.adapter.reached(locator, written, roomVersion) &&
891
979
  !options.adapter.reached(locator, roomVersion, written));
892
980
  }
893
- function want({ locator, roomVersion }) {
894
- if (reachedBy(locator, roomVersion))
895
- return;
896
- const held = wanted.get(locator);
897
- if (held === undefined || options.adapter.reached(locator, roomVersion, held)) {
898
- wanted.set(locator, roomVersion);
899
- }
900
- }
901
- function reachOf(locator) {
902
- let target = wanted.get(locator);
903
- const versions = (replicas.get(locator)?.queue ?? []).flatMap((batch) => {
904
- const accepted = pendings.get(batch.id)?.accepted;
905
- return accepted === undefined ? [] : [accepted];
906
- });
907
- for (const version of versions) {
908
- if (target === undefined || options.adapter.reached(locator, version, target)) {
909
- target = version;
910
- }
911
- }
912
- return target === undefined || reachedBy(locator, target) ? undefined : target;
913
- }
914
- async function pullRefresh(authorized, confirmed, minPosition) {
981
+ async function pullRefresh(authorized, confirmed) {
915
982
  if (!continuing())
916
983
  return unconfirmed;
917
984
  try {
918
- const result = await refreshRoom(options.adapter, authorized, confirmed, continuing, minPosition);
985
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
919
986
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
920
987
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
921
988
  return result;
@@ -925,14 +992,33 @@ export function composeRuntime(options) {
925
992
  throw error;
926
993
  }
927
994
  }
928
- async function pullBootstrap(authorized, room) {
995
+ function staging(local) {
996
+ return {
997
+ held: (locator) => transfers.get(locator) ?? null,
998
+ staged: (record) => local.staged(record),
999
+ stage: (record, page) => exclusive(async () => {
1000
+ if (!continuing())
1001
+ return false;
1002
+ await local.stage(record, page);
1003
+ transfers.set(record.locator, record);
1004
+ return true;
1005
+ }),
1006
+ abandon: (locator) => exclusive(async () => {
1007
+ if (!continuing())
1008
+ return;
1009
+ await local.abandon(locator);
1010
+ transfers.delete(locator);
1011
+ }),
1012
+ };
1013
+ }
1014
+ async function pullBootstrap(authorized, room, local) {
929
1015
  if (!continuing())
930
1016
  return unconfirmed;
931
1017
  try {
932
1018
  const result = await bootstrapRoom(options.adapter, authorized, room, {
933
1019
  collection: modelOf(room.locator),
934
1020
  selection: modelOf(room.locator, 'typed-selection'),
935
- }, continuing);
1021
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
936
1022
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
937
1023
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
938
1024
  return result;
@@ -945,35 +1031,46 @@ export function composeRuntime(options) {
945
1031
  async function advance(room, local, authorized) {
946
1032
  const held = replicas.get(room.locator);
947
1033
  if (held !== undefined) {
948
- const refreshed = await pullRefresh(authorized, held.confirmed, reachOf(room.locator));
1034
+ const refreshed = await pullRefresh(authorized, held.confirmed);
949
1035
  if (!continuing())
950
1036
  return unconfirmed;
951
1037
  if (refreshed.kind === 'still-current') {
1038
+ stale.delete(room.locator);
952
1039
  if (sourceRefusals.has(room.locator))
953
- return unconfirmed;
1040
+ return null;
954
1041
  if (unreadable.delete(room.locator)) {
1042
+ scopedRefusals.delete(room.locator);
955
1043
  list(room, true);
956
1044
  emitRooms();
957
1045
  }
958
1046
  return null;
959
1047
  }
960
1048
  if (refreshed.kind === 'advanced') {
961
- await publish(local, room, refreshed.confirmed);
1049
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1050
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1051
+ const reached = now !== undefined &&
1052
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1053
+ return reached ? null : advance(room, local, authorized);
1054
+ }
1055
+ await retainModel(local, room, 'collection', modelOf(room.locator));
962
1056
  return null;
963
1057
  }
964
1058
  if (refreshed.kind !== 'beyond-horizon')
965
1059
  return refreshed;
966
1060
  }
967
- const bootstrapped = await pullBootstrap(authorized, room);
1061
+ const bootstrapped = await pullBootstrap(authorized, room, local);
968
1062
  if (!continuing())
969
1063
  return unconfirmed;
970
1064
  if (bootstrapped.kind !== 'staged')
971
1065
  return bootstrapped;
972
- await publish(local, room, bootstrapped.confirmed);
1066
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
973
1067
  return null;
974
1068
  }
975
1069
  async function withhold(withheld, room, local, progress) {
976
- progress.complete = false;
1070
+ if (withheld.kind === 'refused')
1071
+ stale.delete(room.locator);
1072
+ else
1073
+ stale.add(room.locator);
977
1074
  if (withheld.kind === 'scoped-refusal') {
978
1075
  await scoped(withheld, room, local);
979
1076
  }
@@ -998,6 +1095,7 @@ export function composeRuntime(options) {
998
1095
  function refusedSource(room, { refusal }) {
999
1096
  sourceRefusals.set(room.locator, {
1000
1097
  code: refusal.rule,
1098
+ retryable: false,
1001
1099
  fields: {
1002
1100
  file: refusal.file,
1003
1101
  ...(refusal.location === undefined ? {} : { location: refusal.location }),
@@ -1012,52 +1110,46 @@ export function composeRuntime(options) {
1012
1110
  }
1013
1111
  function leaving(batch, result) {
1014
1112
  if (result.kind === 'expired')
1015
- return { batch, returned: { kind: 'expired' } };
1016
- return { batch, returned: { kind: result.kind, code: result.code, fields: result.fields } };
1113
+ return { batch, returned: returnedBy('expired') };
1114
+ return {
1115
+ batch,
1116
+ returned: {
1117
+ kind: result.kind,
1118
+ code: result.code,
1119
+ fields: result.fields,
1120
+ retryable: result.retryable,
1121
+ },
1122
+ };
1017
1123
  }
1018
1124
  function logAccepted(locator, submitted, accepted) {
1019
1125
  journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1020
1126
  (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1021
1127
  (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1022
1128
  }
1023
- async function recordAccepted(room, local, submitted, accepted) {
1024
- await exclusive(async () => {
1025
- if (!continuing())
1026
- return;
1027
- const current = replicas.get(room.locator);
1028
- if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1029
- return;
1030
- }
1031
- const state = pendings.get(submitted.id);
1032
- if (state === undefined)
1033
- return;
1034
- await local.accept(room.locator, submitted.id, accepted.roomVersion);
1035
- pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1036
- logAccepted(room.locator, submitted, accepted);
1037
- emitQueue(room.locator);
1038
- });
1039
- }
1040
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
1129
+ async function adoptAccepted(room, local, submitted, accepted) {
1041
1130
  if (!continuing())
1042
1131
  return unconfirmed;
1043
1132
  const current = replicas.get(room.locator);
1044
1133
  if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1045
1134
  return null;
1046
1135
  }
1047
- const remaining = current.queue.filter((batch) => batch.id !== submitted.id);
1048
- const retainedBase = readConfirmed({
1049
- ...base.record,
1050
- model: modelOf(room.locator),
1051
- selectionModel: modelOf(room.locator, 'typed-selection'),
1052
- }, base);
1053
- const applied = overlay && retainedBase.kind === 'staged'
1054
- ? applyReturned(retainedBase.confirmed, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion)
1055
- : retainedBase;
1056
- if (applied.kind !== 'staged')
1136
+ const held = current.confirmed.record.roomVersion;
1137
+ logAccepted(room.locator, submitted, accepted);
1138
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1139
+ ? { kind: 'staged', confirmed: current.confirmed }
1140
+ : applyReturned(current.confirmed, accepted.documents, accepted.revisions, accepted.changes, accepted.removals, accepted.attachments, options.adapter.follows(room.locator, held, accepted.roomVersion)
1141
+ ? accepted.roomVersion
1142
+ : held, accepted.roomVersion);
1143
+ if (applied.kind !== 'staged') {
1144
+ const state = pendings.get(submitted.id);
1145
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1146
+ if (state !== undefined)
1147
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1148
+ emitQueue(room.locator);
1057
1149
  return applied;
1058
- const set = await settle(local, room.locator, current, applied.confirmed, remaining, {
1059
- room: true,
1060
- });
1150
+ }
1151
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1152
+ pendings.delete(submitted.id);
1061
1153
  unreadable.delete(room.locator);
1062
1154
  sourceRefusals.delete(room.locator);
1063
1155
  declarationRefusals.delete(room.locator);
@@ -1066,17 +1158,8 @@ export function composeRuntime(options) {
1066
1158
  moveTo({ ...status, ...attention() });
1067
1159
  return null;
1068
1160
  }
1069
- async function accepting(room, local, submitted, accepted, pulled, fresh) {
1070
- const roomVersion = pulled.record.roomVersion;
1071
- if (options.adapter.reached(room.locator, roomVersion, accepted.roomVersion)) {
1072
- return committed(room, local, submitted, accepted, pulled, roomVersion === accepted.roomVersion);
1073
- }
1074
- if (fresh && continuing())
1075
- await publish(local, room, pulled);
1076
- return false;
1077
- }
1078
- async function committed(room, local, submitted, accepted, base, overlay) {
1079
- const failed = await exclusive(() => commitAccept(room, local, submitted, accepted, base, overlay));
1161
+ async function adopted(room, local, submitted, accepted) {
1162
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1080
1163
  if (failed === null)
1081
1164
  return true;
1082
1165
  if (failed.kind === 'refused' && continuing())
@@ -1089,10 +1172,8 @@ export function composeRuntime(options) {
1089
1172
  const head = replica?.queue[0];
1090
1173
  if (replica === undefined || head === undefined)
1091
1174
  return;
1092
- if (pendings.get(head.id)?.accepted !== undefined) {
1093
- progress.complete = false;
1175
+ if (pendings.get(head.id)?.accepted !== undefined)
1094
1176
  return;
1095
- }
1096
1177
  const state = pendings.get(head.id);
1097
1178
  const { submission, lost: earlier } = state ?? {
1098
1179
  submission: { state: 'waiting' },
@@ -1120,6 +1201,7 @@ export function composeRuntime(options) {
1120
1201
  return;
1121
1202
  }
1122
1203
  const opensBefore = openSignals;
1204
+ const answered = awaitingAnswer(room.locator);
1123
1205
  try {
1124
1206
  result = await options.adapter.mutate(authorized, {
1125
1207
  locator: room.locator,
@@ -1128,11 +1210,14 @@ export function composeRuntime(options) {
1128
1210
  }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1129
1211
  }
1130
1212
  catch {
1213
+ answered();
1131
1214
  progress.complete = false;
1132
1215
  if (continuing())
1133
1216
  progress.reachable = false;
1134
1217
  return;
1135
1218
  }
1219
+ if (result.kind !== 'accepted' || !continuing())
1220
+ answered();
1136
1221
  if (!continuing())
1137
1222
  return;
1138
1223
  if (result.kind === 'transport-uncertainty') {
@@ -1185,6 +1270,7 @@ export function composeRuntime(options) {
1185
1270
  const reset = result.reset;
1186
1271
  progress.complete = false;
1187
1272
  await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1273
+ armTimer();
1188
1274
  return;
1189
1275
  }
1190
1276
  if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
@@ -1197,7 +1283,8 @@ export function composeRuntime(options) {
1197
1283
  return;
1198
1284
  }
1199
1285
  const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1200
- leaving: result.kind === 'expired' && landed(head, current.confirmed)
1286
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1287
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1201
1288
  ? { ...left, returned: null }
1202
1289
  : left,
1203
1290
  });
@@ -1211,95 +1298,33 @@ export function composeRuntime(options) {
1211
1298
  return withhold(withheld, room, local, progress);
1212
1299
  continue;
1213
1300
  }
1214
- await recordAccepted(room, local, head, result);
1215
- const from = replicas.get(room.locator)?.confirmed;
1216
- if (from === undefined)
1217
- return;
1218
- const pulled = await pullRefresh(authorized, from, reachOf(room.locator));
1219
- if (!continuing())
1220
- return;
1221
- if (pulled.kind === 'revoked-authorization') {
1222
- await refuse('revoked-authorization');
1223
- return;
1224
- }
1225
- if (pulled.kind === 'scoped-refusal') {
1226
- await scoped(pulled, room, local);
1301
+ const kept = await adopted(room, local, head, result).finally(answered);
1302
+ if (!kept) {
1227
1303
  progress.complete = false;
1228
1304
  return;
1229
1305
  }
1230
- if (pulled.kind === 'still-current' || pulled.kind === 'advanced') {
1231
- const fresh = pulled.kind === 'advanced';
1232
- if (!(await accepting(room, local, head, result, fresh ? pulled.confirmed : from, fresh))) {
1233
- progress.complete = false;
1234
- return;
1235
- }
1236
- continue;
1237
- }
1238
- if (pulled.kind === 'beyond-horizon') {
1239
- const bootstrapped = await pullBootstrap(authorized, room);
1240
- if (!continuing())
1241
- return;
1242
- if (bootstrapped.kind === 'revoked-authorization') {
1243
- await refuse('revoked-authorization');
1244
- return;
1245
- }
1246
- if (bootstrapped.kind === 'scoped-refusal') {
1247
- await scoped(bootstrapped, room, local);
1248
- progress.complete = false;
1249
- return;
1250
- }
1251
- if (bootstrapped.kind === 'staged') {
1252
- if (!(await accepting(room, local, head, result, bootstrapped.confirmed, true))) {
1253
- progress.complete = false;
1254
- return;
1255
- }
1256
- continue;
1257
- }
1258
- progress.complete = false;
1259
- if (bootstrapped.kind === 'transport-uncertainty') {
1260
- progress.reachable = false;
1261
- }
1262
- else if (bootstrapped.kind === 'room-not-readable') {
1263
- unreadable.add(room.locator);
1264
- list(room, false);
1265
- emitRooms();
1266
- }
1267
- else if (bootstrapped.kind === 'refused') {
1268
- refusedSource(room, bootstrapped);
1269
- }
1270
- return;
1271
- }
1272
- progress.complete = false;
1273
- if (pulled.kind === 'transport-uncertainty') {
1274
- progress.reachable = false;
1275
- }
1276
- else if (pulled.kind === 'room-not-readable') {
1277
- unreadable.add(room.locator);
1278
- list(room, false);
1279
- emitRooms();
1280
- }
1281
- else if (pulled.kind === 'retryable-refusal') {
1282
- journal.record(`refusal ${pulled.kind} ${pulled.code}`);
1283
- }
1284
- else if (pulled.kind === 'refused') {
1285
- refusedSource(room, pulled);
1286
- }
1287
- return;
1288
1306
  }
1289
- return;
1307
+ }
1308
+ function awaitingAnswer(locator) {
1309
+ let release = () => { };
1310
+ const awaited = new Promise((resolve) => {
1311
+ release = resolve;
1312
+ });
1313
+ answering.set(locator, awaited);
1314
+ return () => {
1315
+ if (answering.get(locator) === awaited)
1316
+ answering.delete(locator);
1317
+ release();
1318
+ };
1290
1319
  }
1291
1320
  async function retainModel(local, room, attached, next) {
1292
1321
  const key = modelKey(room.locator, attached);
1293
- if (next.status === 'unavailable')
1294
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1295
- const epoch = modelEpoch.get(key) ?? 0;
1296
1322
  const previous = modelOf(room.locator, attached);
1297
1323
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1298
1324
  if (JSON.stringify(previous) === JSON.stringify(next) &&
1299
1325
  (heldBefore === undefined ||
1300
1326
  (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1301
1327
  JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1302
- JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) &&
1303
1328
  JSON.stringify(heldBefore.typedSelection) ===
1304
1329
  JSON.stringify(room.typedSelection ?? null))))
1305
1330
  return;
@@ -1310,20 +1335,17 @@ export function composeRuntime(options) {
1310
1335
  await exclusive(async () => {
1311
1336
  if (!continuing())
1312
1337
  return;
1313
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0))
1314
- return;
1315
1338
  const held = replicas.get(room.locator);
1316
1339
  if (held !== undefined) {
1317
1340
  const declared = {
1318
1341
  ...held.confirmed.record,
1319
1342
  operations: room.operations,
1320
1343
  replica: room.replica,
1321
- attachedModel: room.attachedModel,
1322
1344
  typedSelection: room.typedSelection ?? null,
1323
1345
  ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1324
1346
  };
1325
1347
  const reclassified = sameMembership(declared, held.confirmed.record)
1326
- ? readConfirmed(declared, held.confirmed)
1348
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1327
1349
  : readReclassified(declared, held.confirmed);
1328
1350
  const record = reclassified.kind === 'staged'
1329
1351
  ? declared
@@ -1331,14 +1353,16 @@ export function composeRuntime(options) {
1331
1353
  ...declared,
1332
1354
  typedSelection: held.confirmed.record.typedSelection,
1333
1355
  };
1334
- const read = reclassified.kind === 'deferred' ? readConfirmed(record, held.confirmed) : reclassified;
1356
+ const read = reclassified.kind === 'deferred'
1357
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1358
+ : reclassified;
1335
1359
  await local.write({
1336
1360
  next: record,
1337
1361
  previous: held.confirmed.record,
1338
1362
  queue: held.queue,
1339
1363
  });
1340
1364
  if (read.kind === 'staged') {
1341
- replicas.set(room.locator, replicaOf(read.confirmed, held.queue, rewriting(), held));
1365
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1342
1366
  if (declarationRefusals.delete(room.locator)) {
1343
1367
  sourceRefusals.delete(room.locator);
1344
1368
  unreadable.delete(room.locator);
@@ -1362,79 +1386,12 @@ export function composeRuntime(options) {
1362
1386
  publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1363
1387
  });
1364
1388
  }
1365
- async function acquireModel(local, room, authorized, selection) {
1366
- const attached = selection === null ? 'collection' : 'typed-selection';
1367
- const key = modelKey(room.locator, attached);
1368
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1369
- let previous = modelOf(room.locator, attached);
1370
- if (previous.status !== 'unavailable' &&
1371
- (declared === null || !sameModel(previous.snapshot, declared))) {
1372
- previous = { status: 'unavailable', reason: 'version-changed' };
1373
- await retainModel(local, room, attached, previous);
1374
- if (discoveryPaused)
1375
- return;
1376
- }
1377
- const epoch = modelEpoch.get(key) ?? 0;
1378
- let answer;
1379
- try {
1380
- answer = await options.adapter.model(authorized, selection === null
1381
- ? { locator: room.locator, attached: 'collection', model: declared }
1382
- : {
1383
- locator: room.locator,
1384
- attached: 'typed-selection',
1385
- roots: selection.roots,
1386
- model: declared,
1387
- });
1388
- }
1389
- catch {
1390
- answer = { kind: 'transport-uncertainty' };
1391
- }
1392
- if (discoveryPaused || epoch !== (modelEpoch.get(key) ?? 0))
1393
- return;
1394
- if (answer.kind === 'revoked-authorization') {
1395
- await refuse('revoked-authorization');
1396
- return;
1397
- }
1398
- if (answer.kind === 'transport-uncertainty') {
1399
- await retainModel(local, room, attached, cached(previous));
1400
- return;
1401
- }
1402
- if (answer.kind === 'model-unavailable') {
1403
- await retainModel(local, room, attached, {
1404
- status: 'unavailable',
1405
- reason: answer.reason,
1406
- });
1407
- return;
1408
- }
1409
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1410
- await retainModel(local, room, attached, {
1411
- status: 'unavailable',
1412
- reason: 'version-changed',
1413
- });
1414
- return;
1415
- }
1416
- if (previous.status !== 'unavailable' &&
1417
- sameModel(previous.snapshot, answer.snapshot) &&
1418
- previous.snapshot.schema !== answer.snapshot.schema) {
1419
- await retainModel(local, room, attached, { status: 'unavailable', reason: 'invalid' });
1420
- return;
1421
- }
1422
- const verified = answer.snapshot.schema === null ? EMPTY_SCHEMA : verify(answer.snapshot.schema);
1423
- if (verified.verdict === 'refuse') {
1424
- const where = verified.location === undefined ? '' : ` ${verified.location}`;
1425
- journal.record(`model invalid ${room.locator} ${attached} ${answer.snapshot.identity} ${verified.rule}${where}`);
1426
- }
1427
- await retainModel(local, room, attached, verified.verdict === 'accept'
1428
- ? {
1429
- status: 'current',
1430
- snapshot: structuredClone(answer.snapshot),
1431
- verifiedWith: MD_MODEL,
1432
- types: verified.model.types,
1433
- }
1434
- : { status: 'unavailable', reason: 'invalid' });
1435
- }
1436
1389
  async function scoped(result, room, local) {
1437
- scopedRefusals.set(room.locator, { code: result.code, fields: result.fields });
1390
+ scopedRefusals.set(room.locator, {
1391
+ code: result.code,
1392
+ fields: result.fields,
1393
+ retryable: result.retryable,
1394
+ });
1438
1395
  let scopedAway = room;
1439
1396
  if (result.scope === 'model')
1440
1397
  await retainModel(local, room, 'collection', {
@@ -1460,14 +1417,36 @@ export function composeRuntime(options) {
1460
1417
  emitRooms();
1461
1418
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1462
1419
  }
1463
- async function synchronize(discovered, local, progress) {
1464
- const prepared = await Promise.allSettled(discovered.map((room) => prepareRoom(room, local, progress)));
1465
- const ready = discovered.filter((_, index) => {
1420
+ function heldRooms(listed) {
1421
+ return listed.flatMap((room) => {
1422
+ const record = replicas.get(room.locator)?.confirmed.record;
1423
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1424
+ return [];
1425
+ return [
1426
+ {
1427
+ locator: room.locator,
1428
+ roomVersion: record.roomVersion,
1429
+ documents: record.documents,
1430
+ revisions: record.revisions,
1431
+ attachments: record.attachments,
1432
+ },
1433
+ ];
1434
+ });
1435
+ }
1436
+ async function synchronize(listed, local, progress, attempt) {
1437
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1438
+ const ready = listed.filter((_, index) => {
1466
1439
  const outcome = prepared[index];
1467
1440
  return outcome?.status === 'fulfilled' && outcome.value;
1468
1441
  });
1469
- const rooms = await Promise.allSettled(ready.map((room) => synchronizeRoom(room, local, progress)));
1470
- const failed = [...prepared, ...rooms].find((room) => room.status === 'rejected');
1442
+ if (attempt === 'opened') {
1443
+ options.adapter.roomStream?.hold(heldRooms(ready));
1444
+ handed = true;
1445
+ }
1446
+ const loaded = await Promise.allSettled(ready
1447
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1448
+ .map((room) => loadRoom(room, local, progress)));
1449
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1471
1450
  if (failed !== undefined)
1472
1451
  throw failed.reason;
1473
1452
  }
@@ -1484,11 +1463,8 @@ export function composeRuntime(options) {
1484
1463
  progress.complete = false;
1485
1464
  return false;
1486
1465
  }
1487
- const selection = room.typedSelection;
1488
- await Promise.all([
1489
- acquireModel(local, room, authorization, null),
1490
- ...(selection === undefined ? [] : [acquireModel(local, room, authorization, selection)]),
1491
- ]);
1466
+ if (prior !== undefined)
1467
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1492
1468
  if (discoveryPaused || authorization === undefined)
1493
1469
  return false;
1494
1470
  if (room.replica.admission !== 'admitted') {
@@ -1496,24 +1472,53 @@ export function composeRuntime(options) {
1496
1472
  progress.complete = false;
1497
1473
  return false;
1498
1474
  }
1499
- const held = replicas.get(room.locator);
1500
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1501
- progress.complete = false;
1502
- return false;
1503
- }
1504
1475
  return true;
1505
1476
  }
1506
- async function synchronizeRoom(room, local, progress) {
1477
+ async function caughtUp(room, local, progress) {
1478
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1479
+ if (held === undefined || authorization === undefined)
1480
+ return;
1481
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1482
+ if (!behind)
1483
+ return;
1484
+ const withheld = await advance(room, local, authorization);
1485
+ if (withheld !== null)
1486
+ await withhold(withheld, room, local, progress);
1487
+ }
1488
+ function revoked(locator) {
1489
+ return ['collection', 'typed-selection'].some((attached) => {
1490
+ const model = modelOf(locator, attached);
1491
+ return model.status === 'unavailable' && model.reason === 'revoked';
1492
+ });
1493
+ }
1494
+ async function loadRoom(room, local, progress) {
1507
1495
  if (discoveryPaused || authorization === undefined)
1508
1496
  return;
1497
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1498
+ await reload(room, local, progress);
1499
+ return;
1500
+ }
1509
1501
  const withheld = await advance(room, local, authorization);
1510
1502
  if (withheld !== null) {
1511
1503
  await withhold(withheld, room, local, progress);
1512
1504
  return;
1513
1505
  }
1514
- await submitRoom(room, local, authorization, progress);
1506
+ await caughtUp(room, local, progress);
1515
1507
  }
1516
- async function cycle(local, progress) {
1508
+ async function reload(room, local, progress) {
1509
+ if (discoveryPaused || authorization === undefined)
1510
+ return;
1511
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1512
+ if (!continuing())
1513
+ return;
1514
+ if (bootstrapped.kind !== 'staged') {
1515
+ await withhold(bootstrapped, room, local, progress);
1516
+ return;
1517
+ }
1518
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1519
+ await caughtUp(room, local, progress);
1520
+ }
1521
+ async function discoverRooms(local, progress, attempt) {
1517
1522
  if (closed || authorization === undefined) {
1518
1523
  notifyReady();
1519
1524
  return;
@@ -1539,12 +1544,17 @@ export function composeRuntime(options) {
1539
1544
  }
1540
1545
  }
1541
1546
  progress.reachable = false;
1547
+ progress.complete = false;
1542
1548
  notifyReady();
1543
1549
  return;
1544
1550
  }
1545
1551
  if (result.kind === 'contract-unavailable') {
1546
1552
  journal.record(`connection contract unavailable ${result.code}`);
1547
- await refuse('missing-required-guarantee', { code: result.code, fields: result.fields });
1553
+ await refuse('missing-required-guarantee', {
1554
+ code: result.code,
1555
+ fields: result.fields,
1556
+ retryable: result.retryable,
1557
+ });
1548
1558
  notifyReady();
1549
1559
  return;
1550
1560
  }
@@ -1553,17 +1563,6 @@ export function composeRuntime(options) {
1553
1563
  notifyReady();
1554
1564
  return;
1555
1565
  }
1556
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1557
- await refuse('no-matching-version');
1558
- notifyReady();
1559
- return;
1560
- }
1561
- if (result.mdModel.identity !== MD_MODEL.identity ||
1562
- result.mdModel.version !== MD_MODEL.version) {
1563
- await refuse('md-model-version-mismatch');
1564
- notifyReady();
1565
- return;
1566
- }
1567
1566
  for (const room of result.rooms) {
1568
1567
  if (room.operations === undefined ||
1569
1568
  !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
@@ -1571,8 +1570,7 @@ export function composeRuntime(options) {
1571
1570
  room.replica === undefined ||
1572
1571
  typeof room.replica.selection !== 'string' ||
1573
1572
  room.replica.selection.length === 0 ||
1574
- !['admitted', 'unavailable'].includes(room.replica.admission) ||
1575
- !declaredIdentity(room.attachedModel)) {
1573
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1576
1574
  journal.record(`invalid model or replica declaration ${room.locator}`);
1577
1575
  await refuse('missing-required-guarantee');
1578
1576
  notifyReady();
@@ -1585,14 +1583,6 @@ export function composeRuntime(options) {
1585
1583
  return;
1586
1584
  }
1587
1585
  }
1588
- const declared = new Set(result.capabilities.map((capability) => capability.name));
1589
- for (const required of options.requiredGuarantees) {
1590
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1591
- await refuse('missing-required-guarantee');
1592
- notifyReady();
1593
- return;
1594
- }
1595
- }
1596
1586
  if (refusal !== null) {
1597
1587
  refusal = null;
1598
1588
  emitRefusal();
@@ -1600,26 +1590,19 @@ export function composeRuntime(options) {
1600
1590
  const previousCapabilities = capabilities;
1601
1591
  capabilities = result.capabilities;
1602
1592
  notifyReady();
1603
- if (declared.has('room-version-notifications'))
1604
- observeRoomVersions();
1605
1593
  if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1606
1594
  JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1607
1595
  limits = result.providerLimits;
1608
1596
  await local.declare(limits, capabilities);
1609
1597
  }
1598
+ const signedIn = result.member ?? null;
1599
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1600
+ await local.keepMember(signedIn);
1601
+ member = signedIn;
1602
+ emitMember();
1603
+ }
1610
1604
  await applyDiscovery(result.rooms, local);
1611
- handSelected(result.rooms
1612
- .filter((room) => room.replica.admission === 'admitted')
1613
- .map((room) => room.locator)
1614
- .sort());
1615
- progress.complete = true;
1616
- await synchronize(result.rooms, local, progress);
1617
- if (closed || discoveryPaused)
1618
- return;
1619
- await conversations.reconcile();
1620
- if (status.providerUnreachable)
1621
- conversations.reconnected();
1622
- await conversations.catchUp(progress);
1605
+ await synchronize(result.rooms, local, progress, attempt);
1623
1606
  }
1624
1607
  async function ensureAuthorization() {
1625
1608
  if (authorization !== undefined)
@@ -1633,64 +1616,255 @@ export function composeRuntime(options) {
1633
1616
  throw error;
1634
1617
  }
1635
1618
  }
1636
- async function connect() {
1637
- if (closed || stopping || discoveryPaused) {
1638
- notifyReady();
1619
+ function task(work) {
1620
+ if (stopping)
1621
+ return chain;
1622
+ chain = chain.then(work, work).catch(() => undefined);
1623
+ return chain;
1624
+ }
1625
+ function sync(attempt) {
1626
+ return task(async () => {
1627
+ if (closed || stopping || discoveryPaused) {
1628
+ notifyReady();
1629
+ return;
1630
+ }
1631
+ const progress = { complete: true, reachable: true };
1632
+ underway = attempt === 'opened' ? progress : null;
1633
+ complete = false;
1634
+ recorded = false;
1635
+ moveTo(transferring(status));
1636
+ handed = false;
1637
+ try {
1638
+ const local = await openLocal();
1639
+ if (closed)
1640
+ return;
1641
+ await ensureAuthorization();
1642
+ if (closed)
1643
+ return;
1644
+ await discoverRooms(local, progress, attempt);
1645
+ }
1646
+ catch {
1647
+ progress.complete = false;
1648
+ if (attempt === 'opened' && !handed)
1649
+ progress.reachable = false;
1650
+ }
1651
+ finally {
1652
+ notifyReady();
1653
+ }
1654
+ if (attempt !== 'opened') {
1655
+ await finishOpening(progress);
1656
+ return;
1657
+ }
1658
+ if (handed || closed || progress.reachable)
1659
+ return;
1660
+ underway = null;
1661
+ journal.record('connection reopen');
1662
+ await rest(progress);
1663
+ options.adapter.roomStream?.reopen();
1664
+ });
1665
+ }
1666
+ function closedSocket(revoked) {
1667
+ const opened = socketOpen;
1668
+ socketOpen = false;
1669
+ if (revoked) {
1670
+ void refuse('revoked-authorization');
1639
1671
  return;
1640
1672
  }
1641
- startTimer();
1642
- const progress = { complete: false, reachable: true };
1643
- let completedAt = new Date().toISOString();
1644
- try {
1645
- const local = await openLocal();
1646
- if (closed)
1647
- return;
1648
- await ensureAuthorization();
1649
- if (closed)
1673
+ if (opened || status.providerUnreachable)
1674
+ return;
1675
+ moveTo({ ...status, providerUnreachable: true });
1676
+ }
1677
+ function applyOpening(expired) {
1678
+ return task(async () => {
1679
+ const progress = underway;
1680
+ underway = null;
1681
+ if (closed || stopping)
1650
1682
  return;
1651
- await cycle(local, progress);
1652
- if (closed)
1683
+ if (progress === null) {
1684
+ await reloadExpired(expired);
1653
1685
  return;
1654
- if (progress.complete) {
1655
- completedAt = new Date().toISOString();
1656
- await local.complete(completedAt);
1657
1686
  }
1687
+ try {
1688
+ const local = await openLocal();
1689
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1690
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1691
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1692
+ if (progress.reachable) {
1693
+ for (const room of held)
1694
+ if (!reloading.includes(room))
1695
+ await confirmModels(room, local);
1696
+ }
1697
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1698
+ if (failed !== undefined)
1699
+ throw failed.reason;
1700
+ }
1701
+ catch {
1702
+ progress.complete = false;
1703
+ progress.reachable = false;
1704
+ }
1705
+ await finishOpening(progress);
1706
+ });
1707
+ }
1708
+ async function reloadExpired(expired) {
1709
+ const progress = { complete: true, reachable: true };
1710
+ try {
1711
+ const local = await openLocal();
1712
+ await Promise.allSettled([...discovered.values()]
1713
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1714
+ .map((room) => reload(room, local, progress)));
1658
1715
  }
1659
1716
  catch {
1660
- progress.complete = false;
1661
- notifyReady();
1717
+ progress.reachable = false;
1718
+ }
1719
+ await rest(progress);
1720
+ }
1721
+ async function confirmModels(room, local) {
1722
+ for (const attached of ['collection', 'typed-selection']) {
1723
+ const model = modelOf(room.locator, attached);
1724
+ if (model.status === 'cached') {
1725
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1726
+ }
1727
+ }
1728
+ }
1729
+ function submitLater(locator) {
1730
+ if (localSaves)
1731
+ return;
1732
+ const running = submitting.get(locator);
1733
+ if (running !== undefined) {
1734
+ running.again = true;
1662
1735
  return;
1663
1736
  }
1664
- finally {
1665
- notifyReady();
1666
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1737
+ const run = { again: false, done: Promise.resolve() };
1738
+ submitting.set(locator, run);
1739
+ run.done = (async () => {
1740
+ try {
1741
+ do {
1742
+ run.again = false;
1743
+ const room = discovered.get(locator);
1744
+ if (closed || stopping || discoveryPaused || room === undefined)
1745
+ return;
1746
+ const progress = { complete: true, reachable: true };
1747
+ try {
1748
+ await ensureAuthorization();
1749
+ if (authorization === undefined)
1750
+ return;
1751
+ await submitRoom(room, await openLocal(), authorization, progress);
1752
+ }
1753
+ catch {
1754
+ progress.reachable = false;
1755
+ }
1756
+ armTimer();
1757
+ await rest(progress);
1758
+ } while (run.again);
1759
+ }
1760
+ finally {
1761
+ submitting.delete(locator);
1762
+ }
1763
+ })();
1764
+ }
1765
+ function submitAll() {
1766
+ for (const [locator, replica] of replicas) {
1767
+ if (replica.queue.length > 0)
1768
+ submitLater(locator);
1667
1769
  }
1668
1770
  }
1669
- async function run() {
1771
+ async function finishOpening(progress) {
1670
1772
  try {
1671
- await connect();
1773
+ if (closed || discoveryPaused) {
1774
+ progress.complete = false;
1775
+ return;
1776
+ }
1777
+ submitAll();
1778
+ await conversations.reconcile();
1779
+ if (status.providerUnreachable)
1780
+ conversations.reconnected();
1781
+ const reading = { complete: true, reachable: true };
1782
+ await conversations.catchUp(reading);
1783
+ if (!reading.reachable)
1784
+ progress.reachable = false;
1785
+ }
1786
+ catch {
1787
+ progress.complete = false;
1672
1788
  }
1673
1789
  finally {
1674
- inflight = false;
1675
- if (queued && !closed) {
1676
- queued = false;
1677
- void enqueue();
1678
- }
1790
+ complete = progress.complete;
1791
+ await rest(progress);
1679
1792
  }
1680
1793
  }
1681
- function observeRoomVersions() {
1682
- const path = options.adapter.roomVersions;
1683
- if (path === undefined || withdrawNotifications !== undefined)
1794
+ async function rest(progress) {
1795
+ if (closed)
1684
1796
  return;
1685
- withdrawNotifications = path.observe((notification) => {
1686
- if (closed || stopping || discoveryPaused)
1797
+ let lastCompleted = status.lastCompleted;
1798
+ let synced = complete && stale.size === 0 && !pending();
1799
+ if (synced && !recorded) {
1800
+ const completedAt = new Date().toISOString();
1801
+ try {
1802
+ await (await openLocal()).complete(completedAt);
1803
+ recorded = true;
1804
+ lastCompleted = completedAt;
1805
+ }
1806
+ catch {
1807
+ synced = false;
1808
+ }
1809
+ }
1810
+ moveTo({
1811
+ state: pending() ? 'pending' : synced ? 'synced' : 'syncing',
1812
+ lastCompleted,
1813
+ providerUnreachable: !progress.reachable,
1814
+ ...attention(),
1815
+ });
1816
+ }
1817
+ function applyRoom(locator, roomVersion) {
1818
+ announced.set(locator, roomVersion);
1819
+ return task(async () => {
1820
+ await answering.get(locator);
1821
+ if (closed || stopping || discoveryPaused || authorization === undefined)
1822
+ return;
1823
+ const room = discovered.get(locator);
1824
+ const held = replicas.get(locator);
1825
+ if (room === undefined || room.replica.admission !== 'admitted')
1687
1826
  return;
1688
- if (notification.modelChanged === true && handed?.includes(notification.locator) === true) {
1689
- void enqueue();
1827
+ if (held !== undefined &&
1828
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
1829
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
1690
1830
  return;
1691
1831
  }
1692
- notices.set(notification.locator, notification.roomVersion);
1693
- chain = chain.then(runNotified, runNotified);
1832
+ const during = underway;
1833
+ const progress = during ?? { complete: true, reachable: true };
1834
+ if (during === null)
1835
+ moveTo(transferring(status));
1836
+ try {
1837
+ const local = await openLocal();
1838
+ const withheld = await advance(room, local, authorization);
1839
+ if (withheld !== null)
1840
+ await withhold(withheld, room, local, progress);
1841
+ }
1842
+ catch {
1843
+ stale.add(locator);
1844
+ }
1845
+ finally {
1846
+ if (during === null)
1847
+ await rest(progress);
1848
+ }
1849
+ });
1850
+ }
1851
+ function observeStream() {
1852
+ const stream = options.adapter.roomStream;
1853
+ if (stream === undefined || withdrawStream !== undefined)
1854
+ return;
1855
+ withdrawStream = stream.observe((event) => {
1856
+ if (closed || stopping)
1857
+ return;
1858
+ if (event.kind === 'opened') {
1859
+ socketOpen = true;
1860
+ void sync('opened');
1861
+ }
1862
+ else if (event.kind === 'closed')
1863
+ closedSocket(event.revoked);
1864
+ else if (event.kind === 'opening')
1865
+ void applyOpening(event.expired);
1866
+ else
1867
+ void applyRoom(event.locator, event.roomVersion);
1694
1868
  });
1695
1869
  }
1696
1870
  function writesHeld() {
@@ -1711,14 +1885,17 @@ export function composeRuntime(options) {
1711
1885
  listener(mode);
1712
1886
  }
1713
1887
  if (releasing)
1714
- void enqueue();
1888
+ submitAll();
1715
1889
  }
1716
1890
  function observeConversations() {
1717
1891
  const stream = options.adapter.conversations;
1718
1892
  if (stream === undefined || withdrawConversations !== undefined)
1719
1893
  return;
1720
1894
  withdrawConversations = stream.observe((event) => {
1721
- conversations.receive(event);
1895
+ if (conversations.receive(event)) {
1896
+ void ready.then(scheduleCurrency);
1897
+ return;
1898
+ }
1722
1899
  if (event.kind !== 'gap' || conversations.current())
1723
1900
  return;
1724
1901
  show();
@@ -1729,7 +1906,7 @@ export function composeRuntime(options) {
1729
1906
  if (currencyQueued || closed || stopping)
1730
1907
  return;
1731
1908
  currencyQueued = true;
1732
- chain = chain.then(runCurrency, runCurrency);
1909
+ void task(runCurrency);
1733
1910
  }
1734
1911
  async function runCurrency() {
1735
1912
  currencyQueued = false;
@@ -1755,95 +1932,12 @@ export function composeRuntime(options) {
1755
1932
  }
1756
1933
  }
1757
1934
  }
1758
- function handSelected(locators) {
1759
- const path = options.adapter.roomVersions;
1760
- if (path === undefined || withdrawNotifications === undefined)
1761
- return;
1762
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed))
1763
- return;
1764
- handed = locators;
1765
- path.select(locators);
1766
- }
1767
- async function runNotified() {
1768
- const announced = [...notices];
1769
- notices.clear();
1770
- if (announced.length === 0 || closed || stopping || discoveryPaused)
1771
- return;
1772
- const resting = status;
1773
- const progress = { complete: false, reachable: !resting.providerUnreachable };
1774
- moveTo(transferring(resting));
1775
- try {
1776
- const local = await openLocal();
1777
- for (const [locator, roomVersion] of announced) {
1778
- if (closed || discoveryPaused || authorization === undefined)
1779
- return;
1780
- if (handed?.includes(locator) !== true)
1781
- continue;
1782
- const room = discovered.get(locator);
1783
- const held = replicas.get(locator);
1784
- if (room === undefined)
1785
- continue;
1786
- if (sourceRefusals.has(locator)) {
1787
- void enqueue();
1788
- continue;
1789
- }
1790
- if (held === undefined)
1791
- continue;
1792
- if (held.confirmed.record.roomVersion === roomVersion)
1793
- continue;
1794
- const withheld = await advance(room, local, authorization);
1795
- if (withheld === null)
1796
- continue;
1797
- if (withheld.kind === 'refused') {
1798
- void enqueue();
1799
- continue;
1800
- }
1801
- if (withheld.kind === 'scoped-refusal') {
1802
- await scoped(withheld, room, local);
1803
- continue;
1804
- }
1805
- if (withheld.kind === 'transport-uncertainty') {
1806
- progress.reachable = false;
1807
- continue;
1808
- }
1809
- if (withheld.kind === 'room-not-readable') {
1810
- unreadable.add(locator);
1811
- list(room, false);
1812
- emitRooms();
1813
- continue;
1814
- }
1815
- if (withheld.kind === 'revoked-authorization') {
1816
- await refuse('revoked-authorization');
1817
- return;
1818
- }
1819
- }
1820
- }
1821
- catch {
1822
- progress.reachable = false;
1823
- }
1824
- finally {
1825
- if (!closed) {
1826
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1827
- }
1828
- }
1829
- }
1830
- function enqueue() {
1831
- if (stopping)
1832
- return chain;
1833
- if (inflight) {
1834
- queued = true;
1835
- return chain;
1836
- }
1837
- inflight = true;
1838
- chain = chain.then(run, run);
1839
- return chain;
1840
- }
1841
1935
  function close() {
1842
1936
  if (closed)
1843
1937
  return;
1844
1938
  closed = true;
1845
- withdrawNotifications?.();
1846
- withdrawNotifications = undefined;
1939
+ withdrawStream?.();
1940
+ withdrawStream = undefined;
1847
1941
  withdrawConversations?.();
1848
1942
  withdrawConversations = undefined;
1849
1943
  withdrawServiceMode?.();
@@ -1858,6 +1952,7 @@ export function composeRuntime(options) {
1858
1952
  storageLossListeners.clear();
1859
1953
  statusListeners.clear();
1860
1954
  serviceModeListeners.clear();
1955
+ memberListeners.clear();
1861
1956
  readSide.close();
1862
1957
  queueSubscriptions.clear();
1863
1958
  roomDraftListeners.clear();
@@ -1867,6 +1962,7 @@ export function composeRuntime(options) {
1867
1962
  stopping = true;
1868
1963
  stopTimer();
1869
1964
  await chain.catch(() => undefined);
1965
+ await Promise.all([...submitting.values()].map((run) => run.done));
1870
1966
  await mutations;
1871
1967
  close();
1872
1968
  }
@@ -1875,11 +1971,32 @@ export function composeRuntime(options) {
1875
1971
  options.signal?.addEventListener('abort', close, { once: true });
1876
1972
  if (options.signal?.aborted)
1877
1973
  close();
1878
- void openLocal().catch(() => {
1974
+ observeStream();
1975
+ void openLocal().then(() => {
1976
+ if (capabilities.length > 0)
1977
+ notifyReady();
1978
+ }, () => {
1879
1979
  notifyReady();
1880
1980
  });
1881
- void enqueue();
1882
- function watch(topic, input, deliver) {
1981
+ if (options.adapter.roomStream === undefined)
1982
+ void sync('unstreamed');
1983
+ function planned(input, reach) {
1984
+ return planOf(input, roomFacts(reach), planReach(reach));
1985
+ }
1986
+ function watchPlan(plan, deliver) {
1987
+ materialize(reachedRooms(plan));
1988
+ return readSide.watch(plan, identityOf(plan), deliver);
1989
+ }
1990
+ async function searched(input, reach) {
1991
+ const plan = searchPlanOf(input);
1992
+ const selected = plan.documents === undefined && reach === null
1993
+ ? null
1994
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
1995
+ if (selected === null)
1996
+ materialize(null);
1997
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
1998
+ }
1999
+ const watch = (topic, input, deliver) => {
1883
2000
  if (topic === 'room-drafts') {
1884
2001
  const listener = { notify: deliver, ready: false };
1885
2002
  roomDraftListeners.add(listener);
@@ -1900,6 +2017,8 @@ export function composeRuntime(options) {
1900
2017
  if (topic === 'conversation') {
1901
2018
  return conversations.watch(input[0], input[1], deliver);
1902
2019
  }
2020
+ if (topic === 'conversations')
2021
+ return conversations.watchList(deliver);
1903
2022
  if (topic === 'presentation') {
1904
2023
  const listener = {
1905
2024
  app: input[0],
@@ -1954,6 +2073,13 @@ export function composeRuntime(options) {
1954
2073
  serviceModeListeners.delete(deliver);
1955
2074
  };
1956
2075
  }
2076
+ if (topic === 'member') {
2077
+ memberListeners.add(deliver);
2078
+ deliver(member);
2079
+ return () => {
2080
+ memberListeners.delete(deliver);
2081
+ };
2082
+ }
1957
2083
  if (topic === 'queue') {
1958
2084
  const queue = { locator: input[0], notify: deliver };
1959
2085
  queueSubscriptions.add(queue);
@@ -1962,9 +2088,8 @@ export function composeRuntime(options) {
1962
2088
  queueSubscriptions.delete(queue);
1963
2089
  };
1964
2090
  }
1965
- const plan = planOf(input[0], roomFacts());
1966
- return readSide.watch(plan, identityOf(plan), deliver);
1967
- }
2091
+ return watchPlan(planned(input[0], null), deliver);
2092
+ };
1968
2093
  const md = {
1969
2094
  loaded() {
1970
2095
  return openLocal().then(() => undefined);
@@ -1972,15 +2097,8 @@ export function composeRuntime(options) {
1972
2097
  rooms() {
1973
2098
  return listed();
1974
2099
  },
1975
- query: ((input) => readSide.query(planOf(input, roomFacts()))),
1976
- async search(input) {
1977
- const plan = searchPlanOf(input);
1978
- const selected = plan.documents === undefined
1979
- ? null
1980
- : (await readSide.query(filterOf(plan.documents, roomFacts())))
1981
- .documents;
1982
- return searchIndex.search(plan, selected);
1983
- },
2100
+ query: ((input) => rowsQuery(planned(input, null))),
2101
+ search: (input) => searched(input, null),
1984
2102
  types(locator) {
1985
2103
  return {
1986
2104
  collection: modelTypes(modelOf(locator)),
@@ -2008,14 +2126,11 @@ export function composeRuntime(options) {
2008
2126
  return exclusive(async () => {
2009
2127
  const parts = readParts(input);
2010
2128
  if (path === '' || normalize(path) !== path)
2011
- throw new Error('room-draft-malformed');
2129
+ throw runtimeError('room-draft-malformed');
2012
2130
  const local = await openLocal();
2013
2131
  const current = roomDrafts.get(room)?.get(path);
2014
2132
  if ((current?.revision ?? null) !== revision) {
2015
- return {
2016
- outcome: 'stale',
2017
- current: current === undefined ? null : roomDraftOf(current),
2018
- };
2133
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2019
2134
  }
2020
2135
  const record = {
2021
2136
  locator: room,
@@ -2035,10 +2150,7 @@ export function composeRuntime(options) {
2035
2150
  const local = await openLocal();
2036
2151
  const current = roomDrafts.get(room)?.get(path);
2037
2152
  if (current === undefined || current.revision !== revision) {
2038
- return {
2039
- outcome: 'stale',
2040
- current: current === undefined ? null : roomDraftOf(current),
2041
- };
2153
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2042
2154
  }
2043
2155
  await local.discardRoomDraft(room, path);
2044
2156
  forgetRoomDraft(room, path);
@@ -2087,9 +2199,7 @@ export function composeRuntime(options) {
2087
2199
  persistence() {
2088
2200
  return options.shell.persistentStorage;
2089
2201
  },
2090
- observe: ((topic, ...input) => opened((deliver) => watch(topic, input, (message) => {
2091
- deliver(structuredClone(message));
2092
- }), topic === 'query')),
2202
+ observe: observing(watch),
2093
2203
  async reauthorize() {
2094
2204
  if (refusal === null || !refusal.reauthorizationOffered)
2095
2205
  return;
@@ -2098,8 +2208,19 @@ export function composeRuntime(options) {
2098
2208
  await mutations;
2099
2209
  authorization = undefined;
2100
2210
  discoveryPaused = false;
2101
- await enqueue();
2211
+ if (options.adapter.roomStream === undefined)
2212
+ await sync('unstreamed');
2213
+ else
2214
+ await task(ensureAuthorization);
2102
2215
  },
2103
2216
  };
2104
- return { md, stop, watch };
2217
+ const reachable = {
2218
+ md,
2219
+ watch,
2220
+ plan: planned,
2221
+ query: (plan) => rowsQuery(plan),
2222
+ watchPlan,
2223
+ search: searched,
2224
+ };
2225
+ return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2105
2226
  }