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.
- package/LICENSE +151 -0
- package/README.md +50 -0
- package/dist/common-settings-actions.d.ts +15 -0
- package/dist/common-settings-actions.js +48 -0
- package/dist/common-settings-store.d.ts +1 -0
- package/dist/common-settings-store.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin-hooks.d.ts +51 -0
- package/dist/plugin-hooks.js +288 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +115 -0
- package/dist/settings-store.d.ts +50 -0
- package/dist/settings-store.js +214 -0
- package/dist/store-paths.d.ts +16 -0
- package/dist/store-paths.js +61 -0
- package/dist/ui/wechat-menu.d.ts +26 -0
- package/dist/ui/wechat-menu.js +90 -0
- package/dist/wechat/bind-flow.d.ts +29 -0
- package/dist/wechat/bind-flow.js +207 -0
- package/dist/wechat/bridge.d.ts +136 -0
- package/dist/wechat/bridge.js +1059 -0
- package/dist/wechat/broker-client.d.ts +23 -0
- package/dist/wechat/broker-client.js +274 -0
- package/dist/wechat/broker-endpoint.d.ts +21 -0
- package/dist/wechat/broker-endpoint.js +78 -0
- package/dist/wechat/broker-entry.d.ts +123 -0
- package/dist/wechat/broker-entry.js +1321 -0
- package/dist/wechat/broker-launcher.d.ts +37 -0
- package/dist/wechat/broker-launcher.js +418 -0
- package/dist/wechat/broker-mutation-queue.d.ts +93 -0
- package/dist/wechat/broker-mutation-queue.js +126 -0
- package/dist/wechat/broker-server.d.ts +86 -0
- package/dist/wechat/broker-server.js +1340 -0
- package/dist/wechat/broker-state-store.d.ts +335 -0
- package/dist/wechat/broker-state-store.js +1964 -0
- package/dist/wechat/command-parser.d.ts +18 -0
- package/dist/wechat/command-parser.js +58 -0
- package/dist/wechat/compat/jiti-loader.d.ts +27 -0
- package/dist/wechat/compat/jiti-loader.js +118 -0
- package/dist/wechat/compat/openclaw-account-helpers.d.ts +29 -0
- package/dist/wechat/compat/openclaw-account-helpers.js +60 -0
- package/dist/wechat/compat/openclaw-bind-helpers.d.ts +29 -0
- package/dist/wechat/compat/openclaw-bind-helpers.js +169 -0
- package/dist/wechat/compat/openclaw-guided-smoke.d.ts +180 -0
- package/dist/wechat/compat/openclaw-guided-smoke.js +1134 -0
- package/dist/wechat/compat/openclaw-public-entry.d.ts +33 -0
- package/dist/wechat/compat/openclaw-public-entry.js +62 -0
- package/dist/wechat/compat/openclaw-public-helpers.d.ts +70 -0
- package/dist/wechat/compat/openclaw-public-helpers.js +68 -0
- package/dist/wechat/compat/openclaw-qr-gateway.d.ts +15 -0
- package/dist/wechat/compat/openclaw-qr-gateway.js +39 -0
- package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
- package/dist/wechat/compat/openclaw-smoke.js +100 -0
- package/dist/wechat/compat/openclaw-sync-buf.d.ts +24 -0
- package/dist/wechat/compat/openclaw-sync-buf.js +80 -0
- package/dist/wechat/compat/openclaw-updates-send.d.ts +47 -0
- package/dist/wechat/compat/openclaw-updates-send.js +38 -0
- package/dist/wechat/compat/qrcode-terminal-loader.d.ts +12 -0
- package/dist/wechat/compat/qrcode-terminal-loader.js +16 -0
- package/dist/wechat/compat/slash-guard.d.ts +11 -0
- package/dist/wechat/compat/slash-guard.js +24 -0
- package/dist/wechat/dead-letter-store.d.ts +48 -0
- package/dist/wechat/dead-letter-store.js +224 -0
- package/dist/wechat/debug-bundle-collector.d.ts +49 -0
- package/dist/wechat/debug-bundle-collector.js +580 -0
- package/dist/wechat/debug-bundle-flow.d.ts +37 -0
- package/dist/wechat/debug-bundle-flow.js +180 -0
- package/dist/wechat/debug-bundle-redaction.d.ts +14 -0
- package/dist/wechat/debug-bundle-redaction.js +339 -0
- package/dist/wechat/handle.d.ts +10 -0
- package/dist/wechat/handle.js +57 -0
- package/dist/wechat/ipc-auth.d.ts +6 -0
- package/dist/wechat/ipc-auth.js +39 -0
- package/dist/wechat/latest-account-state-store.d.ts +8 -0
- package/dist/wechat/latest-account-state-store.js +38 -0
- package/dist/wechat/notification-dispatcher.d.ts +34 -0
- package/dist/wechat/notification-dispatcher.js +266 -0
- package/dist/wechat/notification-format.d.ts +15 -0
- package/dist/wechat/notification-format.js +196 -0
- package/dist/wechat/notification-store.d.ts +72 -0
- package/dist/wechat/notification-store.js +807 -0
- package/dist/wechat/notification-types.d.ts +37 -0
- package/dist/wechat/notification-types.js +1 -0
- package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
- package/dist/wechat/openclaw-account-adapter.js +60 -0
- package/dist/wechat/operator-store.d.ts +9 -0
- package/dist/wechat/operator-store.js +69 -0
- package/dist/wechat/protocol.d.ts +150 -0
- package/dist/wechat/protocol.js +197 -0
- package/dist/wechat/question-interaction.d.ts +24 -0
- package/dist/wechat/question-interaction.js +180 -0
- package/dist/wechat/request-store.d.ts +108 -0
- package/dist/wechat/request-store.js +669 -0
- package/dist/wechat/session-digest.d.ts +50 -0
- package/dist/wechat/session-digest.js +167 -0
- package/dist/wechat/state-paths.d.ts +26 -0
- package/dist/wechat/state-paths.js +92 -0
- package/dist/wechat/status-format.d.ts +26 -0
- package/dist/wechat/status-format.js +616 -0
- package/dist/wechat/token-store.d.ts +20 -0
- package/dist/wechat/token-store.js +193 -0
- package/dist/wechat/wechat-status-runtime.d.ts +89 -0
- package/dist/wechat/wechat-status-runtime.js +518 -0
- package/package.json +74 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createDefaultBrokerEndpoint } from "./broker-endpoint.js";
|
|
2
|
+
type BrokerMetadata = {
|
|
3
|
+
pid: number;
|
|
4
|
+
endpoint: string;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
type LaunchLockContent = {
|
|
9
|
+
pid: number;
|
|
10
|
+
acquiredAt: number;
|
|
11
|
+
lockId: string;
|
|
12
|
+
};
|
|
13
|
+
type LaunchOptions = {
|
|
14
|
+
stateRoot?: string;
|
|
15
|
+
brokerJsonPath?: string;
|
|
16
|
+
launchLockPath?: string;
|
|
17
|
+
backoffMs?: number;
|
|
18
|
+
maxAttempts?: number;
|
|
19
|
+
expectedVersion?: string;
|
|
20
|
+
endpointFactory?: () => string;
|
|
21
|
+
spawnImpl?: (endpoint: string, stateRoot: string) => {
|
|
22
|
+
pid?: number | undefined;
|
|
23
|
+
unref?: (() => void) | undefined;
|
|
24
|
+
};
|
|
25
|
+
retireBrokerImpl?: (metadata: BrokerMetadata) => Promise<void> | void;
|
|
26
|
+
pingImpl?: (endpoint: string) => Promise<boolean>;
|
|
27
|
+
isProcessAliveImpl?: (pid: number) => boolean;
|
|
28
|
+
onLockAcquired?: (lock: LaunchLockContent) => void;
|
|
29
|
+
};
|
|
30
|
+
type ResolveBrokerSpawnCommandOptions = {
|
|
31
|
+
execPath?: string;
|
|
32
|
+
};
|
|
33
|
+
type ResolveBrokerSpawnEnv = NodeJS.ProcessEnv;
|
|
34
|
+
export declare function resolveBrokerSpawnEnv(env?: ResolveBrokerSpawnEnv): NodeJS.ProcessEnv;
|
|
35
|
+
export { createDefaultBrokerEndpoint };
|
|
36
|
+
export declare function resolveBrokerSpawnCommand(options?: ResolveBrokerSpawnCommandOptions): string;
|
|
37
|
+
export declare function connectOrSpawnBroker(options?: LaunchOptions): Promise<BrokerMetadata>;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFile, mkdir, open, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createBrokerSocket, createDefaultBrokerEndpoint } from "./broker-endpoint.js";
|
|
7
|
+
import { WECHAT_FILE_MODE, wechatBrokerDiagnosticsPath, wechatStateRoot } from "./state-paths.js";
|
|
8
|
+
import { parseEnvelopeLine, serializeEnvelope } from "./protocol.js";
|
|
9
|
+
const DEFAULT_BACKOFF_MS = 250;
|
|
10
|
+
const DEFAULT_MAX_ATTEMPTS = 20;
|
|
11
|
+
const DEFAULT_BOOTING_BROKER_WAIT_STEP_MS = 100;
|
|
12
|
+
const DEFAULT_BOOTING_BROKER_WAIT_STEPS = 20;
|
|
13
|
+
async function appendBrokerLauncherDiagnostic(stateRoot, event) {
|
|
14
|
+
try {
|
|
15
|
+
await mkdir(stateRoot, { recursive: true, mode: 0o700 });
|
|
16
|
+
await appendFile(wechatBrokerDiagnosticsPath(stateRoot), `${JSON.stringify({ at: Date.now(), ...event })}\n`, { encoding: "utf8", mode: WECHAT_FILE_MODE });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function resolveBrokerSpawnEnv(env = process.env) {
|
|
22
|
+
return {
|
|
23
|
+
...env,
|
|
24
|
+
BUN_BE_BUN: "1",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isNonEmptyString(value) {
|
|
28
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
29
|
+
}
|
|
30
|
+
function isFiniteNumber(value) {
|
|
31
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
32
|
+
}
|
|
33
|
+
function parseBrokerVersion(version) {
|
|
34
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
|
|
35
|
+
if (!match) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
major: Number(match[1]),
|
|
40
|
+
minor: Number(match[2]),
|
|
41
|
+
patch: Number(match[3]),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isBrokerVersionCompatible(candidateVersion, expectedVersion) {
|
|
45
|
+
const candidate = parseBrokerVersion(candidateVersion);
|
|
46
|
+
const expected = parseBrokerVersion(expectedVersion);
|
|
47
|
+
if (!candidate || !expected) {
|
|
48
|
+
return candidateVersion === expectedVersion;
|
|
49
|
+
}
|
|
50
|
+
if (candidate.major !== expected.major || candidate.minor !== expected.minor) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return candidate.patch >= expected.patch;
|
|
54
|
+
}
|
|
55
|
+
function shouldRetireBrokerForVersion(candidateVersion, expectedVersion) {
|
|
56
|
+
const candidate = parseBrokerVersion(candidateVersion);
|
|
57
|
+
const expected = parseBrokerVersion(expectedVersion);
|
|
58
|
+
if (!candidate || !expected) {
|
|
59
|
+
return candidateVersion !== expectedVersion;
|
|
60
|
+
}
|
|
61
|
+
if (candidate.major !== expected.major || candidate.minor !== expected.minor) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return candidate.patch < expected.patch;
|
|
65
|
+
}
|
|
66
|
+
function delay(ms) {
|
|
67
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
}
|
|
69
|
+
export { createDefaultBrokerEndpoint };
|
|
70
|
+
export function resolveBrokerSpawnCommand(options = {}) {
|
|
71
|
+
const execPath = options.execPath ?? process.execPath;
|
|
72
|
+
return execPath;
|
|
73
|
+
}
|
|
74
|
+
async function readCurrentPackageVersion() {
|
|
75
|
+
try {
|
|
76
|
+
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
77
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
return isNonEmptyString(parsed.version) ? parsed.version : "unknown";
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isProcessAlive(pid) {
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function readBrokerMetadata(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
const raw = await readFile(filePath, "utf8");
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
if (!isFiniteNumber(parsed.pid) || !isNonEmptyString(parsed.endpoint) || !isFiniteNumber(parsed.startedAt)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
pid: parsed.pid,
|
|
103
|
+
endpoint: parsed.endpoint,
|
|
104
|
+
startedAt: parsed.startedAt,
|
|
105
|
+
version: isNonEmptyString(parsed.version) ? parsed.version : "unknown",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function defaultPingImpl(endpoint) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const socket = createBrokerSocket(endpoint);
|
|
115
|
+
let buffer = "";
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
socket.destroy();
|
|
118
|
+
resolve(false);
|
|
119
|
+
}, 500);
|
|
120
|
+
socket.once("error", () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve(false);
|
|
123
|
+
});
|
|
124
|
+
socket.once("connect", () => {
|
|
125
|
+
socket.write(serializeEnvelope({ id: `launcher-ping-${Date.now()}`, type: "ping", payload: {} }));
|
|
126
|
+
});
|
|
127
|
+
socket.on("data", (chunk) => {
|
|
128
|
+
buffer += chunk.toString("utf8");
|
|
129
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
130
|
+
if (newlineIndex === -1) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
socket.end();
|
|
135
|
+
try {
|
|
136
|
+
const response = parseEnvelopeLine(buffer.slice(0, newlineIndex + 1));
|
|
137
|
+
resolve(response.type === "pong");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
resolve(false);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async function readLaunchLock(filePath) {
|
|
146
|
+
try {
|
|
147
|
+
const raw = await readFile(filePath, "utf8");
|
|
148
|
+
const parsed = JSON.parse(raw);
|
|
149
|
+
if (!isFiniteNumber(parsed.pid) || !isFiniteNumber(parsed.acquiredAt) || !isNonEmptyString(parsed.lockId)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
pid: parsed.pid,
|
|
154
|
+
acquiredAt: parsed.acquiredAt,
|
|
155
|
+
lockId: parsed.lockId,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function acquireLaunchLock(filePath, isProcessAliveImpl) {
|
|
163
|
+
const lock = {
|
|
164
|
+
pid: process.pid,
|
|
165
|
+
acquiredAt: Date.now(),
|
|
166
|
+
lockId: randomUUID(),
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
const handle = await open(filePath, "wx", 0o600);
|
|
170
|
+
await handle.writeFile(JSON.stringify(lock, null, 2), "utf8");
|
|
171
|
+
await handle.close();
|
|
172
|
+
return { lock };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error.code !== "EEXIST") {
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
const existing = await readLaunchLock(filePath);
|
|
179
|
+
if (existing && isProcessAliveImpl(existing.pid)) {
|
|
180
|
+
return { lock: null };
|
|
181
|
+
}
|
|
182
|
+
await rm(filePath, { force: true });
|
|
183
|
+
return {
|
|
184
|
+
lock: null,
|
|
185
|
+
...(existing ? { recoveredStaleLock: { pid: existing.pid } } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function getBrokerIdentity(metadata) {
|
|
190
|
+
return `${metadata.pid}:${metadata.startedAt}:${metadata.version}:${metadata.endpoint}`;
|
|
191
|
+
}
|
|
192
|
+
async function readCompatibleBrokerState(brokerFilePath, pingImpl, isProcessAliveImpl, expectedVersion) {
|
|
193
|
+
const metadata = await readBrokerMetadata(brokerFilePath);
|
|
194
|
+
if (!metadata) {
|
|
195
|
+
return { status: "unavailable" };
|
|
196
|
+
}
|
|
197
|
+
if (isNonEmptyString(expectedVersion) && !isBrokerVersionCompatible(metadata.version, expectedVersion)) {
|
|
198
|
+
return { status: "unavailable" };
|
|
199
|
+
}
|
|
200
|
+
if (!isProcessAliveImpl(metadata.pid)) {
|
|
201
|
+
return { status: "unavailable" };
|
|
202
|
+
}
|
|
203
|
+
const ok = await pingImpl(metadata.endpoint);
|
|
204
|
+
if (ok) {
|
|
205
|
+
return {
|
|
206
|
+
status: "ready",
|
|
207
|
+
metadata,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
status: "booting",
|
|
212
|
+
metadata,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function isBrokerAlive(brokerFilePath, pingImpl, isProcessAliveImpl, expectedVersion) {
|
|
216
|
+
const state = await readCompatibleBrokerState(brokerFilePath, pingImpl, isProcessAliveImpl, expectedVersion);
|
|
217
|
+
if (state.status !== "ready") {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return state.metadata;
|
|
221
|
+
}
|
|
222
|
+
async function readBootingCompatibleBroker(brokerFilePath, pingImpl, isProcessAliveImpl, expectedVersion) {
|
|
223
|
+
const state = await readCompatibleBrokerState(brokerFilePath, pingImpl, isProcessAliveImpl, expectedVersion);
|
|
224
|
+
if (state.status !== "booting") {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return state.metadata;
|
|
228
|
+
}
|
|
229
|
+
async function readVersionMismatchedBroker(brokerFilePath, expectedVersion) {
|
|
230
|
+
const metadata = await readBrokerMetadata(brokerFilePath);
|
|
231
|
+
if (!metadata) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (!isNonEmptyString(expectedVersion) || !shouldRetireBrokerForVersion(metadata.version, expectedVersion)) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return metadata;
|
|
238
|
+
}
|
|
239
|
+
async function defaultRetireBrokerImpl(metadata, pingImpl) {
|
|
240
|
+
if (metadata.pid === process.pid) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const reachable = await pingImpl(metadata.endpoint);
|
|
244
|
+
if (!reachable) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!isProcessAlive(metadata.pid)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
process.kill(metadata.pid, "SIGTERM");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const startedAt = Date.now();
|
|
257
|
+
while (Date.now() - startedAt < 5000) {
|
|
258
|
+
if (!isProcessAlive(metadata.pid)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await delay(50);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
process.kill(metadata.pid, "SIGKILL");
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// process already exited
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function defaultSpawnImpl(endpoint, stateRoot) {
|
|
271
|
+
const entry = fileURLToPath(new URL("./broker-entry.js", import.meta.url));
|
|
272
|
+
const child = spawn(resolveBrokerSpawnCommand(), [entry, `--endpoint=${endpoint}`, `--state-root=${stateRoot}`], {
|
|
273
|
+
cwd: path.resolve(fileURLToPath(new URL("../..", import.meta.url))),
|
|
274
|
+
detached: true,
|
|
275
|
+
env: resolveBrokerSpawnEnv(process.env),
|
|
276
|
+
stdio: "ignore",
|
|
277
|
+
});
|
|
278
|
+
child.unref();
|
|
279
|
+
return child;
|
|
280
|
+
}
|
|
281
|
+
async function waitForBootingBrokerToBecomeReady(input) {
|
|
282
|
+
let lastBootingBroker = null;
|
|
283
|
+
for (let step = 0; step < input.maxWaitSteps; step += 1) {
|
|
284
|
+
const state = await readCompatibleBrokerState(input.brokerFilePath, input.pingImpl, input.isProcessAliveImpl, input.expectedVersion);
|
|
285
|
+
if (state.status === "ready") {
|
|
286
|
+
return {
|
|
287
|
+
status: "ready",
|
|
288
|
+
metadata: state.metadata,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (state.status !== "booting") {
|
|
292
|
+
return lastBootingBroker
|
|
293
|
+
? {
|
|
294
|
+
status: "failed",
|
|
295
|
+
metadata: lastBootingBroker,
|
|
296
|
+
}
|
|
297
|
+
: null;
|
|
298
|
+
}
|
|
299
|
+
if (getBrokerIdentity(state.metadata) !== input.bootingBrokerIdentity) {
|
|
300
|
+
return {
|
|
301
|
+
status: "replaced",
|
|
302
|
+
metadata: state.metadata,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
lastBootingBroker = state.metadata;
|
|
306
|
+
if (step < input.maxWaitSteps - 1) {
|
|
307
|
+
await delay(input.waitStepMs);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return lastBootingBroker
|
|
311
|
+
? {
|
|
312
|
+
status: "failed",
|
|
313
|
+
metadata: lastBootingBroker,
|
|
314
|
+
}
|
|
315
|
+
: null;
|
|
316
|
+
}
|
|
317
|
+
export async function connectOrSpawnBroker(options = {}) {
|
|
318
|
+
const stateRoot = options.stateRoot ?? wechatStateRoot();
|
|
319
|
+
const brokerJsonFile = options.brokerJsonPath ?? path.join(stateRoot, "broker.json");
|
|
320
|
+
const launchLockFile = options.launchLockPath ?? path.join(stateRoot, "launch.lock");
|
|
321
|
+
const backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
322
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
323
|
+
const expectedVersion = options.expectedVersion ?? await readCurrentPackageVersion();
|
|
324
|
+
const pingImpl = options.pingImpl ?? defaultPingImpl;
|
|
325
|
+
const isProcessAliveImpl = options.isProcessAliveImpl ?? isProcessAlive;
|
|
326
|
+
const spawnImpl = options.spawnImpl ?? defaultSpawnImpl;
|
|
327
|
+
const retireBrokerImpl = options.retireBrokerImpl ?? ((metadata) => defaultRetireBrokerImpl(metadata, pingImpl));
|
|
328
|
+
const endpointFactory = options.endpointFactory ?? (() => createDefaultBrokerEndpoint({ stateRoot }));
|
|
329
|
+
const bootingBrokerWaitStepMs = Math.min(Math.max(backoffMs, 10), DEFAULT_BOOTING_BROKER_WAIT_STEP_MS);
|
|
330
|
+
const bootingBrokerWaitSteps = DEFAULT_BOOTING_BROKER_WAIT_STEPS;
|
|
331
|
+
await mkdir(stateRoot, { recursive: true, mode: 0o700 });
|
|
332
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
333
|
+
const running = await isBrokerAlive(brokerJsonFile, pingImpl, isProcessAliveImpl, expectedVersion);
|
|
334
|
+
if (running) {
|
|
335
|
+
return running;
|
|
336
|
+
}
|
|
337
|
+
let failedBootingBrokerIdentity = null;
|
|
338
|
+
const bootingCompatibleBroker = await readBootingCompatibleBroker(brokerJsonFile, pingImpl, isProcessAliveImpl, expectedVersion);
|
|
339
|
+
if (bootingCompatibleBroker) {
|
|
340
|
+
let bootingBrokerToWaitFor = bootingCompatibleBroker;
|
|
341
|
+
for (let transition = 0; transition < maxAttempts && bootingBrokerToWaitFor; transition += 1) {
|
|
342
|
+
const waitResult = await waitForBootingBrokerToBecomeReady({
|
|
343
|
+
brokerFilePath: brokerJsonFile,
|
|
344
|
+
bootingBrokerIdentity: getBrokerIdentity(bootingBrokerToWaitFor),
|
|
345
|
+
pingImpl,
|
|
346
|
+
isProcessAliveImpl,
|
|
347
|
+
expectedVersion,
|
|
348
|
+
waitStepMs: bootingBrokerWaitStepMs,
|
|
349
|
+
maxWaitSteps: bootingBrokerWaitSteps,
|
|
350
|
+
});
|
|
351
|
+
if (waitResult?.status === "ready") {
|
|
352
|
+
return waitResult.metadata;
|
|
353
|
+
}
|
|
354
|
+
if (waitResult?.status === "replaced") {
|
|
355
|
+
bootingBrokerToWaitFor = waitResult.metadata;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
failedBootingBrokerIdentity = waitResult ? getBrokerIdentity(waitResult.metadata) : null;
|
|
359
|
+
bootingBrokerToWaitFor = null;
|
|
360
|
+
}
|
|
361
|
+
if (bootingBrokerToWaitFor) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const lockAttempt = await acquireLaunchLock(launchLockFile, isProcessAliveImpl);
|
|
366
|
+
if (lockAttempt.recoveredStaleLock) {
|
|
367
|
+
await appendBrokerLauncherDiagnostic(stateRoot, {
|
|
368
|
+
type: "brokerTakeover",
|
|
369
|
+
code: "brokerTakeover",
|
|
370
|
+
reason: "staleLock",
|
|
371
|
+
previousPid: lockAttempt.recoveredStaleLock.pid,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const lock = lockAttempt.lock;
|
|
375
|
+
if (!lock) {
|
|
376
|
+
await delay(backoffMs);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
options.onLockAcquired?.(lock);
|
|
380
|
+
try {
|
|
381
|
+
const lockWindowBrokerState = await readCompatibleBrokerState(brokerJsonFile, pingImpl, isProcessAliveImpl, expectedVersion);
|
|
382
|
+
if (lockWindowBrokerState.status === "ready") {
|
|
383
|
+
return lockWindowBrokerState.metadata;
|
|
384
|
+
}
|
|
385
|
+
if (lockWindowBrokerState.status === "booting" &&
|
|
386
|
+
getBrokerIdentity(lockWindowBrokerState.metadata) !== failedBootingBrokerIdentity) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const versionMismatchedBroker = await readVersionMismatchedBroker(brokerJsonFile, expectedVersion);
|
|
390
|
+
if (versionMismatchedBroker) {
|
|
391
|
+
await appendBrokerLauncherDiagnostic(stateRoot, {
|
|
392
|
+
type: "brokerTakeover",
|
|
393
|
+
code: "brokerTakeover",
|
|
394
|
+
reason: "versionMismatch",
|
|
395
|
+
previousVersion: versionMismatchedBroker.version,
|
|
396
|
+
nextVersion: expectedVersion,
|
|
397
|
+
previousPid: versionMismatchedBroker.pid,
|
|
398
|
+
});
|
|
399
|
+
await retireBrokerImpl(versionMismatchedBroker);
|
|
400
|
+
}
|
|
401
|
+
const endpoint = endpointFactory();
|
|
402
|
+
const child = spawnImpl(endpoint, stateRoot);
|
|
403
|
+
void child?.unref?.();
|
|
404
|
+
for (let n = 0; n < 20; n += 1) {
|
|
405
|
+
await delay(100);
|
|
406
|
+
const spawned = await isBrokerAlive(brokerJsonFile, pingImpl, isProcessAliveImpl, expectedVersion);
|
|
407
|
+
if (spawned) {
|
|
408
|
+
return spawned;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
throw new Error("spawned broker did not become available");
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
await rm(launchLockFile, { force: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
throw new Error("broker unavailable");
|
|
418
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { SHOW_FALLBACK_TOAST_DELIVERY_FAILED_REASON, type ShowFallbackToastPayload } from "./protocol.js";
|
|
2
|
+
import type { WechatDeadLetterRecord } from "./dead-letter-store.js";
|
|
3
|
+
import type { NotificationRecord } from "./notification-types.js";
|
|
4
|
+
import type { PreparedRecoveryRequestReopen, RequestRecord } from "./request-store.js";
|
|
5
|
+
type DestroyableSocket = {
|
|
6
|
+
destroyed: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type LiveRegistration<TSocket extends DestroyableSocket = DestroyableSocket> = {
|
|
9
|
+
socket: TSocket;
|
|
10
|
+
sessionToken: string;
|
|
11
|
+
registrationEpoch: string;
|
|
12
|
+
};
|
|
13
|
+
export type FallbackToastMutation = {
|
|
14
|
+
type: "fallbackToastMutation";
|
|
15
|
+
instanceID: string;
|
|
16
|
+
wechatAccountId: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
message: string;
|
|
19
|
+
reason: typeof SHOW_FALLBACK_TOAST_DELIVERY_FAILED_REASON;
|
|
20
|
+
registrationEpoch?: string;
|
|
21
|
+
};
|
|
22
|
+
export type BrokerMutationDiagnosticEvent = {
|
|
23
|
+
type: "showFallbackToast" | "fallbackToastDropped";
|
|
24
|
+
code: "showFallbackToast" | "fallbackToastDropped";
|
|
25
|
+
instanceID: string;
|
|
26
|
+
reason?: typeof SHOW_FALLBACK_TOAST_DELIVERY_FAILED_REASON;
|
|
27
|
+
registrationEpoch?: string;
|
|
28
|
+
liveRegistrationEpoch?: string;
|
|
29
|
+
};
|
|
30
|
+
export type RecoveryMutation = {
|
|
31
|
+
type: "recoveryMutation";
|
|
32
|
+
requestedHandle: string;
|
|
33
|
+
deadLetter: WechatDeadLetterRecord;
|
|
34
|
+
originalRequest: RequestRecord;
|
|
35
|
+
pendingNotifications: NotificationRecord[];
|
|
36
|
+
recoveryChainHandles: string[];
|
|
37
|
+
};
|
|
38
|
+
export type RecoveryMutationFailure = {
|
|
39
|
+
recoveryErrorCode: string;
|
|
40
|
+
recoveryErrorMessage: string;
|
|
41
|
+
};
|
|
42
|
+
export type RecoveryMutationResult = {
|
|
43
|
+
ok: true;
|
|
44
|
+
recovered: RequestRecord;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
type ExecuteRecoveryMutationDeps = {
|
|
50
|
+
revalidate: (mutation: RecoveryMutation) => Promise<RecoveryMutationResult | undefined>;
|
|
51
|
+
prepareFreshRecovery: (mutation: RecoveryMutation, recoveredAt: number) => Promise<PreparedRecoveryRequestReopen>;
|
|
52
|
+
suppressPendingNotifications: (mutation: RecoveryMutation) => Promise<void>;
|
|
53
|
+
commitPreparedRecovery: (prepared: PreparedRecoveryRequestReopen, mutation: RecoveryMutation) => Promise<RequestRecord>;
|
|
54
|
+
rollbackPreparedRecovery: (prepared: PreparedRecoveryRequestReopen, mutation: RecoveryMutation) => Promise<void>;
|
|
55
|
+
markRecovered: (input: {
|
|
56
|
+
kind: WechatDeadLetterRecord["kind"];
|
|
57
|
+
routeKey: string;
|
|
58
|
+
recoveredAt: number;
|
|
59
|
+
}) => Promise<void>;
|
|
60
|
+
markFailed: (input: {
|
|
61
|
+
kind: WechatDeadLetterRecord["kind"];
|
|
62
|
+
routeKey: string;
|
|
63
|
+
failure: RecoveryMutationFailure;
|
|
64
|
+
}) => Promise<void>;
|
|
65
|
+
mapFailure: (error: unknown) => RecoveryMutationFailure;
|
|
66
|
+
appendDiagnostic?: (event: BrokerMutationDiagnosticEvent) => Promise<void>;
|
|
67
|
+
now?: () => number;
|
|
68
|
+
testHooks?: {
|
|
69
|
+
afterReopenRequest?: (mutation: RecoveryMutation) => Promise<void> | void;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
type ExecuteFallbackToastMutationDeps<TSocket extends DestroyableSocket = DestroyableSocket> = {
|
|
73
|
+
markTokenStale: (input: {
|
|
74
|
+
wechatAccountId: string;
|
|
75
|
+
userId: string;
|
|
76
|
+
staleReason: string;
|
|
77
|
+
}) => Promise<unknown>;
|
|
78
|
+
appendDiagnostic: (event: BrokerMutationDiagnosticEvent) => Promise<void>;
|
|
79
|
+
getLiveRegistration: (instanceID: string) => LiveRegistration<TSocket> | undefined;
|
|
80
|
+
deliverFallbackToast: (input: {
|
|
81
|
+
instanceID: string;
|
|
82
|
+
registration: LiveRegistration<TSocket>;
|
|
83
|
+
payload: ShowFallbackToastPayload;
|
|
84
|
+
}) => Promise<void> | void;
|
|
85
|
+
};
|
|
86
|
+
export type BrokerMutationQueue = {
|
|
87
|
+
enqueue: <T>(mutationType: string, task: () => Promise<T>) => Promise<T>;
|
|
88
|
+
drain: () => Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
export declare function createBrokerMutationQueue(): BrokerMutationQueue;
|
|
91
|
+
export declare function executeFallbackToastMutation<TSocket extends DestroyableSocket = DestroyableSocket>(mutation: FallbackToastMutation, deps: ExecuteFallbackToastMutationDeps<TSocket>): Promise<void>;
|
|
92
|
+
export declare function executeRecoveryMutation(mutation: RecoveryMutation, deps: ExecuteRecoveryMutationDeps): Promise<RecoveryMutationResult>;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { NOTIFICATION_DELIVERY_FAILED_STALE_REASON } from "./token-store.js";
|
|
2
|
+
function toError(value) {
|
|
3
|
+
if (value instanceof Error) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return new Error(String(value));
|
|
7
|
+
}
|
|
8
|
+
function isNonEmptyString(value) {
|
|
9
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
+
}
|
|
11
|
+
export function createBrokerMutationQueue() {
|
|
12
|
+
let chain = Promise.resolve();
|
|
13
|
+
return {
|
|
14
|
+
enqueue(_mutationType, task) {
|
|
15
|
+
const next = chain.then(task);
|
|
16
|
+
chain = next.then(() => undefined, () => undefined);
|
|
17
|
+
return next;
|
|
18
|
+
},
|
|
19
|
+
drain() {
|
|
20
|
+
return chain;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function executeFallbackToastMutation(mutation, deps) {
|
|
25
|
+
await Promise.resolve(deps.markTokenStale({
|
|
26
|
+
wechatAccountId: mutation.wechatAccountId,
|
|
27
|
+
userId: mutation.userId,
|
|
28
|
+
staleReason: NOTIFICATION_DELIVERY_FAILED_STALE_REASON,
|
|
29
|
+
})).catch(() => { });
|
|
30
|
+
const liveRegistration = deps.getLiveRegistration(mutation.instanceID);
|
|
31
|
+
const registrationEpoch = mutation.registrationEpoch;
|
|
32
|
+
const canDeliver = liveRegistration
|
|
33
|
+
&& liveRegistration.socket.destroyed !== true
|
|
34
|
+
&& isNonEmptyString(registrationEpoch)
|
|
35
|
+
&& liveRegistration.registrationEpoch === registrationEpoch;
|
|
36
|
+
if (!canDeliver) {
|
|
37
|
+
await deps.appendDiagnostic({
|
|
38
|
+
type: "fallbackToastDropped",
|
|
39
|
+
code: "fallbackToastDropped",
|
|
40
|
+
instanceID: mutation.instanceID,
|
|
41
|
+
reason: mutation.reason,
|
|
42
|
+
...(isNonEmptyString(registrationEpoch) ? { registrationEpoch } : {}),
|
|
43
|
+
...(liveRegistration ? { liveRegistrationEpoch: liveRegistration.registrationEpoch } : {}),
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const payload = {
|
|
48
|
+
wechatAccountId: mutation.wechatAccountId,
|
|
49
|
+
userId: mutation.userId,
|
|
50
|
+
message: mutation.message,
|
|
51
|
+
reason: mutation.reason,
|
|
52
|
+
registrationEpoch,
|
|
53
|
+
};
|
|
54
|
+
await deps.appendDiagnostic({
|
|
55
|
+
type: "showFallbackToast",
|
|
56
|
+
code: "showFallbackToast",
|
|
57
|
+
instanceID: mutation.instanceID,
|
|
58
|
+
reason: mutation.reason,
|
|
59
|
+
registrationEpoch,
|
|
60
|
+
liveRegistrationEpoch: liveRegistration.registrationEpoch,
|
|
61
|
+
});
|
|
62
|
+
await Promise.resolve(deps.deliverFallbackToast({
|
|
63
|
+
instanceID: mutation.instanceID,
|
|
64
|
+
registration: liveRegistration,
|
|
65
|
+
payload,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
export async function executeRecoveryMutation(mutation, deps) {
|
|
69
|
+
const revalidated = await deps.revalidate(mutation);
|
|
70
|
+
if (revalidated) {
|
|
71
|
+
return revalidated;
|
|
72
|
+
}
|
|
73
|
+
const recoveredAt = deps.now?.() ?? Date.now();
|
|
74
|
+
let preparedRecovery;
|
|
75
|
+
try {
|
|
76
|
+
preparedRecovery = await deps.prepareFreshRecovery(mutation, recoveredAt);
|
|
77
|
+
await deps.suppressPendingNotifications(mutation);
|
|
78
|
+
const recovered = await deps.commitPreparedRecovery(preparedRecovery, mutation);
|
|
79
|
+
await deps.testHooks?.afterReopenRequest?.(mutation);
|
|
80
|
+
await deps.markRecovered({
|
|
81
|
+
kind: mutation.deadLetter.kind,
|
|
82
|
+
routeKey: mutation.deadLetter.routeKey,
|
|
83
|
+
recoveredAt,
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
recovered,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const failure = deps.mapFailure(error);
|
|
92
|
+
let rollbackError;
|
|
93
|
+
let markFailedError;
|
|
94
|
+
if (preparedRecovery) {
|
|
95
|
+
try {
|
|
96
|
+
await deps.rollbackPreparedRecovery(preparedRecovery, mutation);
|
|
97
|
+
}
|
|
98
|
+
catch (rollbackFailure) {
|
|
99
|
+
rollbackError = toError(rollbackFailure);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await deps.markFailed({
|
|
104
|
+
kind: mutation.deadLetter.kind,
|
|
105
|
+
routeKey: mutation.deadLetter.routeKey,
|
|
106
|
+
failure,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (persistFailure) {
|
|
110
|
+
markFailedError = toError(persistFailure);
|
|
111
|
+
}
|
|
112
|
+
if (rollbackError && markFailedError) {
|
|
113
|
+
throw new Error(`recovery rollback failed: ${rollbackError.message}; recovery failed metadata persistence failed: ${markFailedError.message}`);
|
|
114
|
+
}
|
|
115
|
+
if (rollbackError) {
|
|
116
|
+
throw rollbackError;
|
|
117
|
+
}
|
|
118
|
+
if (markFailedError) {
|
|
119
|
+
throw markFailedError;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
message: failure.recoveryErrorMessage,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|