@agentshouse/mdruntime 0.1.0-alpha.9 → 0.1.0-c0105dbd22806

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 (214) hide show
  1. package/AGENTS.md +299 -0
  2. package/README.md +54 -0
  3. package/dist/adapter.d.ts +246 -40
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -10
  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/attempt.d.ts +16 -0
  10. package/dist/attempt.d.ts.map +1 -0
  11. package/dist/attempt.js +70 -0
  12. package/dist/binary.d.ts +23 -0
  13. package/dist/binary.d.ts.map +1 -0
  14. package/dist/binary.js +8 -0
  15. package/dist/bootstrap.d.ts +17 -7
  16. package/dist/bootstrap.d.ts.map +1 -1
  17. package/dist/bootstrap.js +97 -41
  18. package/dist/change-set.d.ts +33 -0
  19. package/dist/change-set.d.ts.map +1 -0
  20. package/dist/change-set.js +90 -0
  21. package/dist/change.d.ts +24 -0
  22. package/dist/change.d.ts.map +1 -1
  23. package/dist/change.js +31 -5
  24. package/dist/chat/acts.d.ts +34 -0
  25. package/dist/chat/acts.d.ts.map +1 -0
  26. package/dist/chat/acts.js +36 -0
  27. package/dist/chat/context.d.ts +3 -0
  28. package/dist/chat/context.d.ts.map +1 -0
  29. package/dist/chat/context.js +10 -0
  30. package/dist/chat/input.d.ts +9 -0
  31. package/dist/chat/input.d.ts.map +1 -0
  32. package/dist/chat/input.js +99 -0
  33. package/dist/chat/kit.d.ts +60 -0
  34. package/dist/chat/kit.d.ts.map +1 -0
  35. package/dist/chat/kit.js +123 -0
  36. package/dist/chat/lasting.d.ts +6 -0
  37. package/dist/chat/lasting.d.ts.map +1 -0
  38. package/dist/chat/lasting.js +19 -0
  39. package/dist/chat/list.d.ts +26 -0
  40. package/dist/chat/list.d.ts.map +1 -0
  41. package/dist/chat/list.js +62 -0
  42. package/dist/chat/markdown.d.ts +20 -0
  43. package/dist/chat/markdown.d.ts.map +1 -0
  44. package/dist/chat/markdown.js +188 -0
  45. package/dist/chat/message.d.ts +6 -0
  46. package/dist/chat/message.d.ts.map +1 -0
  47. package/dist/chat/message.js +192 -0
  48. package/dist/chat/messages.d.ts +21 -0
  49. package/dist/chat/messages.d.ts.map +1 -0
  50. package/dist/chat/messages.js +151 -0
  51. package/dist/chat/name.d.ts +6 -0
  52. package/dist/chat/name.d.ts.map +1 -0
  53. package/dist/chat/name.js +37 -0
  54. package/dist/chat/opened.d.ts +11 -0
  55. package/dist/chat/opened.d.ts.map +1 -0
  56. package/dist/chat/opened.js +17 -0
  57. package/dist/chat/overlay.d.ts +31 -0
  58. package/dist/chat/overlay.d.ts.map +1 -0
  59. package/dist/chat/overlay.js +97 -0
  60. package/dist/chat/parts.d.ts +57 -0
  61. package/dist/chat/parts.d.ts.map +1 -0
  62. package/dist/chat/parts.js +119 -0
  63. package/dist/chat/process.d.ts +20 -0
  64. package/dist/chat/process.d.ts.map +1 -0
  65. package/dist/chat/process.js +32 -0
  66. package/dist/chat/provider.d.ts +63 -0
  67. package/dist/chat/provider.d.ts.map +1 -0
  68. package/dist/chat/provider.js +645 -0
  69. package/dist/chat/question.d.ts +9 -0
  70. package/dist/chat/question.d.ts.map +1 -0
  71. package/dist/chat/question.js +80 -0
  72. package/dist/chat/questions.d.ts +31 -0
  73. package/dist/chat/questions.d.ts.map +1 -0
  74. package/dist/chat/questions.js +111 -0
  75. package/dist/chat/read.d.ts +5 -0
  76. package/dist/chat/read.d.ts.map +1 -0
  77. package/dist/chat/read.js +13 -0
  78. package/dist/chat/ready.d.ts +33 -0
  79. package/dist/chat/ready.d.ts.map +1 -0
  80. package/dist/chat/ready.js +91 -0
  81. package/dist/chat/style.d.ts +4 -0
  82. package/dist/chat/style.d.ts.map +1 -0
  83. package/dist/chat/style.js +302 -0
  84. package/dist/chat.d.ts +14 -0
  85. package/dist/chat.d.ts.map +1 -0
  86. package/dist/chat.js +8 -0
  87. package/dist/client.d.ts +49 -33
  88. package/dist/client.d.ts.map +1 -1
  89. package/dist/client.js +164 -183
  90. package/dist/collection.d.ts +9 -17
  91. package/dist/collection.d.ts.map +1 -1
  92. package/dist/collection.js +83 -89
  93. package/dist/connection.d.ts +66 -48
  94. package/dist/connection.d.ts.map +1 -1
  95. package/dist/connection.js +1964 -1173
  96. package/dist/conversation.d.ts +65 -24
  97. package/dist/conversation.d.ts.map +1 -1
  98. package/dist/conversation.js +675 -334
  99. package/dist/durability.js +6 -6
  100. package/dist/entry.d.ts +17 -0
  101. package/dist/entry.d.ts.map +1 -0
  102. package/dist/entry.js +156 -0
  103. package/dist/expectation.d.ts +21 -0
  104. package/dist/expectation.d.ts.map +1 -0
  105. package/dist/expectation.js +70 -0
  106. package/dist/failure.d.ts +13 -0
  107. package/dist/failure.d.ts.map +1 -0
  108. package/dist/failure.js +19 -0
  109. package/dist/fence.d.ts +6 -0
  110. package/dist/fence.d.ts.map +1 -0
  111. package/dist/fence.js +10 -0
  112. package/dist/file.d.ts +2 -2
  113. package/dist/file.d.ts.map +1 -1
  114. package/dist/file.js +48 -71
  115. package/dist/host.d.ts +15 -3
  116. package/dist/host.d.ts.map +1 -1
  117. package/dist/host.js +109 -127
  118. package/dist/identity.d.ts +4 -0
  119. package/dist/identity.d.ts.map +1 -1
  120. package/dist/identity.js +22 -9
  121. package/dist/index.d.ts +17 -11
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +4 -3
  124. package/dist/log.js +5 -6
  125. package/dist/memory.d.ts +19 -8
  126. package/dist/memory.d.ts.map +1 -1
  127. package/dist/memory.js +304 -216
  128. package/dist/model.d.ts +16 -30
  129. package/dist/model.d.ts.map +1 -1
  130. package/dist/model.js +21 -21
  131. package/dist/observation.d.ts +27 -0
  132. package/dist/observation.d.ts.map +1 -0
  133. package/dist/observation.js +95 -0
  134. package/dist/open.d.ts +2 -0
  135. package/dist/open.d.ts.map +1 -0
  136. package/dist/open.js +1 -0
  137. package/dist/outcome.d.ts +37 -31
  138. package/dist/outcome.d.ts.map +1 -1
  139. package/dist/outcome.js +36 -18
  140. package/dist/overlay.d.ts +22 -4
  141. package/dist/overlay.d.ts.map +1 -1
  142. package/dist/overlay.js +236 -54
  143. package/dist/paths.d.ts +0 -2
  144. package/dist/paths.d.ts.map +1 -1
  145. package/dist/paths.js +6 -29
  146. package/dist/query.d.ts +219 -10
  147. package/dist/query.d.ts.map +1 -1
  148. package/dist/query.js +462 -15
  149. package/dist/reach.d.ts +42 -0
  150. package/dist/reach.d.ts.map +1 -0
  151. package/dist/reach.js +294 -0
  152. package/dist/react.d.ts +41 -0
  153. package/dist/react.d.ts.map +1 -0
  154. package/dist/react.js +64 -0
  155. package/dist/read-side.d.ts +18 -0
  156. package/dist/read-side.d.ts.map +1 -0
  157. package/dist/read-side.js +440 -0
  158. package/dist/refresh.d.ts +15 -5
  159. package/dist/refresh.d.ts.map +1 -1
  160. package/dist/refresh.js +64 -41
  161. package/dist/replica.d.ts +28 -5
  162. package/dist/replica.d.ts.map +1 -1
  163. package/dist/replica.js +249 -40
  164. package/dist/room-draft.d.ts +144 -0
  165. package/dist/room-draft.d.ts.map +1 -0
  166. package/dist/room-draft.js +438 -0
  167. package/dist/rows.d.ts +46 -0
  168. package/dist/rows.d.ts.map +1 -0
  169. package/dist/rows.js +145 -0
  170. package/dist/save.d.ts +33 -5
  171. package/dist/save.d.ts.map +1 -1
  172. package/dist/save.js +630 -366
  173. package/dist/search.d.ts +62 -0
  174. package/dist/search.d.ts.map +1 -0
  175. package/dist/search.js +125 -0
  176. package/dist/status.d.ts +6 -8
  177. package/dist/status.d.ts.map +1 -1
  178. package/dist/status.js +8 -20
  179. package/dist/store.d.ts +89 -34
  180. package/dist/store.d.ts.map +1 -1
  181. package/dist/store.js +513 -132
  182. package/dist/transfer.d.ts +5 -0
  183. package/dist/transfer.d.ts.map +1 -1
  184. package/dist/transfer.js +1 -5
  185. package/dist/wire.d.ts +59 -19
  186. package/dist/wire.d.ts.map +1 -1
  187. package/dist/wire.js +88 -105
  188. package/package.json +34 -16
  189. package/dist/adapter.js.map +0 -1
  190. package/dist/bootstrap.js.map +0 -1
  191. package/dist/change.js.map +0 -1
  192. package/dist/client.js.map +0 -1
  193. package/dist/collection.js.map +0 -1
  194. package/dist/connection.js.map +0 -1
  195. package/dist/conversation.js.map +0 -1
  196. package/dist/durability.js.map +0 -1
  197. package/dist/file.js.map +0 -1
  198. package/dist/host.js.map +0 -1
  199. package/dist/identity.js.map +0 -1
  200. package/dist/index.js.map +0 -1
  201. package/dist/log.js.map +0 -1
  202. package/dist/memory.js.map +0 -1
  203. package/dist/model.js.map +0 -1
  204. package/dist/outcome.js.map +0 -1
  205. package/dist/overlay.js.map +0 -1
  206. package/dist/paths.js.map +0 -1
  207. package/dist/query.js.map +0 -1
  208. package/dist/refresh.js.map +0 -1
  209. package/dist/replica.js.map +0 -1
  210. package/dist/save.js.map +0 -1
  211. package/dist/status.js.map +0 -1
  212. package/dist/store.js.map +0 -1
  213. package/dist/transfer.js.map +0 -1
  214. package/dist/wire.js.map +0 -1
@@ -1,55 +1,74 @@
1
- import { rawQuery } from './query.js';
2
- import { cached, modelTypes, NO_MODEL, sameModel } from './model.js';
3
- import { ROOM_CONTRACT_VERSION, VERSION_1_CAPABILITIES } from './adapter.js';
1
+ import { attachedModel } from './attached-model.js';
2
+ import { cached, modelTypes, NO_MODEL, sameModel, } from './model.js';
3
+ import { observing } from './observation.js';
4
+ import { filterOf, identityOf, planOf, } from './query.js';
5
+ import { createReadSide } from './read-side.js';
6
+ import { markdownPath, roomOfKey } from './rows.js';
7
+ import { createSearchIndex, searchPlanOf, } from './search.js';
4
8
  import { bootstrapRoom } from './bootstrap.js';
5
- import { createConversationStore } from './conversation.js';
6
- import { MD_MODEL, PRESENTED, validRoot } from './collection.js';
9
+ import { changeSet, mergeChangeSets } from './change-set.js';
10
+ import { createConversationStore, } from './conversation.js';
11
+ import { queuedFile } from './change.js';
12
+ import { validRoot } from './collection.js';
13
+ import { runtimeError } from './failure.js';
7
14
  import { createLog } from './log.js';
8
- import { queuedOutcome, refusalOf, refused, withheldUntilReset } from './outcome.js';
15
+ import { binaryRecord } from './binary.js';
16
+ import { absentBinary, binaryAnswer, calledResult, failure, historyAnswer, undeclared, UNCERTAIN, } from './outcome.js';
9
17
  import { refreshRoom } from './refresh.js';
10
- import { buildBatch } from './save.js';
11
- import { applyReturned, compositionOf, readConfirmed, replicaOf } from './replica.js';
12
- import { settled, same, saving, STARTING, transferring } from './status.js';
18
+ import { buildBatch, joined, limitedIn, requeued } from './save.js';
19
+ import { applyReturned, compositionOf, confirmedOf, landed, readReclassified, replicaOf, sameMembership, withdrawals, } from './replica.js';
20
+ import { binaryBase, changedParts, changesOf, held, heldFile, joinedDraft, readParts, returnedBy, returnedInto, staleWrite, underneath, withdrawnDrafts, } from './room-draft.js';
21
+ import { same, saving, STARTING, transferring, } from './status.js';
13
22
  import { detectStorageLoss, nextGeneration } from './durability.js';
14
- import { beneath } from './paths.js';
15
- import { openRuntimeStore } from './store.js';
23
+ import { FencedMap } from './fence.js';
24
+ import { roomDraftRevision } from './identity.js';
25
+ import { beneath, normalize } from './paths.js';
26
+ import { openRuntimeStore, } from './store.js';
16
27
  import { unconfirmed } from './transfer.js';
17
- const DEFAULT_DISCOVERY_INTERVAL_MS = 60_000;
18
- const knownCapabilities = new Set(VERSION_1_CAPABILITIES);
28
+ import { governs, holds, narrowed, planReach, } from './reach.js';
19
29
  function within(root, path) {
20
30
  return root === '' || beneath(root, path);
21
31
  }
22
- function declaredIdentity(model) {
23
- if (model === undefined) return false;
24
- if (model === null) return true;
25
- return typeof model.identity === 'string' && model.identity.length > 0 && typeof model.version === 'string' && model.version.length > 0;
26
- }
27
32
  function declaredSelection(room) {
28
33
  const selection = room.typedSelection;
29
- if (selection === undefined) return true;
30
- if (!Array.isArray(selection.roots) || selection.roots.length === 0) return false;
31
- if (!declaredIdentity(selection.attachedModel)) return false;
32
- return selection.roots.every((root, index)=>validRoot(root) && root !== '' && !within(room.collectionRoot, root) && selection.roots.every((other, at)=>at === index || !within(other, root)));
34
+ if (selection === undefined)
35
+ return true;
36
+ if (!Array.isArray(selection.roots) || selection.roots.length === 0)
37
+ return false;
38
+ return selection.roots.every((root, index) => validRoot(root) &&
39
+ root !== '' &&
40
+ selection.roots.every((other, at) => at === index || !within(other, root)));
33
41
  }
34
- export function createMdRuntime(options) {
35
- const period = options.discoveryIntervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
42
+ export function composeRuntime(options) {
43
+ const localSaves = options.adapter.localSaves === true;
36
44
  let authorization = options.authorization;
37
45
  let opening;
38
46
  let store;
39
- const replicas = new Map();
40
- const drafts = new Map();
47
+ const fences = new Map();
48
+ const isFenced = (locator) => fences.has(locator);
49
+ let fencings = 0;
50
+ const lastFenced = new Map();
51
+ const fencedSince = (locator, since) => fences.has(locator) || (lastFenced.get(locator) ?? 0) > since;
52
+ let discoveries = 0;
53
+ const replicas = new FencedMap(isFenced);
54
+ const queuesWithoutReplica = new FencedMap(isFenced);
55
+ const pendings = new Map();
56
+ const roomDrafts = new FencedMap(isFenced);
41
57
  const presentations = new Map();
42
- const outcomes = new Map();
43
58
  let limits = [];
44
59
  let capabilities = [];
45
- const rooms = new Map();
60
+ const rooms = new FencedMap(isFenced);
61
+ const counted = new Map();
46
62
  const unreadable = new Set();
47
- const models = new Map();
48
- const liveModels = new Map();
63
+ const isModelFenced = (key) => isFenced(key.slice(key.indexOf(' ') + 1));
64
+ const models = new FencedMap(isModelFenced);
65
+ const liveModels = new FencedMap(isModelFenced);
49
66
  const invalidModels = new Set();
50
- const modelEpoch = new Map();
51
67
  const discovered = new Map();
68
+ let roomsListed = false;
52
69
  const scopedRefusals = new Map();
70
+ const sourceRefusals = new Map();
71
+ const declarationRefusals = new Set();
53
72
  function modelKey(locator, attached) {
54
73
  return `${attached} ${locator}`;
55
74
  }
@@ -57,326 +76,433 @@ export function createMdRuntime(options) {
57
76
  return models.get(modelKey(locator, attached)) ?? NO_MODEL;
58
77
  }
59
78
  function savedModel(locator, attached) {
60
- return invalidModels.has(modelKey(locator, attached)) ? {
61
- status: 'unavailable',
62
- reason: 'revoked'
63
- } : modelOf(locator, attached);
79
+ return invalidModels.has(modelKey(locator, attached))
80
+ ? { status: 'unavailable', reason: 'revoked' }
81
+ : modelOf(locator, attached);
64
82
  }
65
83
  function compositionFor(record) {
66
84
  return compositionOf({
67
85
  ...record,
68
86
  model: savedModel(record.locator, 'collection'),
69
- selectionModel: savedModel(record.locator, 'typed-selection')
87
+ selectionModel: savedModel(record.locator, 'typed-selection'),
70
88
  });
71
89
  }
72
- const mismatchedRoots = new Set();
73
90
  let refusal = null;
74
91
  let storageLoss = null;
75
92
  let status = STARTING;
93
+ let shown = STARTING;
76
94
  let discoveryPaused = false;
77
- let timer;
95
+ let resetTimer;
78
96
  let closed = false;
79
- let withdrawNotifications;
80
- let handed;
81
- const notices = new Map();
97
+ let stopping = false;
98
+ let withdrawStream;
99
+ const announced = new Map();
100
+ let underway = null;
101
+ let streamed = null;
102
+ let handed = false;
103
+ const submitting = new Map();
104
+ const answering = new Map();
105
+ let complete = false;
106
+ let recorded = false;
107
+ const stale = new Set();
82
108
  let chain = Promise.resolve();
83
109
  let mutations = Promise.resolve();
84
- let inflight = false;
85
- let queued = false;
86
110
  const roomListeners = new Set();
87
111
  const refusalListeners = new Set();
88
112
  const storageLossListeners = new Set();
89
113
  const statusListeners = new Set();
90
- const querySubscriptions = new Set();
91
114
  const queueSubscriptions = new Set();
92
- const draftListeners = new Set();
115
+ const roomDraftListeners = new Set();
93
116
  const presentationListeners = new Set();
117
+ const readSide = createReadSide();
94
118
  let withdrawConversations;
119
+ let currencyQueued = false;
120
+ let socketOpen = false;
121
+ let serviceMode = 'open';
122
+ let haltedRefusal = false;
123
+ let openSignals = 0;
124
+ const serviceModeListeners = new Set();
125
+ let member = null;
126
+ const memberListeners = new Set();
127
+ const materialized = new Set();
128
+ let everyRoom = false;
129
+ const transfers = new FencedMap(isFenced);
95
130
  const conversations = createConversationStore({
96
- cards: declaredCards,
97
- documents: ()=>visible({}),
98
- call: (name, input)=>invokeAction(name, input),
131
+ agents: declaredAgents,
132
+ documents: (locator) => rowsQuery(planOf({ rooms: [locator], select: 'all' }, roomFacts())),
133
+ call: (name, input) => invokeAction(name, input),
99
134
  durably,
100
- path: ()=>options.adapter.conversations,
101
- closed: ()=>closed
135
+ stream: () => options.adapter.conversations,
136
+ publish: publishChangeSet,
137
+ closed: () => closed,
138
+ });
139
+ const searchIndex = createSearchIndex({
140
+ documents: () => readSide.documents(),
141
+ document: (room, path) => readSide.document(room, path),
142
+ messages: () => conversations.messages(),
143
+ message: (conversation, identity) => conversations.message(conversation, identity),
102
144
  });
103
145
  const journal = createLog();
104
- let notifyReady = ()=>{};
105
- const ready = new Promise((resolve)=>{
146
+ let notifyReady = () => { };
147
+ const ready = new Promise((resolve) => {
106
148
  notifyReady = resolve;
107
149
  });
108
- function draftContent(locator, path) {
109
- return drafts.get(locator)?.get(path);
110
- }
111
- function rememberDraft(locator, path, content) {
112
- const room = drafts.get(locator) ?? new Map();
113
- room.set(path, content);
114
- drafts.set(locator, room);
115
- }
116
- function forgetDraft(locator, path) {
117
- const room = drafts.get(locator);
118
- if (room === undefined) return;
150
+ function rememberRoomDraft(record) {
151
+ const room = roomDrafts.get(record.locator) ?? new Map();
152
+ room.set(record.path, record);
153
+ roomDrafts.set(record.locator, room);
154
+ }
155
+ function forgetRoomDraft(locator, path) {
156
+ const room = roomDrafts.get(locator);
157
+ if (room === undefined)
158
+ return;
119
159
  room.delete(path);
120
- if (room.size === 0) drafts.delete(locator);
160
+ if (room.size === 0)
161
+ roomDrafts.delete(locator);
162
+ }
163
+ function roomDraftOf(record) {
164
+ return structuredClone({
165
+ room: record.locator,
166
+ path: record.path,
167
+ revision: record.revision,
168
+ parts: record.parts,
169
+ changedUnderneath: underneath(record.parts, record.path, replicas.get(record.locator)?.visible),
170
+ returned: record.returned,
171
+ });
121
172
  }
122
- function snapshotDrafts() {
123
- const entries = [];
124
- for (const [locator, room] of drafts){
125
- for (const [path, content] of room){
126
- entries.push({
127
- locator,
128
- path,
129
- content: structuredClone(content)
130
- });
131
- }
132
- }
133
- entries.sort((left, right)=>{
134
- if (left.locator !== right.locator) return left.locator < right.locator ? -1 : 1;
135
- if (left.path !== right.path) return left.path < right.path ? -1 : 1;
173
+ function snapshotRoomDrafts() {
174
+ const records = [...roomDrafts.values()].flatMap((room) => [...room.values()]);
175
+ records.sort((left, right) => {
176
+ if (left.locator !== right.locator)
177
+ return left.locator < right.locator ? -1 : 1;
178
+ if (left.path !== right.path)
179
+ return left.path < right.path ? -1 : 1;
136
180
  return 0;
137
181
  });
138
- return entries;
139
- }
140
- async function readDrafts(opened) {
141
- await hydrate(opened);
142
- return snapshotDrafts();
182
+ return records.map(roomDraftOf);
143
183
  }
144
- function publishDrafts(waitingOnly) {
145
- if (closed) return;
146
- const value = snapshotDrafts();
147
- for (const listener of draftListeners){
148
- if (waitingOnly && listener.ready) continue;
184
+ function publishRoomDrafts(waitingOnly) {
185
+ if (closed)
186
+ return;
187
+ const value = snapshotRoomDrafts();
188
+ for (const listener of roomDraftListeners) {
189
+ if (waitingOnly && listener.ready)
190
+ continue;
149
191
  listener.ready = true;
150
192
  listener.notify(value);
151
193
  }
152
194
  }
153
- function emitDrafts() {
154
- publishDrafts(false);
155
- }
156
- function releaseWaitingDrafts() {
157
- publishDrafts(true);
158
- }
159
195
  function presentationOf(app) {
160
196
  const value = presentations.get(app);
161
197
  return value === undefined ? null : structuredClone(value);
162
198
  }
163
199
  function emitPresentation(app) {
164
- if (closed) return;
165
- for (const listener of presentationListeners){
166
- if (listener.app === app && listener.ready) listener.notify(presentationOf(app));
200
+ if (closed)
201
+ return;
202
+ for (const listener of presentationListeners) {
203
+ if (listener.app === app && listener.ready)
204
+ listener.notify(presentationOf(app));
167
205
  }
168
206
  }
169
- function commitDraft(work) {
170
- return exclusive(async ()=>{
171
- const local = await openLocal();
172
- await hydrate(local);
173
- await work(local);
174
- }).then(()=>{
175
- emitDrafts();
176
- });
177
- }
178
207
  function pending() {
179
- return [
180
- ...replicas.values()
181
- ].some((replica)=>replica.queue.length > 0);
208
+ return (!localSaves &&
209
+ (queuesWithoutReplica.size > 0 ||
210
+ [...replicas.values()].some((replica) => replica.queue.length > 0)));
182
211
  }
183
212
  function attention() {
184
213
  const attentionRequired = [];
185
- let reset = null;
186
- for (const [locator, replica] of replicas){
187
- const head = replica.queue[0];
188
- if (head === undefined) continue;
189
- const outcome = outcomes.get(head.id);
190
- if (outcome?.kind === 'retryable-refusal') {
191
- if (outcome.reset !== undefined && (reset === null || outcome.reset < reset)) {
192
- reset = outcome.reset;
193
- }
214
+ for (const record of [...roomDrafts.values()].flatMap((room) => [...room.values()])) {
215
+ if (record.returned === null)
194
216
  continue;
195
- }
196
- if (!refused(outcome)) continue;
197
217
  attentionRequired.push({
198
- locator,
199
- batch: head.id,
200
- outcome: outcome.kind,
201
- refusal: refusalOf(outcome),
202
- ...outcome.kind === 'conflicted' && outcome.roomVersion !== undefined ? {
203
- roomVersion: outcome.roomVersion
204
- } : {}
218
+ room: record.locator,
219
+ path: record.path,
220
+ returned: structuredClone(record.returned),
205
221
  });
206
222
  }
207
- return {
208
- attentionRequired,
209
- reset
210
- };
211
- }
212
- function storedOutcomes(batches) {
213
- const stored = {};
214
- for (const batch of batches){
215
- const outcome = queuedOutcome(outcomes.get(batch.id));
216
- if (outcome.kind !== 'pending') stored[batch.id] = outcome;
223
+ attentionRequired.sort((left, right) => {
224
+ if (left.room !== right.room)
225
+ return left.room < right.room ? -1 : 1;
226
+ if (left.path !== right.path)
227
+ return left.path < right.path ? -1 : 1;
228
+ return 0;
229
+ });
230
+ let reset = null;
231
+ for (const { submission } of pendings.values()) {
232
+ if (submission.state !== 'reset')
233
+ continue;
234
+ if (reset === null || submission.reset < reset)
235
+ reset = submission.reset;
217
236
  }
218
- return stored;
237
+ return { attentionRequired, reset };
219
238
  }
220
- async function persistQueue(local, locator, batches) {
221
- await local.writeQueue({
222
- locator,
223
- batches,
224
- outcomes: storedOutcomes(batches)
225
- });
239
+ function documentsIn(locator) {
240
+ const replica = replicas.get(locator);
241
+ if (replica === undefined)
242
+ return 0;
243
+ const roots = replica.confirmed.record.typedSelection?.roots ?? [];
244
+ const paths = [
245
+ ...Object.keys(replica.visible.documents).filter(markdownPath),
246
+ ...Object.keys(replica.visible.binaries),
247
+ ];
248
+ return paths.filter((path) => !roots.some((root) => beneath(root, path))).length;
226
249
  }
227
250
  function listed() {
228
- return structuredClone([
229
- ...rooms.values()
230
- ]);
231
- }
232
- function queryRooms() {
233
- return [
234
- ...replicas.values()
235
- ].map((replica)=>({
236
- room: replica.confirmed.record.locator,
237
- roomVersion: replica.confirmed.record.roomVersion,
238
- documents: replica.visible.collection.documents,
239
- model: modelTypes(modelOf(replica.confirmed.record.locator)),
240
- typedSelection: replica.confirmed.record.typedSelection === null ? null : modelTypes(modelOf(replica.confirmed.record.locator, 'typed-selection'))
241
- }));
251
+ return structuredClone([...rooms.values()].map((room) => {
252
+ const documents = documentsIn(room.locator);
253
+ counted.set(room.locator, documents);
254
+ return { ...room, documents };
255
+ }));
256
+ }
257
+ function roomFacts(reach = null) {
258
+ const held = [...rooms.values()]
259
+ .filter((room) => holds(reach, room.locator))
260
+ .map((room) => {
261
+ const governed = governs(reach, room);
262
+ const attached = [
263
+ ...(governed.collection ? [modelOf(room.locator)] : []),
264
+ ...(governed.selection ? [modelOf(room.locator, 'typed-selection')] : []),
265
+ ];
266
+ return {
267
+ locator: room.locator,
268
+ available: attached.every((model) => model.status !== 'unavailable'),
269
+ types: attached.flatMap((model) => model.status === 'unavailable' ? [] : Object.values(model.types)),
270
+ };
271
+ });
272
+ return { listed: roomsListed, rooms: held };
242
273
  }
243
274
  function emitRooms() {
244
275
  const value = listed();
245
- for (const listener of roomListeners)listener(value);
276
+ for (const listener of roomListeners)
277
+ listener(value);
246
278
  }
247
279
  function emitRefusal() {
248
- for (const listener of refusalListeners)listener(refusal);
280
+ for (const listener of refusalListeners)
281
+ listener(refusal);
249
282
  }
250
283
  function emitStorageLoss() {
251
- for (const listener of storageLossListeners)listener(storageLoss);
284
+ for (const listener of storageLossListeners)
285
+ listener(storageLoss);
286
+ }
287
+ function emitMember() {
288
+ for (const listener of memberListeners)
289
+ listener(member);
252
290
  }
253
291
  function moveTo(next) {
254
- if (closed || same(status, next)) return;
292
+ if (closed)
293
+ return;
255
294
  status = next;
256
- for (const listener of statusListeners)listener(status);
257
- }
258
- function visible(request) {
259
- if (options.queryBinding === null) {
260
- if (Object.keys(request).length > 0) throw new Error('query-binding-unavailable');
261
- return rawQuery(queryRooms());
262
- }
263
- return options.queryBinding(queryRooms(), {
264
- ...request,
265
- selector: {
266
- $and: [
267
- PRESENTED,
268
- request.selector ?? {}
269
- ]
270
- }
271
- });
295
+ show();
296
+ }
297
+ function show() {
298
+ if (closed)
299
+ return;
300
+ const next = status.state === 'synced' && !conversations.current()
301
+ ? { ...status, state: 'syncing' }
302
+ : status;
303
+ if (same(shown, next))
304
+ return;
305
+ shown = next;
306
+ for (const listener of statusListeners)
307
+ listener(shown);
272
308
  }
273
- function emitVisible() {
274
- for (const subscription of querySubscriptions){
275
- subscription.notify(visible(subscription.query));
309
+ function entryOf(batch) {
310
+ if (localSaves)
311
+ return Object.freeze({ batch, state: 'local' });
312
+ const state = pendings.get(batch.id);
313
+ if (state?.accepted !== undefined) {
314
+ return Object.freeze({ batch, state: 'accepted', roomVersion: state.accepted });
276
315
  }
316
+ return Object.freeze({ batch, ...(state?.submission ?? { state: 'waiting' }) });
277
317
  }
278
318
  function entriesOf(locator) {
279
- const replica = replicas.get(locator);
280
- if (replica === undefined) return [];
281
- return replica.queue.map((batch)=>Object.freeze({
282
- batch,
283
- outcome: queuedOutcome(outcomes.get(batch.id))
284
- }));
319
+ return (replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? []).map(entryOf);
285
320
  }
286
321
  function emitQueue(locator) {
287
322
  const entries = entriesOf(locator);
288
- for (const subscription of queueSubscriptions){
289
- if (subscription.locator === locator) subscription.notify(entries);
323
+ for (const subscription of queueSubscriptions) {
324
+ if (subscription.locator === locator)
325
+ subscription.notify(entries);
290
326
  }
291
327
  }
292
- function stopTimer() {
293
- if (timer === undefined) return;
294
- clearInterval(timer);
295
- timer = undefined;
296
- }
297
- function startTimer() {
298
- if (timer !== undefined) return;
299
- timer = setInterval(()=>{
300
- void enqueue();
301
- }, period);
302
- }
303
- function refuse(reason, provider = null) {
304
- if (reason === 'revoked-authorization') {
305
- const revoked = {
306
- status: 'unavailable',
307
- reason: 'revoked'
308
- };
309
- const attachments = [
310
- 'collection',
311
- 'typed-selection'
312
- ];
313
- const keys = new Set(models.keys());
314
- for (const locator of rooms.keys()){
315
- for (const attached of attachments)keys.add(modelKey(locator, attached));
316
- }
317
- for (const key of keys){
318
- invalidModels.add(key);
319
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
320
- models.set(key, revoked);
321
- liveModels.delete(key);
322
- }
323
- for (const [locator, room] of rooms)rooms.set(locator, {
324
- ...room,
325
- model: modelTypes(revoked),
326
- typedSelection: room.typedSelection === null ? null : {
327
- ...room.typedSelection,
328
- model: modelTypes(revoked)
329
- }
330
- });
328
+ function applyChangeSet(set) {
329
+ const held = everyRoom
330
+ ? set
331
+ : { ...set, documents: set.documents.filter(({ locator }) => materialized.has(locator)) };
332
+ readSide.apply(held);
333
+ searchIndex.changed(held);
334
+ }
335
+ function materialize(locators) {
336
+ if (everyRoom)
337
+ return;
338
+ if (locators === null)
339
+ everyRoom = true;
340
+ const sets = [];
341
+ for (const locator of locators ?? [...replicas.keys()]) {
342
+ if (materialized.has(locator))
343
+ continue;
344
+ materialized.add(locator);
345
+ const replica = replicas.get(locator);
346
+ if (replica !== undefined)
347
+ sets.push(changeSet(locator, null, replica));
348
+ }
349
+ if (sets.length === 0)
350
+ return;
351
+ const set = mergeChangeSets(sets);
352
+ readSide.apply(set);
353
+ searchIndex.changed(set);
354
+ }
355
+ function reachedRooms(plan) {
356
+ if (plan.rooms !== null)
357
+ return plan.rooms;
358
+ if (plan.reach === null)
359
+ return null;
360
+ return [...plan.reach.rooms, ...plan.reach.documents.map(roomOfKey)];
361
+ }
362
+ function rowsQuery(plan) {
363
+ materialize(reachedRooms(plan));
364
+ return readSide.query(plan);
365
+ }
366
+ function publishChangeSet(set) {
367
+ applyChangeSet(set);
368
+ if (set.rooms.length > 0 ||
369
+ [...new Set(set.documents.map(({ locator }) => locator))].some((locator) => rooms.has(locator) && counted.get(locator) !== documentsIn(locator)))
331
370
  emitRooms();
332
- void exclusive(async ()=>{
333
- const local = await openLocal();
334
- for (const record of (await local.all()))await local.publish({
335
- ...record,
336
- model: revoked,
337
- selectionModel: revoked
338
- });
339
- }).catch(()=>{
340
- journal.record('model invalidation durable-write failure');
341
- });
371
+ for (const locator of set.queues)
372
+ emitQueue(locator);
373
+ if (set.drafts.length > 0 ||
374
+ set.documents.some((change) => roomDrafts.get(change.locator)?.has(change.path) === true)) {
375
+ publishRoomDrafts(false);
376
+ }
377
+ for (const app of set.presentations)
378
+ emitPresentation(app);
379
+ conversations.changed(set);
380
+ }
381
+ function stopTimer() {
382
+ if (resetTimer === undefined)
383
+ return;
384
+ clearTimeout(resetTimer);
385
+ resetTimer = undefined;
386
+ }
387
+ function armTimer() {
388
+ stopTimer();
389
+ if (closed || stopping)
390
+ return;
391
+ let earliest = null;
392
+ for (const { submission } of pendings.values()) {
393
+ if (submission.state !== 'reset')
394
+ continue;
395
+ const at = Date.parse(submission.reset);
396
+ if (earliest === null || at < earliest)
397
+ earliest = at;
342
398
  }
399
+ if (earliest === null)
400
+ return;
401
+ // A Save House held back goes again only once the reset its answer named has passed.
402
+ resetTimer = setTimeout(() => {
403
+ resetTimer = undefined;
404
+ submitAll();
405
+ }, Math.max(0, earliest - Date.now()));
406
+ }
407
+ async function refuse(reason, provider = null) {
343
408
  stopTimer();
344
409
  discoveryPaused = true;
345
- refusal = {
346
- reason,
347
- reauthorizationOffered: reason === 'revoked-authorization',
348
- refusal: provider
349
- };
350
- journal.record(`connection refused ${reason}`);
351
- emitRefusal();
410
+ try {
411
+ if (reason === 'revoked-authorization') {
412
+ await exclusive(async () => {
413
+ const revoked = { status: 'unavailable', reason: 'revoked' };
414
+ try {
415
+ const local = await openLocal();
416
+ await local.revokeModels([...replicas.values()].map((replica) => replica.confirmed.record));
417
+ }
418
+ catch {
419
+ journal.record('model revocation persistence failed');
420
+ }
421
+ const keys = new Set(models.keys());
422
+ for (const locator of rooms.keys()) {
423
+ keys.add(modelKey(locator, 'collection'));
424
+ keys.add(modelKey(locator, 'typed-selection'));
425
+ }
426
+ for (const key of keys) {
427
+ invalidModels.add(key);
428
+ models.set(key, revoked);
429
+ liveModels.delete(key);
430
+ }
431
+ const changed = [];
432
+ for (const [locator, room] of rooms) {
433
+ const before = replicas.get(locator) ?? null;
434
+ if (before !== null) {
435
+ const record = {
436
+ ...before.confirmed.record,
437
+ model: revoked,
438
+ selectionModel: revoked,
439
+ };
440
+ replicas.set(locator, replicaOf(confirmedOf(record, before.confirmed), before.queue, before));
441
+ }
442
+ rooms.set(locator, {
443
+ ...room,
444
+ model: modelTypes(revoked),
445
+ typedSelection: room.typedSelection === null
446
+ ? null
447
+ : { ...room.typedSelection, model: modelTypes(revoked) },
448
+ });
449
+ changed.push(changeSet(locator, before, replicas.get(locator) ?? null, { room: true }));
450
+ }
451
+ publishChangeSet(mergeChangeSets(changed));
452
+ });
453
+ }
454
+ }
455
+ finally {
456
+ refusal = {
457
+ code: reason,
458
+ retryable: false,
459
+ reauthorizationOffered: reason === 'revoked-authorization',
460
+ refusal: provider,
461
+ };
462
+ journal.record(`connection refused ${reason}`);
463
+ emitRefusal();
464
+ }
352
465
  }
353
466
  function list(room, synchronizationAvailable) {
354
467
  const selection = room.typedSelection ?? null;
355
468
  rooms.set(room.locator, {
356
469
  name: room.name,
357
- collectionRoot: room.collectionRoot,
358
470
  locator: room.locator,
471
+ handle: room.handle,
359
472
  operations: room.operations,
360
473
  replica: room.replica,
361
- attachedModel: room.attachedModel,
362
474
  model: modelTypes(modelOf(room.locator)),
363
- typedSelection: selection === null ? null : {
364
- roots: selection.roots,
365
- attachedModel: selection.attachedModel,
366
- model: modelTypes(modelOf(room.locator, 'typed-selection'))
367
- },
475
+ typedSelection: selection === null
476
+ ? null
477
+ : {
478
+ roots: selection.roots,
479
+ model: modelTypes(modelOf(room.locator, 'typed-selection')),
480
+ },
368
481
  providerOwnedPaths: room.providerOwnedPaths,
369
482
  roomVersion: replicas.get(room.locator)?.confirmed.record.roomVersion ?? null,
370
483
  synchronizationAvailable,
371
- refusal: scopedRefusals.get(room.locator) ?? null
484
+ refusal: scopedRefusals.get(room.locator) ?? sourceRefusals.get(room.locator) ?? null,
372
485
  });
373
486
  }
374
- function applyDiscovery(listed) {
375
- const listedLocators = new Set(listed.map((room)=>room.locator));
376
- for (const locator of listedLocators)scopedRefusals.delete(locator);
487
+ async function applyDiscovery(found, started) {
488
+ for (const room of found) {
489
+ const fence = fences.get(room.locator);
490
+ if (fence !== undefined && fence.erased && fence.at < started)
491
+ fences.delete(room.locator);
492
+ }
493
+ const listed = found.filter((room) => !fences.has(room.locator));
494
+ const listedLocators = new Set(listed.map((room) => room.locator));
495
+ for (const locator of listedLocators)
496
+ scopedRefusals.delete(locator);
377
497
  discovered.clear();
378
- for (const room of listed)discovered.set(room.locator, room);
379
- for (const room of listed){
498
+ for (const room of listed)
499
+ discovered.set(room.locator, room);
500
+ const unlisted = unlistedHeld(new Set(found.map((room) => room.locator)));
501
+ if (options.adapter.exhaustiveDiscovery === true)
502
+ await loseRooms(unlisted);
503
+ else
504
+ unlistedKept(unlisted);
505
+ for (const room of listed) {
380
506
  const held = replicas.get(room.locator)?.confirmed.record;
381
507
  if (held !== undefined && held.replica.selection !== room.replica.selection) {
382
508
  list(held, false);
@@ -384,23 +510,93 @@ export function createMdRuntime(options) {
384
510
  }
385
511
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
386
512
  }
387
- for (const [locator, room] of rooms){
388
- if (listedLocators.has(locator)) continue;
389
- rooms.set(locator, {
390
- ...room,
391
- synchronizationAvailable: false
392
- });
393
- }
513
+ roomsListed = true;
394
514
  emitRooms();
395
- return listedLocators;
515
+ return listed;
516
+ }
517
+ function unlistedHeld(listed) {
518
+ return [
519
+ ...new Set([
520
+ ...rooms.keys(),
521
+ ...roomDrafts.keys(),
522
+ ...transfers.keys(),
523
+ ...queuesWithoutReplica.keys(),
524
+ ]),
525
+ ].filter((locator) => !listed.has(locator));
526
+ }
527
+ function unlistedKept(unlisted) {
528
+ for (const locator of unlisted) {
529
+ const held = replicas.get(locator)?.confirmed.record;
530
+ if (held !== undefined)
531
+ list(held, false);
532
+ else
533
+ rooms.delete(locator);
534
+ }
535
+ }
536
+ function fence(locators) {
537
+ const fresh = [...new Set(locators)].filter((locator) => !fences.has(locator));
538
+ const removed = fresh.map((locator) => changeSet(locator, replicas.get(locator) ?? null, null, {
539
+ room: true,
540
+ queue: true,
541
+ drafts: [...(roomDrafts.get(locator)?.keys() ?? [])],
542
+ }));
543
+ for (const locator of fresh) {
544
+ fences.set(locator, { at: discoveries, erased: false });
545
+ lastFenced.set(locator, ++fencings);
546
+ for (const batch of replicas.get(locator)?.queue ?? queuesWithoutReplica.get(locator) ?? [])
547
+ pendings.delete(batch.id);
548
+ replicas.delete(locator);
549
+ queuesWithoutReplica.delete(locator);
550
+ rooms.delete(locator);
551
+ roomDrafts.delete(locator);
552
+ transfers.delete(locator);
553
+ discovered.delete(locator);
554
+ announced.delete(locator);
555
+ unreadable.delete(locator);
556
+ stale.delete(locator);
557
+ scopedRefusals.delete(locator);
558
+ sourceRefusals.delete(locator);
559
+ declarationRefusals.delete(locator);
560
+ for (const attached of ['collection', 'typed-selection']) {
561
+ const key = modelKey(locator, attached);
562
+ models.delete(key);
563
+ liveModels.delete(key);
564
+ invalidModels.delete(key);
565
+ }
566
+ journal.record(`room lost ${locator}`);
567
+ }
568
+ if (fresh.length === 0)
569
+ return fresh;
570
+ publishChangeSet(mergeChangeSets(removed));
571
+ for (const locator of fresh)
572
+ materialized.delete(locator);
573
+ moveTo({ ...status, ...attention() });
574
+ return fresh;
575
+ }
576
+ async function erased(local, locators) {
577
+ await local.erase(locators);
578
+ for (const locator of locators) {
579
+ const held = fences.get(locator);
580
+ if (held !== undefined)
581
+ held.erased = true;
582
+ }
583
+ }
584
+ async function loseRooms(locators) {
585
+ const fresh = fence(locators);
586
+ if (fresh.length > 0)
587
+ await durably((local) => erased(local, fresh));
396
588
  }
397
589
  function restored(key, persisted) {
398
590
  const prior = models.get(key);
399
- if (prior !== undefined && prior.status !== 'unavailable' && (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
400
- modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
591
+ if (prior !== undefined &&
592
+ prior.status !== 'unavailable' &&
593
+ (persisted.status === 'unavailable' || !sameModel(prior.snapshot, persisted.snapshot))) {
401
594
  invalidModels.add(key);
402
595
  }
403
- const model = persisted.status === 'unavailable' || liveModels.get(key) === JSON.stringify(persisted.snapshot) ? persisted : cached(persisted);
596
+ const model = persisted.status === 'unavailable' ||
597
+ liveModels.get(key) === JSON.stringify(persisted.snapshot)
598
+ ? persisted
599
+ : cached(persisted);
404
600
  models.set(key, model);
405
601
  if (model.status === 'unavailable') {
406
602
  liveModels.delete(key);
@@ -409,108 +605,125 @@ export function createMdRuntime(options) {
409
605
  return model;
410
606
  }
411
607
  async function hydrate(opened) {
412
- const [records, queues, kept, presented, cached, unsent, lastCompleted, declared] = await Promise.all([
608
+ const [records, queues, kept, presented, held, chatDrafts, lastCompleted, declared, named, known, staged,] = await Promise.all([
413
609
  opened.all(),
414
610
  opened.queues(),
415
- opened.drafts(),
611
+ opened.roomDrafts(),
416
612
  opened.presentation(),
417
613
  opened.conversations(),
418
- opened.instructions(),
614
+ opened.chatDrafts(),
419
615
  opened.lastCompleted(),
420
- opened.providerLimits()
616
+ opened.providerLimits(),
617
+ opened.capabilities(),
618
+ opened.member(),
619
+ opened.transfers(),
421
620
  ]);
621
+ transfers.clear();
622
+ for (const record of staged)
623
+ transfers.set(record.locator, record);
422
624
  limits = declared;
423
- drafts.clear();
424
- for (const draft of kept)rememberDraft(draft.locator, draft.path, draft.content);
625
+ capabilities = named;
626
+ member = known;
627
+ roomDrafts.clear();
628
+ for (const record of kept)
629
+ rememberRoomDraft(record);
425
630
  presentations.clear();
426
- for (const record of presented)presentations.set(record.app, record.value);
427
- conversations.hydrate(cached, unsent);
428
- const queuedBatches = new Map(queues.map((queue)=>[
429
- queue.locator,
430
- queue.batches
431
- ]));
432
- for (const queue of queues){
433
- for (const [id, outcome] of Object.entries(queue.outcomes)){
434
- outcomes.set(id, outcome);
435
- }
436
- }
631
+ for (const record of presented)
632
+ presentations.set(record.app, record.value);
633
+ conversations.hydrate(held, chatDrafts);
634
+ const queuedBatches = new Map(queues.map((queue) => [queue.locator, queue.pending.map(({ batch }) => batch)]));
635
+ pendings.clear();
636
+ for (const queue of queues) {
637
+ for (const { batch, state } of queue.pending)
638
+ pendings.set(batch.id, state);
639
+ }
640
+ const previousReplicas = new Map(replicas);
437
641
  replicas.clear();
438
- for (const record of records){
642
+ queuesWithoutReplica.clear();
643
+ const replicated = new Set(records.map((record) => record.locator));
644
+ for (const [locator, queue] of queuedBatches) {
645
+ if (!replicated.has(locator))
646
+ queuesWithoutReplica.set(locator, queue);
647
+ }
648
+ for (const record of records) {
439
649
  const model = restored(modelKey(record.locator, 'collection'), record.model);
440
650
  const selectionModel = restored(modelKey(record.locator, 'typed-selection'), record.selectionModel);
441
- const confirmed = readConfirmed({
442
- ...record,
443
- model,
444
- selectionModel
445
- });
446
- if (confirmed === null) continue;
447
- replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? []));
651
+ const prior = previousReplicas.get(record.locator);
652
+ const confirmed = confirmedOf({ ...record, model, selectionModel }, prior?.confirmed);
653
+ replicas.set(record.locator, replicaOf(confirmed, queuedBatches.get(record.locator) ?? [], prior));
448
654
  const existing = rooms.get(record.locator);
449
- if (existing === undefined) list(record, false);
655
+ if (existing === undefined)
656
+ list(record, false);
450
657
  else {
451
658
  rooms.set(record.locator, {
452
659
  ...existing,
453
660
  roomVersion: record.roomVersion,
454
661
  operations: record.operations,
455
662
  replica: record.replica,
456
- attachedModel: record.attachedModel,
457
663
  model: modelTypes(model),
458
- typedSelection: record.typedSelection === null ? null : {
459
- roots: record.typedSelection.roots,
460
- attachedModel: record.typedSelection.attachedModel,
461
- model: modelTypes(selectionModel)
462
- }
664
+ typedSelection: record.typedSelection === null
665
+ ? null
666
+ : {
667
+ roots: record.typedSelection.roots,
668
+ model: modelTypes(selectionModel),
669
+ },
463
670
  });
464
671
  }
465
672
  }
466
673
  return {
467
674
  lastCompleted,
468
- locators: new Set([
469
- ...queuedBatches.keys(),
470
- ...replicas.keys()
471
- ])
675
+ locators: new Set([...queuedBatches.keys(), ...replicas.keys()]),
676
+ read: mergeChangeSets([...new Set([...previousReplicas.keys(), ...replicas.keys()])].map((locator) => changeSet(locator, previousReplicas.get(locator) ?? null, replicas.get(locator) ?? null))),
472
677
  };
473
678
  }
474
679
  async function readState(opened) {
475
- const { lastCompleted, locators } = await hydrate(opened);
476
- if (replicas.size > 0) {
680
+ const { lastCompleted, locators, read } = await hydrate(opened);
681
+ applyChangeSet(read);
682
+ if (replicas.size > 0)
477
683
  emitRooms();
478
- emitVisible();
479
- }
480
- for (const locator of locators)emitQueue(locator);
481
- emitDrafts();
684
+ if (member !== null)
685
+ emitMember();
686
+ for (const locator of locators)
687
+ emitQueue(locator);
688
+ publishRoomDrafts(false);
482
689
  return lastCompleted;
483
690
  }
484
691
  async function open() {
485
- const opened = await openRuntimeStore();
692
+ if (closed)
693
+ throw new Error('runtime closed');
694
+ const opened = await openRuntimeStore(isFenced);
486
695
  store = opened;
696
+ const interrupted = await opened.fences();
697
+ for (const locator of interrupted) {
698
+ fences.set(locator, { at: discoveries, erased: false });
699
+ lastFenced.set(locator, ++fencings);
700
+ }
487
701
  const kept = options.shell.generation;
488
702
  const storedGeneration = await opened.generation();
489
703
  const loss = detectStorageLoss(storedGeneration, kept);
490
704
  const generation = nextGeneration(storedGeneration, kept);
491
- if (generation !== storedGeneration) await opened.rememberGeneration(generation);
492
- if (generation !== kept) await options.shell.keepGeneration(generation);
705
+ if (generation !== storedGeneration)
706
+ await opened.rememberGeneration(generation);
707
+ if (generation !== kept)
708
+ await options.shell.keepGeneration(generation);
493
709
  if (loss !== null) {
494
710
  storageLoss = loss;
495
711
  emitStorageLoss();
496
712
  }
497
713
  const lastCompleted = await readState(opened);
498
- moveTo({
499
- ...status,
500
- lastCompleted,
501
- ...attention()
502
- });
714
+ if (interrupted.length > 0)
715
+ await erased(opened, interrupted);
716
+ moveTo({ ...status, lastCompleted, ...attention() });
503
717
  return opened;
504
718
  }
505
719
  function durably(work) {
506
- return exclusive(async ()=>{
720
+ return exclusive(async () => {
507
721
  const local = await openLocal();
508
- await hydrate(local);
509
722
  return work(local);
510
723
  });
511
724
  }
512
725
  function openLocal() {
513
- opening ??= open().catch((error)=>{
726
+ opening ??= open().catch((error) => {
514
727
  opening = undefined;
515
728
  throw error;
516
729
  });
@@ -518,247 +731,560 @@ export function createMdRuntime(options) {
518
731
  }
519
732
  function exclusive(work) {
520
733
  const next = mutations.then(work, work);
521
- mutations = next.then(()=>undefined, ()=>undefined);
734
+ mutations = next.then(() => undefined, () => undefined);
522
735
  return next;
523
736
  }
524
- function publish(local, room, confirmed) {
525
- return exclusive(async ()=>{
526
- await hydrate(local);
527
- const retained = readConfirmed({
528
- ...confirmed.record,
529
- model: modelOf(room.locator),
530
- selectionModel: modelOf(room.locator, 'typed-selection')
737
+ function withdrawable(batch) {
738
+ const state = pendings.get(batch.id);
739
+ return state !== undefined && state.submission.state !== 'submitted' && !state.lost;
740
+ }
741
+ async function settle(local, locator, before, confirmed, queue, settling = {}) {
742
+ const previousQueue = before?.queue ?? queuesWithoutReplica.get(locator) ?? [];
743
+ const withdrawn = withdrawals(confirmed, queue, withdrawable, localSaves);
744
+ const kept = queue.filter((batch) => !withdrawn.includes(batch));
745
+ const unqueued = withdrawn.map((batch) => ({
746
+ batch,
747
+ returned: localSaves || landed(batch, confirmed) ? null : returnedBy('withdrawn'),
748
+ }));
749
+ const returning = [
750
+ ...[...unqueued].reverse(),
751
+ ...(settling.leaving === undefined ? [] : [settling.leaving]),
752
+ ];
753
+ const written = new Map();
754
+ for (const { batch, returned } of returning) {
755
+ if (returned === null)
756
+ continue;
757
+ for (const saved of pendings.get(batch.id)?.drafts ?? []) {
758
+ const existing = written.get(saved.path) ?? roomDrafts.get(locator)?.get(saved.path);
759
+ written.set(saved.path, {
760
+ locator,
761
+ path: saved.path,
762
+ revision: roomDraftRevision(),
763
+ parts: returnedInto(existing?.parts, saved.parts),
764
+ returned,
765
+ });
766
+ }
767
+ }
768
+ await local.keepAuthored({ locator, queue: kept, roomDrafts: [...written.values()] });
769
+ for (const batch of previousQueue) {
770
+ if (!kept.includes(batch))
771
+ pendings.delete(batch.id);
772
+ }
773
+ for (const record of written.values())
774
+ rememberRoomDraft(record);
775
+ try {
776
+ await local.write({
777
+ next: confirmed.record,
778
+ previous: before?.confirmed.record ?? null,
779
+ ...(settling.stored === undefined ? {} : { stored: settling.stored }),
531
780
  });
532
- if (retained !== null) {
533
- await local.publish(retained.record);
534
- unreadable.delete(room.locator);
535
- replicas.set(room.locator, replicaOf(retained, replicas.get(room.locator)?.queue ?? []));
781
+ }
782
+ catch (error) {
783
+ const held = before === undefined ? null : replicaOf(before.confirmed, kept, before);
784
+ if (held !== null)
785
+ replicas.set(locator, held);
786
+ else if (kept.length > 0)
787
+ queuesWithoutReplica.set(locator, kept);
788
+ else
789
+ queuesWithoutReplica.delete(locator);
790
+ publishChangeSet(changeSet(locator, before ?? null, held, {
791
+ queue: kept.length !== previousQueue.length,
792
+ drafts: [...written.keys()],
793
+ }));
794
+ moveTo({ ...status, ...attention() });
795
+ throw error;
796
+ }
797
+ queuesWithoutReplica.delete(locator);
798
+ for (const { batch, returned } of unqueued) {
799
+ const outcome = localSaves ? 'dropped' : returned === null ? 'landed' : 'withdrawn';
800
+ journal.record(`outcome ${locator} ${batch.id} ${outcome}`);
801
+ }
802
+ replicas.set(locator, replicaOf(confirmed, kept, before));
803
+ return changeSet(locator, before ?? null, replicas.get(locator) ?? null, {
804
+ room: settling.room,
805
+ queue: kept.length !== previousQueue.length,
806
+ drafts: [...written.keys()],
807
+ });
808
+ }
809
+ function adoptModels(locator, record) {
810
+ for (const [attached, model] of [
811
+ ['collection', record.model],
812
+ ['typed-selection', record.selectionModel],
813
+ ]) {
814
+ const key = modelKey(locator, attached);
815
+ models.set(key, model);
816
+ invalidModels.delete(key);
817
+ if (model.status === 'current')
818
+ liveModels.set(key, JSON.stringify(model.snapshot));
819
+ else if (model.status === 'unavailable')
820
+ liveModels.delete(key);
821
+ }
822
+ }
823
+ function remodel(locator, answers, held) {
824
+ const taken = (answer, previous, attached) => {
825
+ if (answer === undefined)
826
+ return previous;
827
+ const { state, refused } = attachedModel(answer, previous);
828
+ if (refused !== null) {
829
+ const where = refused.location === undefined ? '' : ` ${refused.location}`;
830
+ journal.record(`model invalid ${locator} ${attached} ${refused.identity} ${refused.rule}${where}`);
831
+ }
832
+ models.set(modelKey(locator, attached), state);
833
+ return state;
834
+ };
835
+ return {
836
+ collection: taken(answers?.collection, held.collection, 'collection'),
837
+ selection: taken(answers?.selection, held.selection, 'typed-selection'),
838
+ };
839
+ }
840
+ function publish(local, room, confirmed, landing = {}) {
841
+ const { base, stored } = landing;
842
+ return exclusive(async () => {
843
+ if (!continuing())
844
+ return true;
845
+ const prior = replicas.get(room.locator);
846
+ if (base !== undefined &&
847
+ (prior?.confirmed.record.roomVersion !== base.record.roomVersion ||
848
+ prior.confirmed.record.revisions !== base.record.revisions)) {
849
+ return false;
536
850
  }
851
+ adoptModels(room.locator, confirmed.record);
852
+ const queue = prior?.queue ?? queuesWithoutReplica.get(room.locator) ?? [];
853
+ const roomVersion = confirmed.record.roomVersion;
854
+ const accepted = queue.filter((batch) => {
855
+ const accepted = pendings.get(batch.id)?.accepted;
856
+ return (accepted !== undefined && options.adapter.reached(room.locator, roomVersion, accepted));
857
+ });
858
+ const set = await settle(local, room.locator, prior, confirmed, queue.filter((batch) => !accepted.includes(batch)), { room: true, ...(stored === undefined ? {} : { stored }) });
859
+ if (stored !== undefined)
860
+ transfers.delete(room.locator);
861
+ for (const batch of accepted)
862
+ pendings.delete(batch.id);
863
+ stale.delete(room.locator);
864
+ if (unreadable.delete(room.locator))
865
+ scopedRefusals.delete(room.locator);
866
+ sourceRefusals.delete(room.locator);
867
+ declarationRefusals.delete(room.locator);
537
868
  list(room, true);
538
- emitRooms();
539
- emitVisible();
869
+ publishChangeSet(set);
870
+ moveTo({ ...status, ...attention() });
871
+ return true;
540
872
  });
541
873
  }
542
- async function write(locator, changes, replacing) {
874
+ async function saveRoomDrafts(locator, named, writtenBy) {
543
875
  const local = await openLocal();
544
- await hydrate(local);
545
876
  const replica = replicas.get(locator);
546
877
  const room = rooms.get(locator);
547
878
  if (replica === undefined || room === undefined) {
548
- throw new Error(`No Local Replica for ${locator}`);
879
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
549
880
  }
550
- if (mismatchedRoots.has(locator)) return {
551
- outcome: 'refused',
552
- rule: 'collection-root-mismatch'
553
- };
554
- const built = buildBatch({
881
+ const saved = [];
882
+ for (const { path, revision } of named) {
883
+ const record = roomDrafts.get(locator)?.get(path);
884
+ if (record === undefined || record.revision !== revision) {
885
+ return { outcome: 'refused', code: 'stale', retryable: false, file: path };
886
+ }
887
+ if (!saved.some((draft) => draft.path === path))
888
+ saved.push({ path, parts: record.parts });
889
+ }
890
+ const changed = saved.flatMap(({ path, parts }) => changedParts(path, underneath(parts, path, replica.visible)));
891
+ if (changed.length > 0)
892
+ return { outcome: 'refused', code: 'changed-underneath', retryable: false, parts: changed };
893
+ const parts = saved.map(({ path, parts }) => changesOf(path, parts));
894
+ const changes = parts.flatMap(({ changes }) => changes);
895
+ const requeue = requeued(replica.queue, changes, replica.visible, withdrawable, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
896
+ if (requeue.kind === 'refused')
897
+ return { outcome: 'refused', ...requeue.refusal };
898
+ const foreign = [...requeue.withdrawn.keys()].some((origin) => {
899
+ const writer = replica.queue.find((batch) => batch.id === origin)?.writtenBy;
900
+ return writer?.app !== writtenBy?.app || writer?.release !== writtenBy?.release;
901
+ });
902
+ if (foreign) {
903
+ return {
904
+ outcome: 'refused',
905
+ code: 'unknown-revision',
906
+ retryable: false,
907
+ file: [...requeue.queued].at(-1),
908
+ };
909
+ }
910
+ const withdrawing = requeue.withdrawn.size > 0;
911
+ const input = {
912
+ local: localSaves,
555
913
  baseRoomVersion: replica.confirmed.record.roomVersion,
556
914
  composition: compositionFor(replica.confirmed.record),
557
915
  operations: room.operations,
558
- binding: options.modelBinding,
559
916
  providerOwnedPaths: room.providerOwnedPaths,
560
917
  providerLimits: limits,
918
+ handle: room.handle,
561
919
  visible: replica.visible,
562
- changes
563
- });
564
- if (built.kind === 'refused') return {
565
- outcome: 'refused',
566
- ...built.refusal
920
+ changes,
921
+ bases: parts.flatMap(({ bases }) => bases),
922
+ writtenBy,
567
923
  };
568
- const head = replica.queue[0];
569
- if (replacing !== undefined) {
570
- if (head === undefined || head.id !== replacing || !refused(outcomes.get(head.id))) {
571
- return {
572
- outcome: 'refused',
573
- rule: 'no-refused-head'
574
- };
575
- }
576
- }
577
- const batches = replacing === undefined ? [
578
- ...replica.queue,
579
- built.batch
580
- ] : [
581
- built.batch,
582
- ...replica.queue.slice(1)
583
- ];
584
- const consumed = [
585
- ...new Set(changes.map((change)=>change.path))
586
- ];
587
- try {
588
- await local.append({
589
- locator,
590
- batches,
591
- outcomes: storedOutcomes(batches)
592
- }, consumed);
593
- } catch {
924
+ const checked = withdrawing ? buildBatch({ ...input, queued: requeue.queued }) : null;
925
+ if (checked?.kind === 'refused')
926
+ return { outcome: 'refused', ...checked.refusal };
927
+ const owners = [...requeue.owners].flatMap((origin) => {
928
+ const batch = requeue.withdrawn.get(origin);
929
+ return batch === undefined || batch === null ? [] : [{ origin, batch }];
930
+ });
931
+ if (owners.length > 1) {
594
932
  return {
595
933
  outcome: 'refused',
596
- rule: 'durable-write'
934
+ code: 'unknown-revision',
935
+ retryable: false,
936
+ file: [...requeue.queued].at(-1),
597
937
  };
598
938
  }
599
- if (replacing !== undefined) outcomes.delete(replacing);
600
- for (const path of consumed)forgetDraft(locator, path);
601
- replicas.set(locator, replicaOf(replica.confirmed, batches));
602
- emitQueue(locator);
603
- emitVisible();
604
- emitDrafts();
605
- moveTo({
606
- ...saving(status),
607
- ...attention()
608
- });
609
- void enqueue();
610
- return {
611
- outcome: 'saved',
612
- id: built.batch.id
613
- };
614
- }
615
- async function discardHead(locator, identifier) {
616
- const local = await openLocal();
617
- await hydrate(local);
618
- const replica = replicas.get(locator);
619
- const head = replica?.queue[0];
620
- if (replica === undefined || head === undefined || head.id !== identifier || !refused(outcomes.get(head.id))) {
939
+ const built = !withdrawing
940
+ ? buildBatch(input)
941
+ : requeue.changes.length === 0
942
+ ? null
943
+ : buildBatch({
944
+ ...input,
945
+ visible: replicaOf(replica.confirmed, requeue.queue, replica).visible,
946
+ changes: requeue.changes,
947
+ });
948
+ if (built?.kind === 'refused')
949
+ return { outcome: 'refused', ...built.refusal };
950
+ const [owner] = owners;
951
+ const join = built === null || owner === undefined
952
+ ? null
953
+ : joined(requeue.queue, owner.batch, built.batch);
954
+ if (join?.kind === 'refused')
955
+ return { outcome: 'refused', ...join.refusal };
956
+ const withdrawn = new Map(requeue.withdrawn);
957
+ if (join !== null && owner !== undefined)
958
+ withdrawn.set(owner.origin, join.batch);
959
+ const drafted = new Map();
960
+ for (const [origin, replaced] of withdrawn) {
961
+ if (replaced === null)
962
+ continue;
963
+ const carried = withdrawnDrafts(pendings.get(origin)?.drafts ?? [], changes);
964
+ drafted.set(replaced.id, replaced === join?.batch
965
+ ? [
966
+ ...carried.map((held) => joinedDraft(held, saved)),
967
+ ...saved.filter((draft) => !carried.some((held) => held.path === draft.path)),
968
+ ]
969
+ : carried);
970
+ }
971
+ if (built !== null && join === null)
972
+ drafted.set(built.batch.id, saved);
973
+ const batches = join !== null ? join.queue : built === null ? requeue.queue : [...requeue.queue, built.batch];
974
+ for (const replaced of withdrawn.values()) {
975
+ if (replaced === null || !batches.includes(replaced))
976
+ continue;
977
+ const limited = limitedIn(batches, replaced, limits, (queue) => replicaOf(replica.confirmed, queue, replica).visible);
978
+ if (limited !== null)
979
+ return { outcome: 'refused', ...limited };
980
+ }
981
+ const consumed = saved.map(({ path }) => path);
982
+ try {
983
+ await local.keepAuthored({
984
+ locator,
985
+ queue: batches,
986
+ saved: drafted,
987
+ consumedRoomDrafts: consumed,
988
+ });
989
+ }
990
+ catch {
991
+ return { outcome: 'refused', code: 'durable-write', retryable: true };
992
+ }
993
+ if (fences.has(locator))
994
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
995
+ for (const origin of withdrawn.keys())
996
+ pendings.delete(origin);
997
+ for (const [id, drafts] of drafted) {
998
+ pendings.set(id, { drafts, submission: { state: 'waiting' }, lost: false });
999
+ }
1000
+ if (!localSaves)
1001
+ recorded = false;
1002
+ for (const path of consumed)
1003
+ forgetRoomDraft(locator, path);
1004
+ replicas.set(locator, replicaOf(replica.confirmed, batches, replica));
1005
+ publishChangeSet(changeSet(locator, replica, replicas.get(locator) ?? null, {
1006
+ queue: true,
1007
+ drafts: consumed,
1008
+ }));
1009
+ moveTo({ ...(localSaves ? status : saving(status)), ...attention() });
1010
+ submitLater(locator);
1011
+ const identifier = join?.batch.id ??
1012
+ built?.batch.id ??
1013
+ [...requeue.withdrawn].map(([origin, replaced]) => replaced?.id ?? origin).at(-1);
1014
+ return { outcome: 'saved', identifier };
1015
+ }
1016
+ async function mark(local, locator, identifier, submission, lost) {
1017
+ const state = pendings.get(identifier);
1018
+ if (!continuing() ||
1019
+ state === undefined ||
1020
+ (JSON.stringify(state.submission) === JSON.stringify(submission) && state.lost === lost)) {
621
1021
  return;
622
1022
  }
623
- const batches = replica.queue.slice(1);
624
- outcomes.delete(identifier);
625
- await persistQueue(local, locator, batches);
626
- replicas.set(locator, replicaOf(replica.confirmed, batches));
627
- emitQueue(locator);
628
- emitVisible();
629
- moveTo({
630
- ...status,
631
- state: pending() ? 'pending' : status.state,
632
- ...attention()
633
- });
634
- void enqueue();
1023
+ await local.submit(locator, identifier, submission, lost);
1024
+ if (fences.has(locator))
1025
+ return;
1026
+ pendings.set(identifier, { ...state, submission, lost });
1027
+ const replica = replicas.get(locator);
1028
+ if (replica !== undefined)
1029
+ publishChangeSet(changeSet(locator, replica, replica, { queue: true }));
1030
+ moveTo({ ...status, ...attention() });
1031
+ }
1032
+ async function keepUpload(local, locator, identifier, upload) {
1033
+ if (!continuing())
1034
+ throw new Error('connection refused');
1035
+ await local.keepUpload(locator, identifier, upload);
1036
+ if (fences.has(locator))
1037
+ throw runtimeError('room-not-held', `No Local Replica for ${locator}`);
1038
+ pendings.set(identifier, { ...pendings.get(identifier), upload });
1039
+ if (!continuing())
1040
+ throw new Error('connection refused');
635
1041
  }
636
1042
  async function readHistory(locator, path, query = {}) {
637
- if (closed || refusal !== null) return {
638
- kind: 'transport-uncertainty'
639
- };
1043
+ if (closed || refusal !== null)
1044
+ return UNCERTAIN;
640
1045
  try {
641
1046
  await ensureAuthorization();
642
- } catch {
643
- return {
644
- kind: 'transport-uncertainty'
645
- };
646
- }
647
- if (closed || authorization === undefined || refusal !== null) {
648
- return {
649
- kind: 'transport-uncertainty'
650
- };
651
1047
  }
652
- const transfer = [
653
- 'transfer',
654
- 'history',
655
- locator,
656
- path
657
- ];
658
- if (query.revision !== undefined) transfer.push(query.revision);
659
- if (query.pageToken !== undefined) transfer.push(query.pageToken);
1048
+ catch {
1049
+ return UNCERTAIN;
1050
+ }
1051
+ if (closed || authorization === undefined || refusal !== null)
1052
+ return UNCERTAIN;
1053
+ const since = fencings;
1054
+ const transfer = ['transfer', 'history', locator, path];
1055
+ if (query.revision !== undefined)
1056
+ transfer.push(query.revision);
1057
+ if (query.pageToken !== undefined)
1058
+ transfer.push(query.pageToken);
660
1059
  journal.record(transfer.join(' '));
661
1060
  try {
662
1061
  const result = await options.adapter.history(authorization, {
663
1062
  locator,
664
1063
  path,
665
- ...query.revision === undefined ? {} : {
666
- revision: query.revision
667
- },
668
- ...query.pageToken === undefined ? {} : {
669
- pageToken: query.pageToken
670
- }
1064
+ ...(query.revision === undefined ? {} : { revision: query.revision }),
1065
+ ...(query.pageToken === undefined ? {} : { pageToken: query.pageToken }),
671
1066
  });
672
1067
  journal.record(`outcome history ${result.kind}`);
1068
+ if (fencedSince(locator, since))
1069
+ return failure({ kind: 'room-not-readable', locator });
673
1070
  if (result.kind === 'scoped-refusal') {
674
1071
  const room = replicas.get(result.locator)?.confirmed.record;
675
- if (room !== undefined) await scoped(result, room, await openLocal());
1072
+ if (room !== undefined)
1073
+ await scoped(result, room, await openLocal());
676
1074
  }
677
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
678
- return result;
679
- } catch {
1075
+ if (result.kind === 'revoked-authorization')
1076
+ await refuse('revoked-authorization');
1077
+ return historyAnswer(result);
1078
+ }
1079
+ catch {
680
1080
  journal.record('outcome history transport-uncertainty');
681
- return {
682
- kind: 'transport-uncertainty'
683
- };
1081
+ return UNCERTAIN;
1082
+ }
1083
+ }
1084
+ function queuedBlob(locator, path, localId) {
1085
+ for (const batch of replicas.get(locator)?.queue ?? []) {
1086
+ const change = batch.changes[queuedFile(batch, path, localId)];
1087
+ if (change?.kind === 'binary')
1088
+ return change.blob;
684
1089
  }
1090
+ return null;
1091
+ }
1092
+ function binaryRoom(request) {
1093
+ if (request.kind === 'path')
1094
+ return request.locator;
1095
+ const scope = options.adapter.referrerScope(request.referrer);
1096
+ return scope?.kind === 'room' ? scope.locator : null;
1097
+ }
1098
+ async function readBinary(request) {
1099
+ if (closed || refusal !== null)
1100
+ return UNCERTAIN;
1101
+ try {
1102
+ await ensureAuthorization();
1103
+ }
1104
+ catch {
1105
+ return UNCERTAIN;
1106
+ }
1107
+ if (closed || authorization === undefined || refusal !== null)
1108
+ return UNCERTAIN;
1109
+ const since = fencings;
1110
+ journal.record(request.kind === 'path'
1111
+ ? `transfer binary ${request.locator} ${request.path}`
1112
+ : `transfer binary ${request.fileRef}`);
1113
+ try {
1114
+ const result = await options.adapter.binary(authorization, request);
1115
+ journal.record(`outcome binary ${result.kind}`);
1116
+ const locator = binaryRoom(request);
1117
+ if (locator !== null && fencedSince(locator, since))
1118
+ return failure({ kind: 'room-not-readable', locator });
1119
+ if (result.kind === 'scoped-refusal') {
1120
+ const room = replicas.get(result.locator)?.confirmed.record;
1121
+ if (room !== undefined)
1122
+ await scoped(result, room, await openLocal());
1123
+ }
1124
+ if (result.kind === 'revoked-authorization')
1125
+ await refuse('revoked-authorization');
1126
+ return binaryAnswer(result);
1127
+ }
1128
+ catch {
1129
+ journal.record('outcome binary transport-uncertainty');
1130
+ return UNCERTAIN;
1131
+ }
1132
+ }
1133
+ async function readBinaryPath(locator, path, revision) {
1134
+ await ready;
1135
+ if (revision !== undefined)
1136
+ return readBinary({ kind: 'path', locator, path, revision });
1137
+ const visible = replicas.get(locator)?.visible;
1138
+ const held = visible?.binaries[path];
1139
+ if (visible === undefined || held === undefined)
1140
+ return absentBinary(path);
1141
+ if (held.state === 'pending') {
1142
+ const blob = queuedBlob(locator, path, held.localId);
1143
+ return blob === null ? absentBinary(path) : { kind: 'binary', blob };
1144
+ }
1145
+ const record = binaryRecord(path, held, visible.revisions[path]);
1146
+ if (record.state !== 'accepted')
1147
+ return absentBinary(path);
1148
+ const replica = replicas.get(locator);
1149
+ const queued = (replica?.queue ?? []).flatMap((batch) => batch.changes);
1150
+ const holds = (at) => {
1151
+ const kept = replica?.confirmed.record.binaries[at];
1152
+ return (kept?.state === 'accepted' &&
1153
+ kept.fileRef === record.fileRef &&
1154
+ replica?.confirmed.record.revisions[at] === record.revision);
1155
+ };
1156
+ let confirmed = path;
1157
+ for (let index = queued.length - 1; index >= 0 && !holds(confirmed); index -= 1) {
1158
+ const change = queued[index];
1159
+ if (change.kind === 'move' && change.to === confirmed)
1160
+ confirmed = change.path;
1161
+ }
1162
+ return readBinary({
1163
+ kind: 'path',
1164
+ locator,
1165
+ path: confirmed,
1166
+ revision: record.revision,
1167
+ fileRef: record.fileRef,
1168
+ });
1169
+ }
1170
+ async function readBinaryReference(reference) {
1171
+ await ready;
1172
+ return readBinary({
1173
+ kind: 'reference',
1174
+ fileRef: reference.fileRef,
1175
+ referrer: reference.referrer,
1176
+ });
685
1177
  }
686
- function declaredCards() {
687
- for (const capability of capabilities){
688
- if (capability.conversationCards !== undefined) return capability.conversationCards;
1178
+ function declaredAgents() {
1179
+ for (const capability of capabilities) {
1180
+ if (capability.conversationAgents !== undefined)
1181
+ return capability.conversationAgents;
689
1182
  }
690
1183
  return undefined;
691
1184
  }
692
1185
  function declaredAction(name) {
693
- for (const capability of capabilities){
694
- for (const action of capability.actions ?? []){
695
- if (action.name === name) return action;
1186
+ for (const capability of capabilities) {
1187
+ for (const action of capability.actions ?? []) {
1188
+ if (action.name === name)
1189
+ return action;
696
1190
  }
697
1191
  }
698
1192
  return undefined;
699
1193
  }
700
1194
  async function invokeAction(name, input) {
701
1195
  await ready;
702
- if (closed || refusal !== null) return {
703
- kind: 'transport-uncertainty'
704
- };
705
- if (declaredAction(name) === undefined) return {
706
- kind: 'undeclared'
707
- };
1196
+ if (closed || refusal !== null)
1197
+ return UNCERTAIN;
1198
+ if (declaredAction(name) === undefined)
1199
+ return undeclared(name);
708
1200
  try {
709
1201
  await ensureAuthorization();
710
- } catch {
711
- return {
712
- kind: 'transport-uncertainty'
713
- };
714
1202
  }
715
- if (closed || authorization === undefined || refusal !== null) {
716
- return {
717
- kind: 'transport-uncertainty'
718
- };
1203
+ catch {
1204
+ return UNCERTAIN;
719
1205
  }
1206
+ if (closed || authorization === undefined || refusal !== null)
1207
+ return UNCERTAIN;
720
1208
  journal.record(`transfer action ${name}`);
721
1209
  try {
722
- const result = await options.adapter.action(authorization, {
723
- name,
724
- input
725
- });
1210
+ const result = await options.adapter.action(authorization, { name, input });
726
1211
  if (result.kind === 'scoped-refusal') {
727
1212
  const room = replicas.get(result.locator)?.confirmed.record;
728
- if (room !== undefined) await scoped(result, room, await openLocal());
1213
+ if (room !== undefined)
1214
+ await scoped(result, room, await openLocal());
729
1215
  }
730
- if (result.kind === 'revoked-authorization') refuse('revoked-authorization');
1216
+ if (result.kind === 'revoked-authorization')
1217
+ await refuse('revoked-authorization');
731
1218
  journal.record(`outcome action ${result.kind}`);
732
- if (result.kind === 'rejected') journal.record(`refusal rejected ${result.code}`);
733
- return result;
734
- } catch {
1219
+ if (result.kind === 'rejected')
1220
+ journal.record(`refusal rejected ${result.code}`);
1221
+ return calledResult(result);
1222
+ }
1223
+ catch {
735
1224
  journal.record('outcome action transport-uncertainty');
736
- return {
737
- kind: 'transport-uncertainty'
738
- };
1225
+ return UNCERTAIN;
739
1226
  }
740
1227
  }
1228
+ function continuing() {
1229
+ return !closed && !discoveryPaused;
1230
+ }
1231
+ function reachedBy(locator, roomVersion) {
1232
+ const held = replicas.get(locator);
1233
+ return (held !== undefined &&
1234
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion));
1235
+ }
1236
+ function writtenAfter(locator, roomVersion, path) {
1237
+ const written = replicas.get(locator)?.confirmed.record.written[path];
1238
+ return (written !== undefined &&
1239
+ options.adapter.reached(locator, written, roomVersion) &&
1240
+ !options.adapter.reached(locator, roomVersion, written));
1241
+ }
741
1242
  async function pullRefresh(authorized, confirmed) {
1243
+ if (!continuing())
1244
+ return unconfirmed;
742
1245
  try {
743
- const result = await refreshRoom(options.adapter, authorized, confirmed, options.modelBinding);
1246
+ const result = await refreshRoom(options.adapter, authorized, confirmed, (models, held) => remodel(confirmed.record.locator, models, held), continuing);
744
1247
  const detail = result.kind === 'advanced' ? result.confirmed.record.roomVersion : result.kind;
745
1248
  journal.record(`transfer refresh ${confirmed.record.locator} ${detail}`);
746
1249
  return result;
747
- } catch (error) {
1250
+ }
1251
+ catch (error) {
748
1252
  journal.record(`transfer refresh ${confirmed.record.locator} transport-uncertainty`);
749
1253
  throw error;
750
1254
  }
751
1255
  }
752
- async function pullBootstrap(authorized, room) {
1256
+ function staging(local) {
1257
+ return {
1258
+ held: (locator) => transfers.get(locator) ?? null,
1259
+ staged: (record) => local.staged(record),
1260
+ stage: (record, page) => exclusive(async () => {
1261
+ if (!continuing())
1262
+ return false;
1263
+ await local.stage(record, page);
1264
+ transfers.set(record.locator, record);
1265
+ return true;
1266
+ }),
1267
+ abandon: (locator) => exclusive(async () => {
1268
+ if (!continuing())
1269
+ return;
1270
+ await local.abandon(locator);
1271
+ transfers.delete(locator);
1272
+ }),
1273
+ };
1274
+ }
1275
+ async function pullBootstrap(authorized, room, local) {
1276
+ if (!continuing())
1277
+ return unconfirmed;
753
1278
  try {
754
1279
  const result = await bootstrapRoom(options.adapter, authorized, room, {
755
1280
  collection: modelOf(room.locator),
756
- selection: modelOf(room.locator, 'typed-selection')
757
- }, options.modelBinding);
1281
+ selection: modelOf(room.locator, 'typed-selection'),
1282
+ }, (models, held) => remodel(room.locator, models, held), continuing, staging(local));
758
1283
  const detail = result.kind === 'staged' ? result.confirmed.record.roomVersion : result.kind;
759
1284
  journal.record(`transfer bootstrap ${room.locator} ${detail}`);
760
1285
  return result;
761
- } catch (error) {
1286
+ }
1287
+ catch (error) {
762
1288
  journal.record(`transfer bootstrap ${room.locator} transport-uncertainty`);
763
1289
  throw error;
764
1290
  }
@@ -767,276 +1293,343 @@ export function createMdRuntime(options) {
767
1293
  const held = replicas.get(room.locator);
768
1294
  if (held !== undefined) {
769
1295
  const refreshed = await pullRefresh(authorized, held.confirmed);
1296
+ if (!continuing())
1297
+ return unconfirmed;
770
1298
  if (refreshed.kind === 'still-current') {
1299
+ stale.delete(room.locator);
1300
+ if (sourceRefusals.has(room.locator))
1301
+ return null;
771
1302
  if (unreadable.delete(room.locator)) {
1303
+ scopedRefusals.delete(room.locator);
772
1304
  list(room, true);
773
1305
  emitRooms();
774
1306
  }
775
1307
  return null;
776
1308
  }
777
1309
  if (refreshed.kind === 'advanced') {
778
- if (closed) return unconfirmed;
779
- await publish(local, room, refreshed.confirmed);
1310
+ if (!(await publish(local, room, refreshed.confirmed, { base: held.confirmed }))) {
1311
+ const now = replicas.get(room.locator)?.confirmed.record.roomVersion;
1312
+ const reached = now !== undefined &&
1313
+ options.adapter.reached(room.locator, now, refreshed.confirmed.record.roomVersion);
1314
+ return reached ? null : advance(room, local, authorized);
1315
+ }
1316
+ await retainModel(local, room, 'collection', modelOf(room.locator));
780
1317
  return null;
781
1318
  }
782
- if (refreshed.kind !== 'beyond-horizon') return refreshed;
783
- }
784
- const bootstrapped = await pullBootstrap(authorized, room);
785
- if (bootstrapped.kind !== 'staged') return bootstrapped;
786
- if (closed) return unconfirmed;
787
- await publish(local, room, bootstrapped.confirmed);
1319
+ if (refreshed.kind !== 'beyond-horizon')
1320
+ return refreshed;
1321
+ }
1322
+ const bootstrapped = await pullBootstrap(authorized, room, local);
1323
+ if (!continuing())
1324
+ return unconfirmed;
1325
+ if (bootstrapped.kind !== 'staged')
1326
+ return bootstrapped;
1327
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
788
1328
  return null;
789
1329
  }
790
- function recordRefusal(result) {
791
- if (result.kind === 'expired') return {
792
- kind: 'expired'
793
- };
794
- if (result.kind === 'retryable-refusal') {
795
- return {
796
- kind: 'retryable-refusal',
797
- code: result.code,
798
- fields: result.fields,
799
- ...result.reset === undefined ? {} : {
800
- reset: result.reset
801
- }
802
- };
1330
+ async function withhold(withheld, room, local, progress) {
1331
+ if (withheld.kind === 'refused')
1332
+ stale.delete(room.locator);
1333
+ else
1334
+ stale.add(room.locator);
1335
+ if (withheld.kind === 'scoped-refusal') {
1336
+ await scoped(withheld, room, local);
803
1337
  }
804
- if (result.kind === 'conflicted') {
805
- return {
806
- kind: 'conflicted',
807
- ...result.roomVersion === undefined ? {} : {
808
- roomVersion: result.roomVersion
809
- },
810
- affected: result.affected,
811
- code: result.code,
812
- fields: result.fields
813
- };
1338
+ else if (withheld.kind === 'transport-uncertainty') {
1339
+ progress.reachable = false;
1340
+ }
1341
+ else if (withheld.kind === 'retryable-refusal') {
1342
+ journal.record(`refusal ${withheld.kind} ${withheld.code}`);
1343
+ }
1344
+ else if (withheld.kind === 'room-not-readable') {
1345
+ unreadable.add(room.locator);
1346
+ list(room, false);
1347
+ emitRooms();
1348
+ }
1349
+ else if (withheld.kind === 'refused') {
1350
+ refusedSource(room, withheld);
1351
+ }
1352
+ else if (withheld.kind === 'revoked-authorization') {
1353
+ await refuse('revoked-authorization');
814
1354
  }
1355
+ }
1356
+ function refusedSource(room, { refusal }) {
1357
+ sourceRefusals.set(room.locator, {
1358
+ code: refusal.rule,
1359
+ retryable: false,
1360
+ fields: {
1361
+ file: refusal.file,
1362
+ ...(refusal.location === undefined ? {} : { location: refusal.location }),
1363
+ ...(refusal.referrers === undefined ? {} : { referrers: refusal.referrers }),
1364
+ },
1365
+ });
1366
+ declarationRefusals.delete(room.locator);
1367
+ unreadable.add(room.locator);
1368
+ list(room, false);
1369
+ emitRooms();
1370
+ journal.record(`source refused ${room.locator} ${refusal.rule} ${refusal.file}`);
1371
+ }
1372
+ function leaving(batch, result) {
1373
+ if (result.kind === 'expired')
1374
+ return { batch, returned: returnedBy('expired') };
815
1375
  return {
816
- kind: 'rejected',
817
- affected: result.affected,
818
- code: result.code,
819
- fields: result.fields
1376
+ batch,
1377
+ returned: {
1378
+ kind: result.kind,
1379
+ code: result.code,
1380
+ fields: result.fields,
1381
+ retryable: result.retryable,
1382
+ },
820
1383
  };
821
1384
  }
822
- async function commitAccept(room, local, submitted, accepted, base, overlay) {
823
- await hydrate(local);
1385
+ function logAccepted(locator, submitted, accepted) {
1386
+ journal.record(`outcome ${locator} ${submitted.id} accepted ${accepted.roomVersion}` +
1387
+ (accepted.author === undefined ? '' : ` author ${accepted.author}`) +
1388
+ (accepted.time === undefined ? '' : ` time ${accepted.time}`));
1389
+ }
1390
+ async function adoptAccepted(room, local, submitted, accepted) {
1391
+ if (!continuing())
1392
+ return unconfirmed;
824
1393
  const current = replicas.get(room.locator);
825
- if (current === undefined || !current.queue.some((batch)=>batch.id === submitted.id)) {
826
- return true;
1394
+ if (current === undefined || !current.queue.some((batch) => batch.id === submitted.id)) {
1395
+ return null;
827
1396
  }
828
- const remaining = current.queue.filter((batch)=>batch.id !== submitted.id);
829
- const retainedBase = readConfirmed({
830
- ...base.record,
831
- model: modelOf(room.locator),
832
- selectionModel: modelOf(room.locator, 'typed-selection')
833
- });
834
- if (retainedBase === null) return false;
835
- const applied = overlay ? applyReturned(retainedBase, accepted.documents, accepted.revisions, accepted.removals, accepted.roomVersion, options.modelBinding) : retainedBase;
836
- if (applied === null) return false;
837
- await local.publish(applied.record);
838
- await persistQueue(local, room.locator, remaining);
839
- outcomes.set(submitted.id, {
840
- kind: 'accepted',
841
- roomVersion: accepted.roomVersion,
842
- ...accepted.author === undefined ? {} : {
843
- author: accepted.author
844
- },
845
- ...accepted.time === undefined ? {} : {
846
- time: accepted.time
847
- }
848
- });
1397
+ const held = current.confirmed.record.roomVersion;
1398
+ logAccepted(room.locator, submitted, accepted);
1399
+ const applied = options.adapter.reached(room.locator, held, accepted.roomVersion)
1400
+ ? { kind: 'staged', confirmed: current.confirmed }
1401
+ : applyReturned(current.confirmed, submitted, accepted, options.adapter.follows(room.locator, held, accepted.roomVersion)
1402
+ ? accepted.roomVersion
1403
+ : held, accepted.roomVersion);
1404
+ if (applied.kind !== 'staged') {
1405
+ const state = pendings.get(submitted.id);
1406
+ await local.accept(room.locator, submitted.id, accepted.roomVersion);
1407
+ if (state !== undefined && !fences.has(room.locator))
1408
+ pendings.set(submitted.id, { ...state, accepted: accepted.roomVersion });
1409
+ emitQueue(room.locator);
1410
+ return applied;
1411
+ }
1412
+ const set = await settle(local, room.locator, current, applied.confirmed, current.queue.filter((batch) => batch.id !== submitted.id), { room: true });
1413
+ pendings.delete(submitted.id);
849
1414
  unreadable.delete(room.locator);
850
- replicas.set(room.locator, replicaOf(applied, remaining));
1415
+ sourceRefusals.delete(room.locator);
1416
+ declarationRefusals.delete(room.locator);
851
1417
  list(room, true);
852
- emitRooms();
853
- emitQueue(room.locator);
854
- emitVisible();
855
- journal.record(`outcome ${room.locator} ${submitted.id} accepted ${accepted.roomVersion}` + (accepted.author === undefined ? '' : ` author ${accepted.author}`) + (accepted.time === undefined ? '' : ` time ${accepted.time}`));
856
- return true;
1418
+ publishChangeSet(set);
1419
+ moveTo({ ...status, ...attention() });
1420
+ return null;
1421
+ }
1422
+ async function adopted(room, local, submitted, accepted) {
1423
+ const failed = await exclusive(() => adoptAccepted(room, local, submitted, accepted));
1424
+ if (failed === null)
1425
+ return true;
1426
+ if (failed.kind === 'refused' && continuing())
1427
+ refusedSource(room, failed);
1428
+ return false;
857
1429
  }
858
1430
  async function submitRoom(room, local, authorized, progress) {
859
- while(!closed && authorization !== undefined){
1431
+ while (continuing() && authorization !== undefined) {
860
1432
  const replica = replicas.get(room.locator);
861
1433
  const head = replica?.queue[0];
862
- if (replica === undefined || head === undefined) return null;
863
- const held = outcomes.get(head.id);
864
- if (refused(held)) return null;
865
- if (held?.kind === 'retryable-refusal') {
866
- if (withheldUntilReset(held, Date.now())) {
867
- progress.complete = false;
868
- return null;
869
- }
870
- await exclusive(async ()=>{
871
- await hydrate(local);
872
- const current = replicas.get(room.locator);
873
- if (current === undefined || current.queue[0]?.id !== head.id) return;
874
- outcomes.delete(head.id);
875
- await persistQueue(local, room.locator, current.queue);
876
- emitQueue(room.locator);
877
- moveTo({
878
- ...status,
879
- ...attention()
880
- });
881
- });
1434
+ if (replica === undefined || head === undefined)
1435
+ return;
1436
+ if (pendings.get(head.id)?.accepted !== undefined)
1437
+ return;
1438
+ const state = pendings.get(head.id);
1439
+ const { submission, lost: earlier } = state ?? {
1440
+ submission: { state: 'waiting' },
1441
+ lost: false,
1442
+ };
1443
+ if (submission.state === 'reset' && Date.now() < Date.parse(submission.reset)) {
1444
+ progress.complete = false;
1445
+ return;
1446
+ }
1447
+ if (writesHeld()) {
1448
+ progress.complete = false;
1449
+ journal.record(`submission ${room.locator} ${head.id} held`);
1450
+ return;
882
1451
  }
1452
+ const lost = earlier || submission.state === 'submitted';
883
1453
  let result;
884
1454
  journal.record(`submission ${room.locator} ${head.id}`);
1455
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'submitted' }, lost));
1456
+ if (discoveryPaused || fences.has(room.locator))
1457
+ return;
1458
+ if (writesHeld()) {
1459
+ await exclusive(() => mark(local, room.locator, head.id, submission, earlier));
1460
+ progress.complete = false;
1461
+ journal.record(`submission ${room.locator} ${head.id} held`);
1462
+ return;
1463
+ }
1464
+ const opensBefore = openSignals;
1465
+ const answered = awaitingAnswer(room.locator);
885
1466
  try {
886
1467
  result = await options.adapter.mutate(authorized, {
887
1468
  locator: room.locator,
888
- changeBatch: head
889
- });
890
- } catch {
1469
+ changeBatch: head,
1470
+ ...(state?.upload === undefined ? {} : { upload: state.upload }),
1471
+ }, (upload) => exclusive(() => keepUpload(local, room.locator, head.id, upload)));
1472
+ }
1473
+ catch {
1474
+ answered();
891
1475
  progress.complete = false;
892
- progress.reachable = false;
893
- return null;
1476
+ if (continuing())
1477
+ progress.reachable = false;
1478
+ return;
894
1479
  }
895
- if (closed) return null;
1480
+ if (result.kind !== 'accepted' || !continuing())
1481
+ answered();
1482
+ if (!continuing())
1483
+ return;
896
1484
  if (result.kind === 'transport-uncertainty') {
897
1485
  progress.complete = false;
898
1486
  progress.reachable = false;
899
- return null;
1487
+ return;
900
1488
  }
901
1489
  if (result.kind === 'room-not-readable') {
902
1490
  progress.complete = false;
903
1491
  unreadable.add(room.locator);
904
1492
  list(room, false);
905
1493
  emitRooms();
906
- return null;
1494
+ return;
907
1495
  }
908
1496
  if (result.kind === 'revoked-authorization') {
909
- refuse('revoked-authorization');
910
- return 'stop';
1497
+ await refuse('revoked-authorization');
1498
+ return;
911
1499
  }
912
1500
  if (result.kind === 'scoped-refusal') {
913
1501
  await scoped(result, room, local);
914
1502
  progress.complete = false;
915
- return null;
916
- }
917
- if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired' || result.kind === 'retryable-refusal') {
918
- journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` + (result.kind === 'conflicted' && result.roomVersion !== undefined ? ` ${result.roomVersion}` : ''));
919
- if (result.kind !== 'expired') journal.record(`refusal ${result.kind} ${result.code}`);
920
- if (result.kind === 'retryable-refusal') progress.complete = false;
921
- await exclusive(async ()=>{
922
- await hydrate(local);
923
- const current = replicas.get(room.locator);
924
- if (current === undefined || !current.queue.some((batch)=>batch.id === head.id)) {
925
- return;
926
- }
927
- outcomes.set(head.id, recordRefusal(result));
928
- await persistQueue(local, room.locator, current.queue);
929
- emitQueue(room.locator);
930
- moveTo({
931
- ...status,
932
- ...attention()
933
- });
934
- });
935
- return null;
936
- }
937
- const from = replicas.get(room.locator)?.confirmed;
938
- if (from === undefined) return null;
939
- const pulled = await pullRefresh(authorized, from);
940
- if (pulled.kind === 'revoked-authorization') {
941
- refuse('revoked-authorization');
942
- return 'stop';
1503
+ return;
943
1504
  }
944
- if (pulled.kind === 'scoped-refusal') {
945
- await scoped(pulled, room, local);
1505
+ if (result.kind === 'write-halted') {
1506
+ journal.record(`outcome ${room.locator} ${head.id} write-halted`);
1507
+ await exclusive(() => mark(local, room.locator, head.id, { state: 'waiting' }, lost));
1508
+ if (serviceMode === 'open' && openSignals !== opensBefore)
1509
+ continue;
1510
+ haltedRefusal = true;
946
1511
  progress.complete = false;
947
- return null;
1512
+ return;
948
1513
  }
949
- if (pulled.kind === 'still-current') {
950
- const committed = await exclusive(()=>commitAccept(room, local, head, result, from, true));
951
- if (!committed) {
952
- progress.complete = false;
953
- return null;
1514
+ if (result.kind === 'conflicted' ||
1515
+ result.kind === 'rejected' ||
1516
+ result.kind === 'expired' ||
1517
+ result.kind === 'retryable-refusal') {
1518
+ if (result.kind === 'expired') {
1519
+ const withheld = await advance(room, local, authorized);
1520
+ if (withheld !== null)
1521
+ return withhold(withheld, room, local, progress);
954
1522
  }
955
- continue;
1523
+ journal.record(`outcome ${room.locator} ${head.id} ${result.kind}` +
1524
+ (result.kind === 'conflicted' && result.roomVersion !== undefined
1525
+ ? ` ${result.roomVersion}`
1526
+ : ''));
1527
+ if (result.kind !== 'expired')
1528
+ journal.record(`refusal ${result.kind} ${result.code}`);
956
1529
  }
957
- if (pulled.kind === 'advanced') {
958
- const overlay = pulled.confirmed.record.roomVersion === result.roomVersion;
959
- const committed = await exclusive(()=>commitAccept(room, local, head, result, pulled.confirmed, overlay));
960
- if (!committed) {
961
- progress.complete = false;
962
- return null;
963
- }
964
- continue;
1530
+ if (result.kind === 'retryable-refusal') {
1531
+ const reset = result.reset;
1532
+ progress.complete = false;
1533
+ await exclusive(() => mark(local, room.locator, head.id, reset === undefined ? { state: 'waiting' } : { state: 'reset', reset }, lost));
1534
+ armTimer();
1535
+ return;
965
1536
  }
966
- if (pulled.kind === 'beyond-horizon') {
967
- const bootstrapped = await pullBootstrap(authorized, room);
968
- if (bootstrapped.kind === 'revoked-authorization') {
969
- refuse('revoked-authorization');
970
- return 'stop';
971
- }
972
- if (bootstrapped.kind === 'scoped-refusal') {
973
- await scoped(bootstrapped, room, local);
974
- progress.complete = false;
975
- return null;
976
- }
977
- if (bootstrapped.kind === 'staged') {
978
- const overlay = bootstrapped.confirmed.record.roomVersion === result.roomVersion;
979
- const committed = await exclusive(()=>commitAccept(room, local, head, result, bootstrapped.confirmed, overlay));
980
- if (!committed) {
981
- progress.complete = false;
982
- return null;
1537
+ if (result.kind === 'conflicted' || result.kind === 'rejected' || result.kind === 'expired') {
1538
+ const left = leaving(head, result);
1539
+ await exclusive(async () => {
1540
+ if (!continuing())
1541
+ return;
1542
+ const current = replicas.get(room.locator);
1543
+ if (current === undefined || !current.queue.some((batch) => batch.id === head.id)) {
1544
+ return;
983
1545
  }
1546
+ const set = await settle(local, room.locator, current, current.confirmed, current.queue.filter((batch) => batch.id !== head.id), {
1547
+ leaving: (result.kind === 'expired' && landed(head, current.confirmed)) ||
1548
+ (result.kind === 'conflicted' && landed(head, current.confirmed, result.affected))
1549
+ ? { ...left, returned: null }
1550
+ : left,
1551
+ });
1552
+ publishChangeSet(set);
1553
+ moveTo({ ...status, ...attention() });
1554
+ });
1555
+ if (result.kind === 'expired')
984
1556
  continue;
985
- }
986
- progress.complete = false;
987
- if (bootstrapped.kind === 'transport-uncertainty') {
988
- progress.reachable = false;
989
- } else if (bootstrapped.kind === 'room-not-readable') {
990
- unreadable.add(room.locator);
991
- list(room, false);
992
- emitRooms();
993
- }
994
- return null;
1557
+ const withheld = await advance(room, local, authorized);
1558
+ if (withheld !== null)
1559
+ return withhold(withheld, room, local, progress);
1560
+ continue;
995
1561
  }
996
- progress.complete = false;
997
- if (pulled.kind === 'transport-uncertainty') {
998
- progress.reachable = false;
999
- } else if (pulled.kind === 'room-not-readable') {
1000
- unreadable.add(room.locator);
1001
- list(room, false);
1002
- emitRooms();
1562
+ const kept = await adopted(room, local, head, result).finally(answered);
1563
+ if (!kept) {
1564
+ progress.complete = false;
1565
+ return;
1003
1566
  }
1004
- return null;
1005
1567
  }
1006
- return null;
1568
+ }
1569
+ function awaitingAnswer(locator) {
1570
+ let release = () => { };
1571
+ const awaited = new Promise((resolve) => {
1572
+ release = resolve;
1573
+ });
1574
+ answering.set(locator, awaited);
1575
+ return () => {
1576
+ if (answering.get(locator) === awaited)
1577
+ answering.delete(locator);
1578
+ release();
1579
+ };
1007
1580
  }
1008
1581
  async function retainModel(local, room, attached, next) {
1009
1582
  const key = modelKey(room.locator, attached);
1010
- if (next.status === 'unavailable') modelEpoch.set(key, (modelEpoch.get(key) ?? 0) + 1);
1011
- const epoch = modelEpoch.get(key) ?? 0;
1012
1583
  const previous = modelOf(room.locator, attached);
1013
1584
  const heldBefore = replicas.get(room.locator)?.confirmed.record;
1014
- if (JSON.stringify(previous) === JSON.stringify(next) && (heldBefore === undefined || JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) && JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) && JSON.stringify(heldBefore.attachedModel) === JSON.stringify(room.attachedModel) && JSON.stringify(heldBefore.typedSelection) === JSON.stringify(room.typedSelection ?? null))) return;
1015
- const queryChanged = previous.status === 'unavailable' || next.status === 'unavailable' ? previous.status !== next.status : JSON.stringify(previous.types) !== JSON.stringify(next.types);
1585
+ if (JSON.stringify(previous) === JSON.stringify(next) &&
1586
+ (heldBefore === undefined ||
1587
+ (JSON.stringify(heldBefore.operations) === JSON.stringify(room.operations) &&
1588
+ JSON.stringify(heldBefore.replica) === JSON.stringify(room.replica) &&
1589
+ JSON.stringify(heldBefore.typedSelection) ===
1590
+ JSON.stringify(room.typedSelection ?? null))))
1591
+ return;
1016
1592
  if (next.status === 'unavailable') {
1017
1593
  invalidModels.add(key);
1018
1594
  liveModels.delete(key);
1019
1595
  }
1020
- await exclusive(async ()=>{
1021
- await hydrate(local);
1022
- if (next.status !== 'unavailable' && epoch !== (modelEpoch.get(key) ?? 0)) return;
1596
+ await exclusive(async () => {
1597
+ if (!continuing())
1598
+ return;
1023
1599
  const held = replicas.get(room.locator);
1024
1600
  if (held !== undefined) {
1025
- const record = {
1601
+ const declared = {
1026
1602
  ...held.confirmed.record,
1027
1603
  operations: room.operations,
1028
1604
  replica: room.replica,
1029
- attachedModel: room.attachedModel,
1030
1605
  typedSelection: room.typedSelection ?? null,
1031
- ...attached === 'collection' ? {
1032
- model: next
1033
- } : {
1034
- selectionModel: next
1035
- }
1606
+ ...(attached === 'collection' ? { model: next } : { selectionModel: next }),
1036
1607
  };
1037
- await local.publish(record);
1038
- const confirmed = readConfirmed(record);
1039
- if (confirmed !== null) replicas.set(room.locator, replicaOf(confirmed, held.queue));
1608
+ const reclassified = sameMembership(declared, held.confirmed.record)
1609
+ ? { kind: 'staged', confirmed: confirmedOf(declared, held.confirmed) }
1610
+ : readReclassified(declared, held.confirmed);
1611
+ const record = reclassified.kind === 'staged'
1612
+ ? declared
1613
+ : {
1614
+ ...declared,
1615
+ typedSelection: held.confirmed.record.typedSelection,
1616
+ };
1617
+ const read = reclassified.kind === 'deferred'
1618
+ ? { kind: 'staged', confirmed: confirmedOf(record, held.confirmed) }
1619
+ : reclassified;
1620
+ await local.write({ next: record, previous: held.confirmed.record });
1621
+ if (read.kind === 'staged') {
1622
+ replicas.set(room.locator, replicaOf(read.confirmed, held.queue, held));
1623
+ if (declarationRefusals.delete(room.locator)) {
1624
+ sourceRefusals.delete(room.locator);
1625
+ unreadable.delete(room.locator);
1626
+ }
1627
+ }
1628
+ else if (read.kind === 'refused') {
1629
+ replicas.set(room.locator, { ...held, confirmed: { ...held.confirmed, record } });
1630
+ refusedSource(room, read);
1631
+ declarationRefusals.add(room.locator);
1632
+ }
1040
1633
  }
1041
1634
  models.set(key, next);
1042
1635
  if (next.status === 'current') {
@@ -1044,188 +1637,157 @@ export function createMdRuntime(options) {
1044
1637
  invalidModels.delete(key);
1045
1638
  }
1046
1639
  list(room, room.replica.admission === 'admitted' && !unreadable.has(room.locator));
1047
- emitRooms();
1048
- if (queryChanged && held !== undefined) emitVisible();
1049
- });
1050
- }
1051
- async function acquireModel(local, room, authorized, selection) {
1052
- const attached = selection === null ? 'collection' : 'typed-selection';
1053
- const key = modelKey(room.locator, attached);
1054
- const declared = selection === null ? room.attachedModel : selection.attachedModel;
1055
- let previous = modelOf(room.locator, attached);
1056
- if (previous.status !== 'unavailable' && (declared === null || !sameModel(previous.snapshot, declared))) {
1057
- previous = {
1058
- status: 'unavailable',
1059
- reason: 'version-changed'
1060
- };
1061
- await retainModel(local, room, attached, previous);
1062
- }
1063
- const epoch = modelEpoch.get(key) ?? 0;
1064
- let answer;
1065
- try {
1066
- answer = await options.adapter.model(authorized, selection === null ? {
1067
- locator: room.locator,
1068
- attached: 'collection',
1069
- collectionRoot: room.collectionRoot,
1070
- model: declared
1071
- } : {
1072
- locator: room.locator,
1073
- attached: 'typed-selection',
1074
- roots: selection.roots,
1075
- model: declared
1076
- });
1077
- } catch {
1078
- answer = {
1079
- kind: 'transport-uncertainty'
1080
- };
1081
- }
1082
- if (epoch !== (modelEpoch.get(key) ?? 0)) return;
1083
- if (answer.kind === 'revoked-authorization') {
1084
- await retainModel(local, room, attached, {
1085
- status: 'unavailable',
1086
- reason: 'revoked'
1087
- });
1088
- refuse('revoked-authorization');
1089
- return;
1090
- }
1091
- if (answer.kind === 'transport-uncertainty') {
1092
- await retainModel(local, room, attached, cached(previous));
1093
- return;
1094
- }
1095
- if (answer.kind === 'model-unavailable') {
1096
- await retainModel(local, room, attached, {
1097
- status: 'unavailable',
1098
- reason: answer.reason
1099
- });
1100
- return;
1101
- }
1102
- if (declared === null || !sameModel(answer.snapshot, declared)) {
1103
- await retainModel(local, room, attached, {
1104
- status: 'unavailable',
1105
- reason: 'version-changed'
1106
- });
1107
- return;
1108
- }
1109
- if (previous.status !== 'unavailable' && sameModel(previous.snapshot, answer.snapshot) && JSON.stringify(Object.entries(previous.snapshot.definitions).sort()) !== JSON.stringify(Object.entries(answer.snapshot.definitions).sort())) {
1110
- await retainModel(local, room, attached, {
1111
- status: 'unavailable',
1112
- reason: 'invalid'
1113
- });
1114
- return;
1115
- }
1116
- const verified = options.modelBinding?.verify(answer.snapshot);
1117
- await retainModel(local, room, attached, verified?.kind === 'verified' ? {
1118
- status: 'current',
1119
- snapshot: structuredClone(answer.snapshot),
1120
- verifiedWith: MD_MODEL,
1121
- types: verified.types
1122
- } : {
1123
- status: 'unavailable',
1124
- reason: verified === undefined ? 'binding-unavailable' : 'invalid'
1640
+ if (held === undefined)
1641
+ emitRooms();
1642
+ else
1643
+ publishChangeSet(changeSet(room.locator, held, replicas.get(room.locator) ?? null, { room: true }));
1125
1644
  });
1126
1645
  }
1127
1646
  async function scoped(result, room, local) {
1128
1647
  scopedRefusals.set(room.locator, {
1129
1648
  code: result.code,
1130
- fields: result.fields
1649
+ fields: result.fields,
1650
+ retryable: result.retryable,
1131
1651
  });
1132
1652
  let scopedAway = room;
1133
- if (result.scope === 'model') await retainModel(local, room, 'collection', {
1134
- status: 'unavailable',
1135
- reason: 'not-readable'
1136
- });
1653
+ if (result.scope === 'model')
1654
+ await retainModel(local, room, 'collection', {
1655
+ status: 'unavailable',
1656
+ reason: 'not-readable',
1657
+ });
1137
1658
  else if (result.scope !== 'operation') {
1138
1659
  unreadable.add(room.locator);
1139
1660
  scopedAway = {
1140
1661
  ...room,
1141
- replica: {
1142
- ...room.replica,
1143
- admission: 'unavailable'
1144
- },
1662
+ replica: { ...room.replica, admission: 'unavailable' },
1145
1663
  operations: {
1146
1664
  ...room.operations,
1147
- ...result.scope === 'content' ? {
1148
- read: []
1149
- } : {},
1150
- ...result.scope === 'sync' ? {
1151
- sync: []
1152
- } : {}
1153
- }
1665
+ ...(result.scope === 'content' ? { read: [] } : {}),
1666
+ ...(result.scope === 'sync' ? { sync: [] } : {}),
1667
+ },
1154
1668
  };
1155
1669
  await retainModel(local, scopedAway, 'collection', modelOf(room.locator));
1156
1670
  }
1671
+ if (!continuing())
1672
+ return;
1157
1673
  list(scopedAway, scopedAway.replica.admission === 'admitted' && !unreadable.has(room.locator));
1158
1674
  emitRooms();
1159
1675
  journal.record(`scoped refusal ${room.locator} ${result.scope} ${result.code}`);
1160
1676
  }
1161
- async function synchronize(discovered, local, progress) {
1162
- for (const room of discovered){
1163
- if (closed || authorization === undefined) {
1164
- progress.complete = false;
1165
- return;
1166
- }
1167
- const prior = replicas.get(room.locator)?.confirmed.record;
1168
- if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
1169
- list(prior, false);
1170
- emitRooms();
1171
- journal.record(`selection changed ${room.locator}`);
1172
- progress.complete = false;
1173
- continue;
1174
- }
1175
- await acquireModel(local, room, authorization, null);
1176
- if (discoveryPaused) return;
1177
- if (room.typedSelection !== undefined) {
1178
- await acquireModel(local, room, authorization, room.typedSelection);
1179
- if (discoveryPaused) return;
1180
- }
1181
- if (room.replica.admission !== 'admitted') {
1182
- progress.complete = false;
1183
- continue;
1184
- }
1185
- const held = replicas.get(room.locator);
1186
- if (held !== undefined && held.confirmed.record.replica.selection !== room.replica.selection) {
1187
- progress.complete = false;
1188
- continue;
1189
- }
1190
- const withheld = await advance(room, local, authorization);
1191
- if (withheld !== null) {
1677
+ function heldRooms(listed) {
1678
+ return listed.flatMap((room) => {
1679
+ const record = replicas.get(room.locator)?.confirmed.record;
1680
+ if (record === undefined || record.replica.selection !== room.replica.selection)
1681
+ return [];
1682
+ return [
1683
+ {
1684
+ locator: room.locator,
1685
+ roomVersion: record.roomVersion,
1686
+ documents: record.documents,
1687
+ revisions: record.revisions,
1688
+ },
1689
+ ];
1690
+ });
1691
+ }
1692
+ async function synchronize(listed, local, progress, attempt) {
1693
+ const prepared = await Promise.allSettled(listed.map((room) => prepareRoom(room, local, progress)));
1694
+ const ready = listed.filter((_, index) => {
1695
+ const outcome = prepared[index];
1696
+ return outcome?.status === 'fulfilled' && outcome.value;
1697
+ });
1698
+ if (attempt === 'opened') {
1699
+ options.adapter.roomStream?.hold(heldRooms(ready));
1700
+ handed = true;
1701
+ }
1702
+ const loaded = await Promise.allSettled(ready
1703
+ .filter((room) => attempt === 'unstreamed' || !replicas.has(room.locator))
1704
+ .map((room) => loadRoom(room, local, progress)));
1705
+ const failed = [...prepared, ...loaded].find((room) => room.status === 'rejected');
1706
+ if (failed !== undefined)
1707
+ throw failed.reason;
1708
+ }
1709
+ async function prepareRoom(room, local, progress) {
1710
+ if (closed || authorization === undefined) {
1711
+ progress.complete = false;
1712
+ return false;
1713
+ }
1714
+ const prior = replicas.get(room.locator)?.confirmed.record;
1715
+ if (prior !== undefined && prior.replica.selection !== room.replica.selection) {
1716
+ list(prior, false);
1717
+ emitRooms();
1718
+ journal.record(`selection changed ${room.locator}`);
1719
+ progress.complete = false;
1720
+ return false;
1721
+ }
1722
+ if (prior !== undefined)
1723
+ await retainModel(local, room, 'collection', modelOf(room.locator));
1724
+ if (discoveryPaused || authorization === undefined)
1725
+ return false;
1726
+ if (room.replica.admission !== 'admitted') {
1727
+ if (replicas.has(room.locator))
1192
1728
  progress.complete = false;
1193
- if (withheld.kind === 'scoped-refusal') {
1194
- await scoped(withheld, room, local);
1195
- continue;
1196
- }
1197
- if (withheld.kind === 'transport-uncertainty') {
1198
- progress.reachable = false;
1199
- continue;
1200
- }
1201
- if (withheld.kind === 'room-not-readable') {
1202
- unreadable.add(room.locator);
1203
- list(room, false);
1204
- emitRooms();
1205
- continue;
1206
- }
1207
- if (withheld.kind === 'revoked-authorization') {
1208
- refuse('revoked-authorization');
1209
- return;
1210
- }
1211
- continue;
1212
- }
1213
- if (await submitRoom(room, local, authorization, progress) === 'stop') return;
1729
+ return false;
1730
+ }
1731
+ return true;
1732
+ }
1733
+ async function caughtUp(room, local, progress) {
1734
+ const held = replicas.get(room.locator)?.confirmed.record.roomVersion;
1735
+ if (held === undefined || authorization === undefined)
1736
+ return;
1737
+ const behind = [announced.get(room.locator), room.roomVersion].some((later) => later !== undefined && !options.adapter.reached(room.locator, held, later));
1738
+ if (!behind)
1739
+ return;
1740
+ const withheld = await advance(room, local, authorization);
1741
+ if (withheld !== null)
1742
+ await withhold(withheld, room, local, progress);
1743
+ }
1744
+ function revoked(locator) {
1745
+ return ['collection', 'typed-selection'].some((attached) => {
1746
+ const model = modelOf(locator, attached);
1747
+ return model.status === 'unavailable' && model.reason === 'revoked';
1748
+ });
1749
+ }
1750
+ async function loadRoom(room, local, progress) {
1751
+ if (discoveryPaused || authorization === undefined)
1752
+ return;
1753
+ if (replicas.has(room.locator) && revoked(room.locator)) {
1754
+ await reload(room, local, progress);
1755
+ return;
1756
+ }
1757
+ const withheld = await advance(room, local, authorization);
1758
+ if (withheld !== null) {
1759
+ await withhold(withheld, room, local, progress);
1760
+ return;
1214
1761
  }
1762
+ await caughtUp(room, local, progress);
1215
1763
  }
1216
- async function cycle(local, progress) {
1764
+ async function reload(room, local, progress) {
1765
+ if (discoveryPaused || authorization === undefined)
1766
+ return;
1767
+ const bootstrapped = await pullBootstrap(authorization, room, local);
1768
+ if (!continuing())
1769
+ return;
1770
+ if (bootstrapped.kind !== 'staged') {
1771
+ await withhold(bootstrapped, room, local, progress);
1772
+ return;
1773
+ }
1774
+ await publish(local, room, bootstrapped.confirmed, { stored: bootstrapped.stored });
1775
+ await caughtUp(room, local, progress);
1776
+ }
1777
+ async function discoverRooms(local, progress, attempt) {
1217
1778
  if (closed || authorization === undefined) {
1218
1779
  notifyReady();
1219
1780
  return;
1220
1781
  }
1221
1782
  moveTo(transferring(status));
1783
+ discoveries += 1;
1784
+ const started = discoveries;
1222
1785
  let result;
1223
1786
  try {
1224
1787
  result = await options.adapter.discover(authorization);
1225
- } catch {
1226
- result = {
1227
- kind: 'transport-uncertainty'
1228
- };
1788
+ }
1789
+ catch {
1790
+ result = { kind: 'transport-uncertainty' };
1229
1791
  }
1230
1792
  if (closed) {
1231
1793
  notifyReady();
@@ -1233,78 +1795,48 @@ export function createMdRuntime(options) {
1233
1795
  }
1234
1796
  journal.record(`connection discover ${result.kind}`);
1235
1797
  if (result.kind === 'transport-uncertainty') {
1236
- for (const room of rooms.values()){
1798
+ for (const room of rooms.values()) {
1237
1799
  await retainModel(local, room, 'collection', cached(modelOf(room.locator)));
1238
1800
  if (room.typedSelection !== null) {
1239
1801
  await retainModel(local, room, 'typed-selection', cached(modelOf(room.locator, 'typed-selection')));
1240
1802
  }
1241
1803
  }
1242
1804
  progress.reachable = false;
1805
+ progress.complete = false;
1243
1806
  notifyReady();
1244
1807
  return;
1245
1808
  }
1246
1809
  if (result.kind === 'contract-unavailable') {
1247
1810
  journal.record(`connection contract unavailable ${result.code}`);
1248
- refuse('missing-required-guarantee', {
1811
+ await refuse('missing-required-guarantee', {
1249
1812
  code: result.code,
1250
- fields: result.fields
1813
+ fields: result.fields,
1814
+ retryable: result.retryable,
1251
1815
  });
1252
1816
  notifyReady();
1253
1817
  return;
1254
1818
  }
1255
1819
  if (result.kind === 'revoked-authorization') {
1256
- refuse('revoked-authorization');
1820
+ await refuse('revoked-authorization');
1257
1821
  notifyReady();
1258
1822
  return;
1259
1823
  }
1260
- if (!result.supportedVersions.includes(ROOM_CONTRACT_VERSION)) {
1261
- refuse('no-matching-version');
1262
- notifyReady();
1263
- return;
1264
- }
1265
- if (result.mdModel.identity !== MD_MODEL.identity || result.mdModel.version !== MD_MODEL.version) {
1266
- refuse('md-model-version-mismatch');
1267
- notifyReady();
1268
- return;
1269
- }
1270
- mismatchedRoots.clear();
1271
- for (const room of result.rooms){
1272
- const held = replicas.get(room.locator)?.confirmed.record;
1273
- const malformed = !validRoot(room.collectionRoot);
1274
- if (room.operations === undefined || ![
1275
- 'read',
1276
- 'sync',
1277
- 'write',
1278
- 'delete'
1279
- ].every((operation)=>Array.isArray(room.operations[operation]) && room.operations[operation].every((scope)=>typeof scope === 'string')) || room.replica === undefined || typeof room.replica.selection !== 'string' || room.replica.selection.length === 0 || ![
1280
- 'admitted',
1281
- 'unavailable'
1282
- ].includes(room.replica.admission) || !declaredIdentity(room.attachedModel)) {
1824
+ for (const room of result.rooms) {
1825
+ if (room.operations === undefined ||
1826
+ !['read', 'sync', 'write', 'delete'].every((operation) => Array.isArray(room.operations[operation]) &&
1827
+ room.operations[operation].every((scope) => typeof scope === 'string')) ||
1828
+ room.replica === undefined ||
1829
+ typeof room.replica.selection !== 'string' ||
1830
+ room.replica.selection.length === 0 ||
1831
+ !['admitted', 'unavailable'].includes(room.replica.admission)) {
1283
1832
  journal.record(`invalid model or replica declaration ${room.locator}`);
1284
- refuse('missing-required-guarantee');
1833
+ await refuse('missing-required-guarantee');
1285
1834
  notifyReady();
1286
1835
  return;
1287
1836
  }
1288
1837
  if (!declaredSelection(room)) {
1289
1838
  journal.record(`invalid typed selection declaration ${room.locator}`);
1290
- refuse('missing-required-guarantee');
1291
- notifyReady();
1292
- return;
1293
- }
1294
- if (malformed || held !== undefined && held.collectionRoot !== room.collectionRoot) {
1295
- mismatchedRoots.add(room.locator);
1296
- journal.record(`collection root ${malformed ? 'invalid' : 'changed'} ${room.locator}`);
1297
- }
1298
- }
1299
- if (mismatchedRoots.size > 0) {
1300
- refuse('missing-required-guarantee');
1301
- notifyReady();
1302
- return;
1303
- }
1304
- const declared = new Set(result.capabilities.map((capability)=>capability.name));
1305
- for (const required of options.requiredGuarantees){
1306
- if (!knownCapabilities.has(required) || !declared.has(required)) {
1307
- refuse('missing-required-guarantee');
1839
+ await refuse('missing-required-guarantee');
1308
1840
  notifyReady();
1309
1841
  return;
1310
1842
  }
@@ -1313,392 +1845,651 @@ export function createMdRuntime(options) {
1313
1845
  refusal = null;
1314
1846
  emitRefusal();
1315
1847
  }
1848
+ const previousCapabilities = capabilities;
1316
1849
  capabilities = result.capabilities;
1317
1850
  notifyReady();
1318
- if (declared.has('room-version-notifications')) observeRoomVersions();
1319
- if (declared.has('provider-actions')) observeConversations();
1320
- if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits)) {
1851
+ if (JSON.stringify(result.providerLimits) !== JSON.stringify(limits) ||
1852
+ JSON.stringify(result.capabilities) !== JSON.stringify(previousCapabilities)) {
1321
1853
  limits = result.providerLimits;
1322
- await local.declare(limits);
1854
+ await local.declare(limits, capabilities);
1855
+ }
1856
+ const signedIn = result.member ?? null;
1857
+ if (JSON.stringify(signedIn) !== JSON.stringify(member)) {
1858
+ await local.keepMember(signedIn);
1859
+ member = signedIn;
1860
+ emitMember();
1323
1861
  }
1324
- const listedLocators = applyDiscovery(result.rooms);
1325
- handSelected(result.rooms.filter((room)=>room.replica.admission === 'admitted').map((room)=>room.locator).sort());
1326
- progress.complete = [
1327
- ...replicas.keys()
1328
- ].every((locator)=>listedLocators.has(locator));
1329
- await synchronize(result.rooms, local, progress);
1862
+ await synchronize(await applyDiscovery(result.rooms, started), local, progress, attempt);
1330
1863
  }
1331
1864
  async function ensureAuthorization() {
1332
- if (authorization !== undefined) return;
1865
+ if (authorization !== undefined)
1866
+ return;
1333
1867
  try {
1334
1868
  authorization = await options.adapter.authorize();
1335
1869
  journal.record('connection authorize');
1336
- } catch (error) {
1870
+ }
1871
+ catch (error) {
1337
1872
  journal.record('connection authorize transport-uncertainty');
1338
1873
  throw error;
1339
1874
  }
1340
1875
  }
1341
- async function connect() {
1342
- if (closed || discoveryPaused) {
1343
- notifyReady();
1876
+ function task(work) {
1877
+ if (stopping)
1878
+ return chain;
1879
+ chain = chain.then(work, work).catch(() => undefined);
1880
+ return chain;
1881
+ }
1882
+ function sync(attempt) {
1883
+ return task(async () => {
1884
+ if (closed || stopping || discoveryPaused) {
1885
+ notifyReady();
1886
+ return;
1887
+ }
1888
+ const progress = { complete: true, reachable: true };
1889
+ underway = attempt === 'opened' ? progress : null;
1890
+ if (attempt === 'opened')
1891
+ streamed = progress;
1892
+ complete = false;
1893
+ recorded = false;
1894
+ moveTo(transferring(status));
1895
+ handed = false;
1896
+ try {
1897
+ const local = await openLocal();
1898
+ if (closed)
1899
+ return;
1900
+ await ensureAuthorization();
1901
+ if (closed)
1902
+ return;
1903
+ await discoverRooms(local, progress, attempt);
1904
+ }
1905
+ catch {
1906
+ progress.complete = false;
1907
+ if (attempt === 'opened' && !handed)
1908
+ progress.reachable = false;
1909
+ }
1910
+ finally {
1911
+ notifyReady();
1912
+ }
1913
+ if (attempt !== 'opened') {
1914
+ await finishOpening(progress);
1915
+ return;
1916
+ }
1917
+ if (handed || closed || progress.reachable)
1918
+ return;
1919
+ underway = null;
1920
+ journal.record('connection reopen');
1921
+ await rest(progress);
1922
+ options.adapter.roomStream?.reopen();
1923
+ });
1924
+ }
1925
+ function closedSocket(revoked) {
1926
+ const opened = socketOpen;
1927
+ socketOpen = false;
1928
+ if (revoked) {
1929
+ void refuse('revoked-authorization');
1344
1930
  return;
1345
1931
  }
1346
- startTimer();
1347
- const progress = {
1348
- complete: false,
1349
- reachable: true
1350
- };
1351
- let completedAt = new Date().toISOString();
1932
+ complete = false;
1933
+ if (streamed !== null)
1934
+ streamed.complete = false;
1935
+ moveTo({
1936
+ ...status,
1937
+ state: status.state === 'synced' ? 'syncing' : status.state,
1938
+ providerUnreachable: status.providerUnreachable || !opened,
1939
+ });
1940
+ }
1941
+ function applyOpening(expired) {
1942
+ return task(async () => {
1943
+ const progress = underway;
1944
+ underway = null;
1945
+ if (closed || stopping)
1946
+ return;
1947
+ if (progress === null) {
1948
+ await reloadExpired(expired);
1949
+ return;
1950
+ }
1951
+ try {
1952
+ const local = await openLocal();
1953
+ const held = [...discovered.values()].filter((room) => replicas.has(room.locator));
1954
+ const reloading = held.filter((room) => expired.includes(room.locator) || revoked(room.locator));
1955
+ const reloaded = await Promise.allSettled(reloading.map((room) => reload(room, local, progress)));
1956
+ if (progress.reachable) {
1957
+ for (const room of held)
1958
+ if (!reloading.includes(room))
1959
+ await confirmModels(room, local);
1960
+ }
1961
+ const failed = reloaded.find((outcome) => outcome.status === 'rejected');
1962
+ if (failed !== undefined)
1963
+ throw failed.reason;
1964
+ }
1965
+ catch {
1966
+ progress.complete = false;
1967
+ progress.reachable = false;
1968
+ }
1969
+ await finishOpening(progress);
1970
+ });
1971
+ }
1972
+ async function reloadExpired(expired) {
1973
+ const progress = { complete: true, reachable: true };
1352
1974
  try {
1353
1975
  const local = await openLocal();
1354
- if (closed) return;
1355
- await ensureAuthorization();
1356
- if (closed) return;
1357
- await cycle(local, progress);
1358
- if (closed) return;
1359
- if (progress.complete) {
1360
- completedAt = new Date().toISOString();
1361
- await local.complete(completedAt);
1976
+ await Promise.allSettled([...discovered.values()]
1977
+ .filter((room) => replicas.has(room.locator) && expired.includes(room.locator))
1978
+ .map((room) => reload(room, local, progress)));
1979
+ }
1980
+ catch {
1981
+ progress.reachable = false;
1982
+ }
1983
+ await rest(progress);
1984
+ }
1985
+ async function confirmModels(room, local) {
1986
+ for (const attached of ['collection', 'typed-selection']) {
1987
+ const model = modelOf(room.locator, attached);
1988
+ if (model.status === 'cached') {
1989
+ await retainModel(local, room, attached, { ...model, status: 'current' });
1362
1990
  }
1363
- } catch {
1364
- progress.complete = false;
1365
- notifyReady();
1991
+ }
1992
+ }
1993
+ function submitLater(locator) {
1994
+ if (localSaves)
1995
+ return;
1996
+ const running = submitting.get(locator);
1997
+ if (running !== undefined) {
1998
+ running.again = true;
1366
1999
  return;
1367
- } finally{
1368
- notifyReady();
1369
- moveTo(settled(status, progress, completedAt, pending(), attention()));
1370
2000
  }
2001
+ const run = { again: false, done: Promise.resolve() };
2002
+ submitting.set(locator, run);
2003
+ run.done = (async () => {
2004
+ try {
2005
+ do {
2006
+ run.again = false;
2007
+ const room = discovered.get(locator);
2008
+ if (closed || stopping || discoveryPaused || room === undefined)
2009
+ return;
2010
+ const progress = { complete: true, reachable: true };
2011
+ try {
2012
+ await ensureAuthorization();
2013
+ if (authorization === undefined)
2014
+ return;
2015
+ await submitRoom(room, await openLocal(), authorization, progress);
2016
+ }
2017
+ catch {
2018
+ progress.reachable = false;
2019
+ }
2020
+ armTimer();
2021
+ await rest(progress);
2022
+ } while (run.again);
2023
+ }
2024
+ finally {
2025
+ submitting.delete(locator);
2026
+ }
2027
+ })();
1371
2028
  }
1372
- async function run() {
2029
+ function submitAll() {
2030
+ for (const [locator, replica] of replicas) {
2031
+ if (replica.queue.length > 0)
2032
+ submitLater(locator);
2033
+ }
2034
+ }
2035
+ async function finishOpening(progress) {
1373
2036
  try {
1374
- await connect();
1375
- } finally{
1376
- inflight = false;
1377
- if (queued && !closed) {
1378
- queued = false;
1379
- void enqueue();
2037
+ if (closed || discoveryPaused) {
2038
+ progress.complete = false;
2039
+ return;
2040
+ }
2041
+ submitAll();
2042
+ await conversations.reconcile();
2043
+ if (status.providerUnreachable)
2044
+ conversations.reconnected();
2045
+ const reading = { complete: true, reachable: true };
2046
+ await conversations.catchUp(reading);
2047
+ if (!reading.reachable)
2048
+ progress.reachable = false;
2049
+ }
2050
+ catch {
2051
+ progress.complete = false;
2052
+ }
2053
+ finally {
2054
+ complete = progress.complete;
2055
+ await rest(progress);
2056
+ }
2057
+ }
2058
+ async function rest(progress) {
2059
+ if (closed)
2060
+ return;
2061
+ let lastCompleted = status.lastCompleted;
2062
+ let synced = complete && stale.size === 0 && !pending();
2063
+ if (synced && !recorded) {
2064
+ const completedAt = new Date().toISOString();
2065
+ try {
2066
+ await (await openLocal()).complete(completedAt);
2067
+ recorded = true;
2068
+ lastCompleted = completedAt;
2069
+ }
2070
+ catch {
2071
+ synced = false;
1380
2072
  }
1381
2073
  }
2074
+ moveTo({
2075
+ state: pending() ? 'pending' : synced && complete ? 'synced' : 'syncing',
2076
+ lastCompleted,
2077
+ providerUnreachable: !progress.reachable,
2078
+ ...attention(),
2079
+ });
1382
2080
  }
1383
- function observeRoomVersions() {
1384
- const path = options.adapter.roomVersions;
1385
- if (path === undefined || withdrawNotifications !== undefined) return;
1386
- withdrawNotifications = path.observe((notification)=>{
1387
- if (closed || discoveryPaused) return;
1388
- notices.set(notification.locator, notification.roomVersion);
1389
- chain = chain.then(runNotified, runNotified);
2081
+ function applyRoom(locator, roomVersion) {
2082
+ announced.set(locator, roomVersion);
2083
+ return task(async () => {
2084
+ await answering.get(locator);
2085
+ if (closed || stopping || discoveryPaused || authorization === undefined)
2086
+ return;
2087
+ const room = discovered.get(locator);
2088
+ const held = replicas.get(locator);
2089
+ if (room === undefined || room.replica.admission !== 'admitted')
2090
+ return;
2091
+ if (held !== undefined &&
2092
+ (held.confirmed.record.replica.selection !== room.replica.selection ||
2093
+ options.adapter.reached(locator, held.confirmed.record.roomVersion, roomVersion))) {
2094
+ return;
2095
+ }
2096
+ const during = underway;
2097
+ const progress = during ?? { complete: true, reachable: true };
2098
+ if (during === null)
2099
+ moveTo(transferring(status));
2100
+ try {
2101
+ const local = await openLocal();
2102
+ const withheld = await advance(room, local, authorization);
2103
+ if (withheld !== null)
2104
+ await withhold(withheld, room, local, progress);
2105
+ }
2106
+ catch {
2107
+ stale.add(locator);
2108
+ }
2109
+ finally {
2110
+ if (during === null)
2111
+ await rest(progress);
2112
+ }
1390
2113
  });
1391
2114
  }
2115
+ function observeStream() {
2116
+ const stream = options.adapter.roomStream;
2117
+ if (stream === undefined || withdrawStream !== undefined)
2118
+ return;
2119
+ withdrawStream = stream.observe((event) => {
2120
+ if (closed || stopping)
2121
+ return;
2122
+ if (event.kind === 'opened') {
2123
+ socketOpen = true;
2124
+ void sync('opened');
2125
+ }
2126
+ else if (event.kind === 'closed')
2127
+ closedSocket(event.revoked);
2128
+ else if (event.kind === 'opening')
2129
+ void applyOpening(event.expired);
2130
+ else
2131
+ void applyRoom(event.locator, event.roomVersion);
2132
+ });
2133
+ }
2134
+ function writesHeld() {
2135
+ return serviceMode === 'write-halted' || haltedRefusal;
2136
+ }
2137
+ function signalled(mode) {
2138
+ if (closed)
2139
+ return;
2140
+ if (mode === 'open')
2141
+ openSignals += 1;
2142
+ const releasing = mode === 'open' && (haltedRefusal || serviceMode === 'write-halted');
2143
+ if (mode === 'open')
2144
+ haltedRefusal = false;
2145
+ if (mode !== serviceMode) {
2146
+ serviceMode = mode;
2147
+ journal.record(`service mode ${mode}`);
2148
+ for (const listener of serviceModeListeners)
2149
+ listener(mode);
2150
+ }
2151
+ if (releasing)
2152
+ submitAll();
2153
+ }
1392
2154
  function observeConversations() {
1393
- const path = options.adapter.conversations;
1394
- if (path === undefined || withdrawConversations !== undefined) return;
1395
- withdrawConversations = path.observe((event)=>{
1396
- conversations.receive(event);
2155
+ const stream = options.adapter.conversations;
2156
+ if (stream === undefined || withdrawConversations !== undefined)
2157
+ return;
2158
+ withdrawConversations = stream.observe((event) => {
2159
+ if (conversations.receive(event)) {
2160
+ void ready.then(scheduleCurrency);
2161
+ return;
2162
+ }
2163
+ if (event.kind !== 'gap' || conversations.current())
2164
+ return;
2165
+ show();
2166
+ scheduleCurrency();
1397
2167
  });
1398
2168
  }
1399
- function handSelected(locators) {
1400
- const path = options.adapter.roomVersions;
1401
- if (path === undefined || withdrawNotifications === undefined) return;
1402
- if (handed !== undefined && JSON.stringify(locators) === JSON.stringify(handed)) return;
1403
- handed = locators;
1404
- path.select(locators);
2169
+ function scheduleCurrency() {
2170
+ if (currencyQueued || closed || stopping)
2171
+ return;
2172
+ currencyQueued = true;
2173
+ void task(runCurrency);
1405
2174
  }
1406
- async function runNotified() {
1407
- const announced = [
1408
- ...notices
1409
- ];
1410
- notices.clear();
1411
- if (announced.length === 0 || closed || discoveryPaused) return;
1412
- const resting = status;
1413
- const progress = {
1414
- complete: false,
1415
- reachable: !resting.providerUnreachable
1416
- };
1417
- moveTo(transferring(resting));
2175
+ async function runCurrency() {
2176
+ currencyQueued = false;
2177
+ if (closed || stopping || discoveryPaused)
2178
+ return;
2179
+ await conversations.reconcile();
2180
+ if (conversations.current()) {
2181
+ show();
2182
+ return;
2183
+ }
2184
+ const progress = { complete: false, reachable: !status.providerUnreachable };
2185
+ moveTo(transferring(status));
1418
2186
  try {
1419
- const local = await openLocal();
1420
- for (const [locator, roomVersion] of announced){
1421
- if (closed || discoveryPaused || authorization === undefined) return;
1422
- if (handed?.includes(locator) !== true) continue;
1423
- const room = discovered.get(locator);
1424
- const held = replicas.get(locator);
1425
- if (room === undefined || held === undefined) continue;
1426
- if (held.confirmed.record.roomVersion === roomVersion) continue;
1427
- const withheld = await advance(room, local, authorization);
1428
- if (withheld === null) continue;
1429
- if (withheld.kind === 'scoped-refusal') {
1430
- await scoped(withheld, room, local);
1431
- continue;
1432
- }
1433
- if (withheld.kind === 'transport-uncertainty') {
1434
- progress.reachable = false;
1435
- continue;
1436
- }
1437
- if (withheld.kind === 'room-not-readable') {
1438
- unreadable.add(locator);
1439
- list(room, false);
1440
- emitRooms();
1441
- continue;
1442
- }
1443
- if (withheld.kind === 'revoked-authorization') {
1444
- refuse('revoked-authorization');
1445
- return;
1446
- }
1447
- }
1448
- } catch {
1449
- progress.reachable = false;
1450
- } finally{
1451
- if (!closed) {
1452
- moveTo(settled(resting, progress, resting.lastCompleted, pending(), attention()));
1453
- }
2187
+ await conversations.catchUp(progress);
1454
2188
  }
1455
- }
1456
- function enqueue() {
1457
- if (inflight) {
1458
- queued = true;
1459
- return chain;
2189
+ catch {
2190
+ progress.reachable = false;
1460
2191
  }
1461
- inflight = true;
1462
- chain = chain.then(run, run);
1463
- return chain;
2192
+ await rest(progress);
1464
2193
  }
1465
2194
  function close() {
2195
+ if (closed)
2196
+ return;
1466
2197
  closed = true;
1467
- withdrawNotifications?.();
1468
- withdrawNotifications = undefined;
2198
+ withdrawStream?.();
2199
+ withdrawStream = undefined;
1469
2200
  withdrawConversations?.();
1470
2201
  withdrawConversations = undefined;
2202
+ withdrawServiceMode?.();
1471
2203
  conversations.close();
1472
2204
  stopTimer();
1473
2205
  store?.close();
1474
- void opening?.then((opened)=>{
2206
+ void opening?.then((opened) => {
1475
2207
  opened.close();
1476
- }, ()=>{});
2208
+ }, () => { });
1477
2209
  roomListeners.clear();
1478
2210
  refusalListeners.clear();
1479
2211
  storageLossListeners.clear();
1480
2212
  statusListeners.clear();
1481
- querySubscriptions.clear();
2213
+ serviceModeListeners.clear();
2214
+ memberListeners.clear();
2215
+ readSide.close();
1482
2216
  queueSubscriptions.clear();
1483
- draftListeners.clear();
2217
+ roomDraftListeners.clear();
1484
2218
  presentationListeners.clear();
1485
2219
  }
1486
- options.signal?.addEventListener('abort', close, {
1487
- once: true
1488
- });
1489
- if (options.signal?.aborted) close();
1490
- void openLocal().catch(()=>{
2220
+ async function stop() {
2221
+ stopping = true;
2222
+ stopTimer();
2223
+ await chain.catch(() => undefined);
2224
+ await Promise.all([...submitting.values()].map((run) => run.done));
2225
+ await mutations;
2226
+ close();
2227
+ }
2228
+ const withdrawServiceMode = options.adapter.serviceMode?.observe(signalled);
2229
+ observeConversations();
2230
+ options.signal?.addEventListener('abort', close, { once: true });
2231
+ if (options.signal?.aborted)
2232
+ close();
2233
+ observeStream();
2234
+ void openLocal().then(() => {
2235
+ if (capabilities.length > 0)
2236
+ notifyReady();
2237
+ }, () => {
1491
2238
  notifyReady();
1492
2239
  });
1493
- void enqueue();
1494
- return {
1495
- rooms () {
1496
- return listed();
2240
+ if (options.adapter.roomStream === undefined)
2241
+ void sync('unstreamed');
2242
+ function planned(input, reach) {
2243
+ return planOf(input, roomFacts(reach), planReach(reach));
2244
+ }
2245
+ function watchPlan(plan, deliver) {
2246
+ materialize(reachedRooms(plan));
2247
+ return readSide.watch(plan, identityOf(plan), deliver);
2248
+ }
2249
+ async function searched(input, reach) {
2250
+ const plan = searchPlanOf(input);
2251
+ const selected = plan.documents === undefined && reach === null
2252
+ ? null
2253
+ : (await rowsQuery(filterOf(plan.documents ?? {}, roomFacts(reach), planReach(reach)))).documents;
2254
+ if (selected === null)
2255
+ materialize(null);
2256
+ return searchIndex.search(reach === null || reach.conversations ? plan : { ...plan, conversations: [] }, selected);
2257
+ }
2258
+ const watch = (topic, input, deliver) => {
2259
+ if (topic === 'room-drafts') {
2260
+ const listener = { notify: deliver, ready: false };
2261
+ roomDraftListeners.add(listener);
2262
+ void exclusive(async () => {
2263
+ await openLocal();
2264
+ if (closed || !roomDraftListeners.has(listener))
2265
+ return;
2266
+ listener.ready = true;
2267
+ }).then(() => {
2268
+ if (closed || !listener.ready || !roomDraftListeners.has(listener))
2269
+ return;
2270
+ listener.notify(snapshotRoomDrafts());
2271
+ }, () => { });
2272
+ return () => {
2273
+ roomDraftListeners.delete(listener);
2274
+ };
2275
+ }
2276
+ if (topic === 'conversation') {
2277
+ return conversations.watch(input[0], input[1], deliver);
2278
+ }
2279
+ if (topic === 'conversations')
2280
+ return conversations.watchList(deliver);
2281
+ if (topic === 'agents')
2282
+ return conversations.watchAgents(deliver);
2283
+ if (topic === 'presentation') {
2284
+ const listener = {
2285
+ app: input[0],
2286
+ notify: deliver,
2287
+ ready: false,
2288
+ };
2289
+ presentationListeners.add(listener);
2290
+ void exclusive(async () => {
2291
+ await openLocal();
2292
+ }).then(() => {
2293
+ if (closed || !presentationListeners.has(listener))
2294
+ return;
2295
+ listener.ready = true;
2296
+ listener.notify(presentationOf(listener.app));
2297
+ }, () => { });
2298
+ return () => {
2299
+ presentationListeners.delete(listener);
2300
+ };
2301
+ }
2302
+ if (topic === 'rooms') {
2303
+ roomListeners.add(deliver);
2304
+ deliver(listed());
2305
+ return () => {
2306
+ roomListeners.delete(deliver);
2307
+ };
2308
+ }
2309
+ if (topic === 'connection-refusal') {
2310
+ refusalListeners.add(deliver);
2311
+ deliver(refusal);
2312
+ return () => {
2313
+ refusalListeners.delete(deliver);
2314
+ };
2315
+ }
2316
+ if (topic === 'storage-loss') {
2317
+ storageLossListeners.add(deliver);
2318
+ deliver(storageLoss);
2319
+ return () => {
2320
+ storageLossListeners.delete(deliver);
2321
+ };
2322
+ }
2323
+ if (topic === 'sync-status') {
2324
+ statusListeners.add(deliver);
2325
+ deliver(shown);
2326
+ return () => {
2327
+ statusListeners.delete(deliver);
2328
+ };
2329
+ }
2330
+ if (topic === 'service-mode') {
2331
+ serviceModeListeners.add(deliver);
2332
+ deliver(serviceMode);
2333
+ return () => {
2334
+ serviceModeListeners.delete(deliver);
2335
+ };
2336
+ }
2337
+ if (topic === 'member') {
2338
+ memberListeners.add(deliver);
2339
+ deliver(member);
2340
+ return () => {
2341
+ memberListeners.delete(deliver);
2342
+ };
2343
+ }
2344
+ if (topic === 'queue') {
2345
+ const queue = { locator: input[0], notify: deliver };
2346
+ queueSubscriptions.add(queue);
2347
+ queue.notify(entriesOf(queue.locator));
2348
+ return () => {
2349
+ queueSubscriptions.delete(queue);
2350
+ };
2351
+ }
2352
+ return watchPlan(planned(input[0], null), deliver);
2353
+ };
2354
+ const md = {
2355
+ loaded() {
2356
+ return openLocal().then(() => undefined);
1497
2357
  },
1498
- query (request = {}) {
1499
- return visible(request);
2358
+ rooms() {
2359
+ return listed();
1500
2360
  },
1501
- types (locator) {
2361
+ query: ((input) => rowsQuery(planned(input, null))),
2362
+ search: (input) => searched(input, null),
2363
+ types(locator) {
1502
2364
  return {
1503
2365
  collection: modelTypes(modelOf(locator)),
1504
- typedSelection: rooms.get(locator)?.typedSelection === undefined || rooms.get(locator)?.typedSelection === null ? null : modelTypes(modelOf(locator, 'typed-selection'))
2366
+ typedSelection: rooms.get(locator)?.typedSelection === undefined ||
2367
+ rooms.get(locator)?.typedSelection === null
2368
+ ? null
2369
+ : modelTypes(modelOf(locator, 'typed-selection')),
1505
2370
  };
1506
2371
  },
1507
- syncStatus () {
1508
- return status;
1509
- },
1510
- draft: {
1511
- list () {
1512
- return exclusive(async ()=>{
1513
- const local = await openLocal();
1514
- return readDrafts(local);
1515
- }).then((entries)=>{
1516
- releaseWaitingDrafts();
1517
- return entries;
2372
+ roomDraft: {
2373
+ list() {
2374
+ return exclusive(async () => {
2375
+ await openLocal();
2376
+ return snapshotRoomDrafts();
2377
+ }).then((listed) => {
2378
+ publishRoomDrafts(true);
2379
+ return listed;
1518
2380
  });
1519
2381
  },
1520
- read (locator, path) {
1521
- const content = draftContent(locator, path);
1522
- return content === undefined ? null : structuredClone(content);
2382
+ read(room, path) {
2383
+ const record = roomDrafts.get(room)?.get(path);
2384
+ return record === undefined ? null : roomDraftOf(record);
1523
2385
  },
1524
- replace (locator, path, content) {
1525
- return commitDraft(async (local)=>{
1526
- const stored = structuredClone(content);
1527
- await local.keep({
1528
- locator,
2386
+ write(room, path, revision, input) {
2387
+ return exclusive(async () => {
2388
+ const parts = readParts(input);
2389
+ if (path === '' || normalize(path) !== path)
2390
+ throw runtimeError('room-draft-malformed');
2391
+ const local = await openLocal();
2392
+ const current = roomDrafts.get(room)?.get(path);
2393
+ if ((current?.revision ?? null) !== revision) {
2394
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2395
+ }
2396
+ const file = parts.file === undefined
2397
+ ? undefined
2398
+ : await heldFile(parts.file.blob, binaryBase(replicas.get(room)?.visible, path));
2399
+ const record = {
2400
+ locator: room,
1529
2401
  path,
1530
- content: stored
1531
- });
1532
- rememberDraft(locator, path, stored);
2402
+ revision: roomDraftRevision(),
2403
+ parts: held(current?.parts, parts, file),
2404
+ returned: current?.returned ?? null,
2405
+ };
2406
+ await local.keepRoomDraft(record);
2407
+ rememberRoomDraft(record);
2408
+ publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2409
+ return { outcome: 'written', roomDraft: roomDraftOf(record) };
1533
2410
  });
1534
2411
  },
1535
- discard (locator, path) {
1536
- return commitDraft(async (local)=>{
1537
- await local.discard(locator, path);
1538
- forgetDraft(locator, path);
2412
+ discard(room, path, revision) {
2413
+ return exclusive(async () => {
2414
+ const local = await openLocal();
2415
+ const current = roomDrafts.get(room)?.get(path);
2416
+ if (current === undefined || current.revision !== revision) {
2417
+ return staleWrite(current === undefined ? null : roomDraftOf(current));
2418
+ }
2419
+ await local.discardRoomDraft(room, path);
2420
+ forgetRoomDraft(room, path);
2421
+ publishChangeSet(changeSet(room, null, null, { drafts: [path] }));
2422
+ moveTo({ ...status, ...attention() });
2423
+ return { outcome: 'written', roomDraft: null };
1539
2424
  });
1540
- }
2425
+ },
1541
2426
  },
1542
2427
  conversation: conversations.surface,
2428
+ chatDraft: conversations.chatDraft,
1543
2429
  presentation: {
1544
- read (app) {
1545
- return exclusive(async ()=>{
1546
- await hydrate(await openLocal());
2430
+ read(app) {
2431
+ return exclusive(async () => {
2432
+ await openLocal();
1547
2433
  return presentationOf(app);
1548
2434
  });
1549
2435
  },
1550
- write (app, value) {
1551
- return exclusive(async ()=>{
2436
+ write(app, value) {
2437
+ return exclusive(async () => {
1552
2438
  const local = await openLocal();
1553
- await hydrate(local);
1554
2439
  const stored = structuredClone(value);
1555
- await local.present({
1556
- app,
1557
- value: stored
1558
- });
2440
+ await local.present({ app, value: stored });
1559
2441
  presentations.set(app, stored);
1560
- }).then(()=>{
1561
- emitPresentation(app);
2442
+ publishChangeSet(changeSet('', null, null, { presentation: app }));
1562
2443
  });
1563
- }
1564
- },
1565
- save (locator, changes, replacing) {
1566
- return exclusive(()=>write(locator, changes, replacing));
1567
- },
1568
- outcome (identifier) {
1569
- const recorded = outcomes.get(identifier);
1570
- if (recorded !== undefined) return recorded;
1571
- for (const replica of replicas.values()){
1572
- if (replica.queue.some((batch)=>batch.id === identifier)) return {
1573
- kind: 'pending'
1574
- };
1575
- }
1576
- return null;
2444
+ },
1577
2445
  },
1578
- queue: {
1579
- discard (locator, identifier) {
1580
- return exclusive(()=>discardHead(locator, identifier));
1581
- }
2446
+ save(room, named, writtenBy) {
2447
+ return exclusive(() => saveRoomDrafts(room, named, writtenBy));
1582
2448
  },
1583
- history (locator, path, query) {
2449
+ history(locator, path, query) {
1584
2450
  return readHistory(locator, path, query);
1585
2451
  },
1586
- call (name, input) {
2452
+ binaryFile: {
2453
+ read: readBinaryPath,
2454
+ readReference: readBinaryReference,
2455
+ },
2456
+ call(name, input) {
1587
2457
  return invokeAction(name, input);
1588
2458
  },
1589
- log () {
1590
- return journal.export();
2459
+ reached(locator, roomVersion, path) {
2460
+ return path === undefined
2461
+ ? reachedBy(locator, roomVersion)
2462
+ : writtenAfter(locator, roomVersion, path);
1591
2463
  },
1592
- storageLoss () {
1593
- return storageLoss;
2464
+ log() {
2465
+ return journal.export();
1594
2466
  },
1595
- persistence () {
2467
+ persistence() {
1596
2468
  return options.shell.persistentStorage;
1597
2469
  },
1598
- observe (topic, second, third) {
1599
- if (topic === 'drafts') {
1600
- const notify = second;
1601
- const listener = {
1602
- notify,
1603
- ready: false
1604
- };
1605
- draftListeners.add(listener);
1606
- void exclusive(async ()=>{
1607
- const local = await openLocal();
1608
- await readDrafts(local);
1609
- if (closed || !draftListeners.has(listener)) return;
1610
- listener.ready = true;
1611
- }).then(()=>{
1612
- if (closed || !listener.ready || !draftListeners.has(listener)) return;
1613
- listener.notify(snapshotDrafts());
1614
- }, ()=>{});
1615
- return ()=>{
1616
- draftListeners.delete(listener);
1617
- };
1618
- }
1619
- if (topic === 'conversation') {
1620
- return conversations.observe(second, third);
1621
- }
1622
- if (topic === 'presentation') {
1623
- const listener = {
1624
- app: second,
1625
- notify: third,
1626
- ready: false
1627
- };
1628
- presentationListeners.add(listener);
1629
- void exclusive(async ()=>{
1630
- await hydrate(await openLocal());
1631
- }).then(()=>{
1632
- if (closed || !presentationListeners.has(listener)) return;
1633
- listener.ready = true;
1634
- listener.notify(presentationOf(listener.app));
1635
- }, ()=>{});
1636
- return ()=>{
1637
- presentationListeners.delete(listener);
1638
- };
1639
- }
1640
- if (topic === 'rooms') {
1641
- const notify = second;
1642
- roomListeners.add(notify);
1643
- notify(listed());
1644
- return ()=>{
1645
- roomListeners.delete(notify);
1646
- };
1647
- }
1648
- if (topic === 'connection-refusal') {
1649
- const notify = second;
1650
- refusalListeners.add(notify);
1651
- notify(refusal);
1652
- return ()=>{
1653
- refusalListeners.delete(notify);
1654
- };
1655
- }
1656
- if (topic === 'storage-loss') {
1657
- const notify = second;
1658
- storageLossListeners.add(notify);
1659
- notify(storageLoss);
1660
- return ()=>{
1661
- storageLossListeners.delete(notify);
1662
- };
1663
- }
1664
- if (topic === 'sync-status') {
1665
- const notify = second;
1666
- statusListeners.add(notify);
1667
- notify(status);
1668
- return ()=>{
1669
- statusListeners.delete(notify);
1670
- };
1671
- }
1672
- if (topic === 'queue') {
1673
- const queue = {
1674
- locator: second,
1675
- notify: third
1676
- };
1677
- queueSubscriptions.add(queue);
1678
- queue.notify(entriesOf(queue.locator));
1679
- return ()=>{
1680
- queueSubscriptions.delete(queue);
1681
- };
1682
- }
1683
- const subscription = {
1684
- query: second,
1685
- notify: third
1686
- };
1687
- const first = visible(subscription.query);
1688
- querySubscriptions.add(subscription);
1689
- subscription.notify(first);
1690
- return ()=>{
1691
- querySubscriptions.delete(subscription);
1692
- };
1693
- },
1694
- async reauthorize () {
1695
- if (refusal === null || !refusal.reauthorizationOffered) return;
2470
+ observe: observing(watch),
2471
+ async reauthorize() {
2472
+ if (refusal === null || !refusal.reauthorizationOffered)
2473
+ return;
1696
2474
  journal.record('connection reauthorize');
2475
+ await chain.catch(() => undefined);
2476
+ await mutations;
1697
2477
  authorization = undefined;
1698
2478
  discoveryPaused = false;
1699
- await enqueue();
1700
- }
2479
+ if (options.adapter.roomStream === undefined)
2480
+ await sync('unstreamed');
2481
+ else
2482
+ await task(ensureAuthorization);
2483
+ },
2484
+ };
2485
+ const reachable = {
2486
+ md,
2487
+ watch,
2488
+ referrerScope: (referrer) => options.adapter.referrerScope(referrer),
2489
+ plan: planned,
2490
+ query: (plan) => rowsQuery(plan),
2491
+ watchPlan,
2492
+ search: searched,
1701
2493
  };
2494
+ return { md, stop, lose: loseRooms, watch, within: (reach) => narrowed(reachable, reach) };
1702
2495
  }
1703
-
1704
- //# sourceMappingURL=connection.js.map