ocuclaw 1.2.4 → 1.3.1
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.
- package/README.md +21 -6
- package/dist/config/runtime-config.js +84 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +56 -182
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +754 -83
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-version-service.js +23 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1293 -225
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +281 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1004 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +638 -27
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +581 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1111 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +14 -5
- package/skills/glasses-ui/SKILL.md +156 -0
- package/dist/runtime/downstream-server.js +0 -1891
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
import {
|
|
4
|
+
APP_PROTOCOL,
|
|
5
|
+
DEFAULT_NUDGE_THRESHOLDS,
|
|
6
|
+
DEFAULT_WORKER_HEALTH_THRESHOLDS,
|
|
7
|
+
DEFAULT_WORKER_QUEUE_CAPS,
|
|
8
|
+
DEFAULT_WORKER_RPC_LIMITS,
|
|
9
|
+
WORKER_FEATURES,
|
|
10
|
+
formatMainOperationReceived,
|
|
11
|
+
normalizeRequestId,
|
|
12
|
+
parseNonNegativeRevision,
|
|
13
|
+
} from "./relay-worker-protocol.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_WORKER_TYPE = "module";
|
|
16
|
+
const AUTOMATION_STATE_FALLBACK_MS = 1000;
|
|
17
|
+
|
|
18
|
+
function normalizeLogger(logger) {
|
|
19
|
+
if (!logger || typeof logger !== "object") return console;
|
|
20
|
+
return {
|
|
21
|
+
info: typeof logger.info === "function" ? logger.info.bind(logger) : console.log,
|
|
22
|
+
warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn,
|
|
23
|
+
error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error,
|
|
24
|
+
debug: typeof logger.debug === "function" ? logger.debug.bind(logger) : console.debug,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function defaultWorkerFactory() {
|
|
29
|
+
return new Worker(new URL("./relay-worker-entry.js", import.meta.url), {
|
|
30
|
+
type: DEFAULT_WORKER_TYPE,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeFrameList(value) {
|
|
35
|
+
if (value === null || value === undefined) return [];
|
|
36
|
+
return Array.isArray(value) ? value : [value];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function frameMatchesOperationReceipt(frame, requestId) {
|
|
40
|
+
if (!requestId || typeof frame !== "string") return false;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(frame);
|
|
43
|
+
return parsed &&
|
|
44
|
+
parsed.type === "ocuclaw.operation.received" &&
|
|
45
|
+
parsed.requestId === requestId;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function addWorkerEpochToSendAck(frame, workerEpoch) {
|
|
52
|
+
if (typeof frame !== "string") return frame;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(frame);
|
|
55
|
+
if (parsed && parsed.type === "ocuclaw.message.send.ack") {
|
|
56
|
+
return JSON.stringify({
|
|
57
|
+
...parsed,
|
|
58
|
+
workerEpoch,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
return frame;
|
|
63
|
+
}
|
|
64
|
+
return frame;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseFrame(frame) {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(frame);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function responseToWorkerMessage(requestId, result) {
|
|
76
|
+
const headers = result && result.headers && typeof result.headers === "object"
|
|
77
|
+
? result.headers
|
|
78
|
+
: {};
|
|
79
|
+
const body = result && Buffer.isBuffer(result.body)
|
|
80
|
+
? result.body
|
|
81
|
+
: Buffer.from(result && result.body !== undefined ? String(result.body) : "");
|
|
82
|
+
return {
|
|
83
|
+
kind: "http.response",
|
|
84
|
+
requestId,
|
|
85
|
+
statusCode: Number.isFinite(result && result.statusCode) ? result.statusCode : 200,
|
|
86
|
+
headers,
|
|
87
|
+
bodyBase64: body.toString("base64"),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseRequestIdFromRaw(raw) {
|
|
92
|
+
return normalizeRequestId((parseFrame(raw) || {}).requestId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createRelayWorkerSupervisor(options = {}) {
|
|
96
|
+
const logger = normalizeLogger(options.logger);
|
|
97
|
+
const handler = options.handler || options.downstreamHandler || null;
|
|
98
|
+
const operationRegistry = options.operationRegistry || null;
|
|
99
|
+
const workerFactory =
|
|
100
|
+
typeof options.workerFactory === "function" ? options.workerFactory : defaultWorkerFactory;
|
|
101
|
+
const wssEvents = new EventEmitter();
|
|
102
|
+
let worker = null;
|
|
103
|
+
let workerEpoch = 0;
|
|
104
|
+
let addressValue = null;
|
|
105
|
+
let startPromise = null;
|
|
106
|
+
let readyPromise = Promise.resolve();
|
|
107
|
+
let resolveReady = null;
|
|
108
|
+
let rejectReady = null;
|
|
109
|
+
let closing = false;
|
|
110
|
+
let activeOperationBarrier = null;
|
|
111
|
+
let mainHeartbeatTimer = null;
|
|
112
|
+
const clients = new Map();
|
|
113
|
+
const pendingReadinessProbeRequests = new Map();
|
|
114
|
+
const pendingAutomationStateRequests = new Map();
|
|
115
|
+
|
|
116
|
+
function clearPendingAutomationStateRequest(requestId) {
|
|
117
|
+
const pending = pendingAutomationStateRequests.get(requestId);
|
|
118
|
+
if (pending && pending.fallbackTimer) {
|
|
119
|
+
clearTimeout(pending.fallbackTimer);
|
|
120
|
+
}
|
|
121
|
+
pendingAutomationStateRequests.delete(requestId);
|
|
122
|
+
return pending || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function clearPendingAutomationStateRequests() {
|
|
126
|
+
for (const requestId of pendingAutomationStateRequests.keys()) {
|
|
127
|
+
clearPendingAutomationStateRequest(requestId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resetReadyPromise() {
|
|
132
|
+
readyPromise = new Promise((resolve, reject) => {
|
|
133
|
+
resolveReady = resolve;
|
|
134
|
+
rejectReady = reject;
|
|
135
|
+
});
|
|
136
|
+
startPromise = readyPromise;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function postToWorker(message) {
|
|
140
|
+
if (worker && typeof worker.postMessage === "function") {
|
|
141
|
+
worker.postMessage(message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function mainHeartbeatIntervalMs() {
|
|
146
|
+
if (Number.isFinite(options.mainHeartbeatIntervalMs)) {
|
|
147
|
+
return Math.max(10, Math.floor(options.mainHeartbeatIntervalMs));
|
|
148
|
+
}
|
|
149
|
+
return DEFAULT_WORKER_HEALTH_THRESHOLDS.heartbeatIntervalMs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildMainHeartbeat(workerEpochValue) {
|
|
153
|
+
const resumeState =
|
|
154
|
+
typeof options.getCurrentResumeState === "function"
|
|
155
|
+
? options.getCurrentResumeState() || {}
|
|
156
|
+
: {};
|
|
157
|
+
const heartbeat = {
|
|
158
|
+
kind: "main.heartbeat",
|
|
159
|
+
emittedAtMs: Date.now(),
|
|
160
|
+
workerEpoch: workerEpochValue,
|
|
161
|
+
};
|
|
162
|
+
const pagesRevision = parseNonNegativeRevision(resumeState.pagesRevision);
|
|
163
|
+
const statusRevision = parseNonNegativeRevision(resumeState.statusRevision);
|
|
164
|
+
if (pagesRevision !== null) heartbeat.cachedPagesRevision = pagesRevision;
|
|
165
|
+
if (statusRevision !== null) heartbeat.cachedStatusRevision = statusRevision;
|
|
166
|
+
return heartbeat;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stopMainHeartbeat() {
|
|
170
|
+
if (!mainHeartbeatTimer) return;
|
|
171
|
+
clearInterval(mainHeartbeatTimer);
|
|
172
|
+
mainHeartbeatTimer = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function startMainHeartbeat(workerEpochValue) {
|
|
176
|
+
stopMainHeartbeat();
|
|
177
|
+
postToWorker(buildMainHeartbeat(workerEpochValue));
|
|
178
|
+
mainHeartbeatTimer = setInterval(() => {
|
|
179
|
+
postToWorker(buildMainHeartbeat(workerEpochValue));
|
|
180
|
+
}, mainHeartbeatIntervalMs());
|
|
181
|
+
if (typeof mainHeartbeatTimer.unref === "function") mainHeartbeatTimer.unref();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildManifest() {
|
|
185
|
+
workerEpoch += 1;
|
|
186
|
+
return {
|
|
187
|
+
kind: "manifest",
|
|
188
|
+
manifestId: `worker-${workerEpoch}-${Date.now()}`,
|
|
189
|
+
workerEpoch,
|
|
190
|
+
host: options.host || "127.0.0.1",
|
|
191
|
+
port: Number.isFinite(options.port) ? options.port : 0,
|
|
192
|
+
relayToken: options.token || "",
|
|
193
|
+
pluginId: options.pluginId || "ocuclaw",
|
|
194
|
+
pluginVersion:
|
|
195
|
+
typeof options.getPluginVersion === "function"
|
|
196
|
+
? options.getPluginVersion()
|
|
197
|
+
: "",
|
|
198
|
+
requiresClientVersion:
|
|
199
|
+
typeof options.getRequiresClientVersion === "function"
|
|
200
|
+
? options.getRequiresClientVersion()
|
|
201
|
+
: "",
|
|
202
|
+
supportedProtocolVersions: ["v2"],
|
|
203
|
+
featureFlags: WORKER_FEATURES,
|
|
204
|
+
routes: {
|
|
205
|
+
webSocketPaths: ["/"],
|
|
206
|
+
mainForwardedHttpPaths: ["/v1/chat/completions"],
|
|
207
|
+
},
|
|
208
|
+
externalDebugToolsEnabled: options.externalDebugToolsEnabled === true,
|
|
209
|
+
nudge: {
|
|
210
|
+
...DEFAULT_NUDGE_THRESHOLDS,
|
|
211
|
+
...(options.nudge || {}),
|
|
212
|
+
},
|
|
213
|
+
queue: {
|
|
214
|
+
...DEFAULT_WORKER_QUEUE_CAPS,
|
|
215
|
+
},
|
|
216
|
+
health: {
|
|
217
|
+
...DEFAULT_WORKER_HEALTH_THRESHOLDS,
|
|
218
|
+
},
|
|
219
|
+
rpc: {
|
|
220
|
+
...DEFAULT_WORKER_RPC_LIMITS,
|
|
221
|
+
httpRequestTimeoutMs:
|
|
222
|
+
Number.isFinite(options.evenAiRequestTimeoutMs)
|
|
223
|
+
? Math.max(
|
|
224
|
+
DEFAULT_WORKER_RPC_LIMITS.httpRequestTimeoutMs,
|
|
225
|
+
Math.floor(options.evenAiRequestTimeoutMs),
|
|
226
|
+
)
|
|
227
|
+
: DEFAULT_WORKER_RPC_LIMITS.httpRequestTimeoutMs,
|
|
228
|
+
httpMaxBodyBytes:
|
|
229
|
+
Number.isFinite(options.evenAiMaxBodyBytes)
|
|
230
|
+
? Math.max(
|
|
231
|
+
DEFAULT_WORKER_RPC_LIMITS.httpMaxBodyBytes,
|
|
232
|
+
Math.floor(options.evenAiMaxBodyBytes),
|
|
233
|
+
)
|
|
234
|
+
: DEFAULT_WORKER_RPC_LIMITS.httpMaxBodyBytes,
|
|
235
|
+
httpMaxResponseBytes:
|
|
236
|
+
Number.isFinite(options.evenAiMaxResponseBytes)
|
|
237
|
+
? Math.max(1, Math.floor(options.evenAiMaxResponseBytes))
|
|
238
|
+
: DEFAULT_WORKER_RPC_LIMITS.httpMaxResponseBytes,
|
|
239
|
+
},
|
|
240
|
+
initialCache: buildInitialCache(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function buildInitialCache() {
|
|
245
|
+
const initialCache = {};
|
|
246
|
+
if (typeof options.getCurrentPages === "function") {
|
|
247
|
+
const pages = options.getCurrentPages();
|
|
248
|
+
if (typeof pages === "string") initialCache.pages = pages;
|
|
249
|
+
}
|
|
250
|
+
if (typeof options.getCurrentStatus === "function") {
|
|
251
|
+
const status = options.getCurrentStatus();
|
|
252
|
+
if (typeof status === "string") initialCache.status = status;
|
|
253
|
+
}
|
|
254
|
+
if (typeof options.getCurrentDebugConfig === "function") {
|
|
255
|
+
const debugConfig = options.getCurrentDebugConfig();
|
|
256
|
+
if (typeof debugConfig === "string") initialCache.debugConfig = debugConfig;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const resumeState =
|
|
260
|
+
typeof options.getCurrentResumeState === "function"
|
|
261
|
+
? options.getCurrentResumeState() || {}
|
|
262
|
+
: {};
|
|
263
|
+
let pagesRevision = parseNonNegativeRevision(resumeState.pagesRevision);
|
|
264
|
+
let statusRevision = parseNonNegativeRevision(resumeState.statusRevision);
|
|
265
|
+
if (pagesRevision === null && initialCache.pages) {
|
|
266
|
+
pagesRevision = parseNonNegativeRevision((parseFrame(initialCache.pages) || {}).revision);
|
|
267
|
+
}
|
|
268
|
+
if (statusRevision === null && initialCache.status) {
|
|
269
|
+
statusRevision = parseNonNegativeRevision((parseFrame(initialCache.status) || {}).revision);
|
|
270
|
+
}
|
|
271
|
+
if (pagesRevision !== null) initialCache.pagesRevision = pagesRevision;
|
|
272
|
+
if (statusRevision !== null) initialCache.statusRevision = statusRevision;
|
|
273
|
+
if (initialCache.pages || initialCache.status || initialCache.debugConfig) {
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
initialCache.lastMainFrameAtMs = now;
|
|
276
|
+
if (initialCache.status) initialCache.lastMainStatusAtMs = now;
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
typeof options.getAgentAvatarHash === "function" &&
|
|
280
|
+
typeof options.getAgentAvatarDataUriByHash === "function"
|
|
281
|
+
) {
|
|
282
|
+
const hash = options.getAgentAvatarHash();
|
|
283
|
+
if (typeof hash === "string" && hash) {
|
|
284
|
+
const dataUri = options.getAgentAvatarDataUriByHash(hash);
|
|
285
|
+
if (typeof dataUri === "string" && dataUri) {
|
|
286
|
+
initialCache.agentAvatar = { hash, dataUri };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return initialCache;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let lastPushedAgentAvatarHash = null;
|
|
294
|
+
function notifyAgentAvatarChanged(hash, dataUri) {
|
|
295
|
+
const nextHash =
|
|
296
|
+
typeof hash === "string" && hash && typeof dataUri === "string" && dataUri
|
|
297
|
+
? hash
|
|
298
|
+
: null;
|
|
299
|
+
if (nextHash === lastPushedAgentAvatarHash) return;
|
|
300
|
+
lastPushedAgentAvatarHash = nextHash;
|
|
301
|
+
postToWorker({
|
|
302
|
+
kind: "main.avatar",
|
|
303
|
+
agentAvatar: nextHash ? { hash: nextHash, dataUri } : null,
|
|
304
|
+
emittedAtMs: Date.now(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function postMainFrame(target, frame, clientId) {
|
|
309
|
+
if (typeof frame !== "string") return;
|
|
310
|
+
// audit #19: parse the frame once. parseMessageType is itself a full JSON.parse,
|
|
311
|
+
// so deriving type from a single parseFrame avoids a second parse on the hot
|
|
312
|
+
// pages/status path below (and is neutral — still one parse — on other frames).
|
|
313
|
+
const parsed = parseFrame(frame);
|
|
314
|
+
const type = parsed && typeof parsed.type === "string" ? parsed.type : null;
|
|
315
|
+
const message = {
|
|
316
|
+
kind: "main.frame",
|
|
317
|
+
target,
|
|
318
|
+
clientId,
|
|
319
|
+
frame,
|
|
320
|
+
emittedAtMs: Date.now(),
|
|
321
|
+
type,
|
|
322
|
+
};
|
|
323
|
+
if (type === APP_PROTOCOL.pages || type === APP_PROTOCOL.status) {
|
|
324
|
+
const revision = parseNonNegativeRevision((parsed || {}).revision);
|
|
325
|
+
if (revision !== null) {
|
|
326
|
+
message.revisions =
|
|
327
|
+
type === APP_PROTOCOL.pages
|
|
328
|
+
? { pagesRevision: revision }
|
|
329
|
+
: { statusRevision: revision };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// F17: requestId-scope the operation barrier. The barrier exists to
|
|
333
|
+
// preserve send-ordering in the worker path, but it used to divert EVERY
|
|
334
|
+
// broadcast/broadcastApp frame for the whole send→ack window — stalling
|
|
335
|
+
// unrelated live broadcasts (streaming deltas / activity / typing / status
|
|
336
|
+
// from a prior, still-streaming turn) until flush. Only hold a broadcast
|
|
337
|
+
// that is causally tied to the in-flight barrier.requestId; let everything
|
|
338
|
+
// else pass straight through. The send's own operation-received and ack are
|
|
339
|
+
// unicast (never diverted) and the op-received is posted synchronously
|
|
340
|
+
// ahead of the async ack, so "operation-received precedes that op's ack"
|
|
341
|
+
// holds independent of this barrier.
|
|
342
|
+
if (
|
|
343
|
+
activeOperationBarrier &&
|
|
344
|
+
(target === "broadcast" || target === "broadcastApp") &&
|
|
345
|
+
parsed &&
|
|
346
|
+
normalizeRequestId(parsed.requestId) === activeOperationBarrier.requestId
|
|
347
|
+
) {
|
|
348
|
+
activeOperationBarrier.frames.push(message);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
postToWorker(message);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function flushOperationBarrier(barrier) {
|
|
355
|
+
if (!barrier) return;
|
|
356
|
+
if (activeOperationBarrier === barrier) activeOperationBarrier = null;
|
|
357
|
+
for (const frame of barrier.frames) {
|
|
358
|
+
postToWorker(frame);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function processResult(clientId, result, processOptions = {}) {
|
|
363
|
+
const resolved = await Promise.resolve(result);
|
|
364
|
+
if (!resolved) return;
|
|
365
|
+
for (const frame of normalizeFrameList(resolved.unicast)) {
|
|
366
|
+
const nextFrame = addWorkerEpochToSendAck(frame, workerEpoch);
|
|
367
|
+
if (
|
|
368
|
+
processOptions.suppressMainReceiptForRequestId &&
|
|
369
|
+
frameMatchesOperationReceipt(nextFrame, processOptions.suppressMainReceiptForRequestId)
|
|
370
|
+
) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
postMainFrame("unicast", nextFrame, clientId);
|
|
374
|
+
}
|
|
375
|
+
if (resolved.readinessProbe) {
|
|
376
|
+
const requestId = normalizeRequestId(resolved.readinessProbe.requestId);
|
|
377
|
+
const targetClientId = normalizeRequestId(resolved.readinessProbe.targetClientId);
|
|
378
|
+
const message =
|
|
379
|
+
typeof resolved.readinessProbe.message === "string"
|
|
380
|
+
? resolved.readinessProbe.message
|
|
381
|
+
: null;
|
|
382
|
+
if (!requestId || !targetClientId || !message || !isAppClient(targetClientId)) {
|
|
383
|
+
postMainFrame(
|
|
384
|
+
"unicast",
|
|
385
|
+
formatReadinessProbeFailure(
|
|
386
|
+
requestId,
|
|
387
|
+
"no_downstream_client",
|
|
388
|
+
"No downstream app client connected",
|
|
389
|
+
),
|
|
390
|
+
clientId,
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
pendingReadinessProbeRequests.set(requestId, {
|
|
394
|
+
requesterClientId: clientId,
|
|
395
|
+
targetClientId,
|
|
396
|
+
createdAtMs: Date.now(),
|
|
397
|
+
});
|
|
398
|
+
postMainFrame("unicast", message, targetClientId);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (resolved.automationStateRequest) {
|
|
402
|
+
const requestId = normalizeRequestId(resolved.automationStateRequest.requestId);
|
|
403
|
+
const targetClientId = normalizeRequestId(resolved.automationStateRequest.targetClientId);
|
|
404
|
+
const message =
|
|
405
|
+
typeof resolved.automationStateRequest.message === "string"
|
|
406
|
+
? resolved.automationStateRequest.message
|
|
407
|
+
: null;
|
|
408
|
+
if (!requestId || !targetClientId || !message || !isAppClient(targetClientId)) {
|
|
409
|
+
postMainFrame(
|
|
410
|
+
"unicast",
|
|
411
|
+
formatAutomationStateFailure(
|
|
412
|
+
requestId,
|
|
413
|
+
"snapshot_unavailable",
|
|
414
|
+
"Automation state snapshot is unavailable",
|
|
415
|
+
),
|
|
416
|
+
clientId,
|
|
417
|
+
);
|
|
418
|
+
} else {
|
|
419
|
+
const fallbackTimer = setTimeout(() => {
|
|
420
|
+
const pending = pendingAutomationStateRequests.get(requestId);
|
|
421
|
+
if (!pending || pending.targetClientId !== targetClientId) return;
|
|
422
|
+
pendingAutomationStateRequests.delete(requestId);
|
|
423
|
+
postMainFrame(
|
|
424
|
+
"unicast",
|
|
425
|
+
formatAutomationStateFailure(
|
|
426
|
+
requestId,
|
|
427
|
+
"snapshot_unavailable",
|
|
428
|
+
"Automation state snapshot is unavailable",
|
|
429
|
+
),
|
|
430
|
+
clientId,
|
|
431
|
+
);
|
|
432
|
+
}, AUTOMATION_STATE_FALLBACK_MS);
|
|
433
|
+
if (fallbackTimer && typeof fallbackTimer.unref === "function") {
|
|
434
|
+
fallbackTimer.unref();
|
|
435
|
+
}
|
|
436
|
+
pendingAutomationStateRequests.set(requestId, {
|
|
437
|
+
requesterClientId: clientId,
|
|
438
|
+
targetClientId,
|
|
439
|
+
createdAtMs: Date.now(),
|
|
440
|
+
fallbackTimer,
|
|
441
|
+
});
|
|
442
|
+
postMainFrame("unicast", message, targetClientId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const frame of normalizeFrameList(resolved.broadcast)) {
|
|
446
|
+
postMainFrame("broadcast", frame);
|
|
447
|
+
}
|
|
448
|
+
for (const frame of normalizeFrameList(resolved.broadcastApp)) {
|
|
449
|
+
postMainFrame("broadcastApp", frame);
|
|
450
|
+
}
|
|
451
|
+
if (resolved.followup) {
|
|
452
|
+
await processResult(clientId, resolved.followup, processOptions);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function handleHttpRequest(message) {
|
|
457
|
+
if (typeof options.handleBufferedEvenAiHttpRequest !== "function") {
|
|
458
|
+
postToWorker(responseToWorkerMessage(message.requestId, {
|
|
459
|
+
statusCode: 404,
|
|
460
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
461
|
+
body: Buffer.from("not found"),
|
|
462
|
+
}));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const result = await Promise.resolve(options.handleBufferedEvenAiHttpRequest(message));
|
|
467
|
+
postToWorker(responseToWorkerMessage(message.requestId, result));
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logger.warn(`[relay-worker] buffered HTTP request failed: ${err && err.message ? err.message : err}`);
|
|
470
|
+
postToWorker(responseToWorkerMessage(message.requestId, {
|
|
471
|
+
statusCode: 503,
|
|
472
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
473
|
+
body: Buffer.from("relay worker HTTP bridge failed"),
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function handleHttpCancel(message) {
|
|
479
|
+
if (typeof options.cancelBufferedEvenAiHttpRequest !== "function") {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
options.cancelBufferedEvenAiHttpRequest(message);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
logger.warn(`[relay-worker] buffered HTTP cancel failed: ${err && err.message ? err.message : err}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function getActiveSessionKey() {
|
|
490
|
+
return typeof options.getActiveSessionKey === "function"
|
|
491
|
+
? options.getActiveSessionKey() || null
|
|
492
|
+
: null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function emitRelaySession(event, sessionKey, payloadFactory) {
|
|
496
|
+
if (typeof options.emitDebug !== "function") return;
|
|
497
|
+
options.emitDebug(
|
|
498
|
+
"relay.session",
|
|
499
|
+
event,
|
|
500
|
+
"info",
|
|
501
|
+
{ sessionKey: sessionKey || undefined },
|
|
502
|
+
payloadFactory,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function handleMessage(message) {
|
|
507
|
+
if (!message || typeof message !== "object") return;
|
|
508
|
+
if (message.kind === "worker.ready") {
|
|
509
|
+
addressValue = message.address || null;
|
|
510
|
+
wssEvents.emit("listening");
|
|
511
|
+
if (resolveReady) {
|
|
512
|
+
resolveReady(message);
|
|
513
|
+
resolveReady = null;
|
|
514
|
+
rejectReady = null;
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (message.kind === "worker.error") {
|
|
519
|
+
logger.warn(`[relay-worker] ${message.message || "worker error"}`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (message.kind === "app.message") {
|
|
523
|
+
if (!handler || typeof handler.handleMessage !== "function") return;
|
|
524
|
+
const processOptions = {};
|
|
525
|
+
if (message.operation === "message.send" && message.requestId) {
|
|
526
|
+
processOptions.suppressMainReceiptForRequestId = message.requestId;
|
|
527
|
+
activeOperationBarrier = {
|
|
528
|
+
requestId: message.requestId,
|
|
529
|
+
frames: [],
|
|
530
|
+
};
|
|
531
|
+
postMainFrame(
|
|
532
|
+
"unicast",
|
|
533
|
+
formatMainOperationReceived({
|
|
534
|
+
requestId: message.requestId,
|
|
535
|
+
operation: "message.send",
|
|
536
|
+
}),
|
|
537
|
+
message.clientId,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const barrier = activeOperationBarrier;
|
|
541
|
+
(async () => {
|
|
542
|
+
try {
|
|
543
|
+
await processResult(
|
|
544
|
+
message.clientId,
|
|
545
|
+
handler.handleMessage(message.clientId, message.raw),
|
|
546
|
+
processOptions,
|
|
547
|
+
);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
logger.warn(`[relay-worker] app message handling failed: ${err && err.message ? err.message : err}`);
|
|
550
|
+
} finally {
|
|
551
|
+
flushOperationBarrier(barrier);
|
|
552
|
+
}
|
|
553
|
+
})();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (message.kind === "operation.reconcile") {
|
|
557
|
+
const results =
|
|
558
|
+
operationRegistry && typeof operationRegistry.reconcileRequestIds === "function"
|
|
559
|
+
? operationRegistry.reconcileRequestIds(message.requestIds)
|
|
560
|
+
: [];
|
|
561
|
+
postToWorker({
|
|
562
|
+
kind: "operation.reconcile.result",
|
|
563
|
+
clientId: message.clientId,
|
|
564
|
+
requestIds: message.requestIds,
|
|
565
|
+
results,
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (message.kind === "http.request") {
|
|
570
|
+
handleHttpRequest(message);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (message.kind === "http.cancel") {
|
|
574
|
+
handleHttpCancel(message);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (message.kind === "client.identified") {
|
|
578
|
+
clients.set(message.clientId, {
|
|
579
|
+
clientId: message.clientId,
|
|
580
|
+
clientKind: message.clientKind || "unknown",
|
|
581
|
+
clientName: message.clientName || null,
|
|
582
|
+
clientVersion: message.clientVersion || null,
|
|
583
|
+
sessionKey: message.sessionKey || null,
|
|
584
|
+
readinessSnapshot: message.readinessSnapshot || null,
|
|
585
|
+
connectedAtMs: Number.isFinite(message.connectedAtMs)
|
|
586
|
+
? message.connectedAtMs
|
|
587
|
+
: Date.now(),
|
|
588
|
+
updatedAtMs: Date.now(),
|
|
589
|
+
});
|
|
590
|
+
const connectedEntry = clients.get(message.clientId) || null;
|
|
591
|
+
emitRelaySession("downstream_client_connected", getActiveSessionKey(), () => ({
|
|
592
|
+
clientId: message.clientId,
|
|
593
|
+
connectedCount: clients.size,
|
|
594
|
+
connectedAtMs: connectedEntry ? connectedEntry.connectedAtMs : null,
|
|
595
|
+
remoteAddress: null,
|
|
596
|
+
userAgentTail: null,
|
|
597
|
+
}));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (message.kind === "client.disconnected") {
|
|
601
|
+
const disconnectedEntry = clients.get(message.clientId) || null;
|
|
602
|
+
for (const [requestId, pending] of pendingReadinessProbeRequests) {
|
|
603
|
+
if (
|
|
604
|
+
pending.requesterClientId === message.clientId ||
|
|
605
|
+
pending.targetClientId === message.clientId
|
|
606
|
+
) {
|
|
607
|
+
pendingReadinessProbeRequests.delete(requestId);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
for (const [requestId, pending] of pendingAutomationStateRequests) {
|
|
611
|
+
if (
|
|
612
|
+
pending.requesterClientId === message.clientId ||
|
|
613
|
+
pending.targetClientId === message.clientId
|
|
614
|
+
) {
|
|
615
|
+
clearPendingAutomationStateRequest(requestId);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
clients.delete(message.clientId);
|
|
619
|
+
const disconnectedConnectedAtMs = disconnectedEntry ? disconnectedEntry.connectedAtMs : null;
|
|
620
|
+
const disconnectedLifetimeMs = Number.isFinite(disconnectedConnectedAtMs)
|
|
621
|
+
? Math.max(0, Date.now() - disconnectedConnectedAtMs)
|
|
622
|
+
: null;
|
|
623
|
+
emitRelaySession("downstream_client_disconnected", getActiveSessionKey(), () => ({
|
|
624
|
+
clientId: message.clientId,
|
|
625
|
+
connectedCount: clients.size,
|
|
626
|
+
connectedAtMs: disconnectedConnectedAtMs,
|
|
627
|
+
lifetimeMs: disconnectedLifetimeMs,
|
|
628
|
+
closeCode: Number.isFinite(message.closeCode) ? message.closeCode : null,
|
|
629
|
+
closeReasonTail: message.closeReasonTail || null,
|
|
630
|
+
role: disconnectedEntry ? disconnectedEntry.clientKind : null,
|
|
631
|
+
clientKind: disconnectedEntry ? disconnectedEntry.clientKind : null,
|
|
632
|
+
protocolVersion: null,
|
|
633
|
+
protocolReason: null,
|
|
634
|
+
clientName: disconnectedEntry ? disconnectedEntry.clientName : null,
|
|
635
|
+
clientVersion: disconnectedEntry ? disconnectedEntry.clientVersion : null,
|
|
636
|
+
}));
|
|
637
|
+
if (
|
|
638
|
+
disconnectedEntry &&
|
|
639
|
+
disconnectedEntry.clientKind === "app" &&
|
|
640
|
+
typeof options.onAppClientDisconnect === "function"
|
|
641
|
+
) {
|
|
642
|
+
// Drain a session's live-refresh crons only when THAT session's last
|
|
643
|
+
// app client disconnects (round-2: a same-session duplicate keeps it
|
|
644
|
+
// live; round-6: per-session so A drains independently of B). The
|
|
645
|
+
// disconnecting client is already removed from `clients`, so the
|
|
646
|
+
// excludeClientId is belt-and-suspenders.
|
|
647
|
+
const drainSessionKey =
|
|
648
|
+
typeof disconnectedEntry.sessionKey === "string" && disconnectedEntry.sessionKey
|
|
649
|
+
? disconnectedEntry.sessionKey
|
|
650
|
+
: null;
|
|
651
|
+
if (drainSessionKey) {
|
|
652
|
+
if (getConnectedAppEntries(message.clientId, drainSessionKey).length === 0) {
|
|
653
|
+
options.onAppClientDisconnect(drainSessionKey);
|
|
654
|
+
}
|
|
655
|
+
} else if (getConnectedAppEntries(message.clientId).length === 0) {
|
|
656
|
+
options.onAppClientDisconnect(getActiveSessionKey());
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (message.kind === "client.visibility") {
|
|
662
|
+
const visibilityEntry = clients.get(message.clientId) || null;
|
|
663
|
+
emitRelaySession(
|
|
664
|
+
"downstream_transport_visibility",
|
|
665
|
+
(visibilityEntry && visibilityEntry.sessionKey) || getActiveSessionKey(),
|
|
666
|
+
() => ({
|
|
667
|
+
clientId: message.clientId,
|
|
668
|
+
state: message.state || null,
|
|
669
|
+
connectedCount: clients.size,
|
|
670
|
+
role: visibilityEntry ? visibilityEntry.clientKind : null,
|
|
671
|
+
clientKind: visibilityEntry ? visibilityEntry.clientKind : message.clientKind || null,
|
|
672
|
+
clientName: visibilityEntry ? visibilityEntry.clientName : message.clientName || null,
|
|
673
|
+
clientVersion: visibilityEntry
|
|
674
|
+
? visibilityEntry.clientVersion
|
|
675
|
+
: message.clientVersion || null,
|
|
676
|
+
protocolVersion: message.protocolVersion || null,
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (message.kind === "client.readinessSnapshot") {
|
|
682
|
+
const entry = clients.get(message.clientId);
|
|
683
|
+
if (entry) {
|
|
684
|
+
entry.readinessSnapshot = message.readinessSnapshot || null;
|
|
685
|
+
entry.updatedAtMs = Number.isFinite(message.updatedAtMs)
|
|
686
|
+
? message.updatedAtMs
|
|
687
|
+
: Date.now();
|
|
688
|
+
}
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (message.kind === "client.readinessProbeAck") {
|
|
693
|
+
const ack = message.ack && typeof message.ack === "object" ? message.ack : null;
|
|
694
|
+
const requestId = normalizeRequestId(ack && ack.requestId);
|
|
695
|
+
const pending = requestId ? pendingReadinessProbeRequests.get(requestId) : null;
|
|
696
|
+
if (!pending || pending.targetClientId !== message.clientId) return;
|
|
697
|
+
pendingReadinessProbeRequests.delete(requestId);
|
|
698
|
+
const protocol = clients.get(message.clientId) || {};
|
|
699
|
+
const frame =
|
|
700
|
+
handler && typeof handler.formatReadinessProbeAck === "function"
|
|
701
|
+
? handler.formatReadinessProbeAck({
|
|
702
|
+
ok: ack.ok !== false,
|
|
703
|
+
requestId,
|
|
704
|
+
reasonCode: ack.reasonCode || null,
|
|
705
|
+
message: ack.message || null,
|
|
706
|
+
activeSessionKey: ack.activeSessionKey || null,
|
|
707
|
+
emittedAtMs: ack.emittedAtMs,
|
|
708
|
+
clientId: message.clientId,
|
|
709
|
+
clientName: protocol.clientName || null,
|
|
710
|
+
clientVersion: protocol.clientVersion || null,
|
|
711
|
+
})
|
|
712
|
+
: JSON.stringify({
|
|
713
|
+
type: APP_PROTOCOL.readinessProbeAck,
|
|
714
|
+
ok: ack.ok !== false,
|
|
715
|
+
requestId,
|
|
716
|
+
reasonCode: ack.reasonCode || null,
|
|
717
|
+
message: ack.message || null,
|
|
718
|
+
activeSessionKey: ack.activeSessionKey || null,
|
|
719
|
+
emittedAtMs: ack.emittedAtMs,
|
|
720
|
+
clientId: message.clientId,
|
|
721
|
+
clientName: protocol.clientName || null,
|
|
722
|
+
clientVersion: protocol.clientVersion || null,
|
|
723
|
+
});
|
|
724
|
+
postMainFrame("unicast", frame, pending.requesterClientId);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (message.kind === "client.automationStateSnapshot") {
|
|
729
|
+
const snapshot =
|
|
730
|
+
message.snapshot && typeof message.snapshot === "object" ? message.snapshot : null;
|
|
731
|
+
const requestId = normalizeRequestId(snapshot && snapshot.requestId);
|
|
732
|
+
const pending = requestId ? pendingAutomationStateRequests.get(requestId) : null;
|
|
733
|
+
if (!pending || pending.targetClientId !== message.clientId) return;
|
|
734
|
+
clearPendingAutomationStateRequest(requestId);
|
|
735
|
+
const frame =
|
|
736
|
+
handler && typeof handler.formatAutomationStateSnapshot === "function"
|
|
737
|
+
? handler.formatAutomationStateSnapshot({
|
|
738
|
+
ok: snapshot.ok !== false,
|
|
739
|
+
requestId,
|
|
740
|
+
state: snapshot.state || null,
|
|
741
|
+
reasonCode: snapshot.reasonCode || null,
|
|
742
|
+
message: snapshot.message || null,
|
|
743
|
+
})
|
|
744
|
+
: JSON.stringify({
|
|
745
|
+
type: APP_PROTOCOL.automationStateSnapshot,
|
|
746
|
+
ok: snapshot.ok !== false,
|
|
747
|
+
requestId,
|
|
748
|
+
state: snapshot.state || null,
|
|
749
|
+
reasonCode: snapshot.reasonCode || null,
|
|
750
|
+
message: snapshot.message || null,
|
|
751
|
+
});
|
|
752
|
+
postMainFrame("unicast", frame, pending.requesterClientId);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (message.kind === "debug" && typeof options.emitDebug === "function") {
|
|
756
|
+
options.emitDebug(
|
|
757
|
+
message.category || "relay.worker.health",
|
|
758
|
+
message.event || "worker_event",
|
|
759
|
+
message.severity || "debug",
|
|
760
|
+
null,
|
|
761
|
+
() => message.data || {},
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function start() {
|
|
767
|
+
if (startPromise) return startPromise;
|
|
768
|
+
closing = false;
|
|
769
|
+
resetReadyPromise();
|
|
770
|
+
startWorker();
|
|
771
|
+
return startPromise;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function startWorker() {
|
|
775
|
+
const nextWorker = workerFactory();
|
|
776
|
+
worker = nextWorker;
|
|
777
|
+
nextWorker.on("message", handleMessage);
|
|
778
|
+
nextWorker.on("error", (err) => {
|
|
779
|
+
logger.error(`[relay-worker] worker error: ${err && err.message ? err.message : err}`);
|
|
780
|
+
if (rejectReady) rejectReady(err);
|
|
781
|
+
wssEvents.emit("error", err);
|
|
782
|
+
});
|
|
783
|
+
const startedWorker = nextWorker;
|
|
784
|
+
nextWorker.on("exit", (code) => {
|
|
785
|
+
const unexpected = !closing;
|
|
786
|
+
if (worker === startedWorker) worker = null;
|
|
787
|
+
stopMainHeartbeat();
|
|
788
|
+
if (unexpected) {
|
|
789
|
+
const err = new Error(`relay worker exited with code ${code}`);
|
|
790
|
+
logger.warn(`[relay-worker] ${err.message}`);
|
|
791
|
+
if (rejectReady) rejectReady(err);
|
|
792
|
+
resolveReady = null;
|
|
793
|
+
rejectReady = null;
|
|
794
|
+
addressValue = null;
|
|
795
|
+
clients.clear();
|
|
796
|
+
pendingReadinessProbeRequests.clear();
|
|
797
|
+
clearPendingAutomationStateRequests();
|
|
798
|
+
if (wssEvents.listenerCount("error") > 0) {
|
|
799
|
+
wssEvents.emit("error", err);
|
|
800
|
+
}
|
|
801
|
+
resetReadyPromise();
|
|
802
|
+
startWorker();
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
const manifest = buildManifest();
|
|
806
|
+
postToWorker(manifest);
|
|
807
|
+
startMainHeartbeat(manifest.workerEpoch);
|
|
808
|
+
return nextWorker;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function close() {
|
|
812
|
+
closing = true;
|
|
813
|
+
stopMainHeartbeat();
|
|
814
|
+
if (!worker) {
|
|
815
|
+
startPromise = null;
|
|
816
|
+
return Promise.resolve();
|
|
817
|
+
}
|
|
818
|
+
const activeWorker = worker;
|
|
819
|
+
return new Promise((resolve) => {
|
|
820
|
+
let resolved = false;
|
|
821
|
+
function finish() {
|
|
822
|
+
if (resolved) return;
|
|
823
|
+
resolved = true;
|
|
824
|
+
if (worker === activeWorker) worker = null;
|
|
825
|
+
startPromise = null;
|
|
826
|
+
addressValue = null;
|
|
827
|
+
clients.clear();
|
|
828
|
+
pendingReadinessProbeRequests.clear();
|
|
829
|
+
clearPendingAutomationStateRequests();
|
|
830
|
+
resolve();
|
|
831
|
+
}
|
|
832
|
+
const timer = setTimeout(() => {
|
|
833
|
+
activeWorker.terminate().finally(finish);
|
|
834
|
+
}, 500);
|
|
835
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
836
|
+
activeWorker.once("exit", () => {
|
|
837
|
+
clearTimeout(timer);
|
|
838
|
+
finish();
|
|
839
|
+
});
|
|
840
|
+
activeWorker.once("message", (message) => {
|
|
841
|
+
if (message && message.kind === "worker.closed") {
|
|
842
|
+
clearTimeout(timer);
|
|
843
|
+
activeWorker.terminate().finally(finish);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
activeWorker.postMessage({ kind: "shutdown" });
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function broadcast(frame) {
|
|
851
|
+
postMainFrame("broadcast", frame);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function broadcastApp(frame) {
|
|
855
|
+
postMainFrame("broadcastApp", frame);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function unicast(clientId, frame) {
|
|
859
|
+
postMainFrame("unicast", frame, clientId);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function getConnectedAppEntries(excludeClientId = null, sessionKey = null) {
|
|
863
|
+
const entries = [];
|
|
864
|
+
for (const entry of clients.values()) {
|
|
865
|
+
if (entry.clientKind !== "app") continue;
|
|
866
|
+
if (excludeClientId && entry.clientId === excludeClientId) continue;
|
|
867
|
+
if (sessionKey != null && entry.sessionKey !== sessionKey) continue;
|
|
868
|
+
entries.push(entry);
|
|
869
|
+
}
|
|
870
|
+
return entries;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function isAppClient(clientId) {
|
|
874
|
+
const entry = clients.get(clientId);
|
|
875
|
+
return entry && entry.clientKind === "app";
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function formatReadinessProbeFailure(requestId, reasonCode, message) {
|
|
879
|
+
if (handler && typeof handler.formatReadinessProbeAck === "function") {
|
|
880
|
+
return handler.formatReadinessProbeAck({
|
|
881
|
+
ok: false,
|
|
882
|
+
requestId,
|
|
883
|
+
reasonCode,
|
|
884
|
+
message,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
return JSON.stringify({
|
|
888
|
+
type: APP_PROTOCOL.readinessProbeAck,
|
|
889
|
+
ok: false,
|
|
890
|
+
requestId,
|
|
891
|
+
reasonCode,
|
|
892
|
+
message,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function formatAutomationStateFailure(requestId, reasonCode, message) {
|
|
897
|
+
if (handler && typeof handler.formatAutomationStateSnapshot === "function") {
|
|
898
|
+
return handler.formatAutomationStateSnapshot({
|
|
899
|
+
ok: false,
|
|
900
|
+
requestId,
|
|
901
|
+
reasonCode,
|
|
902
|
+
message,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
return JSON.stringify({
|
|
906
|
+
type: APP_PROTOCOL.automationStateSnapshot,
|
|
907
|
+
ok: false,
|
|
908
|
+
requestId,
|
|
909
|
+
reasonCode,
|
|
910
|
+
message,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function getReadinessSnapshot() {
|
|
915
|
+
const appClients = getConnectedAppEntries();
|
|
916
|
+
const updatedAtMs = appClients.reduce((latest, entry) => {
|
|
917
|
+
const candidate = Number.isFinite(entry.updatedAtMs) ? entry.updatedAtMs : null;
|
|
918
|
+
return candidate === null ? latest : Math.max(latest || 0, candidate);
|
|
919
|
+
}, null);
|
|
920
|
+
return {
|
|
921
|
+
connectedClientCount: appClients.length,
|
|
922
|
+
fanoutRecipientCount: appClients.length,
|
|
923
|
+
updatedAtMs,
|
|
924
|
+
clients: appClients.map((entry) => ({
|
|
925
|
+
clientId: entry.clientId,
|
|
926
|
+
clientKind: entry.clientKind,
|
|
927
|
+
clientName: entry.clientName,
|
|
928
|
+
clientVersion: entry.clientVersion,
|
|
929
|
+
protocolVersion: "v2",
|
|
930
|
+
protocolSessionKey: entry.sessionKey,
|
|
931
|
+
readinessSnapshot: entry.readinessSnapshot,
|
|
932
|
+
connectedAtMs: entry.connectedAtMs,
|
|
933
|
+
})),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
start,
|
|
939
|
+
close,
|
|
940
|
+
broadcast,
|
|
941
|
+
broadcastApp,
|
|
942
|
+
unicast,
|
|
943
|
+
notifyAgentAvatarChanged,
|
|
944
|
+
getClientIds() {
|
|
945
|
+
return Array.from(clients.keys());
|
|
946
|
+
},
|
|
947
|
+
getConnectedAppCount(excludeClientId = null, sessionKey = null) {
|
|
948
|
+
return getConnectedAppEntries(excludeClientId, sessionKey).length;
|
|
949
|
+
},
|
|
950
|
+
getReadinessSnapshot,
|
|
951
|
+
closeConnectedAppClients(opts = {}) {
|
|
952
|
+
const excludeClientId =
|
|
953
|
+
typeof opts.excludeClientId === "string" && opts.excludeClientId.trim()
|
|
954
|
+
? opts.excludeClientId.trim()
|
|
955
|
+
: null;
|
|
956
|
+
const reason =
|
|
957
|
+
typeof opts.reason === "string" && opts.reason.trim()
|
|
958
|
+
? opts.reason.trim()
|
|
959
|
+
: "server_close";
|
|
960
|
+
const closedClientIds = getConnectedAppEntries(excludeClientId).map((entry) => entry.clientId);
|
|
961
|
+
if (closedClientIds.length > 0) {
|
|
962
|
+
postToWorker({
|
|
963
|
+
kind: "worker.closeClients",
|
|
964
|
+
clientIds: closedClientIds,
|
|
965
|
+
reason,
|
|
966
|
+
workerEpoch,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
closedCount: closedClientIds.length,
|
|
971
|
+
closedClientIds,
|
|
972
|
+
reason,
|
|
973
|
+
};
|
|
974
|
+
},
|
|
975
|
+
get readyPromise() {
|
|
976
|
+
return readyPromise;
|
|
977
|
+
},
|
|
978
|
+
get httpServer() {
|
|
979
|
+
return null;
|
|
980
|
+
},
|
|
981
|
+
get wss() {
|
|
982
|
+
return {
|
|
983
|
+
address() {
|
|
984
|
+
return addressValue;
|
|
985
|
+
},
|
|
986
|
+
on(eventName, listener) {
|
|
987
|
+
wssEvents.on(eventName, listener);
|
|
988
|
+
return this;
|
|
989
|
+
},
|
|
990
|
+
once(eventName, listener) {
|
|
991
|
+
wssEvents.once(eventName, listener);
|
|
992
|
+
return this;
|
|
993
|
+
},
|
|
994
|
+
off(eventName, listener) {
|
|
995
|
+
wssEvents.off(eventName, listener);
|
|
996
|
+
return this;
|
|
997
|
+
},
|
|
998
|
+
emit(eventName, ...args) {
|
|
999
|
+
return wssEvents.emit(eventName, ...args);
|
|
1000
|
+
},
|
|
1001
|
+
};
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|