forked from openclaw/openclaw
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnode-sqlite.mjs
More file actions
180 lines (171 loc) 路 6.67 KB
/
Copy pathnode-sqlite.mjs
File metadata and controls
180 lines (171 loc) 路 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { isSupportedOpenClawNodeVersion, parseNodeReleaseVersion } from "./node-version.mjs";
// Self-contained so a different runtime can execute exactly the same probe with -e.
function probeSqlite(DatabaseSync) {
const result = { available: false, version: null, text: false, blob: false, json: false };
let database;
try {
database = new DatabaseSync(":memory:");
result.available = true;
const version = database.prepare("SELECT sqlite_version() AS version").get()?.version;
result.version = typeof version === "string" ? version : null;
const text = "a\u0000b\u0000";
const bytes = Buffer.from(text, "utf8");
const json = JSON.stringify({ value: text });
database.exec("CREATE TABLE probe (text_value TEXT, blob_value BLOB, json_value TEXT)");
database.prepare("INSERT INTO probe VALUES (?, ?, ?)").run(text, bytes, json);
const row = database.prepare("SELECT text_value, blob_value, json_value FROM probe").get();
result.text =
typeof row?.text_value === "string" &&
row.text_value.length === text.length &&
Buffer.from(row.text_value, "utf8").equals(bytes);
result.blob =
row?.blob_value instanceof Uint8Array && Buffer.from(row.blob_value).equals(bytes);
result.json = row?.json_value === json && JSON.parse(row.json_value).value === text;
} catch (error) {
result.error = error instanceof Error ? error.message : String(error);
} finally {
database?.close();
}
return result;
}
// The launcher and bundled runtime chunks must share one process-local result.
const capabilityCacheKey = Symbol.for("openclaw.sqliteCapabilities");
function unavailableSqliteCapabilities(error) {
return {
available: false,
version: null,
text: false,
blob: false,
json: false,
error: error instanceof Error ? error.message : String(error),
};
}
function isSqliteCapabilities(value) {
return (
value !== null &&
typeof value === "object" &&
typeof value.available === "boolean" &&
(value.version === null || typeof value.version === "string") &&
typeof value.text === "boolean" &&
typeof value.blob === "boolean" &&
typeof value.json === "boolean" &&
(value.error === undefined || typeof value.error === "string")
);
}
async function probeCurrentSqliteInWorker() {
let worker;
try {
if (typeof process.getBuiltinModule?.("node:sqlite")?.DatabaseSync !== "function") {
return unavailableSqliteCapabilities(new Error("node:sqlite is unavailable"));
}
const env = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !/^(NODE_OPTIONS|BUN_OPTIONS)$/i.test(name)),
);
env.NODE_NO_WARNINGS = "1";
// Older diagnostic runtimes need SQLite enabled, but must not replay entry preloads.
const execArgv =
!process.versions.bun && process.allowedNodeEnvironmentFlags?.has("--experimental-sqlite")
? ["--experimental-sqlite"]
: [];
const { Worker } = await import("node:worker_threads");
worker = new Worker(
`const { parentPort } = require("node:worker_threads");
parentPort.postMessage(${SQLITE_CAPABILITY_PROBE});
parentPort.close();`,
{ eval: true, env, execArgv },
);
} catch (error) {
return unavailableSqliteCapabilities(error);
}
return new Promise((resolve) => {
let result;
let failure;
let retirement;
const fail = (error) => {
failure ??= { error };
retirement ??= Promise.resolve()
.then(() => worker.terminate())
.catch((error) => {
failure ??= { error };
});
};
const onMessage = (value) => {
if (result !== undefined || !isSqliteCapabilities(value)) {
fail(new Error("SQLite capability worker returned an invalid result"));
return;
}
result = value;
};
const onExit = (code) => {
worker.off("message", onMessage);
worker.off("error", fail);
worker.off("messageerror", fail);
if (failure) {
resolve(unavailableSqliteCapabilities(failure.error));
} else if (code !== 0 || result === undefined) {
resolve(
unavailableSqliteCapabilities(
new Error(`SQLite capability worker exited without a complete result (code ${code})`),
),
);
} else {
// The scratch database and worker must both close before startup is admitted.
resolve(result);
}
};
worker.on("message", onMessage);
worker.on("error", fail);
worker.on("messageerror", fail);
worker.once("exit", onExit);
});
}
export function detectCurrentSqliteCapabilities() {
// Publish the Promise before Worker construction can notify another startup caller.
globalThis[capabilityCacheKey] ??= Promise.resolve().then(probeCurrentSqliteInWorker);
return Promise.resolve(globalThis[capabilityCacheKey]);
}
export function isSqliteWalResetSafeVersion(value) {
const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(value.trim());
if (!match) {
return false;
}
const [major, minor, patch] = match.slice(1).map(Number);
if (![major, minor, patch].every(Number.isSafeInteger)) {
return false;
}
return (
major > 3 ||
(major === 3 &&
(minor > 51 ||
(minor === 51 && patch >= 3) ||
(minor === 50 && patch >= 7) ||
(minor === 44 && patch >= 6)))
);
}
export function nodeRuntimeFailure(version, probe) {
const label = `Node ${version ?? "unknown"}`;
if (!probe.available) {
return `${label}: node:sqlite is unavailable; use 24.16+/26.1+ or a build with the fix.`;
}
if (!probe.text && !probe.error) {
return `${label}: node:sqlite truncates TEXT at embedded NUL (nodejs/node#61954); use 24.16+/26.1+ or a build with the fix`;
}
if (probe.error || !probe.text || !probe.blob || !probe.json) {
return `${label}: node:sqlite NUL round-trip capability probe failed; use 24.16+/26.1+ or a build with the fix.`;
}
if (!probe.version || !isSqliteWalResetSafeVersion(probe.version)) {
return `${label}: SQLite ${probe.version ?? "unknown"} is not WAL-reset-safe; use SQLite 3.51.3+, 3.50.7+, or 3.44.6+ on its patched release line.`;
}
// Node 22/23 remain excluded until their separate compatibility work is complete.
const release = parseNodeReleaseVersion(version);
if (!release || release.major < 24) {
return `${label}: openclaw requires Node >=24.16.0 <25, or >=26.1.0.`;
}
return null;
}
export function nodeRuntimeNote(version, probe) {
return !nodeRuntimeFailure(version, probe) && !isSupportedOpenClawNodeVersion(version)
? `Node ${version}: unsupported version, capability probe passed. Supported releases: 24.16+/26.1+.`
: null;
}
export const SQLITE_CAPABILITY_PROBE = `(${probeSqlite.toString()})(require("node:sqlite").DatabaseSync)`;