@agentshouse/mdruntime 0.1.0-c8ca66ac9e99d → 0.1.0-c8f3163f32f93

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 (47) hide show
  1. package/AGENTS.md +5 -5
  2. package/dist/adapter.d.ts +24 -2
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/bootstrap.d.ts +2 -2
  5. package/dist/bootstrap.d.ts.map +1 -1
  6. package/dist/bootstrap.js +28 -6
  7. package/dist/chat/acts.d.ts +3 -4
  8. package/dist/chat/acts.d.ts.map +1 -1
  9. package/dist/chat/acts.js +10 -10
  10. package/dist/chat/provider.d.ts.map +1 -1
  11. package/dist/chat/provider.js +25 -50
  12. package/dist/client.d.ts +2 -2
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +1 -1
  15. package/dist/connection.d.ts +1 -0
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +332 -144
  18. package/dist/conversation.d.ts +4 -2
  19. package/dist/conversation.d.ts.map +1 -1
  20. package/dist/conversation.js +143 -58
  21. package/dist/entry.d.ts +17 -0
  22. package/dist/entry.d.ts.map +1 -0
  23. package/dist/entry.js +156 -0
  24. package/dist/fence.d.ts +6 -0
  25. package/dist/fence.d.ts.map +1 -0
  26. package/dist/fence.js +10 -0
  27. package/dist/host.d.ts +2 -1
  28. package/dist/host.d.ts.map +1 -1
  29. package/dist/host.js +5 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/memory.d.ts +1 -0
  33. package/dist/memory.d.ts.map +1 -1
  34. package/dist/memory.js +9 -0
  35. package/dist/outcome.d.ts +4 -0
  36. package/dist/outcome.d.ts.map +1 -1
  37. package/dist/outcome.js +1 -0
  38. package/dist/overlay.d.ts.map +1 -1
  39. package/dist/overlay.js +8 -0
  40. package/dist/reach.js +1 -1
  41. package/dist/save.d.ts +1 -2
  42. package/dist/save.d.ts.map +1 -1
  43. package/dist/save.js +6 -5
  44. package/dist/store.d.ts +22 -7
  45. package/dist/store.d.ts.map +1 -1
  46. package/dist/store.js +125 -36
  47. package/package.json +5 -1
@@ -13,13 +13,14 @@ import { validRoot } from './collection.js';
13
13
  import { runtimeError } from './failure.js';
14
14
  import { createLog } from './log.js';
15
15
  import { binaryRecord } from './binary.js';
16
- import { absentBinary, binaryAnswer, calledResult, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
16
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
17
17
  import { refreshRoom } from './refresh.js';
18
18
  import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
19
  import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
20
  import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
21
  import { same, saving, STARTING, transferring, } from './status.js';
22
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
23
+ import { FencedMap } from './fence.js';
23
24
  import { roomDraftRevision } from './identity.js';
24
25
  import { beneath, normalize } from './paths.js';
25
26
  import { openRuntimeStore, } from './store.js';
@@ -38,23 +39,36 @@ function declaredSelection(room) {
38
39
  root !== '' &&
39
40
  selection.roots.every((other, at) => at === index || !within(other, root)));
40
41
  }
42
+ const OPENING_LANE = 'opening';
43
+ const CONVERSATION_LANE = 'conversations';
44
+ const ROOM_READS = 256;
45
+ function roomLane(locator) {
46
+ return `room ${locator}`;
47
+ }
41
48
  export function composeRuntime(options) {
42
49
  const localSaves = options.adapter.localSaves === true;
43
50
  let authorization = options.authorization;
44
51
  let opening;
45
52
  let store;
46
- const replicas = new Map();
47
- const queuesWithoutReplica = new Map();
53
+ const fences = new Map();
54
+ const isFenced = (locator) => fences.has(locator);
55
+ let fencings = 0;
56
+ const lastFenced = new Map();
57
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
58
+ let discoveries = 0;
59
+ const replicas = new FencedMap(isFenced);
60
+ const queuesWithoutReplica = new FencedMap(isFenced);
48
61
  const pendings = new Map();
49
- const roomDrafts = new Map();
62
+ const roomDrafts = new FencedMap(isFenced);
50
63
  const presentations = new Map();
51
64
  let limits = [];
52
65
  let capabilities = [];
53
- const rooms = new Map();
66
+ const rooms = new FencedMap(isFenced);
54
67
  const counted = new Map();
55
68
  const unreadable = new Set();
56
- const models = new Map();
57
- const liveModels = new Map();
69
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
70
+ const models = new FencedMap(isModelFenced);
71
+ const liveModels = new FencedMap(isModelFenced);
58
72
  const invalidModels = new Set();
59
73
  const discovered = new Map();
60
74
  let roomsListed = false;
@@ -97,7 +111,13 @@ export function composeRuntime(options) {
97
111
  let complete = false;
98
112
  let recorded = false;
99
113
  const stale = new Set();
100
- let chain = Promise.resolve();
114
+ let generation = 0;
115
+ let discovering = null;
116
+ let opened = null;
117
+ const owed = new Set();
118
+ const lanes = new Map();
119
+ let roomReads = { generation, free: ROOM_READS, waiting: [] };
120
+ const working = new Set();
101
121
  let mutations = Promise.resolve();
102
122
  const roomListeners = new Set();
103
123
  const refusalListeners = new Set();
@@ -108,7 +128,7 @@ export function composeRuntime(options) {
108
128
  const presentationListeners = new Set();
109
129
  const readSide = createReadSide();
110
130
  let withdrawConversations;
111
- let currencyQueued = false;
131
+ let currencyQueued = null;
112
132
  let socketOpen = false;
113
133
  let serviceMode = 'open';
114
134
  let haltedRefusal = false;
@@ -118,7 +138,7 @@ export function composeRuntime(options) {
118
138
  const memberListeners = new Set();
119
139
  const materialized = new Set();
120
140
  let everyRoom = false;
121
- const transfers = new Map();
141
+ const transfers = new FencedMap(isFenced);
122
142
  const conversations = createConversationStore({
123
143
  agents: declaredAgents,
124
144
  documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
@@ -126,6 +146,7 @@ export function composeRuntime(options) {
126
146
  durably,
127
147
  stream: () => options.adapter.conversations,
128
148
  publish: publishChangeSet,
149
+ online: () => !status.providerUnreachable && refusal === null,
129
150
  closed: () => closed,
130
151
  });
131
152
  const searchIndex = createSearchIndex({
@@ -476,14 +497,24 @@ export function composeRuntime(options) {
476
497
  refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
477
498
  });
478
499
  }
479
- async function applyDiscovery(listed, local) {
500
+ async function applyDiscovery(found, started) {
501
+ for (const room of found) {
502
+ const fence = fences.get(room.locator);
503
+ if (fence !== undefined && fence.erased && fence.at < started)
504
+ fences.delete(room.locator);
505
+ }
506
+ const listed = found.filter((room) => !fences.has(room.locator));
480
507
  const listedLocators = new Set(listed.map((room) => room.locator));
481
508
  for (const locator of listedLocators)
482
509
  scopedRefusals.delete(locator);
483
510
  discovered.clear();
484
511
  for (const room of listed)
485
512
  discovered.set(room.locator, room);
486
- await dropUnlisted(listedLocators, local);
513
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
514
+ if (options.adapter.exhaustiveDiscovery === true)
515
+ await loseRooms(unlisted);
516
+ else
517
+ unlistedKept(unlisted);
487
518
  for (const room of listed) {
488
519
  const held = replicas.get(room.locator)?.confirmed.record;
489
520
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
@@ -494,6 +525,7 @@ export function composeRuntime(options) {
494
525
  }
495
526
  roomsListed = true;
496
527
  emitRooms();
528
+ return listed;
497
529
  }
498
530
  function unlistedHeld(listed) {
499
531
  return [
@@ -505,41 +537,67 @@ export function composeRuntime(options) {
505
537
  ]),
506
538
  ].filter((locator) => !listed.has(locator));
507
539
  }
508
- async function dropUnlisted(listed, local) {
509
- if (unlistedHeld(listed).length === 0)
510
- return;
511
- await exclusive(async () => {
512
- const unlisted = unlistedHeld(listed);
513
- await local.dropRooms(unlisted);
514
- const removed = unlisted.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
515
- room: true,
516
- queue: true,
517
- drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
518
- }));
519
- for (const locator of unlisted) {
520
- for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
521
- pendings.delete(batch.id);
522
- replicas.delete(locator);
523
- queuesWithoutReplica.delete(locator);
540
+ function unlistedKept(unlisted) {
541
+ for (const locator of unlisted) {
542
+ const held = replicas.get(locator)?.confirmed.record;
543
+ if (held !== undefined)
544
+ list(held, false);
545
+ else
524
546
  rooms.delete(locator);
525
- roomDrafts.delete(locator);
526
- transfers.delete(locator);
527
- unreadable.delete(locator);
528
- stale.delete(locator);
529
- scopedRefusals.delete(locator);
530
- sourceRefusals.delete(locator);
531
- declarationRefusals.delete(locator);
532
- for (const attached of ['collection', 'typed-selection']) {
533
- const key = modelKey(locator, attached);
534
- models.delete(key);
535
- liveModels.delete(key);
536
- invalidModels.delete(key);
537
- }
538
- journal.record(`room unlisted ${locator}`);
547
+ }
548
+ }
549
+ function fence(locators) {
550
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
551
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
552
+ room: true,
553
+ queue: true,
554
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
555
+ }));
556
+ for (const locator of fresh) {
557
+ fences.set(locator, { at: discoveries, erased: false });
558
+ lastFenced.set(locator, ++fencings);
559
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
560
+ pendings.delete(batch.id);
561
+ replicas.delete(locator);
562
+ queuesWithoutReplica.delete(locator);
563
+ rooms.delete(locator);
564
+ roomDrafts.delete(locator);
565
+ transfers.delete(locator);
566
+ discovered.delete(locator);
567
+ announced.delete(locator);
568
+ unreadable.delete(locator);
569
+ stale.delete(locator);
570
+ scopedRefusals.delete(locator);
571
+ sourceRefusals.delete(locator);
572
+ declarationRefusals.delete(locator);
573
+ for (const attached of ['collection', 'typed-selection']) {
574
+ const key = modelKey(locator, attached);
575
+ models.delete(key);
576
+ liveModels.delete(key);
577
+ invalidModels.delete(key);
539
578
  }
540
- publishChangeSet(mergeChangeSets(removed));
541
- moveTo({ ...status, ...attention() });
542
- });
579
+ journal.record(`room lost ${locator}`);
580
+ }
581
+ if (fresh.length === 0)
582
+ return fresh;
583
+ publishChangeSet(mergeChangeSets(removed));
584
+ for (const locator of fresh)
585
+ materialized.delete(locator);
586
+ moveTo({ ...status, ...attention() });
587
+ return fresh;
588
+ }
589
+ async function erased(local, locators) {
590
+ await local.erase(locators);
591
+ for (const locator of locators) {
592
+ const held = fences.get(locator);
593
+ if (held !== undefined)
594
+ held.erased = true;
595
+ }
596
+ }
597
+ async function loseRooms(locators) {
598
+ const fresh = fence(locators);
599
+ if (fresh.length > 0)
600
+ await durably((local) => erased(local, fresh));
543
601
  }
544
602
  function restored(key, persisted) {
545
603
  const prior = models.get(key);
@@ -644,8 +702,15 @@ export function composeRuntime(options) {
644
702
  return lastCompleted;
645
703
  }
646
704
  async function open() {
647
- const opened = await openRuntimeStore();
705
+ if (closed)
706
+ throw new Error('runtime closed');
707
+ const opened = await openRuntimeStore(isFenced);
648
708
  store = opened;
709
+ const interrupted = await opened.fences();
710
+ for (const locator of interrupted) {
711
+ fences.set(locator, { at: discoveries, erased: false });
712
+ lastFenced.set(locator, ++fencings);
713
+ }
649
714
  const kept = options.shell.generation;
650
715
  const storedGeneration = await opened.generation();
651
716
  const loss = detectStorageLoss(storedGeneration, kept);
@@ -659,6 +724,8 @@ export function composeRuntime(options) {
659
724
  emitStorageLoss();
660
725
  }
661
726
  const lastCompleted = await readState(opened);
727
+ if (interrupted.length > 0)
728
+ await erased(opened, interrupted);
662
729
  moveTo({ ...status, lastCompleted, ...attention() });
663
730
  return opened;
664
731
  }
@@ -783,10 +850,10 @@ export function composeRuntime(options) {
783
850
  selection: taken(answers?.selection, held.selection, 'typed-selection'),
784
851
  };
785
852
  }
786
- function publish(local, room, confirmed, landing = {}) {
853
+ function publish(local, room, confirmed, landing, live) {
787
854
  const { base, stored } = landing;
788
855
  return exclusive(async () => {
789
- if (!continuing())
856
+ if (!live())
790
857
  return true;
791
858
  const prior = replicas.get(room.locator);
792
859
  if (base !== undefined &&
@@ -814,6 +881,7 @@ export function composeRuntime(options) {
814
881
  list(room, true);
815
882
  publishChangeSet(set);
816
883
  moveTo({ ...status, ...attention() });
884
+ options.adapter.roomStream?.published(room.locator, roomVersion);
817
885
  return true;
818
886
  });
819
887
  }
@@ -936,6 +1004,8 @@ export function composeRuntime(options) {
936
1004
  catch {
937
1005
  return { outcome: 'refused', code: 'durable-write', retryable: true };
938
1006
  }
1007
+ if (fences.has(locator))
1008
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
939
1009
  for (const origin of withdrawn.keys())
940
1010
  pendings.delete(origin);
941
1011
  for (const [id, drafts] of drafted) {
@@ -965,6 +1035,8 @@ export function composeRuntime(options) {
965
1035
  return;
966
1036
  }
967
1037
  await local.submit(locator, identifier, submission, lost);
1038
+ if (fences.has(locator))
1039
+ return;
968
1040
  pendings.set(identifier, { ...state, submission, lost });
969
1041
  const replica = replicas.get(locator);
970
1042
  if (replica !== undefined)
@@ -975,6 +1047,8 @@ export function composeRuntime(options) {
975
1047
  if (!continuing())
976
1048
  throw new Error('connection refused');
977
1049
  await local.keepUpload(locator, identifier, upload);
1050
+ if (fences.has(locator))
1051
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
978
1052
  pendings.set(identifier, { ...pendings.get(identifier), upload });
979
1053
  if (!continuing())
980
1054
  throw new Error('connection refused');
@@ -990,6 +1064,7 @@ export function composeRuntime(options) {
990
1064
  }
991
1065
  if (closed || authorization === undefined || refusal !== null)
992
1066
  return UNCERTAIN;
1067
+ const since = fencings;
993
1068
  const transfer = ['transfer', 'history', locator, path];
994
1069
  if (query.revision !== undefined)
995
1070
  transfer.push(query.revision);
@@ -1004,6 +1079,8 @@ export function composeRuntime(options) {
1004
1079
  ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
1005
1080
  });
1006
1081
  journal.record(`outcome history ${result.kind}`);
1082
+ if (fencedSince(locator, since))
1083
+ return failure({ kind: 'room-not-readable', locator });
1007
1084
  if (result.kind === 'scoped-refusal') {
1008
1085
  const room = replicas.get(result.locator)?.confirmed.record;
1009
1086
  if (room !== undefined)
@@ -1026,6 +1103,12 @@ export function composeRuntime(options) {
1026
1103
  }
1027
1104
  return null;
1028
1105
  }
1106
+ function binaryRoom(request) {
1107
+ if (request.kind === 'path')
1108
+ return request.locator;
1109
+ const scope = options.adapter.referrerScope(request.referrer);
1110
+ return scope?.kind === 'room' ? scope.locator : null;
1111
+ }
1029
1112
  async function readBinary(request) {
1030
1113
  if (closed || refusal !== null)
1031
1114
  return UNCERTAIN;
@@ -1037,12 +1120,16 @@ export function composeRuntime(options) {
1037
1120
  }
1038
1121
  if (closed || authorization === undefined || refusal !== null)
1039
1122
  return UNCERTAIN;
1123
+ const since = fencings;
1040
1124
  journal.record(request.kind === 'path'
1041
1125
  ? `transfer binary ${request.locator} ${request.path}`
1042
1126
  : `transfer binary ${request.fileRef}`);
1043
1127
  try {
1044
1128
  const result = await options.adapter.binary(authorization, request);
1045
1129
  journal.record(`outcome binary ${result.kind}`);
1130
+ const locator = binaryRoom(request);
1131
+ if (locator !== null && fencedSince(locator, since))
1132
+ return failure({ kind: 'room-not-readable', locator });
1046
1133
  if (result.kind === 'scoped-refusal') {
1047
1134
  const room = replicas.get(result.locator)?.confirmed.record;
1048
1135
  if (room !== undefined)
@@ -1166,11 +1253,11 @@ export function composeRuntime(options) {
1166
1253
  options.adapter.reached(locator, written, roomVersion) &&
1167
1254
  !options.adapter.reached(locator, roomVersion, written));
1168
1255
  }
1169
- async function pullRefresh(authorized, confirmed) {
1170
- if (!continuing())
1256
+ async function pullRefresh(authorized, confirmed, live) {
1257
+ if (!live())
1171
1258
  return unconfirmed;
1172
1259
  try {
1173
- const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
1260
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => (live() ? remodel(confirmed.record.locator, models, held) : held), live);
1174
1261
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
1175
1262
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
1176
1263
  return result;
@@ -1180,33 +1267,39 @@ export function composeRuntime(options) {
1180
1267
  throw error;
1181
1268
  }
1182
1269
  }
1183
- function staging(local) {
1270
+ function staging(local, live) {
1184
1271
  return {
1185
1272
  held: (locator) => transfers.get(locator) ?? null,
1186
1273
  staged: (record) => local.staged(record),
1187
1274
  stage: (record, page) => exclusive(async () => {
1188
- if (!continuing())
1275
+ if (!live())
1189
1276
  return false;
1190
1277
  await local.stage(record, page);
1191
1278
  transfers.set(record.locator, record);
1192
1279
  return true;
1193
1280
  }),
1194
1281
  abandon: (locator) => exclusive(async () => {
1195
- if (!continuing())
1282
+ if (!live())
1196
1283
  return;
1197
1284
  await local.abandon(locator);
1198
1285
  transfers.delete(locator);
1199
1286
  }),
1200
1287
  };
1201
1288
  }
1202
- async function pullBootstrap(authorized, room, local) {
1203
- if (!continuing())
1289
+ function reusable(room) {
1290
+ const record = replicas.get(room.locator)?.confirmed.record;
1291
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1292
+ return null;
1293
+ return { documents: record.documents, revisions: record.revisions };
1294
+ }
1295
+ async function pullBootstrap(authorized, room, local, live) {
1296
+ if (!live())
1204
1297
  return unconfirmed;
1205
1298
  try {
1206
1299
  const result = await bootstrapRoom(options.adapter, authorized, room, {
1207
1300
  collection: modelOf(room.locator),
1208
1301
  selection: modelOf(room.locator, 'typed-selection'),
1209
- }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
1302
+ }, (models, held) => (live() ? remodel(room.locator, models, held) : held), live, staging(local, live), reusable(room));
1210
1303
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
1211
1304
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
1212
1305
  return result;
@@ -1216,11 +1309,11 @@ export function composeRuntime(options) {
1216
1309
  throw error;
1217
1310
  }
1218
1311
  }
1219
- async function advance(room, local, authorized) {
1312
+ async function advance(room, local, authorized, live) {
1220
1313
  const held = replicas.get(room.locator);
1221
1314
  if (held !== undefined) {
1222
- const refreshed = await pullRefresh(authorized, held.confirmed);
1223
- if (!continuing())
1315
+ const refreshed = await pullRefresh(authorized, held.confirmed, live);
1316
+ if (!live())
1224
1317
  return unconfirmed;
1225
1318
  if (refreshed.kind === 'still-current') {
1226
1319
  stale.delete(room.locator);
@@ -1234,24 +1327,28 @@ export function composeRuntime(options) {
1234
1327
  return null;
1235
1328
  }
1236
1329
  if (refreshed.kind === 'advanced') {
1237
- if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1330
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }, live))) {
1238
1331
  const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1239
1332
  const reached = now !== undefined &&
1240
1333
  options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1241
- return reached ? null : advance(room, local, authorized);
1334
+ return reached ? null : advance(room, local, authorized, live);
1242
1335
  }
1243
1336
  await retainModel(local, room, 'collection', modelOf(room.locator));
1244
1337
  return null;
1245
1338
  }
1246
1339
  if (refreshed.kind !== 'beyond-horizon')
1247
1340
  return refreshed;
1341
+ if (opened !== null) {
1342
+ owed.add(room.locator);
1343
+ return unconfirmed;
1344
+ }
1248
1345
  }
1249
- const bootstrapped = await pullBootstrap(authorized, room, local);
1250
- if (!continuing())
1346
+ const bootstrapped = await pullBootstrap(authorized, room, local, live);
1347
+ if (!live())
1251
1348
  return unconfirmed;
1252
1349
  if (bootstrapped.kind !== 'staged')
1253
1350
  return bootstrapped;
1254
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1351
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1255
1352
  return null;
1256
1353
  }
1257
1354
  async function withhold(withheld, room, local, progress) {
@@ -1331,7 +1428,7 @@ export function composeRuntime(options) {
1331
1428
  if (applied.kind !== 'staged') {
1332
1429
  const state = pendings.get(submitted.id);
1333
1430
  await local.accept(room.locator, submitted.id, accepted.roomVersion);
1334
- if (state !== undefined)
1431
+ if (state !== undefined && !fences.has(room.locator))
1335
1432
  pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1336
1433
  emitQueue(room.locator);
1337
1434
  return applied;
@@ -1380,7 +1477,7 @@ export function composeRuntime(options) {
1380
1477
  let result;
1381
1478
  journal.record(`submission ${room.locator} ${head.id}`);
1382
1479
  await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1383
- if (discoveryPaused)
1480
+ if (discoveryPaused || fences.has(room.locator))
1384
1481
  return;
1385
1482
  if (writesHeld()) {
1386
1483
  await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
@@ -1443,7 +1540,7 @@ export function composeRuntime(options) {
1443
1540
  result.kind === 'expired' ||
1444
1541
  result.kind === 'retryable-refusal') {
1445
1542
  if (result.kind === 'expired') {
1446
- const withheld = await advance(room, local, authorized);
1543
+ const withheld = await advance(room, local, authorized, continuing);
1447
1544
  if (withheld !== null)
1448
1545
  return withhold(withheld, room, local, progress);
1449
1546
  }
@@ -1481,7 +1578,7 @@ export function composeRuntime(options) {
1481
1578
  });
1482
1579
  if (result.kind === 'expired')
1483
1580
  continue;
1484
- const withheld = await advance(room, local, authorized);
1581
+ const withheld = await advance(room, local, authorized, continuing);
1485
1582
  if (withheld !== null)
1486
1583
  return withhold(withheld, room, local, progress);
1487
1584
  continue;
@@ -1498,9 +1595,9 @@ export function composeRuntime(options) {
1498
1595
  const awaited = new Promise((resolve) => {
1499
1596
  release = resolve;
1500
1597
  });
1501
- answering.set(locator, awaited);
1598
+ answering.set(locator, { generation, answered: awaited });
1502
1599
  return () => {
1503
- if (answering.get(locator) === awaited)
1600
+ if (answering.get(locator)?.answered === awaited)
1504
1601
  answering.delete(locator);
1505
1602
  release();
1506
1603
  };
@@ -1616,8 +1713,10 @@ export function composeRuntime(options) {
1616
1713
  ];
1617
1714
  });
1618
1715
  }
1619
- async function synchronize(listed, local, progress, attempt) {
1716
+ async function synchronize(listed, local, progress, attempt, live) {
1620
1717
  const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1718
+ if (!live())
1719
+ return;
1621
1720
  const ready = listed.filter((_, index) => {
1622
1721
  const outcome = prepared[index];
1623
1722
  return outcome?.status === 'fulfilled' && outcome.value;
@@ -1626,9 +1725,10 @@ export function composeRuntime(options) {
1626
1725
  options.adapter.roomStream?.hold(heldRooms(ready));
1627
1726
  handed = true;
1628
1727
  }
1728
+ discovering?.release();
1629
1729
  const loaded = await Promise.allSettled(ready
1630
1730
  .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1631
- .map((room) => loadRoom(room, local, progress)));
1731
+ .map((room) => roomTask(room.locator, (held) => loadRoom(room, local, progress, held))));
1632
1732
  const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1633
1733
  if (failed !== undefined)
1634
1734
  throw failed.reason;
@@ -1657,14 +1757,14 @@ export function composeRuntime(options) {
1657
1757
  }
1658
1758
  return true;
1659
1759
  }
1660
- async function caughtUp(room, local, progress) {
1760
+ async function caughtUp(room, local, progress, live) {
1661
1761
  const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1662
- if (held === undefined || authorization === undefined)
1762
+ if (held === undefined || authorization === undefined || !live())
1663
1763
  return;
1664
1764
  const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1665
1765
  if (!behind)
1666
1766
  return;
1667
- const withheld = await advance(room, local, authorization);
1767
+ const withheld = await advance(room, local, authorization, live);
1668
1768
  if (withheld !== null)
1669
1769
  await withhold(withheld, room, local, progress);
1670
1770
  }
@@ -1674,39 +1774,41 @@ export function composeRuntime(options) {
1674
1774
  return model.status === 'unavailable' && model.reason === 'revoked';
1675
1775
  });
1676
1776
  }
1677
- async function loadRoom(room, local, progress) {
1678
- if (discoveryPaused || authorization === undefined)
1777
+ async function loadRoom(room, local, progress, live) {
1778
+ if (!live() || authorization === undefined)
1679
1779
  return;
1680
1780
  if (replicas.has(room.locator) && revoked(room.locator)) {
1681
- await reload(room, local, progress);
1781
+ await reload(room, local, progress, live);
1682
1782
  return;
1683
1783
  }
1684
- const withheld = await advance(room, local, authorization);
1784
+ const withheld = await advance(room, local, authorization, live);
1685
1785
  if (withheld !== null) {
1686
1786
  await withhold(withheld, room, local, progress);
1687
1787
  return;
1688
1788
  }
1689
- await caughtUp(room, local, progress);
1789
+ await caughtUp(room, local, progress, live);
1690
1790
  }
1691
- async function reload(room, local, progress) {
1692
- if (discoveryPaused || authorization === undefined)
1791
+ async function reload(room, local, progress, live) {
1792
+ if (!live() || authorization === undefined)
1693
1793
  return;
1694
- const bootstrapped = await pullBootstrap(authorization, room, local);
1695
- if (!continuing())
1794
+ const bootstrapped = await pullBootstrap(authorization, room, local, live);
1795
+ if (!live())
1696
1796
  return;
1697
1797
  if (bootstrapped.kind !== 'staged') {
1698
1798
  await withhold(bootstrapped, room, local, progress);
1699
1799
  return;
1700
1800
  }
1701
- await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1702
- await caughtUp(room, local, progress);
1801
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored }, live);
1802
+ await caughtUp(room, local, progress, live);
1703
1803
  }
1704
- async function discoverRooms(local, progress, attempt) {
1804
+ async function discoverRooms(local, progress, attempt, live) {
1705
1805
  if (closed || authorization === undefined) {
1706
1806
  notifyReady();
1707
1807
  return;
1708
1808
  }
1709
1809
  moveTo(transferring(status));
1810
+ discoveries += 1;
1811
+ const started = discoveries;
1710
1812
  let result;
1711
1813
  try {
1712
1814
  result = await options.adapter.discover(authorization);
@@ -1714,7 +1816,8 @@ export function composeRuntime(options) {
1714
1816
  catch {
1715
1817
  result = { kind: 'transport-uncertainty' };
1716
1818
  }
1717
- if (closed) {
1819
+ if (!live()) {
1820
+ progress.complete = false;
1718
1821
  notifyReady();
1719
1822
  return;
1720
1823
  }
@@ -1784,8 +1887,7 @@ export function composeRuntime(options) {
1784
1887
  member = signedIn;
1785
1888
  emitMember();
1786
1889
  }
1787
- await applyDiscovery(result.rooms, local);
1788
- await synchronize(result.rooms, local, progress, attempt);
1890
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt, live);
1789
1891
  }
1790
1892
  async function ensureAuthorization() {
1791
1893
  if (authorization !== undefined)
@@ -1799,20 +1901,50 @@ export function composeRuntime(options) {
1799
1901
  throw error;
1800
1902
  }
1801
1903
  }
1802
- function task(work) {
1904
+ async function admittedRead(work) {
1905
+ if (roomReads.generation !== generation) {
1906
+ roomReads = { generation, free: ROOM_READS, waiting: [] };
1907
+ }
1908
+ const reads = roomReads;
1909
+ while (reads.free === 0)
1910
+ await new Promise((admit) => reads.waiting.push(admit));
1911
+ reads.free -= 1;
1912
+ try {
1913
+ await work();
1914
+ }
1915
+ finally {
1916
+ reads.free += 1;
1917
+ reads.waiting.shift()?.();
1918
+ }
1919
+ }
1920
+ function roomTask(locator, work) {
1921
+ return task(roomLane(locator), (live) => admittedRead(() => work(live)));
1922
+ }
1923
+ function task(lane, work) {
1803
1924
  if (stopping)
1804
- return chain;
1805
- chain = chain.then(work, work).catch(() => undefined);
1806
- return chain;
1925
+ return Promise.resolve();
1926
+ const held = lanes.get(lane);
1927
+ const current = held?.generation === generation ? held : { generation, chain: Promise.resolve() };
1928
+ const turn = current.generation;
1929
+ const live = () => turn === generation && continuing();
1930
+ const run = current.chain.then(() => work(live)).catch(() => undefined);
1931
+ current.chain = run;
1932
+ lanes.set(lane, current);
1933
+ working.add(run);
1934
+ void run.finally(() => working.delete(run));
1935
+ return run;
1807
1936
  }
1808
1937
  function sync(attempt) {
1809
- return task(async () => {
1810
- if (closed || stopping || discoveryPaused) {
1938
+ return task(OPENING_LANE, async (live) => {
1939
+ if (!live()) {
1811
1940
  notifyReady();
1812
1941
  return;
1813
1942
  }
1814
1943
  const progress = { complete: true, reachable: true };
1815
- underway = attempt === 'opened' ? progress : null;
1944
+ const gate = discover();
1945
+ if (attempt === 'opened')
1946
+ opened = generation;
1947
+ underway = attempt === 'opened' ? { generation, progress } : null;
1816
1948
  if (attempt === 'opened')
1817
1949
  streamed = progress;
1818
1950
  complete = false;
@@ -1821,12 +1953,12 @@ export function composeRuntime(options) {
1821
1953
  handed = false;
1822
1954
  try {
1823
1955
  const local = await openLocal();
1824
- if (closed)
1956
+ if (!live())
1825
1957
  return;
1826
1958
  await ensureAuthorization();
1827
- if (closed)
1959
+ if (!live())
1828
1960
  return;
1829
- await discoverRooms(local, progress, attempt);
1961
+ await discoverRooms(local, progress, attempt, live);
1830
1962
  }
1831
1963
  catch {
1832
1964
  progress.complete = false;
@@ -1834,17 +1966,21 @@ export function composeRuntime(options) {
1834
1966
  progress.reachable = false;
1835
1967
  }
1836
1968
  finally {
1969
+ gate.release();
1837
1970
  notifyReady();
1838
1971
  }
1972
+ if (!live())
1973
+ return;
1839
1974
  if (attempt !== 'opened') {
1840
- await finishOpening(progress);
1975
+ await finishOpening(progress, live);
1841
1976
  return;
1842
1977
  }
1843
1978
  if (handed || closed || progress.reachable)
1844
1979
  return;
1845
1980
  underway = null;
1981
+ opened = null;
1846
1982
  journal.record('connection reopen');
1847
- await rest(progress);
1983
+ await rest(progress, live);
1848
1984
  options.adapter.roomStream?.reopen();
1849
1985
  });
1850
1986
  }
@@ -1865,24 +2001,38 @@ export function composeRuntime(options) {
1865
2001
  });
1866
2002
  }
1867
2003
  function applyOpening(expired) {
1868
- return task(async () => {
1869
- const progress = underway;
2004
+ const progress = underway?.generation === generation ? underway.progress : null;
2005
+ const cycle = progress ?? { complete: true, reachable: true };
2006
+ const replacing = reloadNow((room) => expired.includes(room.locator), cycle);
2007
+ return task(OPENING_LANE, async (live) => {
1870
2008
  underway = null;
1871
- if (closed || stopping)
2009
+ if (!live())
1872
2010
  return;
1873
2011
  if (progress === null) {
1874
- await reloadExpired(expired);
2012
+ await replacing;
2013
+ await rest(cycle, live);
1875
2014
  return;
1876
2015
  }
2016
+ await roomsCarried(expired);
2017
+ if (!live())
2018
+ return;
2019
+ if (opened === generation)
2020
+ opened = null;
2021
+ const replaced = new Set(owed);
2022
+ owed.clear();
1877
2023
  try {
1878
2024
  const local = await openLocal();
1879
2025
  const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1880
- const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1881
- const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
2026
+ const reloading = held.filter((room) => !expired.includes(room.locator) &&
2027
+ (replaced.has(room.locator) || revoked(room.locator)));
2028
+ const reloads = Promise.allSettled(reloading.map((room) => roomTask(room.locator, (current) => reload(room, local, progress, current))));
2029
+ const reloaded = [...(await reloads), ...(await replacing)];
1882
2030
  if (progress.reachable) {
1883
- for (const room of held)
1884
- if (!reloading.includes(room))
2031
+ for (const room of held) {
2032
+ if (!expired.includes(room.locator) && !reloading.includes(room)) {
1885
2033
  await confirmModels(room, local);
2034
+ }
2035
+ }
1886
2036
  }
1887
2037
  const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1888
2038
  if (failed !== undefined)
@@ -1892,21 +2042,21 @@ export function composeRuntime(options) {
1892
2042
  progress.complete = false;
1893
2043
  progress.reachable = false;
1894
2044
  }
1895
- await finishOpening(progress);
2045
+ if (live())
2046
+ await finishOpening(progress, live);
1896
2047
  });
1897
2048
  }
1898
- async function reloadExpired(expired) {
1899
- const progress = { complete: true, reachable: true };
2049
+ async function reloadNow(replaced, progress) {
1900
2050
  try {
1901
2051
  const local = await openLocal();
1902
- await Promise.allSettled([...discovered.values()]
1903
- .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1904
- .map((room) => reload(room, local, progress)));
2052
+ return await Promise.allSettled([...discovered.values()]
2053
+ .filter((room) => replicas.has(room.locator) && replaced(room))
2054
+ .map((room) => roomTask(room.locator, (live) => reload(room, local, progress, live))));
1905
2055
  }
1906
2056
  catch {
1907
2057
  progress.reachable = false;
2058
+ return [];
1908
2059
  }
1909
- await rest(progress);
1910
2060
  }
1911
2061
  async function confirmModels(room, local) {
1912
2062
  for (const attached of ['collection', 'typed-selection']) {
@@ -1958,9 +2108,9 @@ export function composeRuntime(options) {
1958
2108
  submitLater(locator);
1959
2109
  }
1960
2110
  }
1961
- async function finishOpening(progress) {
2111
+ async function finishOpening(progress, live) {
1962
2112
  try {
1963
- if (closed || discoveryPaused) {
2113
+ if (!live()) {
1964
2114
  progress.complete = false;
1965
2115
  return;
1966
2116
  }
@@ -1970,6 +2120,8 @@ export function composeRuntime(options) {
1970
2120
  conversations.reconnected();
1971
2121
  const reading = { complete: true, reachable: true };
1972
2122
  await conversations.catchUp(reading);
2123
+ if (progress.reachable && reading.reachable)
2124
+ await conversations.resend(reading);
1973
2125
  if (!reading.reachable)
1974
2126
  progress.reachable = false;
1975
2127
  }
@@ -1977,12 +2129,14 @@ export function composeRuntime(options) {
1977
2129
  progress.complete = false;
1978
2130
  }
1979
2131
  finally {
1980
- complete = progress.complete;
1981
- await rest(progress);
2132
+ if (live()) {
2133
+ complete = progress.complete;
2134
+ await rest(progress, live);
2135
+ }
1982
2136
  }
1983
2137
  }
1984
- async function rest(progress) {
1985
- if (closed)
2138
+ async function rest(progress, live = () => !closed) {
2139
+ if (!live())
1986
2140
  return;
1987
2141
  let lastCompleted = status.lastCompleted;
1988
2142
  let synced = complete && stale.size === 0 && !pending();
@@ -1990,6 +2144,8 @@ export function composeRuntime(options) {
1990
2144
  const completedAt = new Date().toISOString();
1991
2145
  try {
1992
2146
  await (await openLocal()).complete(completedAt);
2147
+ if (!live())
2148
+ return;
1993
2149
  recorded = true;
1994
2150
  lastCompleted = completedAt;
1995
2151
  }
@@ -2004,11 +2160,23 @@ export function composeRuntime(options) {
2004
2160
  ...attention(),
2005
2161
  });
2006
2162
  }
2163
+ function discover() {
2164
+ let release = () => { };
2165
+ const done = new Promise((resolve) => {
2166
+ release = resolve;
2167
+ });
2168
+ discovering = { generation, done, release };
2169
+ return discovering;
2170
+ }
2007
2171
  function applyRoom(locator, roomVersion) {
2008
2172
  announced.set(locator, roomVersion);
2009
- return task(async () => {
2010
- await answering.get(locator);
2011
- if (closed || stopping || discoveryPaused || authorization === undefined)
2173
+ return task(roomLane(locator), async (live) => {
2174
+ if (discovering?.generation === generation)
2175
+ await discovering.done;
2176
+ const saving = answering.get(locator);
2177
+ if (saving?.generation === generation)
2178
+ await saving.answered;
2179
+ if (!live() || authorization === undefined)
2012
2180
  return;
2013
2181
  const room = discovered.get(locator);
2014
2182
  const held = replicas.get(locator);
@@ -2019,25 +2187,41 @@ export function composeRuntime(options) {
2019
2187
  options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2020
2188
  return;
2021
2189
  }
2022
- const during = underway;
2190
+ const during = underway?.generation === generation ? underway.progress : null;
2023
2191
  const progress = during ?? { complete: true, reachable: true };
2024
2192
  if (during === null)
2025
2193
  moveTo(transferring(status));
2026
2194
  try {
2027
2195
  const local = await openLocal();
2028
- const withheld = await advance(room, local, authorization);
2029
- if (withheld !== null)
2030
- await withhold(withheld, room, local, progress);
2196
+ await admittedRead(async () => {
2197
+ if (!live() || authorization === undefined)
2198
+ return;
2199
+ const withheld = await advance(room, local, authorization, live);
2200
+ if (withheld !== null)
2201
+ await withhold(withheld, room, local, progress);
2202
+ });
2031
2203
  }
2032
2204
  catch {
2033
2205
  stale.add(locator);
2034
2206
  }
2035
2207
  finally {
2036
2208
  if (during === null)
2037
- await rest(progress);
2209
+ await rest(progress, live);
2038
2210
  }
2039
2211
  });
2040
2212
  }
2213
+ function roomsCarried(replacing) {
2214
+ const replaced = new Set(replacing.map(roomLane));
2215
+ return Promise.all([...lanes]
2216
+ .filter(([lane, held]) => lane !== OPENING_LANE && !replaced.has(lane) && held.generation === generation)
2217
+ .map(([, held]) => held.chain));
2218
+ }
2219
+ const receipts = {
2220
+ heads: async () => (await openLocal()).receivedHeads(),
2221
+ parts: async (key, count) => (await openLocal()).receivedParts(key, count),
2222
+ keep: async (key, head, part) => (await openLocal()).keepReceived(key, head, part),
2223
+ drop: async (keys) => (await openLocal()).dropReceived(keys),
2224
+ };
2041
2225
  function observeStream() {
2042
2226
  const stream = options.adapter.roomStream;
2043
2227
  if (stream === undefined || withdrawStream !== undefined)
@@ -2047,6 +2231,7 @@ export function composeRuntime(options) {
2047
2231
  return;
2048
2232
  if (event.kind === 'opened') {
2049
2233
  socketOpen = true;
2234
+ generation += 1;
2050
2235
  void sync('opened');
2051
2236
  }
2052
2237
  else if (event.kind === 'closed')
@@ -2055,7 +2240,7 @@ export function composeRuntime(options) {
2055
2240
  void applyOpening(event.expired);
2056
2241
  else
2057
2242
  void applyRoom(event.locator, event.roomVersion);
2058
- });
2243
+ }, receipts);
2059
2244
  }
2060
2245
  function writesHeld() {
2061
2246
  return serviceMode === 'write-halted' || haltedRefusal;
@@ -2093,14 +2278,15 @@ export function composeRuntime(options) {
2093
2278
  });
2094
2279
  }
2095
2280
  function scheduleCurrency() {
2096
- if (currencyQueued || closed || stopping)
2281
+ if (currencyQueued === generation || closed || stopping)
2097
2282
  return;
2098
- currencyQueued = true;
2099
- void task(runCurrency);
2283
+ currencyQueued = generation;
2284
+ void task(CONVERSATION_LANE, runCurrency);
2100
2285
  }
2101
- async function runCurrency() {
2102
- currencyQueued = false;
2103
- if (closed || stopping || discoveryPaused)
2286
+ async function runCurrency(live) {
2287
+ if (currencyQueued === generation)
2288
+ currencyQueued = null;
2289
+ if (!live())
2104
2290
  return;
2105
2291
  await conversations.reconcile();
2106
2292
  if (conversations.current()) {
@@ -2115,7 +2301,7 @@ export function composeRuntime(options) {
2115
2301
  catch {
2116
2302
  progress.reachable = false;
2117
2303
  }
2118
- await rest(progress);
2304
+ await rest(progress, live);
2119
2305
  }
2120
2306
  function close() {
2121
2307
  if (closed)
@@ -2146,7 +2332,7 @@ export function composeRuntime(options) {
2146
2332
  async function stop() {
2147
2333
  stopping = true;
2148
2334
  stopTimer();
2149
- await chain.catch(() => undefined);
2335
+ await Promise.all(working);
2150
2336
  await Promise.all([...submitting.values()].map((run) => run.done));
2151
2337
  await mutations;
2152
2338
  close();
@@ -2398,14 +2584,16 @@ export function composeRuntime(options) {
2398
2584
  if (refusal === null || !refusal.reauthorizationOffered)
2399
2585
  return;
2400
2586
  journal.record('connection reauthorize');
2401
- await chain.catch(() => undefined);
2587
+ await Promise.all([...lanes.values()]
2588
+ .filter((held) => held.generation === generation)
2589
+ .map((held) => held.chain));
2402
2590
  await mutations;
2403
2591
  authorization = undefined;
2404
2592
  discoveryPaused = false;
2405
2593
  if (options.adapter.roomStream === undefined)
2406
2594
  await sync('unstreamed');
2407
2595
  else
2408
- await task(ensureAuthorization);
2596
+ await task(OPENING_LANE, ensureAuthorization);
2409
2597
  },
2410
2598
  };
2411
2599
  const reachable = {
@@ -2417,5 +2605,5 @@ export function composeRuntime(options) {
2417
2605
  watchPlan,
2418
2606
  search: searched,
2419
2607
  };
2420
- return { md, stop, watch, within: (reach) => narrowed(reachable, reach) };
2608
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
2421
2609
  }