@rezi-ui/node 0.1.0-alpha.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 +17 -0
- package/dist/__e2e__/fixtures/terminal-app.d.ts +2 -0
- package/dist/__e2e__/fixtures/terminal-app.d.ts.map +1 -0
- package/dist/__e2e__/fixtures/terminal-app.js +42 -0
- package/dist/__e2e__/fixtures/terminal-app.js.map +1 -0
- package/dist/__e2e__/terminal-render.e2e.test.d.ts +2 -0
- package/dist/__e2e__/terminal-render.e2e.test.d.ts.map +1 -0
- package/dist/__e2e__/terminal-render.e2e.test.js +125 -0
- package/dist/__e2e__/terminal-render.e2e.test.js.map +1 -0
- package/dist/__tests__/worker_integration.test.d.ts +2 -0
- package/dist/__tests__/worker_integration.test.d.ts.map +1 -0
- package/dist/__tests__/worker_integration.test.js +569 -0
- package/dist/__tests__/worker_integration.test.js.map +1 -0
- package/dist/backend/nodeBackend.d.ts +62 -0
- package/dist/backend/nodeBackend.d.ts.map +1 -0
- package/dist/backend/nodeBackend.js +942 -0
- package/dist/backend/nodeBackend.js.map +1 -0
- package/dist/backend/nodeBackendInline.d.ts +10 -0
- package/dist/backend/nodeBackendInline.d.ts.map +1 -0
- package/dist/backend/nodeBackendInline.js +702 -0
- package/dist/backend/nodeBackendInline.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/worker/engineWorker.d.ts +9 -0
- package/dist/worker/engineWorker.d.ts.map +1 -0
- package/dist/worker/engineWorker.js +1001 -0
- package/dist/worker/engineWorker.js.map +1 -0
- package/dist/worker/protocol.d.ts +242 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +23 -0
- package/dist/worker/protocol.js.map +1 -0
- package/dist/worker/testShims/mockNative.d.ts +48 -0
- package/dist/worker/testShims/mockNative.d.ts.map +1 -0
- package/dist/worker/testShims/mockNative.js +216 -0
- package/dist/worker/testShims/mockNative.js.map +1 -0
- package/dist/worker/testShims/targetFpsNative.d.ts +22 -0
- package/dist/worker/testShims/targetFpsNative.d.ts.map +1 -0
- package/dist/worker/testShims/targetFpsNative.js +74 -0
- package/dist/worker/testShims/targetFpsNative.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline (single-thread) Node RuntimeBackend implementation.
|
|
3
|
+
*
|
|
4
|
+
* This path removes the worker-thread hop and submits frames directly from the
|
|
5
|
+
* main thread. It is intentionally optimized for low-latency transport and is
|
|
6
|
+
* selected explicitly via `executionMode: "inline"`.
|
|
7
|
+
*/
|
|
8
|
+
import { performance } from "node:perf_hooks";
|
|
9
|
+
import { DEFAULT_TERMINAL_CAPS } from "@rezi-ui/core";
|
|
10
|
+
import { ZR_DRAWLIST_VERSION_V1, ZR_DRAWLIST_VERSION_V2, ZR_ENGINE_ABI_MAJOR, ZR_ENGINE_ABI_MINOR, ZR_ENGINE_ABI_PATCH, ZR_EVENT_BATCH_VERSION_V1, ZrUiError, severityToNum, } from "@rezi-ui/core";
|
|
11
|
+
const DEBUG_QUERY_DEFAULT_RECORDS = 4096;
|
|
12
|
+
const DEBUG_QUERY_MAX_RECORDS = 16384;
|
|
13
|
+
const EVENT_POOL_SIZE = 16;
|
|
14
|
+
const POLL_IDLE_MS = 2;
|
|
15
|
+
const POLL_BUSY_MS = 0;
|
|
16
|
+
const RESOLVED_VOID = Promise.resolve();
|
|
17
|
+
const SYNC_FRAME_ACK_MARKER = "__reziSyncFrameAck";
|
|
18
|
+
const RESOLVED_SYNC_FRAME_ACK = Promise.resolve();
|
|
19
|
+
Object.defineProperty(RESOLVED_SYNC_FRAME_ACK, SYNC_FRAME_ACK_MARKER, {
|
|
20
|
+
value: true,
|
|
21
|
+
configurable: false,
|
|
22
|
+
enumerable: false,
|
|
23
|
+
writable: false,
|
|
24
|
+
});
|
|
25
|
+
const PERF_ENABLED = process.env.REZI_PERF === "1";
|
|
26
|
+
const PERF_MAX_SAMPLES = 1024;
|
|
27
|
+
function deferred() {
|
|
28
|
+
let resolve;
|
|
29
|
+
let reject;
|
|
30
|
+
const promise = new Promise((res, rej) => {
|
|
31
|
+
resolve = res;
|
|
32
|
+
reject = (err) => rej(err instanceof Error ? err : new Error(String(err)));
|
|
33
|
+
});
|
|
34
|
+
return { promise, resolve, reject };
|
|
35
|
+
}
|
|
36
|
+
function parsePositiveIntOr(n, fallback) {
|
|
37
|
+
if (typeof n !== "number")
|
|
38
|
+
return fallback;
|
|
39
|
+
if (!Number.isFinite(n))
|
|
40
|
+
return fallback;
|
|
41
|
+
if (!Number.isInteger(n))
|
|
42
|
+
return fallback;
|
|
43
|
+
if (n <= 0)
|
|
44
|
+
return fallback;
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
function parsePositiveInt(n) {
|
|
48
|
+
if (typeof n !== "number")
|
|
49
|
+
return null;
|
|
50
|
+
if (!Number.isFinite(n))
|
|
51
|
+
return null;
|
|
52
|
+
if (!Number.isInteger(n))
|
|
53
|
+
return null;
|
|
54
|
+
if (n <= 0)
|
|
55
|
+
return null;
|
|
56
|
+
return n;
|
|
57
|
+
}
|
|
58
|
+
function readNativeTargetFps(cfg) {
|
|
59
|
+
const targetFpsCfg = cfg;
|
|
60
|
+
return parsePositiveInt(targetFpsCfg.targetFps) ?? parsePositiveInt(targetFpsCfg.target_fps);
|
|
61
|
+
}
|
|
62
|
+
function safeErr(err) {
|
|
63
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
64
|
+
}
|
|
65
|
+
function safeDetail(err) {
|
|
66
|
+
if (err instanceof Error)
|
|
67
|
+
return `${err.name}: ${err.message}`;
|
|
68
|
+
return String(err);
|
|
69
|
+
}
|
|
70
|
+
// Little-endian u32 magic for bytes "ZREV".
|
|
71
|
+
const ZREV_MAGIC = 0x5645525a;
|
|
72
|
+
const ZREV_RECORD_RESIZE = 5;
|
|
73
|
+
function writeResizeBatchV1(buf, cols, rows) {
|
|
74
|
+
// Batch header (24) + RESIZE record (32) = 56 bytes.
|
|
75
|
+
const totalSize = 56;
|
|
76
|
+
if (buf.byteLength < totalSize)
|
|
77
|
+
return 0;
|
|
78
|
+
const dv = new DataView(buf);
|
|
79
|
+
const timeMs = (Date.now() >>> 0) & 0xffff_ffff;
|
|
80
|
+
dv.setUint32(0, ZREV_MAGIC, true);
|
|
81
|
+
dv.setUint32(4, ZR_EVENT_BATCH_VERSION_V1, true);
|
|
82
|
+
dv.setUint32(8, totalSize, true);
|
|
83
|
+
dv.setUint32(12, 1, true); // event_count
|
|
84
|
+
dv.setUint32(16, 0, true); // batch_flags
|
|
85
|
+
dv.setUint32(20, 0, true); // reserved0
|
|
86
|
+
dv.setUint32(24, ZREV_RECORD_RESIZE, true);
|
|
87
|
+
dv.setUint32(28, 32, true); // record_size
|
|
88
|
+
dv.setUint32(32, timeMs, true);
|
|
89
|
+
dv.setUint32(36, 0, true); // flags
|
|
90
|
+
dv.setUint32(40, cols >>> 0, true);
|
|
91
|
+
dv.setUint32(44, rows >>> 0, true);
|
|
92
|
+
dv.setUint32(48, 0, true);
|
|
93
|
+
dv.setUint32(52, 0, true);
|
|
94
|
+
return totalSize;
|
|
95
|
+
}
|
|
96
|
+
async function loadNative(shimModule) {
|
|
97
|
+
const unwrap = (m) => {
|
|
98
|
+
if (typeof m === "object" && m !== null) {
|
|
99
|
+
const rec = m;
|
|
100
|
+
const candidate = (rec.native ?? rec.default ?? rec);
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
return m;
|
|
104
|
+
};
|
|
105
|
+
if (typeof shimModule === "string" && shimModule.length > 0) {
|
|
106
|
+
return unwrap((await import(shimModule)));
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return unwrap((await import("@rezi-ui/native")));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const detail = safeDetail(err);
|
|
113
|
+
throw new Error(`Failed to load @rezi-ui/native.\n\nThis usually means the native addon was not built or not installed for this platform.\n\n${detail}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function createNodeBackendInlineInternal(opts = {}) {
|
|
117
|
+
const cfg = opts.config ?? {};
|
|
118
|
+
const fpsCap = parsePositiveIntOr(cfg.fpsCap, 60);
|
|
119
|
+
const maxEventBytes = parsePositiveIntOr(cfg.maxEventBytes, 1 << 20);
|
|
120
|
+
const useDrawlistV2 = cfg.useDrawlistV2 === true;
|
|
121
|
+
const nativeConfig = typeof cfg.nativeConfig === "object" &&
|
|
122
|
+
cfg.nativeConfig !== null &&
|
|
123
|
+
!Array.isArray(cfg.nativeConfig)
|
|
124
|
+
? cfg.nativeConfig
|
|
125
|
+
: Object.freeze({});
|
|
126
|
+
const nativeTargetFps = readNativeTargetFps(nativeConfig) ?? fpsCap;
|
|
127
|
+
const initConfig = {
|
|
128
|
+
...nativeConfig,
|
|
129
|
+
targetFps: nativeTargetFps,
|
|
130
|
+
requestedEngineAbiMajor: ZR_ENGINE_ABI_MAJOR,
|
|
131
|
+
requestedEngineAbiMinor: ZR_ENGINE_ABI_MINOR,
|
|
132
|
+
requestedEngineAbiPatch: ZR_ENGINE_ABI_PATCH,
|
|
133
|
+
requestedDrawlistVersion: useDrawlistV2 ? ZR_DRAWLIST_VERSION_V2 : ZR_DRAWLIST_VERSION_V1,
|
|
134
|
+
requestedEventBatchVersion: ZR_EVENT_BATCH_VERSION_V1,
|
|
135
|
+
};
|
|
136
|
+
let native = null;
|
|
137
|
+
let nativePromise = null;
|
|
138
|
+
let engineId = null;
|
|
139
|
+
let started = false;
|
|
140
|
+
let disposed = false;
|
|
141
|
+
let stopRequested = false;
|
|
142
|
+
let fatal = null;
|
|
143
|
+
let startDef = null;
|
|
144
|
+
let startSettled = false;
|
|
145
|
+
let stopDef = null;
|
|
146
|
+
let stopSettled = false;
|
|
147
|
+
let eventQueue = [];
|
|
148
|
+
const eventWaiters = [];
|
|
149
|
+
let eventPool = [];
|
|
150
|
+
let discardBuffer = null;
|
|
151
|
+
let droppedSinceLast = 0;
|
|
152
|
+
let pollTimer = null;
|
|
153
|
+
let pollImmediate = null;
|
|
154
|
+
let pollActive = false;
|
|
155
|
+
const perfSamples = [];
|
|
156
|
+
let cachedCaps = null;
|
|
157
|
+
function perfRecord(phase, durationMs) {
|
|
158
|
+
if (!PERF_ENABLED)
|
|
159
|
+
return;
|
|
160
|
+
if (perfSamples.length >= PERF_MAX_SAMPLES) {
|
|
161
|
+
perfSamples.shift();
|
|
162
|
+
}
|
|
163
|
+
perfSamples.push({ phase, durationMs });
|
|
164
|
+
}
|
|
165
|
+
function perfSnapshot() {
|
|
166
|
+
const byPhase = new Map();
|
|
167
|
+
for (const s of perfSamples) {
|
|
168
|
+
let arr = byPhase.get(s.phase);
|
|
169
|
+
if (!arr) {
|
|
170
|
+
arr = [];
|
|
171
|
+
byPhase.set(s.phase, arr);
|
|
172
|
+
}
|
|
173
|
+
arr.push(s.durationMs);
|
|
174
|
+
}
|
|
175
|
+
const phases = {};
|
|
176
|
+
for (const [phase, samples] of byPhase) {
|
|
177
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
178
|
+
const sum = sorted.reduce((acc, v) => acc + v, 0);
|
|
179
|
+
const p50Idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.5));
|
|
180
|
+
const p95Idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95));
|
|
181
|
+
const p99Idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.99));
|
|
182
|
+
const worst10Start = Math.max(0, sorted.length - 10);
|
|
183
|
+
const worst10 = sorted.slice(worst10Start).reverse();
|
|
184
|
+
phases[phase] = {
|
|
185
|
+
count: sorted.length,
|
|
186
|
+
avg: sorted.length > 0 ? sum / sorted.length : 0,
|
|
187
|
+
p50: sorted[p50Idx] ?? 0,
|
|
188
|
+
p95: sorted[p95Idx] ?? 0,
|
|
189
|
+
p99: sorted[p99Idx] ?? 0,
|
|
190
|
+
max: sorted[sorted.length - 1] ?? 0,
|
|
191
|
+
worst10,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { phases };
|
|
195
|
+
}
|
|
196
|
+
function rejectWaiters(err) {
|
|
197
|
+
while (eventWaiters.length > 0)
|
|
198
|
+
eventWaiters.shift()?.reject(err);
|
|
199
|
+
eventQueue = [];
|
|
200
|
+
if (startDef !== null && !startSettled) {
|
|
201
|
+
startSettled = true;
|
|
202
|
+
startDef.reject(err);
|
|
203
|
+
}
|
|
204
|
+
if (stopDef !== null && !stopSettled) {
|
|
205
|
+
stopSettled = true;
|
|
206
|
+
stopDef.reject(err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function failWith(where, code, detail) {
|
|
210
|
+
const err = new ZrUiError("ZRUI_BACKEND_ERROR", `${where} (${String(code)}): ${detail}`);
|
|
211
|
+
fatal = err;
|
|
212
|
+
rejectWaiters(err);
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
function clearPollLoop() {
|
|
216
|
+
if (pollTimer !== null) {
|
|
217
|
+
clearTimeout(pollTimer);
|
|
218
|
+
pollTimer = null;
|
|
219
|
+
}
|
|
220
|
+
if (pollImmediate !== null) {
|
|
221
|
+
clearImmediate(pollImmediate);
|
|
222
|
+
pollImmediate = null;
|
|
223
|
+
}
|
|
224
|
+
pollActive = false;
|
|
225
|
+
}
|
|
226
|
+
function schedulePoll(delayMs) {
|
|
227
|
+
if (!started || disposed || stopRequested || fatal !== null)
|
|
228
|
+
return;
|
|
229
|
+
if (pollImmediate !== null || pollTimer !== null)
|
|
230
|
+
return;
|
|
231
|
+
if (delayMs <= 0) {
|
|
232
|
+
pollImmediate = setImmediate(() => {
|
|
233
|
+
pollImmediate = null;
|
|
234
|
+
runPollOnce();
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
pollTimer = setTimeout(() => {
|
|
239
|
+
pollTimer = null;
|
|
240
|
+
runPollOnce();
|
|
241
|
+
}, delayMs);
|
|
242
|
+
}
|
|
243
|
+
function buildBatch(buf, byteLen, dropped) {
|
|
244
|
+
const bytes = new Uint8Array(buf, 0, byteLen);
|
|
245
|
+
let released = false;
|
|
246
|
+
return {
|
|
247
|
+
bytes,
|
|
248
|
+
droppedBatches: dropped,
|
|
249
|
+
release: () => {
|
|
250
|
+
if (released)
|
|
251
|
+
return;
|
|
252
|
+
released = true;
|
|
253
|
+
if (!started || disposed)
|
|
254
|
+
return;
|
|
255
|
+
eventPool.push(buf);
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function emitInitialResizeIfPossible() {
|
|
260
|
+
if (!started || discardBuffer === null)
|
|
261
|
+
return;
|
|
262
|
+
// Keep test-shim runs deterministic (mirrors worker backend behavior).
|
|
263
|
+
if (typeof opts.nativeShimModule === "string" && opts.nativeShimModule.length > 0)
|
|
264
|
+
return;
|
|
265
|
+
const cols = typeof process.stdout.columns === "number" &&
|
|
266
|
+
Number.isInteger(process.stdout.columns) &&
|
|
267
|
+
process.stdout.columns > 0
|
|
268
|
+
? process.stdout.columns
|
|
269
|
+
: 80;
|
|
270
|
+
const rows = typeof process.stdout.rows === "number" &&
|
|
271
|
+
Number.isInteger(process.stdout.rows) &&
|
|
272
|
+
process.stdout.rows > 0
|
|
273
|
+
? process.stdout.rows
|
|
274
|
+
: 24;
|
|
275
|
+
const buf = eventPool.pop() ?? new ArrayBuffer(maxEventBytes);
|
|
276
|
+
const byteLen = writeResizeBatchV1(buf, cols, rows);
|
|
277
|
+
if (byteLen <= 0) {
|
|
278
|
+
eventPool.push(buf);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const waiter = eventWaiters.shift();
|
|
282
|
+
if (waiter !== undefined) {
|
|
283
|
+
waiter.resolve(buildBatch(buf, byteLen, 0));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
eventQueue.push({ batch: buf, byteLen, droppedSinceLast: 0 });
|
|
287
|
+
}
|
|
288
|
+
function runPollOnce() {
|
|
289
|
+
if (!started || disposed || stopRequested || fatal !== null)
|
|
290
|
+
return;
|
|
291
|
+
if (engineId === null || native === null || discardBuffer === null)
|
|
292
|
+
return;
|
|
293
|
+
if (pollActive)
|
|
294
|
+
return;
|
|
295
|
+
pollActive = true;
|
|
296
|
+
try {
|
|
297
|
+
const outBuf = eventPool.length > 0 ? (eventPool.pop() ?? discardBuffer) : discardBuffer;
|
|
298
|
+
let written = -1;
|
|
299
|
+
const startMs = PERF_ENABLED ? performance.now() : 0;
|
|
300
|
+
try {
|
|
301
|
+
written = native.enginePollEvents(engineId, 0, new Uint8Array(outBuf));
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
failWith("enginePollEvents", -1, `engine_poll_events threw: ${safeDetail(err)}`);
|
|
305
|
+
}
|
|
306
|
+
if (PERF_ENABLED) {
|
|
307
|
+
perfRecord("event_poll", performance.now() - startMs);
|
|
308
|
+
}
|
|
309
|
+
if (written < 0) {
|
|
310
|
+
if (outBuf !== discardBuffer)
|
|
311
|
+
eventPool.push(outBuf);
|
|
312
|
+
failWith("enginePollEvents", written, "engine_poll_events failed");
|
|
313
|
+
}
|
|
314
|
+
if (written === 0) {
|
|
315
|
+
if (outBuf !== discardBuffer)
|
|
316
|
+
eventPool.push(outBuf);
|
|
317
|
+
schedulePoll(POLL_IDLE_MS);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (outBuf === discardBuffer) {
|
|
321
|
+
droppedSinceLast++;
|
|
322
|
+
schedulePoll(POLL_BUSY_MS);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const waiter = eventWaiters.shift();
|
|
326
|
+
if (waiter !== undefined) {
|
|
327
|
+
waiter.resolve(buildBatch(outBuf, written, droppedSinceLast));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
eventQueue.push({ batch: outBuf, byteLen: written, droppedSinceLast });
|
|
331
|
+
}
|
|
332
|
+
droppedSinceLast = 0;
|
|
333
|
+
schedulePoll(POLL_BUSY_MS);
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
pollActive = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function ensureNativeLoaded() {
|
|
340
|
+
if (native !== null)
|
|
341
|
+
return native;
|
|
342
|
+
if (nativePromise !== null)
|
|
343
|
+
return nativePromise;
|
|
344
|
+
nativePromise = loadNative(opts.nativeShimModule).then((mod) => {
|
|
345
|
+
native = mod;
|
|
346
|
+
return mod;
|
|
347
|
+
});
|
|
348
|
+
return nativePromise;
|
|
349
|
+
}
|
|
350
|
+
function ensureDebugApiLoaded(api) {
|
|
351
|
+
if (typeof api.engineDebugEnable !== "function") {
|
|
352
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
353
|
+
}
|
|
354
|
+
if (typeof api.engineDebugDisable !== "function") {
|
|
355
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
356
|
+
}
|
|
357
|
+
if (typeof api.engineDebugQuery !== "function") {
|
|
358
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
359
|
+
}
|
|
360
|
+
if (typeof api.engineDebugGetPayload !== "function") {
|
|
361
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
362
|
+
}
|
|
363
|
+
if (typeof api.engineDebugGetStats !== "function") {
|
|
364
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
365
|
+
}
|
|
366
|
+
if (typeof api.engineDebugExport !== "function") {
|
|
367
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
368
|
+
}
|
|
369
|
+
if (typeof api.engineDebugReset !== "function") {
|
|
370
|
+
throw new Error("inline backend: native debug API is unavailable");
|
|
371
|
+
}
|
|
372
|
+
return api;
|
|
373
|
+
}
|
|
374
|
+
const backend = {
|
|
375
|
+
async start() {
|
|
376
|
+
if (disposed)
|
|
377
|
+
throw new Error("NodeBackend(inline): disposed");
|
|
378
|
+
if (fatal !== null)
|
|
379
|
+
throw fatal;
|
|
380
|
+
if (started)
|
|
381
|
+
return;
|
|
382
|
+
if (startDef !== null) {
|
|
383
|
+
await startDef.promise;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
startDef = deferred();
|
|
387
|
+
startSettled = false;
|
|
388
|
+
stopRequested = false;
|
|
389
|
+
try {
|
|
390
|
+
const api = await ensureNativeLoaded();
|
|
391
|
+
let id = 0;
|
|
392
|
+
try {
|
|
393
|
+
id = api.engineCreate(initConfig);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
throw new Error(`engine_create threw: ${safeDetail(err)}`);
|
|
397
|
+
}
|
|
398
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
399
|
+
throw new Error(`engine_create failed: code=${String(id)}`);
|
|
400
|
+
}
|
|
401
|
+
engineId = id;
|
|
402
|
+
started = true;
|
|
403
|
+
cachedCaps = null;
|
|
404
|
+
eventQueue = [];
|
|
405
|
+
eventPool = [];
|
|
406
|
+
for (let i = 0; i < EVENT_POOL_SIZE; i++) {
|
|
407
|
+
eventPool.push(new ArrayBuffer(maxEventBytes));
|
|
408
|
+
}
|
|
409
|
+
discardBuffer = new ArrayBuffer(maxEventBytes);
|
|
410
|
+
droppedSinceLast = 0;
|
|
411
|
+
emitInitialResizeIfPossible();
|
|
412
|
+
schedulePoll(POLL_IDLE_MS);
|
|
413
|
+
if (!startSettled && startDef !== null) {
|
|
414
|
+
startSettled = true;
|
|
415
|
+
startDef.resolve();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
const e = safeErr(err);
|
|
420
|
+
fatal = new ZrUiError("ZRUI_BACKEND_ERROR", e.message);
|
|
421
|
+
rejectWaiters(fatal);
|
|
422
|
+
}
|
|
423
|
+
if (startDef === null)
|
|
424
|
+
throw new Error("NodeBackend(inline): invariant startDef");
|
|
425
|
+
await startDef.promise;
|
|
426
|
+
startDef = null;
|
|
427
|
+
},
|
|
428
|
+
async stop() {
|
|
429
|
+
if (disposed)
|
|
430
|
+
return;
|
|
431
|
+
if (fatal !== null)
|
|
432
|
+
throw fatal;
|
|
433
|
+
if (!started)
|
|
434
|
+
return;
|
|
435
|
+
if (stopDef !== null) {
|
|
436
|
+
await stopDef.promise;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
stopDef = deferred();
|
|
440
|
+
stopSettled = false;
|
|
441
|
+
stopRequested = true;
|
|
442
|
+
clearPollLoop();
|
|
443
|
+
const stopErr = new Error("NodeBackend(inline): stopped");
|
|
444
|
+
while (eventWaiters.length > 0)
|
|
445
|
+
eventWaiters.shift()?.reject(stopErr);
|
|
446
|
+
eventQueue = [];
|
|
447
|
+
if (engineId !== null && native !== null) {
|
|
448
|
+
try {
|
|
449
|
+
native.engineDestroy(engineId);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// best effort on stop
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
engineId = null;
|
|
456
|
+
started = false;
|
|
457
|
+
if (!stopSettled && stopDef !== null) {
|
|
458
|
+
stopSettled = true;
|
|
459
|
+
stopDef.resolve();
|
|
460
|
+
}
|
|
461
|
+
await stopDef.promise;
|
|
462
|
+
stopDef = null;
|
|
463
|
+
},
|
|
464
|
+
dispose() {
|
|
465
|
+
if (disposed)
|
|
466
|
+
return;
|
|
467
|
+
disposed = true;
|
|
468
|
+
clearPollLoop();
|
|
469
|
+
stopRequested = true;
|
|
470
|
+
if (engineId !== null && native !== null) {
|
|
471
|
+
try {
|
|
472
|
+
native.engineDestroy(engineId);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// ignore
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
engineId = null;
|
|
479
|
+
started = false;
|
|
480
|
+
const err = new Error("NodeBackend(inline): disposed");
|
|
481
|
+
while (eventWaiters.length > 0)
|
|
482
|
+
eventWaiters.shift()?.reject(err);
|
|
483
|
+
eventQueue = [];
|
|
484
|
+
if (startDef !== null && !startSettled) {
|
|
485
|
+
startSettled = true;
|
|
486
|
+
startDef.reject(err);
|
|
487
|
+
}
|
|
488
|
+
if (stopDef !== null && !stopSettled) {
|
|
489
|
+
stopSettled = true;
|
|
490
|
+
stopDef.reject(err);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
requestFrame(drawlist) {
|
|
494
|
+
if (disposed)
|
|
495
|
+
return Promise.reject(new Error("NodeBackend(inline): disposed"));
|
|
496
|
+
if (fatal !== null)
|
|
497
|
+
return Promise.reject(fatal);
|
|
498
|
+
if (stopRequested)
|
|
499
|
+
return Promise.reject(new Error("NodeBackend(inline): stopped"));
|
|
500
|
+
if (!started) {
|
|
501
|
+
return backend.start().then(() => backend.requestFrame(drawlist));
|
|
502
|
+
}
|
|
503
|
+
if (native === null || engineId === null) {
|
|
504
|
+
return Promise.reject(new Error("NodeBackend(inline): engine not started"));
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const submitRc = native.engineSubmitDrawlist(engineId, drawlist);
|
|
508
|
+
if (submitRc < 0) {
|
|
509
|
+
return Promise.reject(new ZrUiError("ZRUI_BACKEND_ERROR", `engine_submit_drawlist failed: code=${String(submitRc)}`));
|
|
510
|
+
}
|
|
511
|
+
const presentRc = native.enginePresent(engineId);
|
|
512
|
+
if (presentRc < 0) {
|
|
513
|
+
return Promise.reject(new ZrUiError("ZRUI_BACKEND_ERROR", `engine_present failed: code=${String(presentRc)}`));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
return Promise.reject(safeErr(err));
|
|
518
|
+
}
|
|
519
|
+
return RESOLVED_SYNC_FRAME_ACK;
|
|
520
|
+
},
|
|
521
|
+
pollEvents() {
|
|
522
|
+
if (disposed)
|
|
523
|
+
return Promise.reject(new Error("NodeBackend(inline): disposed"));
|
|
524
|
+
if (fatal !== null)
|
|
525
|
+
return Promise.reject(fatal);
|
|
526
|
+
if (stopRequested)
|
|
527
|
+
return Promise.reject(new Error("NodeBackend(inline): stopped"));
|
|
528
|
+
const queued = eventQueue.shift();
|
|
529
|
+
if (queued !== undefined) {
|
|
530
|
+
return Promise.resolve(buildBatch(queued.batch, queued.byteLen, queued.droppedSinceLast));
|
|
531
|
+
}
|
|
532
|
+
const d = deferred();
|
|
533
|
+
eventWaiters.push(d);
|
|
534
|
+
schedulePoll(POLL_BUSY_MS);
|
|
535
|
+
return d.promise;
|
|
536
|
+
},
|
|
537
|
+
postUserEvent(tag, payload) {
|
|
538
|
+
if (disposed)
|
|
539
|
+
throw new Error("NodeBackend(inline): disposed");
|
|
540
|
+
if (fatal !== null)
|
|
541
|
+
throw fatal;
|
|
542
|
+
if (!started || engineId === null || native === null)
|
|
543
|
+
throw new Error("NodeBackend(inline): not started");
|
|
544
|
+
if (stopRequested)
|
|
545
|
+
throw new Error("NodeBackend(inline): stopped");
|
|
546
|
+
const rc = native.enginePostUserEvent(engineId, tag, payload);
|
|
547
|
+
if (rc < 0) {
|
|
548
|
+
throw new ZrUiError("ZRUI_BACKEND_ERROR", `engine_post_user_event failed: code=${String(rc)}`);
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
async getCaps() {
|
|
552
|
+
if (disposed)
|
|
553
|
+
throw new Error("NodeBackend(inline): disposed");
|
|
554
|
+
if (fatal !== null)
|
|
555
|
+
throw fatal;
|
|
556
|
+
if (cachedCaps !== null)
|
|
557
|
+
return cachedCaps;
|
|
558
|
+
if (!started || engineId === null || native === null)
|
|
559
|
+
return DEFAULT_TERMINAL_CAPS;
|
|
560
|
+
const caps = native.engineGetCaps(engineId);
|
|
561
|
+
const nextCaps = Object.freeze({
|
|
562
|
+
colorMode: caps.colorMode,
|
|
563
|
+
supportsMouse: caps.supportsMouse,
|
|
564
|
+
supportsBracketedPaste: caps.supportsBracketedPaste,
|
|
565
|
+
supportsFocusEvents: caps.supportsFocusEvents,
|
|
566
|
+
supportsOsc52: caps.supportsOsc52,
|
|
567
|
+
supportsSyncUpdate: caps.supportsSyncUpdate,
|
|
568
|
+
supportsScrollRegion: caps.supportsScrollRegion,
|
|
569
|
+
supportsCursorShape: caps.supportsCursorShape,
|
|
570
|
+
supportsOutputWaitWritable: caps.supportsOutputWaitWritable,
|
|
571
|
+
sgrAttrsSupported: caps.sgrAttrsSupported,
|
|
572
|
+
});
|
|
573
|
+
cachedCaps = nextCaps;
|
|
574
|
+
return nextCaps;
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
const debug = {
|
|
578
|
+
debugEnable: async (config) => {
|
|
579
|
+
await backend.start();
|
|
580
|
+
if (native === null || engineId === null) {
|
|
581
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
582
|
+
}
|
|
583
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
584
|
+
const minSeverity = config.minSeverity !== undefined ? severityToNum(config.minSeverity) : null;
|
|
585
|
+
const configWire = {
|
|
586
|
+
enabled: true,
|
|
587
|
+
...(config.ringCapacity !== undefined ? { ringCapacity: config.ringCapacity } : {}),
|
|
588
|
+
...(minSeverity !== null ? { minSeverity } : {}),
|
|
589
|
+
...(config.categoryMask !== undefined ? { categoryMask: config.categoryMask } : {}),
|
|
590
|
+
...(config.captureRawEvents !== undefined
|
|
591
|
+
? { captureRawEvents: config.captureRawEvents }
|
|
592
|
+
: {}),
|
|
593
|
+
...(config.captureDrawlistBytes !== undefined
|
|
594
|
+
? { captureDrawlistBytes: config.captureDrawlistBytes }
|
|
595
|
+
: {}),
|
|
596
|
+
};
|
|
597
|
+
const rc = dbg.engineDebugEnable(engineId, configWire);
|
|
598
|
+
if (rc < 0) {
|
|
599
|
+
throw new ZrUiError("ZRUI_BACKEND_ERROR", `engineDebugEnable failed: code=${String(rc)}`);
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
debugDisable: async () => {
|
|
603
|
+
await backend.start();
|
|
604
|
+
if (native === null || engineId === null) {
|
|
605
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
606
|
+
}
|
|
607
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
608
|
+
const rc = dbg.engineDebugDisable(engineId);
|
|
609
|
+
if (rc < 0) {
|
|
610
|
+
throw new ZrUiError("ZRUI_BACKEND_ERROR", `engineDebugDisable failed: code=${String(rc)}`);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
debugQuery: async (query) => {
|
|
614
|
+
await backend.start();
|
|
615
|
+
if (native === null || engineId === null) {
|
|
616
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
617
|
+
}
|
|
618
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
619
|
+
const maxRecordsRaw = query.maxRecords === undefined ? DEBUG_QUERY_DEFAULT_RECORDS : query.maxRecords;
|
|
620
|
+
const maxRecords = parsePositiveIntOr(maxRecordsRaw, DEBUG_QUERY_DEFAULT_RECORDS);
|
|
621
|
+
const clampedMaxRecords = Math.min(DEBUG_QUERY_MAX_RECORDS, maxRecords);
|
|
622
|
+
const queryWire = {
|
|
623
|
+
...(query.minRecordId !== undefined ? { minRecordId: query.minRecordId.toString() } : {}),
|
|
624
|
+
...(query.maxRecordId !== undefined ? { maxRecordId: query.maxRecordId.toString() } : {}),
|
|
625
|
+
...(query.categoryMask !== undefined ? { categoryMask: query.categoryMask } : {}),
|
|
626
|
+
...(query.minSeverity !== undefined
|
|
627
|
+
? { minSeverity: severityToNum(query.minSeverity) }
|
|
628
|
+
: {}),
|
|
629
|
+
maxRecords: clampedMaxRecords,
|
|
630
|
+
};
|
|
631
|
+
const headersCap = Math.max(1, clampedMaxRecords) * 40;
|
|
632
|
+
const outHeaders = new Uint8Array(headersCap);
|
|
633
|
+
const result = dbg.engineDebugQuery(engineId, queryWire, outHeaders);
|
|
634
|
+
const headers = outHeaders.subarray(0, result.recordsReturned * 40);
|
|
635
|
+
const wireResult = {
|
|
636
|
+
recordsReturned: result.recordsReturned,
|
|
637
|
+
recordsAvailable: result.recordsAvailable,
|
|
638
|
+
oldestRecordId: result.oldestRecordId,
|
|
639
|
+
newestRecordId: result.newestRecordId,
|
|
640
|
+
recordsDropped: result.recordsDropped,
|
|
641
|
+
};
|
|
642
|
+
return { headers, result: wireResult };
|
|
643
|
+
},
|
|
644
|
+
debugGetPayload: async (recordId) => {
|
|
645
|
+
await backend.start();
|
|
646
|
+
if (native === null || engineId === null) {
|
|
647
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
648
|
+
}
|
|
649
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
650
|
+
const out = new Uint8Array(maxEventBytes);
|
|
651
|
+
const rc = dbg.engineDebugGetPayload(engineId, recordId, out);
|
|
652
|
+
if (rc <= 0)
|
|
653
|
+
return null;
|
|
654
|
+
return out.slice(0, rc);
|
|
655
|
+
},
|
|
656
|
+
debugGetStats: async () => {
|
|
657
|
+
await backend.start();
|
|
658
|
+
if (native === null || engineId === null) {
|
|
659
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
660
|
+
}
|
|
661
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
662
|
+
const s = dbg.engineDebugGetStats(engineId);
|
|
663
|
+
const out = {
|
|
664
|
+
totalRecords: s.totalRecords,
|
|
665
|
+
totalDropped: s.totalDropped,
|
|
666
|
+
errorCount: s.errorCount,
|
|
667
|
+
warnCount: s.warnCount,
|
|
668
|
+
currentRingUsage: s.currentRingUsage,
|
|
669
|
+
ringCapacity: s.ringCapacity,
|
|
670
|
+
};
|
|
671
|
+
return out;
|
|
672
|
+
},
|
|
673
|
+
debugExport: async () => {
|
|
674
|
+
await backend.start();
|
|
675
|
+
if (native === null || engineId === null) {
|
|
676
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
677
|
+
}
|
|
678
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
679
|
+
const out = new Uint8Array(maxEventBytes);
|
|
680
|
+
const rc = dbg.engineDebugExport(engineId, out);
|
|
681
|
+
if (rc <= 0)
|
|
682
|
+
return new Uint8Array(0);
|
|
683
|
+
return out.slice(0, rc);
|
|
684
|
+
},
|
|
685
|
+
debugReset: async () => {
|
|
686
|
+
await backend.start();
|
|
687
|
+
if (native === null || engineId === null) {
|
|
688
|
+
throw new Error("NodeBackend(inline): engine not started");
|
|
689
|
+
}
|
|
690
|
+
const dbg = ensureDebugApiLoaded(native);
|
|
691
|
+
const rc = dbg.engineDebugReset(engineId);
|
|
692
|
+
if (rc < 0) {
|
|
693
|
+
throw new ZrUiError("ZRUI_BACKEND_ERROR", `engineDebugReset failed: code=${String(rc)}`);
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
const perf = {
|
|
698
|
+
perfSnapshot: async () => perfSnapshot(),
|
|
699
|
+
};
|
|
700
|
+
return Object.freeze({ ...backend, debug, perf });
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=nodeBackendInline.js.map
|