opencode-oncall 0.1.0

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 (105) hide show
  1. package/LICENSE +151 -0
  2. package/README.md +50 -0
  3. package/dist/common-settings-actions.d.ts +15 -0
  4. package/dist/common-settings-actions.js +48 -0
  5. package/dist/common-settings-store.d.ts +1 -0
  6. package/dist/common-settings-store.js +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/plugin-hooks.d.ts +51 -0
  10. package/dist/plugin-hooks.js +288 -0
  11. package/dist/plugin.d.ts +10 -0
  12. package/dist/plugin.js +115 -0
  13. package/dist/settings-store.d.ts +50 -0
  14. package/dist/settings-store.js +214 -0
  15. package/dist/store-paths.d.ts +16 -0
  16. package/dist/store-paths.js +61 -0
  17. package/dist/ui/wechat-menu.d.ts +26 -0
  18. package/dist/ui/wechat-menu.js +90 -0
  19. package/dist/wechat/bind-flow.d.ts +29 -0
  20. package/dist/wechat/bind-flow.js +207 -0
  21. package/dist/wechat/bridge.d.ts +136 -0
  22. package/dist/wechat/bridge.js +1059 -0
  23. package/dist/wechat/broker-client.d.ts +23 -0
  24. package/dist/wechat/broker-client.js +274 -0
  25. package/dist/wechat/broker-endpoint.d.ts +21 -0
  26. package/dist/wechat/broker-endpoint.js +78 -0
  27. package/dist/wechat/broker-entry.d.ts +123 -0
  28. package/dist/wechat/broker-entry.js +1321 -0
  29. package/dist/wechat/broker-launcher.d.ts +37 -0
  30. package/dist/wechat/broker-launcher.js +418 -0
  31. package/dist/wechat/broker-mutation-queue.d.ts +93 -0
  32. package/dist/wechat/broker-mutation-queue.js +126 -0
  33. package/dist/wechat/broker-server.d.ts +86 -0
  34. package/dist/wechat/broker-server.js +1340 -0
  35. package/dist/wechat/broker-state-store.d.ts +335 -0
  36. package/dist/wechat/broker-state-store.js +1964 -0
  37. package/dist/wechat/command-parser.d.ts +18 -0
  38. package/dist/wechat/command-parser.js +58 -0
  39. package/dist/wechat/compat/jiti-loader.d.ts +27 -0
  40. package/dist/wechat/compat/jiti-loader.js +118 -0
  41. package/dist/wechat/compat/openclaw-account-helpers.d.ts +29 -0
  42. package/dist/wechat/compat/openclaw-account-helpers.js +60 -0
  43. package/dist/wechat/compat/openclaw-bind-helpers.d.ts +29 -0
  44. package/dist/wechat/compat/openclaw-bind-helpers.js +169 -0
  45. package/dist/wechat/compat/openclaw-guided-smoke.d.ts +180 -0
  46. package/dist/wechat/compat/openclaw-guided-smoke.js +1134 -0
  47. package/dist/wechat/compat/openclaw-public-entry.d.ts +33 -0
  48. package/dist/wechat/compat/openclaw-public-entry.js +62 -0
  49. package/dist/wechat/compat/openclaw-public-helpers.d.ts +70 -0
  50. package/dist/wechat/compat/openclaw-public-helpers.js +68 -0
  51. package/dist/wechat/compat/openclaw-qr-gateway.d.ts +15 -0
  52. package/dist/wechat/compat/openclaw-qr-gateway.js +39 -0
  53. package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
  54. package/dist/wechat/compat/openclaw-smoke.js +100 -0
  55. package/dist/wechat/compat/openclaw-sync-buf.d.ts +24 -0
  56. package/dist/wechat/compat/openclaw-sync-buf.js +80 -0
  57. package/dist/wechat/compat/openclaw-updates-send.d.ts +47 -0
  58. package/dist/wechat/compat/openclaw-updates-send.js +38 -0
  59. package/dist/wechat/compat/qrcode-terminal-loader.d.ts +12 -0
  60. package/dist/wechat/compat/qrcode-terminal-loader.js +16 -0
  61. package/dist/wechat/compat/slash-guard.d.ts +11 -0
  62. package/dist/wechat/compat/slash-guard.js +24 -0
  63. package/dist/wechat/dead-letter-store.d.ts +48 -0
  64. package/dist/wechat/dead-letter-store.js +224 -0
  65. package/dist/wechat/debug-bundle-collector.d.ts +49 -0
  66. package/dist/wechat/debug-bundle-collector.js +580 -0
  67. package/dist/wechat/debug-bundle-flow.d.ts +37 -0
  68. package/dist/wechat/debug-bundle-flow.js +180 -0
  69. package/dist/wechat/debug-bundle-redaction.d.ts +14 -0
  70. package/dist/wechat/debug-bundle-redaction.js +339 -0
  71. package/dist/wechat/handle.d.ts +10 -0
  72. package/dist/wechat/handle.js +57 -0
  73. package/dist/wechat/ipc-auth.d.ts +6 -0
  74. package/dist/wechat/ipc-auth.js +39 -0
  75. package/dist/wechat/latest-account-state-store.d.ts +8 -0
  76. package/dist/wechat/latest-account-state-store.js +38 -0
  77. package/dist/wechat/notification-dispatcher.d.ts +34 -0
  78. package/dist/wechat/notification-dispatcher.js +266 -0
  79. package/dist/wechat/notification-format.d.ts +15 -0
  80. package/dist/wechat/notification-format.js +196 -0
  81. package/dist/wechat/notification-store.d.ts +72 -0
  82. package/dist/wechat/notification-store.js +807 -0
  83. package/dist/wechat/notification-types.d.ts +37 -0
  84. package/dist/wechat/notification-types.js +1 -0
  85. package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
  86. package/dist/wechat/openclaw-account-adapter.js +60 -0
  87. package/dist/wechat/operator-store.d.ts +9 -0
  88. package/dist/wechat/operator-store.js +69 -0
  89. package/dist/wechat/protocol.d.ts +150 -0
  90. package/dist/wechat/protocol.js +197 -0
  91. package/dist/wechat/question-interaction.d.ts +24 -0
  92. package/dist/wechat/question-interaction.js +180 -0
  93. package/dist/wechat/request-store.d.ts +108 -0
  94. package/dist/wechat/request-store.js +669 -0
  95. package/dist/wechat/session-digest.d.ts +50 -0
  96. package/dist/wechat/session-digest.js +167 -0
  97. package/dist/wechat/state-paths.d.ts +26 -0
  98. package/dist/wechat/state-paths.js +92 -0
  99. package/dist/wechat/status-format.d.ts +26 -0
  100. package/dist/wechat/status-format.js +616 -0
  101. package/dist/wechat/token-store.d.ts +20 -0
  102. package/dist/wechat/token-store.js +193 -0
  103. package/dist/wechat/wechat-status-runtime.d.ts +89 -0
  104. package/dist/wechat/wechat-status-runtime.js +518 -0
  105. package/package.json +74 -0
@@ -0,0 +1,1964 @@
1
+ import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
4
+ import { createHandle, normalizeHandle } from "./handle.js";
5
+ import { brokerStateSchemaPath, brokerStateStorePath, notificationsDir, requestKindDir, WECHAT_FILE_MODE, } from "./state-paths.js";
6
+ export const BROKER_STATE_SCHEMA_MARKER_KIND = "wechat-broker-state-store";
7
+ export const LEGACY_STATE_RESET_UPGRADE_CLOSE_REASON = "legacy-state-reset-awaiting-full-sync";
8
+ const BROKER_STATE_REPLACE_MAX_ATTEMPTS = 50;
9
+ const BROKER_STATE_REPLACE_RETRY_DELAY_MS = 10;
10
+ const trackedBrokerStates = [];
11
+ const trackedBrokerStateTouch = new WeakMap();
12
+ let nextTrackedBrokerStateTouch = 0;
13
+ let brokerStateMutationTarget;
14
+ function isNonEmptyString(value) {
15
+ return typeof value === "string" && value.trim().length > 0;
16
+ }
17
+ function isSafeInteger(value) {
18
+ return typeof value === "number" && Number.isSafeInteger(value);
19
+ }
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null;
22
+ }
23
+ function assertNonNegativeInteger(value, fieldName) {
24
+ if (!isSafeInteger(value) || value < 0) {
25
+ throw new Error(`invalid ${fieldName}`);
26
+ }
27
+ }
28
+ function assertCommandType(value) {
29
+ if (value !== "replyQuestion" && value !== "replyPermission" && value !== "replyNaturalStop") {
30
+ throw new Error("invalid broker command type");
31
+ }
32
+ }
33
+ function assertControlType(value) {
34
+ if (value !== "requestReplay" && value !== "requestFullSync") {
35
+ throw new Error("invalid broker control type");
36
+ }
37
+ }
38
+ function createEmptyActiveState() {
39
+ return {
40
+ instances: {},
41
+ sessions: {},
42
+ questions: {},
43
+ permissions: {},
44
+ naturalStops: {},
45
+ retryErrors: {},
46
+ };
47
+ }
48
+ function cloneRecordMap(value) {
49
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUnknownValue(item)]));
50
+ }
51
+ function cloneUnknownValue(value) {
52
+ if (Array.isArray(value)) {
53
+ return value.map((item) => cloneUnknownValue(item));
54
+ }
55
+ if (isRecord(value)) {
56
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUnknownValue(item)]));
57
+ }
58
+ return value;
59
+ }
60
+ function cloneTerminalMetadataMap(value) {
61
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUnknownValue(item)]));
62
+ }
63
+ function cloneRetainedOccupancyMap(value) {
64
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, { ...item }]));
65
+ }
66
+ function cloneLegacyHandleClosure(record) {
67
+ return {
68
+ ...record,
69
+ };
70
+ }
71
+ function cloneLegacyHandleClosureMap(value) {
72
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneLegacyHandleClosure(item)]));
73
+ }
74
+ function cloneIndexedRequestRecord(record) {
75
+ return cloneUnknownValue(record);
76
+ }
77
+ function cloneRequestIndexMap(value) {
78
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneIndexedRequestRecord(item)]));
79
+ }
80
+ function cloneDeliveryTokenState(record) {
81
+ return { ...record };
82
+ }
83
+ function cloneDeliveryTokenMap(value) {
84
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneDeliveryTokenState(item)]));
85
+ }
86
+ function normalizeHandleClosures(value) {
87
+ if (!Array.isArray(value)) {
88
+ return [];
89
+ }
90
+ return Array.from(new Set(value
91
+ .map((item) => normalizeActionString(item))
92
+ .filter((item) => item !== undefined))).sort((left, right) => left.localeCompare(right));
93
+ }
94
+ function cloneCommandRecord(record) {
95
+ return {
96
+ ...record,
97
+ target: cloneUnknownValue(record.target),
98
+ ...(record.payload !== undefined ? { payload: cloneUnknownValue(record.payload) } : {}),
99
+ ...(record.failure ? { failure: cloneUnknownValue(record.failure) } : {}),
100
+ };
101
+ }
102
+ function rememberBrokerState(state) {
103
+ if (!trackedBrokerStateTouch.has(state)) {
104
+ trackedBrokerStates.push(state);
105
+ }
106
+ nextTrackedBrokerStateTouch += 1;
107
+ trackedBrokerStateTouch.set(state, nextTrackedBrokerStateTouch);
108
+ return state;
109
+ }
110
+ function resolveBrokerState(state) {
111
+ if (state) {
112
+ return rememberBrokerState(state);
113
+ }
114
+ const stagedStates = new Set();
115
+ for (const candidate of trackedBrokerStates) {
116
+ for (const stage of Object.values(candidate.fullSync.stagedByControlId)) {
117
+ stagedStates.add(stage.state);
118
+ }
119
+ }
120
+ const candidates = trackedBrokerStates
121
+ .filter((candidate) => trackedBrokerStateTouch.has(candidate) && !stagedStates.has(candidate))
122
+ .sort((left, right) => (trackedBrokerStateTouch.get(right) ?? 0) - (trackedBrokerStateTouch.get(left) ?? 0));
123
+ const latest = candidates[0];
124
+ return latest ? rememberBrokerState(latest) : undefined;
125
+ }
126
+ async function readJsonFile(filePath) {
127
+ try {
128
+ const raw = await readFile(filePath, "utf8");
129
+ return JSON.parse(raw);
130
+ }
131
+ catch (error) {
132
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
133
+ return undefined;
134
+ }
135
+ return undefined;
136
+ }
137
+ }
138
+ async function listJsonFiles(dirPath) {
139
+ try {
140
+ const entries = await readdir(dirPath, { withFileTypes: true });
141
+ return entries
142
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
143
+ .map((entry) => path.join(dirPath, entry.name));
144
+ }
145
+ catch (error) {
146
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
147
+ return [];
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+ function delay(ms) {
153
+ if (ms <= 0)
154
+ return Promise.resolve();
155
+ return new Promise((resolve) => setTimeout(resolve, ms));
156
+ }
157
+ function isRetryableBrokerStateReplaceError(error) {
158
+ const issue = error;
159
+ return issue?.code === "EPERM" || issue?.code === "EBUSY";
160
+ }
161
+ async function replaceBrokerStateFile(tempPath, filePath) {
162
+ let lastError = undefined;
163
+ for (let attempt = 0; attempt < BROKER_STATE_REPLACE_MAX_ATTEMPTS; attempt += 1) {
164
+ try {
165
+ await rename(tempPath, filePath);
166
+ return;
167
+ }
168
+ catch (error) {
169
+ lastError = error;
170
+ if (attempt === BROKER_STATE_REPLACE_MAX_ATTEMPTS - 1 || !isRetryableBrokerStateReplaceError(error)) {
171
+ throw error;
172
+ }
173
+ await delay(BROKER_STATE_REPLACE_RETRY_DELAY_MS);
174
+ }
175
+ }
176
+ if (lastError)
177
+ throw lastError;
178
+ }
179
+ async function writeJsonFileAtomically(filePath, value) {
180
+ const dirPath = path.dirname(filePath);
181
+ const tempPath = path.join(dirPath, `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
182
+ await mkdir(dirPath, { recursive: true });
183
+ try {
184
+ await writeFile(tempPath, JSON.stringify(value, null, 2), { encoding: "utf8", mode: WECHAT_FILE_MODE });
185
+ await replaceBrokerStateFile(tempPath, filePath);
186
+ }
187
+ catch (error) {
188
+ await rm(tempPath, { force: true }).catch(() => { });
189
+ throw error;
190
+ }
191
+ }
192
+ async function collectHandleClosuresFromJsonFiles(filePaths, predicate) {
193
+ const handles = new Set();
194
+ for (const filePath of filePaths) {
195
+ const raw = await readJsonFile(filePath);
196
+ if (!isRecord(raw)) {
197
+ continue;
198
+ }
199
+ if (predicate && !predicate(raw)) {
200
+ continue;
201
+ }
202
+ const handle = getStringField(raw, "handle");
203
+ if (handle) {
204
+ handles.add(handle);
205
+ }
206
+ }
207
+ return Array.from(handles);
208
+ }
209
+ async function collectLegacyHandleClosures() {
210
+ const questionFiles = await listJsonFiles(requestKindDir("question"));
211
+ const permissionFiles = await listJsonFiles(requestKindDir("permission"));
212
+ const notificationFiles = await listJsonFiles(notificationsDir());
213
+ const [questionHandles, permissionHandles, naturalStopHandles] = await Promise.all([
214
+ collectHandleClosuresFromJsonFiles(questionFiles),
215
+ collectHandleClosuresFromJsonFiles(permissionFiles),
216
+ collectHandleClosuresFromJsonFiles(notificationFiles, (record) => getStringField(record, "kind") === "naturalStop"),
217
+ ]);
218
+ return Array.from(new Set([
219
+ ...questionHandles,
220
+ ...permissionHandles,
221
+ ...naturalStopHandles,
222
+ ])).sort((left, right) => left.localeCompare(right));
223
+ }
224
+ function restorePersistedBrokerState(raw, options = {}) {
225
+ if (!isRecord(raw) || !isRecord(raw.connections) || !isRecord(raw.active)) {
226
+ return undefined;
227
+ }
228
+ const active = raw.active;
229
+ if (!isRecord(active.instances)
230
+ || !isRecord(active.sessions)
231
+ || !isRecord(active.questions)
232
+ || !isRecord(active.permissions)
233
+ || !isRecord(active.naturalStops)
234
+ || !isRecord(active.retryErrors)) {
235
+ return undefined;
236
+ }
237
+ const state = createEmptyBrokerState({ track: options.track === false ? false : true });
238
+ state.connections = cloneUnknownValue(raw.connections);
239
+ state.active = cloneUnknownValue(active);
240
+ state.terminalMetadata = isRecord(raw.terminalMetadata)
241
+ ? cloneUnknownValue(raw.terminalMetadata)
242
+ : {};
243
+ state.retainedOccupancy = isRecord(raw.retainedOccupancy)
244
+ ? cloneUnknownValue(raw.retainedOccupancy)
245
+ : {};
246
+ state.legacyHandleClosures = isRecord(raw.legacyHandleClosures)
247
+ ? cloneUnknownValue(raw.legacyHandleClosures)
248
+ : {};
249
+ state.requestIndex = isRecord(raw.requestIndex)
250
+ ? cloneUnknownValue(raw.requestIndex)
251
+ : {};
252
+ state.deliveryTokens = isRecord(raw.deliveryTokens)
253
+ ? cloneUnknownValue(raw.deliveryTokens)
254
+ : {};
255
+ state.commandLedger = isRecord(raw.commandLedger)
256
+ ? cloneUnknownValue(raw.commandLedger)
257
+ : {};
258
+ state.controlLedger = isRecord(raw.controlLedger)
259
+ ? cloneUnknownValue(raw.controlLedger)
260
+ : {};
261
+ state.fullSync = {
262
+ stagedByControlId: {},
263
+ ...(isRecord(raw.fullSync) && isNonEmptyString(raw.fullSync.lastCompletedControlId)
264
+ ? { lastCompletedControlId: raw.fullSync.lastCompletedControlId }
265
+ : {}),
266
+ ...(isRecord(raw.fullSync) && isSafeInteger(raw.fullSync.lastCompletedEventSeq)
267
+ ? { lastCompletedEventSeq: raw.fullSync.lastCompletedEventSeq }
268
+ : {}),
269
+ ...(isRecord(raw.fullSync) && isNonEmptyString(raw.fullSync.lastCompletedInstanceIncarnation)
270
+ ? { lastCompletedInstanceIncarnation: raw.fullSync.lastCompletedInstanceIncarnation }
271
+ : {}),
272
+ };
273
+ return options.track === false ? state : rememberBrokerState(state);
274
+ }
275
+ async function writeBrokerStateStoreSnapshot(state) {
276
+ await writeJsonFileAtomically(brokerStateStorePath(), state);
277
+ }
278
+ export async function persistBrokerStateStoreSnapshot(state) {
279
+ await writeBrokerStateStoreSnapshot(state);
280
+ }
281
+ export async function loadBrokerStateStoreSnapshot() {
282
+ const raw = await readJsonFile(brokerStateStorePath());
283
+ const restored = restorePersistedBrokerState(raw, { track: false });
284
+ if (restored) {
285
+ return restored;
286
+ }
287
+ return resolveBrokerState();
288
+ }
289
+ export async function loadBrokerStateStoreForMutation() {
290
+ if (brokerStateMutationTarget) {
291
+ return rememberBrokerState(brokerStateMutationTarget);
292
+ }
293
+ const restored = await loadBrokerStateStoreSnapshot();
294
+ return restored ? rememberBrokerState(restored) : createEmptyBrokerState();
295
+ }
296
+ export function setBrokerStateMutationTarget(state) {
297
+ brokerStateMutationTarget = state ? rememberBrokerState(state) : undefined;
298
+ }
299
+ export async function readBrokerStateSchemaMarker() {
300
+ const raw = await readJsonFile(brokerStateSchemaPath());
301
+ if (!isRecord(raw) || !isSafeInteger(raw.protocolVersion) || !isNonEmptyString(raw.stateGeneration) || !isSafeInteger(raw.updatedAt)) {
302
+ return undefined;
303
+ }
304
+ return {
305
+ kind: isNonEmptyString(raw.kind) ? raw.kind : BROKER_STATE_SCHEMA_MARKER_KIND,
306
+ protocolVersion: raw.protocolVersion,
307
+ stateGeneration: raw.stateGeneration,
308
+ updatedAt: raw.updatedAt,
309
+ ...(isNonEmptyString(raw.upgradeCloseReason) ? { upgradeCloseReason: raw.upgradeCloseReason } : {}),
310
+ ...(normalizeHandleClosures(raw.legacyHandleClosures).length > 0
311
+ ? { legacyHandleClosures: normalizeHandleClosures(raw.legacyHandleClosures) }
312
+ : {}),
313
+ };
314
+ }
315
+ export async function writeBrokerStateSchemaMarker(input) {
316
+ assertNonNegativeInteger(input.protocolVersion, "protocolVersion");
317
+ if (!isNonEmptyString(input.stateGeneration)) {
318
+ throw new Error("invalid stateGeneration");
319
+ }
320
+ assertNonNegativeInteger(input.updatedAt, "updatedAt");
321
+ const marker = {
322
+ kind: BROKER_STATE_SCHEMA_MARKER_KIND,
323
+ protocolVersion: input.protocolVersion,
324
+ stateGeneration: input.stateGeneration,
325
+ updatedAt: input.updatedAt,
326
+ ...(isNonEmptyString(input.upgradeCloseReason) ? { upgradeCloseReason: input.upgradeCloseReason } : {}),
327
+ ...(normalizeHandleClosures(input.legacyHandleClosures).length > 0
328
+ ? { legacyHandleClosures: normalizeHandleClosures(input.legacyHandleClosures) }
329
+ : {}),
330
+ };
331
+ await writeJsonFileAtomically(brokerStateSchemaPath(), marker);
332
+ return marker;
333
+ }
334
+ export async function prepareBrokerStateStoreForStartup(input) {
335
+ assertNonNegativeInteger(input.protocolVersion, "protocolVersion");
336
+ if (!isNonEmptyString(input.stateGeneration)) {
337
+ throw new Error("invalid stateGeneration");
338
+ }
339
+ const now = input.now ?? Date.now;
340
+ const marker = await readBrokerStateSchemaMarker();
341
+ const rawState = await readJsonFile(brokerStateStorePath());
342
+ const markerMatches = marker?.protocolVersion === input.protocolVersion && marker?.stateGeneration === input.stateGeneration;
343
+ const hasPersistedState = marker !== undefined || rawState !== undefined;
344
+ let recoveredFromLegacyState = false;
345
+ let legacyHandleClosures = markerMatches ? normalizeHandleClosures(marker?.legacyHandleClosures) : [];
346
+ let state = markerMatches ? (restorePersistedBrokerState(rawState) ?? createEmptyBrokerState()) : createEmptyBrokerState();
347
+ if (!markerMatches && hasPersistedState) {
348
+ recoveredFromLegacyState = true;
349
+ legacyHandleClosures = await collectLegacyHandleClosures();
350
+ state = createEmptyBrokerState();
351
+ }
352
+ await writeBrokerStateStoreSnapshot(state);
353
+ const persistedMarker = await writeBrokerStateSchemaMarker({
354
+ protocolVersion: input.protocolVersion,
355
+ stateGeneration: input.stateGeneration,
356
+ updatedAt: now(),
357
+ ...(legacyHandleClosures.length > 0
358
+ ? {
359
+ upgradeCloseReason: marker?.upgradeCloseReason ?? LEGACY_STATE_RESET_UPGRADE_CLOSE_REASON,
360
+ legacyHandleClosures,
361
+ }
362
+ : {}),
363
+ });
364
+ return {
365
+ state,
366
+ recoveredFromLegacyState,
367
+ legacyHandleClosures: normalizeHandleClosures(persistedMarker.legacyHandleClosures),
368
+ };
369
+ }
370
+ export async function readBrokerStateUpgradeCloseReason(handle) {
371
+ if (!isNonEmptyString(handle)) {
372
+ return undefined;
373
+ }
374
+ const marker = await readBrokerStateSchemaMarker();
375
+ if (!marker?.upgradeCloseReason) {
376
+ return undefined;
377
+ }
378
+ const closures = normalizeHandleClosures(marker.legacyHandleClosures);
379
+ if (!closures.includes(handle)) {
380
+ return undefined;
381
+ }
382
+ if (marker.upgradeCloseReason === LEGACY_STATE_RESET_UPGRADE_CLOSE_REASON) {
383
+ return `该句柄来自旧状态代际,broker 正在升级恢复,请等待实例重连并完成 full sync 后再试:${handle}`;
384
+ }
385
+ return `该句柄暂时不可用:${handle}`;
386
+ }
387
+ function normalizeActionString(value) {
388
+ if (typeof value !== "string") {
389
+ return undefined;
390
+ }
391
+ const normalized = value.trim().replace(/\s+/g, " ");
392
+ return normalized.length > 0 ? normalized : undefined;
393
+ }
394
+ function normalizeAnswerMatrix(value) {
395
+ if (!Array.isArray(value)) {
396
+ return [];
397
+ }
398
+ const normalized = [];
399
+ for (const group of value) {
400
+ if (Array.isArray(group)) {
401
+ normalized.push(...group.map((item) => normalizeActionString(item)).filter((item) => item !== undefined));
402
+ continue;
403
+ }
404
+ const next = normalizeActionString(group);
405
+ if (next) {
406
+ normalized.push(next);
407
+ }
408
+ }
409
+ return normalized.sort((left, right) => left.localeCompare(right));
410
+ }
411
+ function createBrokerCommandActionKey(input) {
412
+ const instanceID = normalizeActionString(input.target.instanceID) ?? "";
413
+ if (input.type === "replyQuestion") {
414
+ const requestID = normalizeActionString(input.target.requestID)
415
+ ?? normalizeActionString(isRecord(input.payload) ? input.payload.requestID : undefined)
416
+ ?? "";
417
+ const answers = normalizeAnswerMatrix(isRecord(input.payload) ? input.payload.answers : undefined);
418
+ return JSON.stringify({
419
+ type: input.type,
420
+ target: { instanceID, requestID },
421
+ payload: { answers },
422
+ });
423
+ }
424
+ if (input.type === "replyPermission") {
425
+ const requestID = normalizeActionString(input.target.requestID)
426
+ ?? normalizeActionString(isRecord(input.payload) ? input.payload.requestID : undefined)
427
+ ?? "";
428
+ const reply = normalizeActionString(isRecord(input.payload) ? input.payload.reply : undefined) ?? "";
429
+ const message = normalizeActionString(isRecord(input.payload) ? input.payload.message : undefined) ?? "";
430
+ return JSON.stringify({
431
+ type: input.type,
432
+ target: { instanceID, requestID },
433
+ payload: { reply, message },
434
+ });
435
+ }
436
+ const sessionID = normalizeActionString(input.target.sessionID)
437
+ ?? normalizeActionString(isRecord(input.payload) ? input.payload.sessionID : undefined)
438
+ ?? "";
439
+ const text = normalizeActionString(isRecord(input.payload) ? input.payload.text : undefined) ?? "";
440
+ return JSON.stringify({
441
+ type: input.type,
442
+ target: { instanceID, sessionID },
443
+ payload: { text },
444
+ });
445
+ }
446
+ function cloneConnectionMap(value) {
447
+ return Object.fromEntries(Object.entries(value).map(([instanceID, incarnations]) => [
448
+ instanceID,
449
+ Object.fromEntries(Object.entries(incarnations).map(([incarnation, item]) => [incarnation, { ...item }])),
450
+ ]));
451
+ }
452
+ function cloneActiveState(state) {
453
+ return {
454
+ instances: cloneRecordMap(state.instances),
455
+ sessions: cloneRecordMap(state.sessions),
456
+ questions: cloneRecordMap(state.questions),
457
+ permissions: cloneRecordMap(state.permissions),
458
+ naturalStops: cloneRecordMap(state.naturalStops),
459
+ retryErrors: cloneRecordMap(state.retryErrors),
460
+ };
461
+ }
462
+ function getConnectionGroup(state, instanceID) {
463
+ const current = state.connections[instanceID];
464
+ if (current) {
465
+ return current;
466
+ }
467
+ const created = {};
468
+ state.connections[instanceID] = created;
469
+ return created;
470
+ }
471
+ function getStringField(record, fieldName) {
472
+ const value = record[fieldName];
473
+ return isNonEmptyString(value) ? value : undefined;
474
+ }
475
+ function getNumberField(record, fieldName) {
476
+ const value = record[fieldName];
477
+ return isSafeInteger(value) ? value : undefined;
478
+ }
479
+ function ensureConnection(state, instanceID, instanceIncarnation) {
480
+ const current = getConnectionGroup(state, instanceID)[instanceIncarnation];
481
+ if (current) {
482
+ return current;
483
+ }
484
+ const created = {
485
+ instanceID,
486
+ instanceIncarnation,
487
+ online: false,
488
+ lastEventSeq: 0,
489
+ lastAckedEventSeq: 0,
490
+ lastSentBrokerSeq: 0,
491
+ };
492
+ getConnectionGroup(state, instanceID)[instanceIncarnation] = created;
493
+ return created;
494
+ }
495
+ function resolveInstanceID(payload, context) {
496
+ if (isNonEmptyString(context?.instanceID)) {
497
+ return context.instanceID;
498
+ }
499
+ return getStringField(payload, "instanceID");
500
+ }
501
+ function updateConnectionEventWatermark(state, event, payload, context) {
502
+ const instanceID = resolveInstanceID(payload, context);
503
+ if (!instanceID) {
504
+ return undefined;
505
+ }
506
+ const connection = ensureConnection(state, instanceID, event.instanceIncarnation);
507
+ connection.lastEventSeq = Math.max(connection.lastEventSeq, event.eventSeq);
508
+ return connection;
509
+ }
510
+ function requireCommand(state, commandId) {
511
+ const current = state.commandLedger[commandId];
512
+ if (!current) {
513
+ throw new Error("unknown broker command");
514
+ }
515
+ return current;
516
+ }
517
+ export function createEmptyBrokerState(options = {}) {
518
+ const state = {
519
+ connections: {},
520
+ active: createEmptyActiveState(),
521
+ terminalMetadata: {},
522
+ retainedOccupancy: {},
523
+ legacyHandleClosures: {},
524
+ requestIndex: {},
525
+ deliveryTokens: {},
526
+ commandLedger: {},
527
+ controlLedger: {},
528
+ fullSync: {
529
+ stagedByControlId: {},
530
+ },
531
+ };
532
+ return options.track === false ? state : rememberBrokerState(state);
533
+ }
534
+ function isLegacyHandleKind(value) {
535
+ return value === "question" || value === "permission" || value === "naturalStop";
536
+ }
537
+ function toLegacyHandleClosure(input) {
538
+ if (!isLegacyHandleKind(input.kind) || !isNonEmptyString(input.handle) || !isNonEmptyString(input.reason)) {
539
+ throw new Error("invalid legacy handle closure");
540
+ }
541
+ return {
542
+ kind: input.kind,
543
+ handle: input.handle,
544
+ reason: input.reason,
545
+ ...(isNonEmptyString(input.message) ? { message: input.message.trim() } : {}),
546
+ ...(isNonEmptyString(input.replacementHandle) ? { replacementHandle: input.replacementHandle.trim() } : {}),
547
+ ...(isNonEmptyString(input.routeKey) ? { routeKey: input.routeKey.trim() } : {}),
548
+ ...(isSafeInteger(input.retainedUntil) ? { retainedUntil: input.retainedUntil } : {}),
549
+ };
550
+ }
551
+ export function writeLegacyHandleClosure(state, input) {
552
+ rememberBrokerState(state);
553
+ const next = toLegacyHandleClosure(input);
554
+ state.legacyHandleClosures[next.handle] = next;
555
+ return next;
556
+ }
557
+ export function readLegacyHandleClosure(state, input) {
558
+ if (!isLegacyHandleKind(input.kind) || !isNonEmptyString(input.handle)) {
559
+ return undefined;
560
+ }
561
+ const resolved = resolveBrokerState(state);
562
+ const candidate = resolved?.legacyHandleClosures[input.handle];
563
+ if (!candidate || candidate.kind !== input.kind) {
564
+ return undefined;
565
+ }
566
+ return cloneLegacyHandleClosure(candidate);
567
+ }
568
+ export function applyBridgeEvent(state, event, context) {
569
+ rememberBrokerState(state);
570
+ assertNonNegativeInteger(event.eventSeq, "eventSeq");
571
+ if (!isNonEmptyString(event.instanceIncarnation)) {
572
+ throw new Error("invalid instanceIncarnation");
573
+ }
574
+ if (!isRecord(event.payload)) {
575
+ throw new Error("invalid bridge event payload");
576
+ }
577
+ const payload = event.payload;
578
+ const connection = updateConnectionEventWatermark(state, event, payload, context);
579
+ switch (event.type) {
580
+ case "instanceOnline": {
581
+ const instanceID = resolveInstanceID(payload, context);
582
+ if (!instanceID) {
583
+ throw new Error("invalid instanceOnline payload");
584
+ }
585
+ const nextConnection = ensureConnection(state, instanceID, event.instanceIncarnation);
586
+ nextConnection.online = true;
587
+ nextConnection.connectedAt = getNumberField(payload, "connectedAt");
588
+ state.active.instances[instanceID] = {
589
+ ...payload,
590
+ instanceID,
591
+ instanceIncarnation: event.instanceIncarnation,
592
+ online: true,
593
+ };
594
+ return state;
595
+ }
596
+ case "instanceOffline": {
597
+ const instanceID = resolveInstanceID(payload, context);
598
+ if (!instanceID) {
599
+ throw new Error("invalid instanceOffline payload");
600
+ }
601
+ const nextConnection = ensureConnection(state, instanceID, event.instanceIncarnation);
602
+ nextConnection.online = false;
603
+ nextConnection.disconnectedAt = getNumberField(payload, "disconnectedAt");
604
+ nextConnection.disconnectReason = getStringField(payload, "reason");
605
+ state.active.instances[instanceID] = {
606
+ ...state.active.instances[instanceID],
607
+ ...payload,
608
+ instanceID,
609
+ instanceIncarnation: event.instanceIncarnation,
610
+ online: false,
611
+ };
612
+ return state;
613
+ }
614
+ case "sessionSnapshotChanged": {
615
+ const sessionID = getStringField(payload, "sessionID");
616
+ if (!sessionID) {
617
+ throw new Error("invalid sessionSnapshotChanged payload");
618
+ }
619
+ const instanceID = resolveInstanceID(payload, context);
620
+ state.active.sessions[sessionID] = {
621
+ ...payload,
622
+ ...(instanceID ? { instanceID } : {}),
623
+ instanceIncarnation: event.instanceIncarnation,
624
+ };
625
+ return state;
626
+ }
627
+ case "questionOpened":
628
+ case "questionUpdated": {
629
+ const routeKey = getStringField(payload, "routeKey");
630
+ if (!routeKey) {
631
+ throw new Error("invalid question payload");
632
+ }
633
+ const instanceID = resolveInstanceID(payload, context);
634
+ state.active.questions[routeKey] = {
635
+ ...payload,
636
+ ...(instanceID ? { instanceID } : {}),
637
+ instanceIncarnation: event.instanceIncarnation,
638
+ };
639
+ state.active.questions = ensureUniqueBrokerRequestHandles(state.active.questions, "question", {
640
+ updatedRouteKey: routeKey,
641
+ });
642
+ const handle = getStringField(payload, "handle");
643
+ if (handle) {
644
+ delete state.legacyHandleClosures[handle];
645
+ }
646
+ return state;
647
+ }
648
+ case "questionClosed": {
649
+ const routeKey = getStringField(payload, "routeKey");
650
+ if (!routeKey) {
651
+ throw new Error("invalid question payload");
652
+ }
653
+ const currentQuestion = isRecord(state.active.questions[routeKey]) ? state.active.questions[routeKey] : undefined;
654
+ const previousTerminal = state.terminalMetadata[routeKey];
655
+ delete state.active.questions[routeKey];
656
+ const reason = getStringField(payload, "reason");
657
+ if (reason) {
658
+ const handle = (currentQuestion ? getStringField(currentQuestion, "handle") : undefined)
659
+ ?? (isNonEmptyString(previousTerminal?.handle) ? previousTerminal.handle : undefined)
660
+ ?? getStringField(payload, "handle");
661
+ const requestID = getStringField(payload, "requestID") ?? (currentQuestion ? getStringField(currentQuestion, "requestID") : undefined) ?? previousTerminal?.requestID;
662
+ const scopeKey = getStringField(payload, "scopeKey") ?? (currentQuestion ? readBrokerScopeKey(currentQuestion) : undefined) ?? previousTerminal?.scopeKey;
663
+ const wechatAccountId = getStringField(payload, "wechatAccountId") ?? (currentQuestion ? getStringField(currentQuestion, "wechatAccountId") : undefined) ?? previousTerminal?.wechatAccountId;
664
+ const userId = getStringField(payload, "userId") ?? (currentQuestion ? getStringField(currentQuestion, "userId") : undefined) ?? previousTerminal?.userId;
665
+ const createdAt = getNumberField(payload, "createdAt") ?? (currentQuestion ? getNumberField(currentQuestion, "createdAt") : undefined) ?? previousTerminal?.createdAt;
666
+ const terminalAt = getNumberField(payload, "updatedAt") ?? (currentQuestion ? getNumberField(currentQuestion, "updatedAt") : undefined);
667
+ const terminalResultSent = previousTerminal?.terminalResultSent === true || payload.terminalResultSent === true
668
+ ? true
669
+ : payload.terminalResultSent === false
670
+ ? false
671
+ : undefined;
672
+ state.terminalMetadata[routeKey] = {
673
+ reason,
674
+ ...(handle ? { handle } : {}),
675
+ ...(requestID ? { requestID } : {}),
676
+ ...(scopeKey ? { scopeKey } : {}),
677
+ ...(currentQuestion && Object.hasOwn(currentQuestion, "prompt") ? { prompt: cloneUnknownValue(currentQuestion.prompt) } : {}),
678
+ ...(wechatAccountId ? { wechatAccountId } : {}),
679
+ ...(userId ? { userId } : {}),
680
+ ...(createdAt !== undefined ? { createdAt } : {}),
681
+ ...((reason === "answered" || reason === "handled") && terminalAt !== undefined ? { answeredAt: terminalAt } : {}),
682
+ ...(reason === "rejected" && terminalAt !== undefined ? { rejectedAt: terminalAt } : {}),
683
+ ...(reason === "expired" && terminalAt !== undefined ? { expiredAt: terminalAt } : {}),
684
+ ...(isNonEmptyString(payload.replacementHandle) ? { replacementHandle: payload.replacementHandle } : {}),
685
+ ...(typeof terminalResultSent === "boolean"
686
+ ? { terminalResultSent }
687
+ : {}),
688
+ ...(isSafeInteger(payload.retainedUntil) ? { retainedUntil: payload.retainedUntil } : {}),
689
+ };
690
+ if (handle) {
691
+ writeLegacyHandleClosure(state, {
692
+ kind: "question",
693
+ handle,
694
+ reason,
695
+ routeKey,
696
+ ...(isNonEmptyString(payload.replacementHandle) ? { replacementHandle: payload.replacementHandle } : {}),
697
+ ...(isSafeInteger(payload.retainedUntil) ? { retainedUntil: payload.retainedUntil } : {}),
698
+ });
699
+ }
700
+ }
701
+ return state;
702
+ }
703
+ case "permissionOpened":
704
+ case "permissionUpdated": {
705
+ const routeKey = getStringField(payload, "routeKey");
706
+ if (!routeKey) {
707
+ throw new Error("invalid permission payload");
708
+ }
709
+ const instanceID = resolveInstanceID(payload, context);
710
+ state.active.permissions[routeKey] = {
711
+ ...payload,
712
+ ...(instanceID ? { instanceID } : {}),
713
+ instanceIncarnation: event.instanceIncarnation,
714
+ };
715
+ state.active.permissions = ensureUniqueBrokerRequestHandles(state.active.permissions, "permission", {
716
+ updatedRouteKey: routeKey,
717
+ });
718
+ const handle = getStringField(payload, "handle");
719
+ if (handle) {
720
+ delete state.legacyHandleClosures[handle];
721
+ }
722
+ return state;
723
+ }
724
+ case "permissionClosed": {
725
+ const routeKey = getStringField(payload, "routeKey");
726
+ if (!routeKey) {
727
+ throw new Error("invalid permission payload");
728
+ }
729
+ const currentPermission = isRecord(state.active.permissions[routeKey]) ? state.active.permissions[routeKey] : undefined;
730
+ const previousTerminal = state.terminalMetadata[routeKey];
731
+ delete state.active.permissions[routeKey];
732
+ const reason = getStringField(payload, "reason");
733
+ if (reason) {
734
+ const handle = (currentPermission ? getStringField(currentPermission, "handle") : undefined)
735
+ ?? (isNonEmptyString(previousTerminal?.handle) ? previousTerminal.handle : undefined)
736
+ ?? getStringField(payload, "handle");
737
+ const requestID = getStringField(payload, "requestID") ?? (currentPermission ? getStringField(currentPermission, "requestID") : undefined) ?? previousTerminal?.requestID;
738
+ const scopeKey = getStringField(payload, "scopeKey") ?? (currentPermission ? readBrokerScopeKey(currentPermission) : undefined) ?? previousTerminal?.scopeKey;
739
+ const wechatAccountId = getStringField(payload, "wechatAccountId") ?? (currentPermission ? getStringField(currentPermission, "wechatAccountId") : undefined) ?? previousTerminal?.wechatAccountId;
740
+ const userId = getStringField(payload, "userId") ?? (currentPermission ? getStringField(currentPermission, "userId") : undefined) ?? previousTerminal?.userId;
741
+ const createdAt = getNumberField(payload, "createdAt") ?? (currentPermission ? getNumberField(currentPermission, "createdAt") : undefined) ?? previousTerminal?.createdAt;
742
+ const terminalAt = getNumberField(payload, "updatedAt") ?? (currentPermission ? getNumberField(currentPermission, "updatedAt") : undefined);
743
+ const terminalResultSent = previousTerminal?.terminalResultSent === true || payload.terminalResultSent === true
744
+ ? true
745
+ : payload.terminalResultSent === false
746
+ ? false
747
+ : undefined;
748
+ state.terminalMetadata[routeKey] = {
749
+ reason,
750
+ ...(handle ? { handle } : {}),
751
+ ...(requestID ? { requestID } : {}),
752
+ ...(scopeKey ? { scopeKey } : {}),
753
+ ...(currentPermission && Object.hasOwn(currentPermission, "prompt") ? { prompt: cloneUnknownValue(currentPermission.prompt) } : {}),
754
+ ...(wechatAccountId ? { wechatAccountId } : {}),
755
+ ...(userId ? { userId } : {}),
756
+ ...(createdAt !== undefined ? { createdAt } : {}),
757
+ ...((reason === "answered" || reason === "handled") && terminalAt !== undefined ? { answeredAt: terminalAt } : {}),
758
+ ...(reason === "rejected" && terminalAt !== undefined ? { rejectedAt: terminalAt } : {}),
759
+ ...(reason === "expired" && terminalAt !== undefined ? { expiredAt: terminalAt } : {}),
760
+ ...(isNonEmptyString(payload.replacementHandle) ? { replacementHandle: payload.replacementHandle } : {}),
761
+ ...(typeof terminalResultSent === "boolean"
762
+ ? { terminalResultSent }
763
+ : {}),
764
+ ...(isSafeInteger(payload.retainedUntil) ? { retainedUntil: payload.retainedUntil } : {}),
765
+ };
766
+ if (handle) {
767
+ writeLegacyHandleClosure(state, {
768
+ kind: "permission",
769
+ handle,
770
+ reason,
771
+ routeKey,
772
+ ...(isNonEmptyString(payload.replacementHandle) ? { replacementHandle: payload.replacementHandle } : {}),
773
+ ...(isSafeInteger(payload.retainedUntil) ? { retainedUntil: payload.retainedUntil } : {}),
774
+ });
775
+ }
776
+ }
777
+ return state;
778
+ }
779
+ case "naturalStopOpened": {
780
+ const handle = getStringField(payload, "handle");
781
+ if (!handle) {
782
+ throw new Error("invalid naturalStop payload");
783
+ }
784
+ const instanceID = resolveInstanceID(payload, context);
785
+ state.active.naturalStops[handle] = {
786
+ ...payload,
787
+ ...(instanceID ? { instanceID } : {}),
788
+ instanceIncarnation: event.instanceIncarnation,
789
+ };
790
+ delete state.legacyHandleClosures[handle];
791
+ return state;
792
+ }
793
+ case "naturalStopClosed": {
794
+ const handle = getStringField(payload, "handle");
795
+ if (!handle) {
796
+ throw new Error("invalid naturalStop payload");
797
+ }
798
+ const currentNaturalStop = isRecord(state.active.naturalStops[handle]) ? state.active.naturalStops[handle] : undefined;
799
+ delete state.active.naturalStops[handle];
800
+ const retainedUntil = getNumberField(payload, "retainedUntil");
801
+ if (retainedUntil !== undefined) {
802
+ state.retainedOccupancy[handle] = {
803
+ handle,
804
+ retainedUntil,
805
+ };
806
+ }
807
+ const reason = getStringField(payload, "reason")
808
+ ?? getStringField(payload, "terminalReason")
809
+ ?? getStringField(payload, "naturalStopTerminalReason")
810
+ ?? (currentNaturalStop ? getStringField(currentNaturalStop, "naturalStopTerminalReason") : undefined);
811
+ if (reason) {
812
+ writeLegacyHandleClosure(state, {
813
+ kind: "naturalStop",
814
+ handle,
815
+ reason,
816
+ ...(retainedUntil !== undefined ? { retainedUntil } : {}),
817
+ });
818
+ }
819
+ return state;
820
+ }
821
+ case "retryErrorUpdated": {
822
+ const retryKey = getStringField(payload, "sessionID") ?? getStringField(payload, "instanceID") ?? `retry-${event.eventSeq}`;
823
+ const instanceID = resolveInstanceID(payload, context);
824
+ state.active.retryErrors[retryKey] = {
825
+ ...payload,
826
+ ...(instanceID ? { instanceID } : {}),
827
+ instanceIncarnation: event.instanceIncarnation,
828
+ };
829
+ return state;
830
+ }
831
+ case "commandAccepted": {
832
+ const commandId = getStringField(payload, "commandId");
833
+ if (!commandId || !connection) {
834
+ throw new Error("invalid commandAccepted payload");
835
+ }
836
+ markBrokerCommandAccepted(state, {
837
+ commandId,
838
+ instanceID: connection.instanceID,
839
+ instanceIncarnation: event.instanceIncarnation,
840
+ eventSeq: event.eventSeq,
841
+ acceptedAt: getNumberField(payload, "acceptedAt"),
842
+ });
843
+ return state;
844
+ }
845
+ case "commandResult": {
846
+ const commandId = getStringField(payload, "commandId");
847
+ const status = payload.status;
848
+ if (!commandId || !connection || (status !== "completed" && status !== "failed")) {
849
+ throw new Error("invalid commandResult payload");
850
+ }
851
+ markBrokerCommandResult(state, {
852
+ commandId,
853
+ instanceID: connection.instanceID,
854
+ instanceIncarnation: event.instanceIncarnation,
855
+ eventSeq: event.eventSeq,
856
+ status,
857
+ completedAt: getNumberField(payload, "completedAt"),
858
+ failure: isRecord(payload.failure) ? payload.failure : undefined,
859
+ });
860
+ return state;
861
+ }
862
+ case "fullSyncCompleted": {
863
+ const controlId = event.controlId ?? getStringField(payload, "controlId");
864
+ if (!controlId) {
865
+ throw new Error("invalid fullSyncCompleted payload");
866
+ }
867
+ state.fullSync = {
868
+ ...state.fullSync,
869
+ lastCompletedControlId: controlId,
870
+ lastCompletedEventSeq: event.eventSeq,
871
+ lastCompletedInstanceIncarnation: event.instanceIncarnation,
872
+ };
873
+ return state;
874
+ }
875
+ default:
876
+ return state;
877
+ }
878
+ }
879
+ function matchesActiveScope(record, scope) {
880
+ const instanceID = getStringField(record, "instanceID");
881
+ if (instanceID !== scope.instanceID) {
882
+ return false;
883
+ }
884
+ const instanceIncarnation = getStringField(record, "instanceIncarnation");
885
+ return instanceIncarnation === undefined || instanceIncarnation === scope.instanceIncarnation;
886
+ }
887
+ function addScopedRecord(record, scope) {
888
+ return {
889
+ ...record,
890
+ instanceID: getStringField(record, "instanceID") ?? scope.instanceID,
891
+ instanceIncarnation: getStringField(record, "instanceIncarnation") ?? scope.instanceIncarnation,
892
+ };
893
+ }
894
+ function replaceScopedActiveRecords(current, incoming, scope) {
895
+ const retainedEntries = Object.entries(current).filter(([, record]) => !matchesActiveScope(record, scope));
896
+ const scopedEntries = Object.entries(incoming).map(([key, record]) => [key, addScopedRecord(record, scope)]);
897
+ return Object.fromEntries([...retainedEntries, ...scopedEntries]);
898
+ }
899
+ export function applyFullSyncSnapshot(state, scope, snapshot) {
900
+ rememberBrokerState(state);
901
+ if (!isNonEmptyString(scope.instanceID) || !isNonEmptyString(scope.instanceIncarnation)) {
902
+ throw new Error("invalid full sync scope");
903
+ }
904
+ if (!isRecord(snapshot) || !isRecord(snapshot.active)) {
905
+ throw new Error("invalid full sync snapshot");
906
+ }
907
+ if (snapshot.connections?.[scope.instanceID]?.[scope.instanceIncarnation]) {
908
+ const incomingConnection = snapshot.connections[scope.instanceID][scope.instanceIncarnation];
909
+ getConnectionGroup(state, scope.instanceID)[scope.instanceIncarnation] = { ...incomingConnection };
910
+ }
911
+ // Full sync only replaces the targeted instance/incarnation live domains.
912
+ state.active = {
913
+ instances: replaceScopedActiveRecords(state.active.instances, snapshot.active.instances, scope),
914
+ sessions: replaceScopedActiveRecords(state.active.sessions, snapshot.active.sessions, scope),
915
+ questions: ensureUniqueBrokerRequestHandles(replaceScopedActiveRecords(state.active.questions, snapshot.active.questions, scope), "question", { updatedScope: scope }),
916
+ permissions: ensureUniqueBrokerRequestHandles(replaceScopedActiveRecords(state.active.permissions, snapshot.active.permissions, scope), "permission", { updatedScope: scope }),
917
+ naturalStops: replaceScopedActiveRecords(state.active.naturalStops, snapshot.active.naturalStops, scope),
918
+ retryErrors: replaceScopedActiveRecords(state.active.retryErrors, snapshot.active.retryErrors, scope),
919
+ };
920
+ return state;
921
+ }
922
+ export function upsertBrokerCommand(state, input) {
923
+ rememberBrokerState(state);
924
+ if (!isNonEmptyString(input.commandId)) {
925
+ throw new Error("invalid commandId");
926
+ }
927
+ assertNonNegativeInteger(input.brokerSeq, "brokerSeq");
928
+ assertCommandType(input.type);
929
+ if (input.status !== "queued" && input.status !== "delivered") {
930
+ throw new Error("invalid broker command status");
931
+ }
932
+ if (!isRecord(input.target)) {
933
+ throw new Error("invalid broker command target");
934
+ }
935
+ const current = state.commandLedger[input.commandId];
936
+ const next = {
937
+ commandId: input.commandId,
938
+ brokerSeq: input.brokerSeq,
939
+ type: input.type,
940
+ target: { ...input.target },
941
+ ...(input.payload !== undefined
942
+ ? { payload: cloneUnknownValue(input.payload) }
943
+ : current?.payload !== undefined
944
+ ? { payload: cloneUnknownValue(current.payload) }
945
+ : {}),
946
+ status: input.status,
947
+ ...(current?.acceptedAt !== undefined ? { acceptedAt: current.acceptedAt } : {}),
948
+ ...(current?.completedAt !== undefined ? { completedAt: current.completedAt } : {}),
949
+ ...(current?.failure ? { failure: { ...current.failure } } : {}),
950
+ ...(input.instanceID !== undefined
951
+ ? { instanceID: input.instanceID }
952
+ : current?.instanceID
953
+ ? { instanceID: current.instanceID }
954
+ : {}),
955
+ ...(input.instanceIncarnation !== undefined
956
+ ? { instanceIncarnation: input.instanceIncarnation }
957
+ : current?.instanceIncarnation
958
+ ? { instanceIncarnation: current.instanceIncarnation }
959
+ : {}),
960
+ ...(current?.acceptedEventSeq !== undefined ? { acceptedEventSeq: current.acceptedEventSeq } : {}),
961
+ ...(current?.resultEventSeq !== undefined ? { resultEventSeq: current.resultEventSeq } : {}),
962
+ };
963
+ state.commandLedger[input.commandId] = next;
964
+ return next;
965
+ }
966
+ function requireControlRecord(state, controlId) {
967
+ const current = state.controlLedger[controlId];
968
+ if (!current) {
969
+ throw new Error("unknown broker control");
970
+ }
971
+ return current;
972
+ }
973
+ function createRetryErrorKey(input) {
974
+ return isNonEmptyString(input.sessionID) ? input.sessionID.trim() : input.instanceID.trim();
975
+ }
976
+ function createBrokerRequestIndexKey(input) {
977
+ return `${input.kind}:${input.routeKey}`;
978
+ }
979
+ function createBrokerDeliveryTokenKey(input) {
980
+ return `${input.wechatAccountId}:${input.userId}`;
981
+ }
982
+ function readBrokerScopeKey(record) {
983
+ return getStringField(record, "scopeKey") ?? getStringField(record, "instanceID");
984
+ }
985
+ function normalizeHandleOrUndefined(value) {
986
+ if (!isNonEmptyString(value)) {
987
+ return undefined;
988
+ }
989
+ try {
990
+ return normalizeHandle(value);
991
+ }
992
+ catch {
993
+ return undefined;
994
+ }
995
+ }
996
+ function brokerHandleIdentityKey(value) {
997
+ if (!isNonEmptyString(value)) {
998
+ return undefined;
999
+ }
1000
+ return normalizeHandleOrUndefined(value) ?? value.trim().toLowerCase();
1001
+ }
1002
+ function sortBrokerRequestRecords(left, right) {
1003
+ const leftCreated = getNumberField(left[1], "createdAt") ?? 0;
1004
+ const rightCreated = getNumberField(right[1], "createdAt") ?? 0;
1005
+ if (leftCreated !== rightCreated) {
1006
+ return leftCreated - rightCreated;
1007
+ }
1008
+ return left[0].localeCompare(right[0]);
1009
+ }
1010
+ const VISIBLE_REQUEST_RESERVATION_READ_RETRIES = 3;
1011
+ const VISIBLE_REQUEST_RESERVATION_READ_RETRY_DELAY_MS = 5;
1012
+ function isVisibleRequestNotificationStatus(value) {
1013
+ return value === "pending" || value === "sent";
1014
+ }
1015
+ function toRequestHandleReservation(record) {
1016
+ const kind = record.kind === "question" || record.kind === "permission" ? record.kind : undefined;
1017
+ const routeKey = getStringField(record, "routeKey");
1018
+ const handle = normalizeHandleOrUndefined(getStringField(record, "handle"));
1019
+ const createdAt = getNumberField(record, "createdAt");
1020
+ if (!kind || !routeKey || !handle || createdAt === undefined || !isVisibleRequestNotificationStatus(record.status)) {
1021
+ return undefined;
1022
+ }
1023
+ const sentAt = getNumberField(record, "sentAt");
1024
+ return {
1025
+ kind,
1026
+ routeKey,
1027
+ handle,
1028
+ createdAt,
1029
+ ...(sentAt !== undefined ? { sentAt } : {}),
1030
+ };
1031
+ }
1032
+ function waitForVisibleRequestReservationRetry() {
1033
+ return new Promise((resolve) => setTimeout(resolve, VISIBLE_REQUEST_RESERVATION_READ_RETRY_DELAY_MS));
1034
+ }
1035
+ async function readJsonFileForVisibleRequestReservation(filePath) {
1036
+ for (let attempt = 0; attempt < VISIBLE_REQUEST_RESERVATION_READ_RETRIES; attempt += 1) {
1037
+ try {
1038
+ const raw = await readFile(filePath, "utf8");
1039
+ return JSON.parse(raw);
1040
+ }
1041
+ catch (error) {
1042
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
1043
+ return undefined;
1044
+ }
1045
+ if (attempt < VISIBLE_REQUEST_RESERVATION_READ_RETRIES - 1) {
1046
+ await waitForVisibleRequestReservationRetry();
1047
+ }
1048
+ }
1049
+ }
1050
+ return undefined;
1051
+ }
1052
+ async function listVisibleRequestHandleReservations() {
1053
+ const notificationFiles = await listJsonFiles(notificationsDir());
1054
+ const reservations = [];
1055
+ for (const filePath of notificationFiles) {
1056
+ const raw = await readJsonFileForVisibleRequestReservation(filePath);
1057
+ if (!isRecord(raw)) {
1058
+ continue;
1059
+ }
1060
+ const reservation = toRequestHandleReservation(raw);
1061
+ if (reservation) {
1062
+ reservations.push(reservation);
1063
+ }
1064
+ }
1065
+ return reservations.sort((left, right) => {
1066
+ const leftAt = left.sentAt ?? left.createdAt;
1067
+ const rightAt = right.sentAt ?? right.createdAt;
1068
+ if (leftAt !== rightAt)
1069
+ return leftAt - rightAt;
1070
+ return left.routeKey.localeCompare(right.routeKey);
1071
+ });
1072
+ }
1073
+ function selectVisibleRequestHandlesByRoute(records, kind, reservations) {
1074
+ const activeRoutes = new Set(Object.keys(records));
1075
+ const byRoute = new Map();
1076
+ for (const reservation of reservations) {
1077
+ if (reservation.kind !== kind || !activeRoutes.has(reservation.routeKey)) {
1078
+ continue;
1079
+ }
1080
+ byRoute.set(reservation.routeKey, reservation.handle);
1081
+ }
1082
+ return byRoute;
1083
+ }
1084
+ function isReservedForAnotherRoute(routeKey, handleKey, routeReservations) {
1085
+ for (const [reservedRouteKey, reservedHandle] of routeReservations) {
1086
+ if (reservedRouteKey === routeKey) {
1087
+ continue;
1088
+ }
1089
+ if (brokerHandleIdentityKey(reservedHandle) === handleKey) {
1090
+ return true;
1091
+ }
1092
+ }
1093
+ return false;
1094
+ }
1095
+ function listReservedHandlesForOtherRoutes(routeKey, routeReservations) {
1096
+ const handles = [];
1097
+ for (const [reservedRouteKey, reservedHandle] of routeReservations) {
1098
+ if (reservedRouteKey !== routeKey) {
1099
+ handles.push(reservedHandle);
1100
+ }
1101
+ }
1102
+ return handles;
1103
+ }
1104
+ function orderBrokerRequestRecordsForHandleAllocation(records, options) {
1105
+ const entries = Object.entries(records);
1106
+ if (isNonEmptyString(options.updatedRouteKey)) {
1107
+ const retained = entries.filter(([routeKey]) => routeKey !== options.updatedRouteKey).sort(sortBrokerRequestRecords);
1108
+ const updated = entries.filter(([routeKey]) => routeKey === options.updatedRouteKey);
1109
+ return [...retained, ...updated];
1110
+ }
1111
+ if (options.updatedScope) {
1112
+ const retained = entries
1113
+ .filter(([, record]) => !matchesActiveScope(record, options.updatedScope))
1114
+ .sort(sortBrokerRequestRecords);
1115
+ const updated = entries
1116
+ .filter(([, record]) => matchesActiveScope(record, options.updatedScope))
1117
+ .sort(sortBrokerRequestRecords);
1118
+ return [...retained, ...updated];
1119
+ }
1120
+ return entries.sort(sortBrokerRequestRecords);
1121
+ }
1122
+ function ensureUniqueBrokerRequestHandles(records, kind, options = {}) {
1123
+ const usedHandles = [];
1124
+ const usedIdentityKeys = new Set();
1125
+ const nextEntries = [];
1126
+ for (const [routeKey, record] of orderBrokerRequestRecordsForHandleAllocation(records, options)) {
1127
+ const rawHandle = getStringField(record, "handle");
1128
+ const requestedHandle = normalizeHandleOrUndefined(rawHandle) ?? rawHandle?.trim();
1129
+ const requestedKey = brokerHandleIdentityKey(requestedHandle);
1130
+ const handle = requestedHandle && requestedKey && !usedIdentityKeys.has(requestedKey)
1131
+ ? requestedHandle
1132
+ : createHandle(kind, usedHandles);
1133
+ usedHandles.push(handle);
1134
+ const handleKey = brokerHandleIdentityKey(handle);
1135
+ if (handleKey) {
1136
+ usedIdentityKeys.add(handleKey);
1137
+ }
1138
+ nextEntries.push([routeKey, { ...record, handle }]);
1139
+ }
1140
+ return Object.fromEntries(nextEntries);
1141
+ }
1142
+ function ensureUniqueBrokerRequestHandlesWithVisibleReservations(records, kind, routeReservations) {
1143
+ if (routeReservations.size === 0) {
1144
+ return records;
1145
+ }
1146
+ const usedHandles = [];
1147
+ const usedIdentityKeys = new Set();
1148
+ const nextEntries = [];
1149
+ for (const [routeKey, record] of orderBrokerRequestRecordsForHandleAllocation(records, {})) {
1150
+ const reservedHandle = routeReservations.get(routeKey);
1151
+ const reservedKey = brokerHandleIdentityKey(reservedHandle);
1152
+ const rawHandle = getStringField(record, "handle");
1153
+ const requestedHandle = normalizeHandleOrUndefined(rawHandle) ?? rawHandle?.trim();
1154
+ const requestedKey = brokerHandleIdentityKey(requestedHandle);
1155
+ let handle;
1156
+ if (reservedHandle && reservedKey && !usedIdentityKeys.has(reservedKey)) {
1157
+ handle = reservedHandle;
1158
+ }
1159
+ else if (requestedHandle
1160
+ && requestedKey
1161
+ && !usedIdentityKeys.has(requestedKey)
1162
+ && !isReservedForAnotherRoute(routeKey, requestedKey, routeReservations)) {
1163
+ handle = requestedHandle;
1164
+ }
1165
+ else {
1166
+ handle = createHandle(kind, [
1167
+ ...usedHandles,
1168
+ ...listReservedHandlesForOtherRoutes(routeKey, routeReservations),
1169
+ ]);
1170
+ }
1171
+ usedHandles.push(handle);
1172
+ const handleKey = brokerHandleIdentityKey(handle);
1173
+ if (handleKey) {
1174
+ usedIdentityKeys.add(handleKey);
1175
+ }
1176
+ nextEntries.push([routeKey, { ...record, handle }]);
1177
+ }
1178
+ return Object.fromEntries(nextEntries);
1179
+ }
1180
+ function syncOpenRequestIndexHandlesWithActive(state, kind, activeRecords) {
1181
+ for (const [routeKey, activeRecord] of Object.entries(activeRecords)) {
1182
+ const handle = getStringField(activeRecord, "handle");
1183
+ if (!handle) {
1184
+ continue;
1185
+ }
1186
+ const indexKey = createBrokerRequestIndexKey({ kind, routeKey });
1187
+ const indexed = state.requestIndex[indexKey];
1188
+ if (indexed?.status === "open" && indexed.handle !== handle) {
1189
+ state.requestIndex[indexKey] = {
1190
+ ...indexed,
1191
+ handle,
1192
+ };
1193
+ }
1194
+ }
1195
+ }
1196
+ export async function reconcileBrokerActiveRequestHandlesWithVisibleNotifications(state) {
1197
+ rememberBrokerState(state);
1198
+ const reservations = await listVisibleRequestHandleReservations();
1199
+ if (reservations.length === 0) {
1200
+ return state;
1201
+ }
1202
+ state.active.questions = ensureUniqueBrokerRequestHandlesWithVisibleReservations(state.active.questions, "question", selectVisibleRequestHandlesByRoute(state.active.questions, "question", reservations));
1203
+ state.active.permissions = ensureUniqueBrokerRequestHandlesWithVisibleReservations(state.active.permissions, "permission", selectVisibleRequestHandlesByRoute(state.active.permissions, "permission", reservations));
1204
+ syncOpenRequestIndexHandlesWithActive(state, "question", state.active.questions);
1205
+ syncOpenRequestIndexHandlesWithActive(state, "permission", state.active.permissions);
1206
+ return state;
1207
+ }
1208
+ function toBrokerIndexedRequestRecord(input) {
1209
+ if ((input.kind !== "question" && input.kind !== "permission")
1210
+ || !isNonEmptyString(input.requestID)
1211
+ || !isNonEmptyString(input.routeKey)
1212
+ || !isNonEmptyString(input.handle)
1213
+ || !isNonEmptyString(input.wechatAccountId)
1214
+ || !isNonEmptyString(input.userId)
1215
+ || !isSafeInteger(input.createdAt)) {
1216
+ throw new Error("invalid broker indexed request");
1217
+ }
1218
+ if (!["open", "answered", "rejected", "expired", "cleaned"].includes(input.status)) {
1219
+ throw new Error("invalid broker indexed request");
1220
+ }
1221
+ return cloneUnknownValue({
1222
+ kind: input.kind,
1223
+ requestID: input.requestID,
1224
+ routeKey: input.routeKey,
1225
+ handle: input.handle,
1226
+ ...(isNonEmptyString(input.scopeKey) ? { scopeKey: input.scopeKey } : {}),
1227
+ ...(input.prompt !== undefined ? { prompt: input.prompt } : {}),
1228
+ wechatAccountId: input.wechatAccountId,
1229
+ userId: input.userId,
1230
+ status: input.status,
1231
+ createdAt: input.createdAt,
1232
+ ...(isSafeInteger(input.answeredAt) ? { answeredAt: input.answeredAt } : {}),
1233
+ ...(isSafeInteger(input.rejectedAt) ? { rejectedAt: input.rejectedAt } : {}),
1234
+ ...(isSafeInteger(input.expiredAt) ? { expiredAt: input.expiredAt } : {}),
1235
+ ...(isSafeInteger(input.cleanedAt) ? { cleanedAt: input.cleanedAt } : {}),
1236
+ ...(isNonEmptyString(input.terminalReason) ? { terminalReason: input.terminalReason } : {}),
1237
+ ...(isNonEmptyString(input.replacementHandle) ? { replacementHandle: input.replacementHandle } : {}),
1238
+ ...(input.terminalResultSent === true ? { terminalResultSent: true } : {}),
1239
+ });
1240
+ }
1241
+ function toBrokerDeliveryTokenState(input) {
1242
+ if (!isNonEmptyString(input.wechatAccountId)
1243
+ || !isNonEmptyString(input.userId)
1244
+ || !isNonEmptyString(input.contextToken)
1245
+ || !isSafeInteger(input.updatedAt)
1246
+ || (input.source !== "question" && input.source !== "permission" && input.source !== "message")) {
1247
+ throw new Error("invalid broker delivery token");
1248
+ }
1249
+ if ((input.sourceRef !== undefined && !isNonEmptyString(input.sourceRef))
1250
+ || (input.staleReason !== undefined && !isNonEmptyString(input.staleReason))) {
1251
+ throw new Error("invalid broker delivery token");
1252
+ }
1253
+ return {
1254
+ wechatAccountId: input.wechatAccountId,
1255
+ userId: input.userId,
1256
+ contextToken: input.contextToken,
1257
+ updatedAt: input.updatedAt,
1258
+ source: input.source,
1259
+ ...(isNonEmptyString(input.sourceRef) ? { sourceRef: input.sourceRef } : {}),
1260
+ ...(isNonEmptyString(input.staleReason) ? { staleReason: input.staleReason } : {}),
1261
+ };
1262
+ }
1263
+ export function readBrokerControlRecord(state, controlId) {
1264
+ return state.controlLedger[controlId];
1265
+ }
1266
+ export function upsertRetryErrorSummary(state, input) {
1267
+ rememberBrokerState(state);
1268
+ if (!isNonEmptyString(input.instanceID)
1269
+ || !isNonEmptyString(input.action)
1270
+ || !isNonEmptyString(input.redactedSummary)
1271
+ || !isNonEmptyString(input.severityAdvice)) {
1272
+ throw new Error("invalid retry error summary");
1273
+ }
1274
+ if (input.updatedAt !== undefined) {
1275
+ assertNonNegativeInteger(input.updatedAt, "updatedAt");
1276
+ }
1277
+ const key = createRetryErrorKey(input);
1278
+ const next = {
1279
+ instanceID: input.instanceID.trim(),
1280
+ action: input.action.trim(),
1281
+ redactedSummary: input.redactedSummary.trim(),
1282
+ severityAdvice: input.severityAdvice.trim(),
1283
+ ...(isNonEmptyString(input.sessionID) ? { sessionID: input.sessionID.trim() } : {}),
1284
+ ...(input.updatedAt !== undefined ? { updatedAt: input.updatedAt } : {}),
1285
+ ...(isNonEmptyString(input.instanceIncarnation) ? { instanceIncarnation: input.instanceIncarnation.trim() } : {}),
1286
+ };
1287
+ state.active.retryErrors[key] = next;
1288
+ return next;
1289
+ }
1290
+ export function upsertBrokerIndexedRequest(state, input) {
1291
+ rememberBrokerState(state);
1292
+ const next = toBrokerIndexedRequestRecord(input);
1293
+ state.requestIndex[createBrokerRequestIndexKey(next)] = cloneIndexedRequestRecord(next);
1294
+ if (next.status === "open") {
1295
+ const activeKey = next.kind === "question" ? "questions" : "permissions";
1296
+ state.active[activeKey][next.routeKey] = {
1297
+ routeKey: next.routeKey,
1298
+ handle: next.handle,
1299
+ requestID: next.requestID,
1300
+ ...(isNonEmptyString(next.scopeKey) ? { scopeKey: next.scopeKey, instanceID: next.scopeKey } : {}),
1301
+ ...(next.prompt !== undefined ? { prompt: cloneUnknownValue(next.prompt) } : {}),
1302
+ wechatAccountId: next.wechatAccountId,
1303
+ userId: next.userId,
1304
+ createdAt: next.createdAt,
1305
+ };
1306
+ delete state.terminalMetadata[next.routeKey];
1307
+ delete state.legacyHandleClosures[next.handle];
1308
+ return cloneIndexedRequestRecord(next);
1309
+ }
1310
+ const activeKey = next.kind === "question" ? "questions" : "permissions";
1311
+ delete state.active[activeKey][next.routeKey];
1312
+ state.terminalMetadata[next.routeKey] = {
1313
+ reason: next.terminalReason ?? next.status,
1314
+ ...(isNonEmptyString(next.replacementHandle) ? { replacementHandle: next.replacementHandle } : {}),
1315
+ ...(next.terminalResultSent === true ? { terminalResultSent: true } : {}),
1316
+ handle: next.handle,
1317
+ requestID: next.requestID,
1318
+ ...(isNonEmptyString(next.scopeKey) ? { scopeKey: next.scopeKey } : {}),
1319
+ ...(next.prompt !== undefined ? { prompt: cloneUnknownValue(next.prompt) } : {}),
1320
+ wechatAccountId: next.wechatAccountId,
1321
+ userId: next.userId,
1322
+ createdAt: next.createdAt,
1323
+ ...(isSafeInteger(next.answeredAt) ? { answeredAt: next.answeredAt } : {}),
1324
+ ...(isSafeInteger(next.rejectedAt) ? { rejectedAt: next.rejectedAt } : {}),
1325
+ ...(isSafeInteger(next.expiredAt) ? { expiredAt: next.expiredAt } : {}),
1326
+ ...(isSafeInteger(next.cleanedAt) ? { cleanedAt: next.cleanedAt } : {}),
1327
+ };
1328
+ writeLegacyHandleClosure(state, {
1329
+ kind: next.kind,
1330
+ handle: next.handle,
1331
+ reason: next.terminalReason ?? next.status,
1332
+ routeKey: next.routeKey,
1333
+ ...(isNonEmptyString(next.replacementHandle) ? { replacementHandle: next.replacementHandle } : {}),
1334
+ });
1335
+ return cloneIndexedRequestRecord(next);
1336
+ }
1337
+ export async function readBrokerIndexedRequest(input, state) {
1338
+ const resolved = state ?? await loadBrokerStateStoreSnapshot();
1339
+ if (!resolved || !isNonEmptyString(input.routeKey)) {
1340
+ return undefined;
1341
+ }
1342
+ const current = resolved.requestIndex[createBrokerRequestIndexKey(input)];
1343
+ if (current) {
1344
+ return cloneIndexedRequestRecord(current);
1345
+ }
1346
+ const activeKey = input.kind === "question" ? "questions" : "permissions";
1347
+ const active = resolved.active[activeKey][input.routeKey];
1348
+ if (isRecord(active)) {
1349
+ const requestID = getStringField(active, "requestID");
1350
+ const handle = getStringField(active, "handle");
1351
+ const wechatAccountId = getStringField(active, "wechatAccountId");
1352
+ const userId = getStringField(active, "userId");
1353
+ const createdAt = getNumberField(active, "createdAt");
1354
+ if (requestID && handle && wechatAccountId && userId && createdAt !== undefined) {
1355
+ return {
1356
+ kind: input.kind,
1357
+ requestID,
1358
+ routeKey: input.routeKey,
1359
+ handle,
1360
+ ...(getStringField(active, "scopeKey") ? { scopeKey: getStringField(active, "scopeKey") } : {}),
1361
+ ...(Object.hasOwn(active, "prompt") ? { prompt: cloneUnknownValue(active.prompt) } : {}),
1362
+ wechatAccountId,
1363
+ userId,
1364
+ status: "open",
1365
+ createdAt,
1366
+ };
1367
+ }
1368
+ }
1369
+ const terminal = resolved.terminalMetadata[input.routeKey];
1370
+ if (terminal
1371
+ && isNonEmptyString(terminal.handle)
1372
+ && isNonEmptyString(terminal.requestID)
1373
+ && isNonEmptyString(terminal.wechatAccountId)
1374
+ && isNonEmptyString(terminal.userId)
1375
+ && isSafeInteger(terminal.createdAt)) {
1376
+ return {
1377
+ kind: input.kind,
1378
+ requestID: terminal.requestID,
1379
+ routeKey: input.routeKey,
1380
+ handle: terminal.handle,
1381
+ ...(isNonEmptyString(terminal.scopeKey) ? { scopeKey: terminal.scopeKey } : {}),
1382
+ ...(terminal.prompt !== undefined ? { prompt: cloneUnknownValue(terminal.prompt) } : {}),
1383
+ wechatAccountId: terminal.wechatAccountId,
1384
+ userId: terminal.userId,
1385
+ status: terminal.cleanedAt !== undefined ? "cleaned" : terminal.expiredAt !== undefined ? "expired" : terminal.rejectedAt !== undefined ? "rejected" : "answered",
1386
+ createdAt: terminal.createdAt,
1387
+ ...(isSafeInteger(terminal.answeredAt) ? { answeredAt: terminal.answeredAt } : {}),
1388
+ ...(isSafeInteger(terminal.rejectedAt) ? { rejectedAt: terminal.rejectedAt } : {}),
1389
+ ...(isSafeInteger(terminal.expiredAt) ? { expiredAt: terminal.expiredAt } : {}),
1390
+ ...(isSafeInteger(terminal.cleanedAt) ? { cleanedAt: terminal.cleanedAt } : {}),
1391
+ ...(isNonEmptyString(terminal.reason) ? { terminalReason: terminal.reason } : {}),
1392
+ ...(isNonEmptyString(terminal.replacementHandle) ? { replacementHandle: terminal.replacementHandle } : {}),
1393
+ ...(terminal.terminalResultSent === true ? { terminalResultSent: true } : {}),
1394
+ };
1395
+ }
1396
+ return undefined;
1397
+ }
1398
+ export function upsertBrokerDeliveryToken(state, input) {
1399
+ rememberBrokerState(state);
1400
+ const next = toBrokerDeliveryTokenState(input);
1401
+ state.deliveryTokens[createBrokerDeliveryTokenKey(next)] = cloneDeliveryTokenState(next);
1402
+ return cloneDeliveryTokenState(next);
1403
+ }
1404
+ export async function readBrokerDeliveryToken(input, state) {
1405
+ const resolved = state ?? await loadBrokerStateStoreSnapshot();
1406
+ if (!resolved || !isNonEmptyString(input.wechatAccountId) || !isNonEmptyString(input.userId)) {
1407
+ return undefined;
1408
+ }
1409
+ const current = resolved.deliveryTokens[createBrokerDeliveryTokenKey(input)];
1410
+ return current ? cloneDeliveryTokenState(current) : undefined;
1411
+ }
1412
+ function collectBrokerOpenRequestsForScope(state, scopeKey) {
1413
+ const indexed = new Map();
1414
+ for (const record of Object.values(state.requestIndex)) {
1415
+ if (record.status !== "open") {
1416
+ continue;
1417
+ }
1418
+ if (record.scopeKey !== scopeKey) {
1419
+ continue;
1420
+ }
1421
+ indexed.set(createBrokerRequestIndexKey(record), cloneIndexedRequestRecord(record));
1422
+ }
1423
+ for (const kind of ["question", "permission"]) {
1424
+ const source = kind === "question" ? state.active.questions : state.active.permissions;
1425
+ for (const [routeKey, rawRecord] of Object.entries(source)) {
1426
+ if (!isRecord(rawRecord)) {
1427
+ continue;
1428
+ }
1429
+ if (readBrokerScopeKey(rawRecord) !== scopeKey) {
1430
+ continue;
1431
+ }
1432
+ const key = createBrokerRequestIndexKey({ kind, routeKey });
1433
+ if (indexed.has(key)) {
1434
+ continue;
1435
+ }
1436
+ const requestID = getStringField(rawRecord, "requestID");
1437
+ const handle = getStringField(rawRecord, "handle");
1438
+ const wechatAccountId = getStringField(rawRecord, "wechatAccountId");
1439
+ const userId = getStringField(rawRecord, "userId");
1440
+ const createdAt = getNumberField(rawRecord, "createdAt");
1441
+ if (!requestID || !handle || !wechatAccountId || !userId || createdAt === undefined) {
1442
+ continue;
1443
+ }
1444
+ indexed.set(key, {
1445
+ kind,
1446
+ requestID,
1447
+ routeKey,
1448
+ handle,
1449
+ scopeKey,
1450
+ ...(Object.hasOwn(rawRecord, "prompt") ? { prompt: cloneUnknownValue(rawRecord.prompt) } : {}),
1451
+ wechatAccountId,
1452
+ userId,
1453
+ status: "open",
1454
+ createdAt,
1455
+ });
1456
+ }
1457
+ }
1458
+ return [...indexed.values()].sort((left, right) => left.createdAt - right.createdAt);
1459
+ }
1460
+ export function expireBrokerIndexedRequestsForScope(state, input) {
1461
+ rememberBrokerState(state);
1462
+ if (!isNonEmptyString(input.scopeKey)) {
1463
+ throw new Error("invalid broker request expiration scope");
1464
+ }
1465
+ assertNonNegativeInteger(input.expiredAt, "expiredAt");
1466
+ const expired = [];
1467
+ for (const record of collectBrokerOpenRequestsForScope(state, input.scopeKey)) {
1468
+ const next = upsertBrokerIndexedRequest(state, {
1469
+ ...record,
1470
+ status: "expired",
1471
+ expiredAt: input.expiredAt,
1472
+ terminalReason: "expired",
1473
+ terminalResultSent: record.terminalResultSent === true,
1474
+ });
1475
+ expired.push(next);
1476
+ }
1477
+ return expired;
1478
+ }
1479
+ export function closeBrokerNaturalStopsForScope(state, input) {
1480
+ rememberBrokerState(state);
1481
+ if (!isNonEmptyString(input.scopeKey) || !isNonEmptyString(input.terminalReason)) {
1482
+ throw new Error("invalid broker natural-stop closure scope");
1483
+ }
1484
+ const closed = [];
1485
+ for (const [handle, rawRecord] of Object.entries(state.active.naturalStops)) {
1486
+ if (!isRecord(rawRecord) || readBrokerScopeKey(rawRecord) !== input.scopeKey) {
1487
+ continue;
1488
+ }
1489
+ delete state.active.naturalStops[handle];
1490
+ const retainedUntil = getNumberField(rawRecord, "retainedUntil");
1491
+ if (retainedUntil !== undefined) {
1492
+ state.retainedOccupancy[handle] = {
1493
+ handle,
1494
+ retainedUntil,
1495
+ };
1496
+ }
1497
+ writeLegacyHandleClosure(state, {
1498
+ kind: "naturalStop",
1499
+ handle,
1500
+ reason: input.terminalReason,
1501
+ ...(retainedUntil !== undefined ? { retainedUntil } : {}),
1502
+ });
1503
+ closed.push({
1504
+ handle,
1505
+ ...(readBrokerScopeKey(rawRecord) ? { scopeKey: readBrokerScopeKey(rawRecord) } : {}),
1506
+ ...(getStringField(rawRecord, "idempotencyKey") ? { idempotencyKey: getStringField(rawRecord, "idempotencyKey") } : {}),
1507
+ terminalReason: input.terminalReason,
1508
+ });
1509
+ }
1510
+ return closed;
1511
+ }
1512
+ export function clearBrokerActiveScope(state, input) {
1513
+ rememberBrokerState(state);
1514
+ if (!isNonEmptyString(input.scopeKey)) {
1515
+ throw new Error("invalid broker active scope");
1516
+ }
1517
+ for (const [sessionID, rawRecord] of Object.entries(state.active.sessions)) {
1518
+ if (!isRecord(rawRecord) || readBrokerScopeKey(rawRecord) !== input.scopeKey) {
1519
+ continue;
1520
+ }
1521
+ delete state.active.sessions[sessionID];
1522
+ }
1523
+ for (const [routeKey, rawRecord] of Object.entries(state.active.questions)) {
1524
+ if (!isRecord(rawRecord) || readBrokerScopeKey(rawRecord) !== input.scopeKey) {
1525
+ continue;
1526
+ }
1527
+ delete state.active.questions[routeKey];
1528
+ }
1529
+ for (const [routeKey, rawRecord] of Object.entries(state.active.permissions)) {
1530
+ if (!isRecord(rawRecord) || readBrokerScopeKey(rawRecord) !== input.scopeKey) {
1531
+ continue;
1532
+ }
1533
+ delete state.active.permissions[routeKey];
1534
+ }
1535
+ for (const [handle, rawRecord] of Object.entries(state.active.naturalStops)) {
1536
+ if (!isRecord(rawRecord) || readBrokerScopeKey(rawRecord) !== input.scopeKey) {
1537
+ continue;
1538
+ }
1539
+ delete state.active.naturalStops[handle];
1540
+ }
1541
+ for (const [retryKey, rawRecord] of Object.entries(state.active.retryErrors)) {
1542
+ if (!isRecord(rawRecord) || readBrokerScopeKey(rawRecord) !== input.scopeKey) {
1543
+ continue;
1544
+ }
1545
+ delete state.active.retryErrors[retryKey];
1546
+ }
1547
+ delete state.active.instances[input.scopeKey];
1548
+ }
1549
+ export function removeBrokerConnectionScope(state, input) {
1550
+ rememberBrokerState(state);
1551
+ if (!isNonEmptyString(input.instanceID) || !isNonEmptyString(input.instanceIncarnation)) {
1552
+ throw new Error("invalid broker connection scope");
1553
+ }
1554
+ const group = state.connections[input.instanceID];
1555
+ if (!group) {
1556
+ return;
1557
+ }
1558
+ delete group[input.instanceIncarnation];
1559
+ if (Object.keys(group).length === 0) {
1560
+ delete state.connections[input.instanceID];
1561
+ }
1562
+ }
1563
+ export function reconcileBrokerDisconnectedScopes(state, input) {
1564
+ rememberBrokerState(state);
1565
+ assertNonNegativeInteger(input.disconnectedAt, "disconnectedAt");
1566
+ for (const [instanceID, incarnations] of Object.entries(state.connections)) {
1567
+ if (Object.values(incarnations).some((connection) => connection.online)) {
1568
+ continue;
1569
+ }
1570
+ expireBrokerIndexedRequestsForScope(state, {
1571
+ scopeKey: instanceID,
1572
+ expiredAt: input.disconnectedAt,
1573
+ });
1574
+ closeBrokerNaturalStopsForScope(state, {
1575
+ scopeKey: instanceID,
1576
+ terminalReason: "expired",
1577
+ });
1578
+ clearBrokerActiveScope(state, { scopeKey: instanceID });
1579
+ delete state.connections[instanceID];
1580
+ }
1581
+ }
1582
+ function readBrokerRequestTerminalTimestamp(record) {
1583
+ if (record.status === "answered") {
1584
+ return record.answeredAt;
1585
+ }
1586
+ if (record.status === "rejected") {
1587
+ return record.rejectedAt;
1588
+ }
1589
+ if (record.status === "expired") {
1590
+ return record.expiredAt;
1591
+ }
1592
+ if (record.status === "cleaned") {
1593
+ return record.cleanedAt;
1594
+ }
1595
+ return undefined;
1596
+ }
1597
+ export function cleanupBrokerRuntimeTerminalRequests(state, input) {
1598
+ rememberBrokerState(state);
1599
+ assertNonNegativeInteger(input.now, "now");
1600
+ assertNonNegativeInteger(input.cleanAfterMs, "cleanAfterMs");
1601
+ assertNonNegativeInteger(input.purgeRetentionMs, "purgeRetentionMs");
1602
+ const cleanedRequests = [];
1603
+ const purgedRequests = [];
1604
+ for (const record of Object.values(state.requestIndex)) {
1605
+ if (!["answered", "rejected", "expired"].includes(record.status)) {
1606
+ continue;
1607
+ }
1608
+ const terminalAt = readBrokerRequestTerminalTimestamp(record);
1609
+ if (terminalAt === undefined || input.now - terminalAt < input.cleanAfterMs) {
1610
+ continue;
1611
+ }
1612
+ cleanedRequests.push(upsertBrokerIndexedRequest(state, {
1613
+ ...record,
1614
+ status: "cleaned",
1615
+ cleanedAt: input.now,
1616
+ terminalReason: record.terminalReason,
1617
+ replacementHandle: record.replacementHandle,
1618
+ terminalResultSent: record.terminalResultSent,
1619
+ }));
1620
+ }
1621
+ const purgeCutoff = input.now - input.purgeRetentionMs;
1622
+ for (const [key, record] of Object.entries(state.requestIndex)) {
1623
+ if (record.status !== "cleaned") {
1624
+ continue;
1625
+ }
1626
+ if (!isSafeInteger(record.cleanedAt) || record.cleanedAt >= purgeCutoff) {
1627
+ continue;
1628
+ }
1629
+ purgedRequests.push(cloneIndexedRequestRecord(record));
1630
+ delete state.requestIndex[key];
1631
+ }
1632
+ return { cleanedRequests, purgedRequests };
1633
+ }
1634
+ export function markBrokerConnectionObserved(state, input) {
1635
+ rememberBrokerState(state);
1636
+ if (!isNonEmptyString(input.instanceID) || !isNonEmptyString(input.instanceIncarnation)) {
1637
+ throw new Error("invalid broker connection observation");
1638
+ }
1639
+ assertNonNegativeInteger(input.observedAt, "observedAt");
1640
+ if (input.connectedAt !== undefined) {
1641
+ assertNonNegativeInteger(input.connectedAt, "connectedAt");
1642
+ }
1643
+ const connection = ensureConnection(state, input.instanceID, input.instanceIncarnation);
1644
+ connection.online = true;
1645
+ connection.lastObservedAt = input.observedAt;
1646
+ if (input.connectedAt !== undefined) {
1647
+ connection.connectedAt = input.connectedAt;
1648
+ }
1649
+ delete connection.disconnectedAt;
1650
+ delete connection.disconnectReason;
1651
+ if (isRecord(state.active.instances[input.instanceID])) {
1652
+ state.active.instances[input.instanceID] = {
1653
+ ...state.active.instances[input.instanceID],
1654
+ instanceID: input.instanceID,
1655
+ instanceIncarnation: input.instanceIncarnation,
1656
+ online: true,
1657
+ };
1658
+ }
1659
+ return { ...connection };
1660
+ }
1661
+ export function markBrokerConnectionOffline(state, input) {
1662
+ rememberBrokerState(state);
1663
+ if (!isNonEmptyString(input.instanceID) || !isNonEmptyString(input.instanceIncarnation) || !isNonEmptyString(input.reason)) {
1664
+ throw new Error("invalid broker connection offline");
1665
+ }
1666
+ assertNonNegativeInteger(input.disconnectedAt, "disconnectedAt");
1667
+ const connection = ensureConnection(state, input.instanceID, input.instanceIncarnation);
1668
+ connection.online = false;
1669
+ connection.disconnectedAt = input.disconnectedAt;
1670
+ connection.disconnectReason = input.reason;
1671
+ if (isRecord(state.active.instances[input.instanceID])) {
1672
+ state.active.instances[input.instanceID] = {
1673
+ ...state.active.instances[input.instanceID],
1674
+ instanceID: input.instanceID,
1675
+ instanceIncarnation: input.instanceIncarnation,
1676
+ online: false,
1677
+ disconnectedAt: input.disconnectedAt,
1678
+ disconnectReason: input.reason,
1679
+ };
1680
+ }
1681
+ return { ...connection };
1682
+ }
1683
+ export function listTimedOutBrokerConnectionScopes(state, input) {
1684
+ assertNonNegativeInteger(input.now, "now");
1685
+ assertNonNegativeInteger(input.timeoutMs, "timeoutMs");
1686
+ const timedOut = [];
1687
+ for (const [instanceID, incarnations] of Object.entries(state.connections)) {
1688
+ for (const [instanceIncarnation, connection] of Object.entries(incarnations)) {
1689
+ if (!connection.online) {
1690
+ continue;
1691
+ }
1692
+ const lastObservedAt = connection.lastObservedAt ?? connection.connectedAt;
1693
+ if (!isSafeInteger(lastObservedAt)) {
1694
+ continue;
1695
+ }
1696
+ if (input.now - lastObservedAt < input.timeoutMs) {
1697
+ continue;
1698
+ }
1699
+ timedOut.push({ instanceID, instanceIncarnation });
1700
+ }
1701
+ }
1702
+ return timedOut;
1703
+ }
1704
+ export function readBrokerAuthoritativeView(state) {
1705
+ const resolved = resolveBrokerState(state);
1706
+ if (!resolved) {
1707
+ return {
1708
+ connections: {},
1709
+ active: createEmptyActiveState(),
1710
+ terminalMetadata: {},
1711
+ retainedOccupancy: {},
1712
+ commandLedger: {},
1713
+ legacyHandleClosures: {},
1714
+ };
1715
+ }
1716
+ return {
1717
+ connections: cloneConnectionMap(resolved.connections),
1718
+ active: cloneActiveState(resolved.active),
1719
+ terminalMetadata: cloneTerminalMetadataMap(resolved.terminalMetadata),
1720
+ retainedOccupancy: cloneRetainedOccupancyMap(resolved.retainedOccupancy),
1721
+ commandLedger: Object.fromEntries(Object.entries(resolved.commandLedger).map(([key, item]) => [key, cloneCommandRecord(item)])),
1722
+ legacyHandleClosures: cloneLegacyHandleClosureMap(resolved.legacyHandleClosures),
1723
+ };
1724
+ }
1725
+ export function readBrokerCommandStateByAction(input, state) {
1726
+ const resolved = resolveBrokerState(state);
1727
+ if (!resolved) {
1728
+ return undefined;
1729
+ }
1730
+ const expectedActionKey = createBrokerCommandActionKey(input);
1731
+ const matched = Object.values(resolved.commandLedger)
1732
+ .filter((record) => createBrokerCommandActionKey({
1733
+ type: record.type,
1734
+ target: record.target,
1735
+ payload: record.payload,
1736
+ }) === expectedActionKey)
1737
+ .sort((left, right) => right.brokerSeq - left.brokerSeq)[0];
1738
+ return matched ? cloneCommandRecord(matched) : undefined;
1739
+ }
1740
+ export function readBrokerFullSyncStage(state, controlId) {
1741
+ return state.fullSync.stagedByControlId[controlId]?.state;
1742
+ }
1743
+ export function requestBrokerReplay(state, input) {
1744
+ rememberBrokerState(state);
1745
+ if (!isNonEmptyString(input.controlId)
1746
+ || !isNonEmptyString(input.instanceID)
1747
+ || !isNonEmptyString(input.instanceIncarnation)) {
1748
+ throw new Error("invalid broker replay request");
1749
+ }
1750
+ assertNonNegativeInteger(input.brokerSeq, "brokerSeq");
1751
+ assertNonNegativeInteger(input.fromEventSeq, "fromEventSeq");
1752
+ assertNonNegativeInteger(input.toEventSeq, "toEventSeq");
1753
+ const next = {
1754
+ controlId: input.controlId,
1755
+ brokerSeq: input.brokerSeq,
1756
+ type: "requestReplay",
1757
+ status: "inFlight",
1758
+ instanceID: input.instanceID,
1759
+ instanceIncarnation: input.instanceIncarnation,
1760
+ fromEventSeq: input.fromEventSeq,
1761
+ toEventSeq: input.toEventSeq,
1762
+ };
1763
+ state.controlLedger[input.controlId] = next;
1764
+ markConnectionSentBrokerSeq(state, {
1765
+ instanceID: input.instanceID,
1766
+ instanceIncarnation: input.instanceIncarnation,
1767
+ brokerSeq: input.brokerSeq,
1768
+ });
1769
+ return next;
1770
+ }
1771
+ export function markBrokerReplayCompleted(state, input) {
1772
+ rememberBrokerState(state);
1773
+ if (!isNonEmptyString(input.controlId)) {
1774
+ throw new Error("invalid replay completion");
1775
+ }
1776
+ assertNonNegativeInteger(input.completedEventSeq, "completedEventSeq");
1777
+ const current = requireControlRecord(state, input.controlId);
1778
+ assertControlType(current.type);
1779
+ if (current.type !== "requestReplay") {
1780
+ throw new Error("broker control is not a replay request");
1781
+ }
1782
+ const next = {
1783
+ ...current,
1784
+ status: "completed",
1785
+ completedEventSeq: Math.max(current.completedEventSeq ?? 0, input.completedEventSeq),
1786
+ };
1787
+ state.controlLedger[input.controlId] = next;
1788
+ return next;
1789
+ }
1790
+ export function requestBrokerFullSync(state, input) {
1791
+ rememberBrokerState(state);
1792
+ if (!isNonEmptyString(input.controlId)
1793
+ || !isNonEmptyString(input.instanceID)
1794
+ || !isNonEmptyString(input.instanceIncarnation)
1795
+ || !isNonEmptyString(input.reason)) {
1796
+ throw new Error("invalid broker full sync request");
1797
+ }
1798
+ assertNonNegativeInteger(input.brokerSeq, "brokerSeq");
1799
+ const next = {
1800
+ controlId: input.controlId,
1801
+ brokerSeq: input.brokerSeq,
1802
+ type: "requestFullSync",
1803
+ status: "inFlight",
1804
+ instanceID: input.instanceID,
1805
+ instanceIncarnation: input.instanceIncarnation,
1806
+ reason: input.reason,
1807
+ };
1808
+ state.controlLedger[input.controlId] = next;
1809
+ state.fullSync.stagedByControlId[input.controlId] = {
1810
+ controlId: input.controlId,
1811
+ instanceID: input.instanceID,
1812
+ instanceIncarnation: input.instanceIncarnation,
1813
+ state: createEmptyBrokerState(),
1814
+ };
1815
+ markConnectionSentBrokerSeq(state, {
1816
+ instanceID: input.instanceID,
1817
+ instanceIncarnation: input.instanceIncarnation,
1818
+ brokerSeq: input.brokerSeq,
1819
+ });
1820
+ return next;
1821
+ }
1822
+ export function stageBrokerFullSyncEvent(state, input) {
1823
+ rememberBrokerState(state);
1824
+ if (!isNonEmptyString(input.controlId)) {
1825
+ throw new Error("invalid full sync stage controlId");
1826
+ }
1827
+ const current = requireControlRecord(state, input.controlId);
1828
+ assertControlType(current.type);
1829
+ if (current.type !== "requestFullSync") {
1830
+ throw new Error("broker control is not a full sync request");
1831
+ }
1832
+ if (input.event.type === "fullSyncCompleted") {
1833
+ throw new Error("fullSyncCompleted must commit instead of staging");
1834
+ }
1835
+ const stage = state.fullSync.stagedByControlId[input.controlId];
1836
+ if (!stage) {
1837
+ throw new Error("missing full sync stage");
1838
+ }
1839
+ applyBridgeEvent(stage.state, input.event, input.context);
1840
+ return stage.state;
1841
+ }
1842
+ export function markBrokerFullSyncCompleted(state, input) {
1843
+ rememberBrokerState(state);
1844
+ if (!isNonEmptyString(input.controlId)
1845
+ || !isNonEmptyString(input.instanceID)
1846
+ || !isNonEmptyString(input.instanceIncarnation)) {
1847
+ throw new Error("invalid full sync completion");
1848
+ }
1849
+ assertNonNegativeInteger(input.eventSeq, "eventSeq");
1850
+ const current = requireControlRecord(state, input.controlId);
1851
+ assertControlType(current.type);
1852
+ if (current.type !== "requestFullSync") {
1853
+ throw new Error("broker control is not a full sync request");
1854
+ }
1855
+ const stage = state.fullSync.stagedByControlId[input.controlId];
1856
+ if (!stage) {
1857
+ throw new Error("missing full sync stage");
1858
+ }
1859
+ applyFullSyncSnapshot(state, {
1860
+ instanceID: input.instanceID,
1861
+ instanceIncarnation: input.instanceIncarnation,
1862
+ }, {
1863
+ connections: stage.state.connections,
1864
+ active: cloneActiveState(stage.state.active),
1865
+ });
1866
+ delete state.fullSync.stagedByControlId[input.controlId];
1867
+ state.fullSync = {
1868
+ ...state.fullSync,
1869
+ lastCompletedControlId: input.controlId,
1870
+ lastCompletedEventSeq: input.eventSeq,
1871
+ lastCompletedInstanceIncarnation: input.instanceIncarnation,
1872
+ };
1873
+ const next = {
1874
+ ...current,
1875
+ status: "completed",
1876
+ completedEventSeq: Math.max(current.completedEventSeq ?? 0, input.eventSeq),
1877
+ };
1878
+ state.controlLedger[input.controlId] = next;
1879
+ return next;
1880
+ }
1881
+ export function markBrokerCommandAccepted(state, input) {
1882
+ rememberBrokerState(state);
1883
+ if (!isNonEmptyString(input.commandId)
1884
+ || !isNonEmptyString(input.instanceID)
1885
+ || !isNonEmptyString(input.instanceIncarnation)) {
1886
+ throw new Error("invalid command acceptance");
1887
+ }
1888
+ assertNonNegativeInteger(input.eventSeq, "eventSeq");
1889
+ if (input.acceptedAt !== undefined) {
1890
+ assertNonNegativeInteger(input.acceptedAt, "acceptedAt");
1891
+ }
1892
+ const current = requireCommand(state, input.commandId);
1893
+ if (current.status === "completed" || current.status === "failed") {
1894
+ return current;
1895
+ }
1896
+ const next = {
1897
+ ...current,
1898
+ status: "accepted",
1899
+ instanceID: input.instanceID,
1900
+ instanceIncarnation: input.instanceIncarnation,
1901
+ acceptedEventSeq: input.eventSeq,
1902
+ ...(input.acceptedAt !== undefined
1903
+ ? { acceptedAt: input.acceptedAt }
1904
+ : current.acceptedAt !== undefined
1905
+ ? { acceptedAt: current.acceptedAt }
1906
+ : {}),
1907
+ };
1908
+ state.commandLedger[input.commandId] = next;
1909
+ return next;
1910
+ }
1911
+ export function markBrokerCommandResult(state, input) {
1912
+ rememberBrokerState(state);
1913
+ if (!isNonEmptyString(input.commandId)
1914
+ || !isNonEmptyString(input.instanceID)
1915
+ || !isNonEmptyString(input.instanceIncarnation)) {
1916
+ throw new Error("invalid command result");
1917
+ }
1918
+ assertNonNegativeInteger(input.eventSeq, "eventSeq");
1919
+ if (input.completedAt !== undefined) {
1920
+ assertNonNegativeInteger(input.completedAt, "completedAt");
1921
+ }
1922
+ if (input.status !== "completed" && input.status !== "failed") {
1923
+ throw new Error("invalid broker command result status");
1924
+ }
1925
+ const current = requireCommand(state, input.commandId);
1926
+ if (current.status !== "accepted" && current.status !== "completed" && current.status !== "failed") {
1927
+ throw new Error("command result requires accepted command");
1928
+ }
1929
+ const next = {
1930
+ ...current,
1931
+ status: input.status,
1932
+ instanceID: input.instanceID,
1933
+ instanceIncarnation: input.instanceIncarnation,
1934
+ resultEventSeq: input.eventSeq,
1935
+ ...(input.completedAt !== undefined
1936
+ ? { completedAt: input.completedAt }
1937
+ : current.completedAt !== undefined
1938
+ ? { completedAt: current.completedAt }
1939
+ : {}),
1940
+ ...(input.status === "failed" && input.failure ? { failure: { ...input.failure } } : {}),
1941
+ };
1942
+ state.commandLedger[input.commandId] = next;
1943
+ return next;
1944
+ }
1945
+ export function markConnectionAckedEventSeq(state, input) {
1946
+ rememberBrokerState(state);
1947
+ if (!isNonEmptyString(input.instanceID) || !isNonEmptyString(input.instanceIncarnation)) {
1948
+ throw new Error("invalid broker ack input");
1949
+ }
1950
+ assertNonNegativeInteger(input.ackedEventSeq, "ackedEventSeq");
1951
+ const connection = ensureConnection(state, input.instanceID, input.instanceIncarnation);
1952
+ connection.lastAckedEventSeq = Math.max(connection.lastAckedEventSeq, input.ackedEventSeq);
1953
+ return connection;
1954
+ }
1955
+ export function markConnectionSentBrokerSeq(state, input) {
1956
+ rememberBrokerState(state);
1957
+ if (!isNonEmptyString(input.instanceID) || !isNonEmptyString(input.instanceIncarnation)) {
1958
+ throw new Error("invalid broker sent seq input");
1959
+ }
1960
+ assertNonNegativeInteger(input.brokerSeq, "brokerSeq");
1961
+ const connection = ensureConnection(state, input.instanceID, input.instanceIncarnation);
1962
+ connection.lastSentBrokerSeq = Math.max(connection.lastSentBrokerSeq, input.brokerSeq);
1963
+ return connection;
1964
+ }