slackmark-node 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 +21 -0
- package/README.md +168 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/create-node-converter.d.ts +72 -0
- package/dist/create-node-converter.d.ts.map +1 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/image-output.d.ts +9 -0
- package/dist/image-output.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1792 -0
- package/dist/index.js.map +1 -0
- package/dist/katex-assets.d.ts +19 -0
- package/dist/katex-assets.d.ts.map +1 -0
- package/dist/katex-renderer.d.ts +39 -0
- package/dist/katex-renderer.d.ts.map +1 -0
- package/dist/mermaid-assets.d.ts +9 -0
- package/dist/mermaid-assets.d.ts.map +1 -0
- package/dist/mermaid-renderer.d.ts +33 -0
- package/dist/mermaid-renderer.d.ts.map +1 -0
- package/dist/node-options.d.ts +6 -0
- package/dist/node-options.d.ts.map +1 -0
- package/dist/operation-control.d.ts +18 -0
- package/dist/operation-control.d.ts.map +1 -0
- package/dist/puppeteer-render-runtime.d.ts +76 -0
- package/dist/puppeteer-render-runtime.d.ts.map +1 -0
- package/dist/puppeteer.d.ts +7 -0
- package/dist/puppeteer.d.ts.map +1 -0
- package/dist/puppeteer.js +723 -0
- package/dist/puppeteer.js.map +1 -0
- package/dist/renderers.d.ts +5 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +551 -0
- package/dist/renderers.js.map +1 -0
- package/dist/status.d.ts +20 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/upload-scheduler.d.ts +32 -0
- package/dist/upload-scheduler.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { ConfigurationError, OperationAbortedError, OperationDeadlineExceededError, OperationErrorCode, OperationShutdownError, cloneOperationError, isOperationError } from "slackmark";
|
|
5
|
+
//#region src/errors.ts
|
|
6
|
+
const NodeRenderErrorCode = {
|
|
7
|
+
AssetMissing: "asset_missing",
|
|
8
|
+
BrowserBinaryMissing: "browser_binary_missing",
|
|
9
|
+
BrowserDisconnected: "browser_disconnected",
|
|
10
|
+
BrowserFailed: "browser_failed",
|
|
11
|
+
CleanupFailed: "cleanup_failed",
|
|
12
|
+
CloseFailed: "close_failed",
|
|
13
|
+
ContextFailed: "context_failed",
|
|
14
|
+
DimensionLimit: "dimension_limit",
|
|
15
|
+
OutputLimit: "output_limit",
|
|
16
|
+
QueueFull: "queue_full",
|
|
17
|
+
RenderFailed: "render_failed",
|
|
18
|
+
SourceLimit: "source_limit"
|
|
19
|
+
};
|
|
20
|
+
const NodeRenderStage = {
|
|
21
|
+
Admission: "admission",
|
|
22
|
+
AssetLoad: "asset_load",
|
|
23
|
+
Browser: "browser",
|
|
24
|
+
BrowserCleanup: "browser_cleanup",
|
|
25
|
+
BrowserClose: "browser_close",
|
|
26
|
+
BrowserLaunch: "browser_launch",
|
|
27
|
+
ContextCleanup: "context_cleanup",
|
|
28
|
+
ContextCreate: "context_create",
|
|
29
|
+
Layout: "layout",
|
|
30
|
+
OutputValidation: "output_validation",
|
|
31
|
+
Render: "render",
|
|
32
|
+
SourceValidation: "source_validation"
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Sanitized failure from the Node rendering boundary.
|
|
36
|
+
*/
|
|
37
|
+
var NodeRenderError = class extends Error {
|
|
38
|
+
code;
|
|
39
|
+
stage;
|
|
40
|
+
retryable;
|
|
41
|
+
rendererId;
|
|
42
|
+
constructor(code, message, options = {
|
|
43
|
+
stage: "render",
|
|
44
|
+
retryable: false
|
|
45
|
+
}) {
|
|
46
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
47
|
+
this.name = "NodeRenderError";
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.stage = options.stage;
|
|
50
|
+
this.retryable = options.retryable;
|
|
51
|
+
this.rendererId = options.rendererId;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function normalizeNodeRenderError(error, code, message, stage = NodeRenderStage.Render, retryable = false) {
|
|
55
|
+
return error instanceof NodeRenderError || isOperationError(error) ? error : new NodeRenderError(code, message, {
|
|
56
|
+
stage,
|
|
57
|
+
retryable,
|
|
58
|
+
cause: error
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/katex-assets.ts
|
|
63
|
+
const resolver$1 = createRequire(import.meta.url);
|
|
64
|
+
const WOFF2_URL_RE = /url\(fonts\/([A-Za-z0-9_-]+\.woff2)\)/g;
|
|
65
|
+
const LEGACY_FONT_SOURCE_RE = /, url\(fonts\/[A-Za-z0-9_-]+\.(?:woff|ttf)\) format\("(?:woff|truetype)"\)/g;
|
|
66
|
+
/**
|
|
67
|
+
* Reads KaTeX's installed CSS/fonts and embeds WOFF2 files as data URLs once.
|
|
68
|
+
*/
|
|
69
|
+
var LocalKatexAssetLoader = class {
|
|
70
|
+
cssPromise;
|
|
71
|
+
scriptPromise;
|
|
72
|
+
constructor() {
|
|
73
|
+
this.cssPromise = void 0;
|
|
74
|
+
this.scriptPromise = void 0;
|
|
75
|
+
}
|
|
76
|
+
loadScript() {
|
|
77
|
+
this.scriptPromise ??= readFile(resolver$1.resolve("katex/dist/katex.min.js"), "utf8").catch((error) => {
|
|
78
|
+
this.scriptPromise = void 0;
|
|
79
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.AssetMissing, "Unable to load local KaTeX script", "asset_load", false);
|
|
80
|
+
});
|
|
81
|
+
return this.scriptPromise;
|
|
82
|
+
}
|
|
83
|
+
loadCss() {
|
|
84
|
+
this.cssPromise ??= this.loadCssFromPackage().catch((error) => {
|
|
85
|
+
this.cssPromise = void 0;
|
|
86
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.AssetMissing, "Unable to load local KaTeX assets", "asset_load", false);
|
|
87
|
+
});
|
|
88
|
+
return this.cssPromise;
|
|
89
|
+
}
|
|
90
|
+
async loadCssFromPackage() {
|
|
91
|
+
const cssPath = resolver$1.resolve("katex/dist/katex.css");
|
|
92
|
+
const css = await readFile(cssPath, "utf8");
|
|
93
|
+
const fontNames = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const match of css.matchAll(WOFF2_URL_RE)) {
|
|
95
|
+
const fontName = match[1];
|
|
96
|
+
if (fontName !== void 0) fontNames.add(fontName);
|
|
97
|
+
}
|
|
98
|
+
const encodedFonts = await Promise.all([...fontNames].map(async (fontName) => {
|
|
99
|
+
return [fontName, (await readFile(join(dirname(cssPath), "fonts", fontName))).toString("base64")];
|
|
100
|
+
}));
|
|
101
|
+
let embedded = css.replace(LEGACY_FONT_SOURCE_RE, "");
|
|
102
|
+
for (const [fontName, base64] of encodedFonts) embedded = embedded.replaceAll(`url(fonts/${fontName})`, `url(data:font/woff2;base64,${base64})`);
|
|
103
|
+
return embedded;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/mermaid-assets.ts
|
|
108
|
+
const resolver = createRequire(import.meta.url);
|
|
109
|
+
var LocalMermaidAssetLoader = class {
|
|
110
|
+
scriptPromise;
|
|
111
|
+
constructor() {
|
|
112
|
+
this.scriptPromise = void 0;
|
|
113
|
+
}
|
|
114
|
+
loadScript() {
|
|
115
|
+
this.scriptPromise ??= readFile(resolver.resolve("mermaid/dist/mermaid.min.js"), "utf8").catch((error) => {
|
|
116
|
+
this.scriptPromise = void 0;
|
|
117
|
+
throw new NodeRenderError(NodeRenderErrorCode.AssetMissing, "Unable to load Mermaid browser assets", {
|
|
118
|
+
stage: "asset_load",
|
|
119
|
+
retryable: false,
|
|
120
|
+
cause: error
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return this.scriptPromise;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/node-options.ts
|
|
128
|
+
function snapshotNodeOptions(options, allowed, label) {
|
|
129
|
+
if (!isPlainNodeObject(options)) throw invalidNodeOptions(`${label} options must be a plain object`);
|
|
130
|
+
const allowedKeys = new Set(allowed);
|
|
131
|
+
const snapshot = {};
|
|
132
|
+
let keys;
|
|
133
|
+
try {
|
|
134
|
+
keys = Object.keys(options);
|
|
135
|
+
} catch (cause) {
|
|
136
|
+
throw invalidNodeOptions(`${label} options could not be inspected`, cause);
|
|
137
|
+
}
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
if (!allowedKeys.has(key)) throw invalidNodeOptions(`Unknown ${label} option: ${key}`);
|
|
140
|
+
try {
|
|
141
|
+
snapshot[key] = Reflect.get(options, key);
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
throw invalidNodeOptions(`${label}.${key} could not be read`, cause);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return Object.freeze(snapshot);
|
|
147
|
+
}
|
|
148
|
+
function snapshotOpenNodeOptions(options, label) {
|
|
149
|
+
let keys;
|
|
150
|
+
try {
|
|
151
|
+
keys = Object.keys(options);
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
throw invalidNodeOptions(`${label} could not be inspected`, cause);
|
|
154
|
+
}
|
|
155
|
+
return snapshotNodeOptions(options, keys, label);
|
|
156
|
+
}
|
|
157
|
+
function assertNodeMethod(collaborator, method, label) {
|
|
158
|
+
try {
|
|
159
|
+
if (typeof collaborator !== "object" && typeof collaborator !== "function" || collaborator === null || typeof Reflect.get(collaborator, method) !== "function") throw new TypeError(`${method} must be a function`);
|
|
160
|
+
} catch (cause) {
|
|
161
|
+
throw invalidNodeOptions(`${label} collaborator is invalid`, cause);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function invalidNodeOptions(message, cause) {
|
|
165
|
+
return new ConfigurationError(message, {
|
|
166
|
+
code: "invalid_node_configuration",
|
|
167
|
+
stage: "configuration",
|
|
168
|
+
cause
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function isPlainNodeObject(value) {
|
|
172
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
173
|
+
let prototype;
|
|
174
|
+
try {
|
|
175
|
+
prototype = Object.getPrototypeOf(value);
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return prototype === Object.prototype || prototype === null;
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/operation-control.ts
|
|
183
|
+
var OperationScope = class {
|
|
184
|
+
context;
|
|
185
|
+
parentSignal;
|
|
186
|
+
controller;
|
|
187
|
+
abortFromParent;
|
|
188
|
+
deadlineTimer;
|
|
189
|
+
constructor(context, stage) {
|
|
190
|
+
this.parentSignal = context.signal;
|
|
191
|
+
this.controller = new AbortController();
|
|
192
|
+
this.abortFromParent = () => {
|
|
193
|
+
const reason = this.parentSignal?.reason;
|
|
194
|
+
this.controller.abort(isOperationError(reason) ? cloneOperationError(reason, stage) : new OperationAbortedError(stage, { cause: reason }));
|
|
195
|
+
};
|
|
196
|
+
if (this.parentSignal?.aborted === true) this.abortFromParent();
|
|
197
|
+
else this.parentSignal?.addEventListener("abort", this.abortFromParent, { once: true });
|
|
198
|
+
if (context.deadlineMs === void 0) this.deadlineTimer = void 0;
|
|
199
|
+
else if (context.deadlineMs <= Date.now()) {
|
|
200
|
+
this.deadlineTimer = void 0;
|
|
201
|
+
this.controller.abort(new OperationDeadlineExceededError(stage));
|
|
202
|
+
} else this.deadlineTimer = setTimeout(() => {
|
|
203
|
+
this.controller.abort(new OperationDeadlineExceededError(stage));
|
|
204
|
+
}, context.deadlineMs - Date.now());
|
|
205
|
+
this.context = Object.freeze({
|
|
206
|
+
signal: this.controller.signal,
|
|
207
|
+
deadlineMs: context.deadlineMs
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
abortForShutdown(stage) {
|
|
211
|
+
this.controller.abort(new OperationShutdownError(stage));
|
|
212
|
+
}
|
|
213
|
+
abort(reason) {
|
|
214
|
+
this.controller.abort(reason);
|
|
215
|
+
}
|
|
216
|
+
race(promise) {
|
|
217
|
+
return raceWithContext(promise, this.context);
|
|
218
|
+
}
|
|
219
|
+
cleanup() {
|
|
220
|
+
if (this.deadlineTimer !== void 0) clearTimeout(this.deadlineTimer);
|
|
221
|
+
this.parentSignal?.removeEventListener("abort", this.abortFromParent);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
function raceWithContext(promise, context) {
|
|
225
|
+
const signal = context.signal;
|
|
226
|
+
if (signal?.aborted === true) return Promise.reject(signal.reason ?? new OperationAbortedError("operation"));
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const onAbort = () => {
|
|
229
|
+
signal?.removeEventListener("abort", onAbort);
|
|
230
|
+
reject(signal?.reason ?? new OperationAbortedError("operation"));
|
|
231
|
+
};
|
|
232
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
233
|
+
promise.then((value) => {
|
|
234
|
+
signal?.removeEventListener("abort", onAbort);
|
|
235
|
+
resolve(value);
|
|
236
|
+
}, (error) => {
|
|
237
|
+
signal?.removeEventListener("abort", onAbort);
|
|
238
|
+
reject(error);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function operationReason(signal) {
|
|
243
|
+
return isOperationError(signal.reason) ? signal.reason : new OperationAbortedError("operation", { cause: signal.reason });
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/puppeteer-render-runtime.ts
|
|
247
|
+
const DEFAULT_CONCURRENCY = 2;
|
|
248
|
+
const DEFAULT_MAX_QUEUE_SIZE = 32;
|
|
249
|
+
const INTERNAL_LAUNCH_TIMEOUT_MS = 1e4;
|
|
250
|
+
const INTERNAL_CLEANUP_TIMEOUT_MS = 2e3;
|
|
251
|
+
const DENY_NETWORK_PROXY = "http://127.0.0.1:9";
|
|
252
|
+
var PuppeteerRenderRuntime = class {
|
|
253
|
+
launcher;
|
|
254
|
+
launchOptions;
|
|
255
|
+
concurrency;
|
|
256
|
+
maxQueueSize;
|
|
257
|
+
contextOptions;
|
|
258
|
+
pendingJobs;
|
|
259
|
+
activeScopes;
|
|
260
|
+
idleResolvers;
|
|
261
|
+
activeJobs;
|
|
262
|
+
browserPromise;
|
|
263
|
+
currentBrowser;
|
|
264
|
+
launchController;
|
|
265
|
+
launchWaiters;
|
|
266
|
+
launchPending;
|
|
267
|
+
launchAbandoned;
|
|
268
|
+
launchError;
|
|
269
|
+
forceCloseBrowser;
|
|
270
|
+
forceClosePromise;
|
|
271
|
+
closePromise;
|
|
272
|
+
closeBehavior;
|
|
273
|
+
accepting;
|
|
274
|
+
constructor(options) {
|
|
275
|
+
const validated = snapshotNodeOptions(options, [
|
|
276
|
+
"launcher",
|
|
277
|
+
"launchOptions",
|
|
278
|
+
"concurrency",
|
|
279
|
+
"maxQueueSize"
|
|
280
|
+
], "PuppeteerRenderRuntime");
|
|
281
|
+
assertNodeMethod(validated.launcher, "launch", "BrowserLauncher");
|
|
282
|
+
const launchOptions = validated.launchOptions === void 0 ? void 0 : snapshotOpenNodeOptions(validated.launchOptions, "Puppeteer launchOptions");
|
|
283
|
+
this.launcher = validated.launcher;
|
|
284
|
+
this.launchOptions = {
|
|
285
|
+
headless: "shell",
|
|
286
|
+
...launchOptions
|
|
287
|
+
};
|
|
288
|
+
this.concurrency = validated.concurrency ?? DEFAULT_CONCURRENCY;
|
|
289
|
+
this.maxQueueSize = validated.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
290
|
+
this.contextOptions = {
|
|
291
|
+
proxyServer: DENY_NETWORK_PROXY,
|
|
292
|
+
proxyBypassList: ["<-loopback>"],
|
|
293
|
+
downloadBehavior: { policy: "deny" }
|
|
294
|
+
};
|
|
295
|
+
this.pendingJobs = [];
|
|
296
|
+
this.activeScopes = /* @__PURE__ */ new Set();
|
|
297
|
+
this.idleResolvers = [];
|
|
298
|
+
this.activeJobs = 0;
|
|
299
|
+
this.browserPromise = void 0;
|
|
300
|
+
this.currentBrowser = void 0;
|
|
301
|
+
this.launchController = void 0;
|
|
302
|
+
this.launchWaiters = /* @__PURE__ */ new Set();
|
|
303
|
+
this.launchPending = false;
|
|
304
|
+
this.launchAbandoned = false;
|
|
305
|
+
this.launchError = void 0;
|
|
306
|
+
this.forceCloseBrowser = void 0;
|
|
307
|
+
this.forceClosePromise = void 0;
|
|
308
|
+
this.closePromise = void 0;
|
|
309
|
+
this.closeBehavior = void 0;
|
|
310
|
+
this.accepting = true;
|
|
311
|
+
validatePositiveInteger(this.concurrency, "concurrency");
|
|
312
|
+
validateNonNegativeInteger(this.maxQueueSize, "maxQueueSize");
|
|
313
|
+
}
|
|
314
|
+
run(job, operation) {
|
|
315
|
+
if (!this.accepting) return Promise.reject(new OperationShutdownError("render_admission"));
|
|
316
|
+
if (this.activeJobs >= this.concurrency && this.pendingJobs.length >= this.maxQueueSize) return Promise.reject(new NodeRenderError(NodeRenderErrorCode.QueueFull, `Render queue is full (max ${String(this.maxQueueSize)} pending)`, {
|
|
317
|
+
stage: "admission",
|
|
318
|
+
retryable: true
|
|
319
|
+
}));
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
const scope = new OperationScope(operation, "render");
|
|
322
|
+
let pending;
|
|
323
|
+
const abortWhileQueued = () => {
|
|
324
|
+
const index = this.pendingJobs.indexOf(pending);
|
|
325
|
+
if (index >= 0) {
|
|
326
|
+
this.pendingJobs.splice(index, 1);
|
|
327
|
+
pending.reject(operationReason(requireSignal(scope.context)));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
pending = {
|
|
331
|
+
start: () => {
|
|
332
|
+
scope.context.signal?.removeEventListener("abort", abortWhileQueued);
|
|
333
|
+
this.activeScopes.add(scope);
|
|
334
|
+
this.startUnderlying(job, scope, resolve, reject);
|
|
335
|
+
},
|
|
336
|
+
reject: (error) => {
|
|
337
|
+
scope.context.signal?.removeEventListener("abort", abortWhileQueued);
|
|
338
|
+
scope.cleanup();
|
|
339
|
+
reject(error);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
if (scope.context.signal?.aborted === true) {
|
|
343
|
+
pending.reject(operationReason(requireSignal(scope.context)));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (this.activeJobs < this.concurrency) {
|
|
347
|
+
this.activeJobs += 1;
|
|
348
|
+
pending.start();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
scope.context.signal?.addEventListener("abort", abortWhileQueued, { once: true });
|
|
352
|
+
this.pendingJobs.push(pending);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
warmup(operation) {
|
|
356
|
+
return this.run(async () => void 0, operation);
|
|
357
|
+
}
|
|
358
|
+
close(operation, behavior = "cancel") {
|
|
359
|
+
if (this.closePromise !== void 0) return this.closePromise;
|
|
360
|
+
const closeScope = new OperationScope(operation, "browser_close");
|
|
361
|
+
this.accepting = false;
|
|
362
|
+
this.closeBehavior = behavior;
|
|
363
|
+
if (behavior === "cancel") {
|
|
364
|
+
const shutdown = new OperationShutdownError("render");
|
|
365
|
+
for (const pending of this.pendingJobs.splice(0)) pending.reject(shutdown);
|
|
366
|
+
for (const scope of this.activeScopes) scope.abortForShutdown("render");
|
|
367
|
+
this.abandonLaunch(shutdown);
|
|
368
|
+
}
|
|
369
|
+
this.closePromise = this.finishClose(closeScope.context).finally(() => {
|
|
370
|
+
closeScope.cleanup();
|
|
371
|
+
});
|
|
372
|
+
return this.closePromise;
|
|
373
|
+
}
|
|
374
|
+
status() {
|
|
375
|
+
return Object.freeze({
|
|
376
|
+
accepting: this.accepting,
|
|
377
|
+
active: this.activeJobs,
|
|
378
|
+
queued: this.pendingJobs.length,
|
|
379
|
+
retainedBytes: 0
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
startUnderlying(job, scope, resolve, reject) {
|
|
383
|
+
const underlying = this.executeUnderlying(job, scope);
|
|
384
|
+
scope.race(underlying).then(resolve, reject);
|
|
385
|
+
underlying.then(() => {
|
|
386
|
+
this.finishUnderlying(scope);
|
|
387
|
+
}, () => {
|
|
388
|
+
this.finishUnderlying(scope);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async executeUnderlying(job, scope) {
|
|
392
|
+
let browser;
|
|
393
|
+
let context;
|
|
394
|
+
let contextClosePromise;
|
|
395
|
+
let contextCleanupPromise;
|
|
396
|
+
let outcome;
|
|
397
|
+
const closeContext = () => {
|
|
398
|
+
if (context === void 0) return Promise.resolve();
|
|
399
|
+
contextClosePromise ??= Promise.resolve().then(() => context.close());
|
|
400
|
+
return contextClosePromise;
|
|
401
|
+
};
|
|
402
|
+
const cleanupContext = () => {
|
|
403
|
+
contextCleanupPromise ??= cleanupWithDeadline(closeContext(), async () => {
|
|
404
|
+
await this.forceCloseBrowserOnce(browser);
|
|
405
|
+
});
|
|
406
|
+
return contextCleanupPromise;
|
|
407
|
+
};
|
|
408
|
+
const requestContextClose = () => {
|
|
409
|
+
if (context !== void 0) cleanupContext().catch(() => void 0);
|
|
410
|
+
};
|
|
411
|
+
scope.context.signal?.addEventListener("abort", requestContextClose, { once: true });
|
|
412
|
+
try {
|
|
413
|
+
browser = await this.getBrowser(scope);
|
|
414
|
+
throwIfContextEnded(scope.context);
|
|
415
|
+
try {
|
|
416
|
+
context = await browser.createBrowserContext(this.contextOptions);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (isBrowserDisconnected(error)) throw error;
|
|
419
|
+
throw new NodeRenderError(NodeRenderErrorCode.ContextFailed, "Browser context creation failed", {
|
|
420
|
+
stage: "context_create",
|
|
421
|
+
retryable: true,
|
|
422
|
+
cause: error
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
throwIfContextEnded(scope.context);
|
|
426
|
+
outcome = {
|
|
427
|
+
ok: true,
|
|
428
|
+
value: await Promise.resolve().then(() => job(context, scope.context))
|
|
429
|
+
};
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (scope.context.signal?.aborted === true) outcome = {
|
|
432
|
+
ok: false,
|
|
433
|
+
error: operationReason(requireSignal(scope.context))
|
|
434
|
+
};
|
|
435
|
+
else if (isBrowserDisconnected(error)) outcome = {
|
|
436
|
+
ok: false,
|
|
437
|
+
error: new NodeRenderError(NodeRenderErrorCode.BrowserDisconnected, "Chromium disconnected during rendering", {
|
|
438
|
+
stage: "browser",
|
|
439
|
+
retryable: true,
|
|
440
|
+
cause: error
|
|
441
|
+
})
|
|
442
|
+
};
|
|
443
|
+
else outcome = {
|
|
444
|
+
ok: false,
|
|
445
|
+
error: normalizeNodeRenderError(error, NodeRenderErrorCode.RenderFailed, "Browser rendering failed", "render", true)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
scope.context.signal?.removeEventListener("abort", requestContextClose);
|
|
449
|
+
if (context !== void 0) try {
|
|
450
|
+
await cleanupContext();
|
|
451
|
+
} catch (cleanupError) {
|
|
452
|
+
if (outcome.ok || cleanupError instanceof NodeRenderError && cleanupError.stage === "browser_cleanup") outcome = {
|
|
453
|
+
ok: false,
|
|
454
|
+
error: cleanupError
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (outcome.ok) return outcome.value;
|
|
458
|
+
throw outcome.error;
|
|
459
|
+
}
|
|
460
|
+
finishUnderlying(scope) {
|
|
461
|
+
scope.cleanup();
|
|
462
|
+
this.activeScopes.delete(scope);
|
|
463
|
+
this.activeJobs -= 1;
|
|
464
|
+
const next = this.pendingJobs.shift();
|
|
465
|
+
if (next !== void 0 && this.closeBehavior !== "cancel") {
|
|
466
|
+
this.activeJobs += 1;
|
|
467
|
+
next.start();
|
|
468
|
+
} else if (next !== void 0) next.reject(new OperationShutdownError("render"));
|
|
469
|
+
if (this.activeJobs === 0 && this.pendingJobs.length === 0) for (const resolve of this.idleResolvers.splice(0)) resolve();
|
|
470
|
+
}
|
|
471
|
+
getBrowser(scope) {
|
|
472
|
+
if (this.browserPromise !== void 0) {
|
|
473
|
+
if (this.launchPending) {
|
|
474
|
+
this.launchWaiters.add(scope);
|
|
475
|
+
if (this.launchError !== void 0) scope.abort(this.launchError);
|
|
476
|
+
}
|
|
477
|
+
return this.browserPromise;
|
|
478
|
+
}
|
|
479
|
+
this.forceCloseBrowser = void 0;
|
|
480
|
+
this.forceClosePromise = void 0;
|
|
481
|
+
this.launchPending = true;
|
|
482
|
+
this.launchAbandoned = false;
|
|
483
|
+
this.launchError = void 0;
|
|
484
|
+
this.launchWaiters.add(scope);
|
|
485
|
+
const launchController = new AbortController();
|
|
486
|
+
this.launchController = launchController;
|
|
487
|
+
const deadlineMs = Date.now() + INTERNAL_LAUNCH_TIMEOUT_MS;
|
|
488
|
+
const launchOperation = {
|
|
489
|
+
signal: launchController.signal,
|
|
490
|
+
deadlineMs
|
|
491
|
+
};
|
|
492
|
+
const timeout = setTimeout(() => {
|
|
493
|
+
const error = new NodeRenderError(NodeRenderErrorCode.BrowserFailed, "Chromium launch deadline exceeded", {
|
|
494
|
+
stage: "browser_launch",
|
|
495
|
+
retryable: true
|
|
496
|
+
});
|
|
497
|
+
this.abandonLaunch(error);
|
|
498
|
+
}, INTERNAL_LAUNCH_TIMEOUT_MS);
|
|
499
|
+
const launched = Promise.resolve().then(() => this.launcher.launch(this.launchOptions, launchOperation));
|
|
500
|
+
let tracked;
|
|
501
|
+
tracked = Promise.resolve(launched).then(async (browser) => {
|
|
502
|
+
clearTimeout(timeout);
|
|
503
|
+
this.launchPending = false;
|
|
504
|
+
this.launchWaiters.clear();
|
|
505
|
+
if (this.launchAbandoned || !this.accepting) {
|
|
506
|
+
await this.forceCloseHandleOnce(browser);
|
|
507
|
+
if (this.browserPromise === tracked) this.browserPromise = void 0;
|
|
508
|
+
return browser;
|
|
509
|
+
}
|
|
510
|
+
this.currentBrowser = browser;
|
|
511
|
+
browser.on("disconnected", () => {
|
|
512
|
+
if (this.browserPromise === tracked) {
|
|
513
|
+
this.browserPromise = void 0;
|
|
514
|
+
this.currentBrowser = void 0;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return browser;
|
|
518
|
+
}).catch((error) => {
|
|
519
|
+
clearTimeout(timeout);
|
|
520
|
+
this.launchPending = false;
|
|
521
|
+
this.launchWaiters.clear();
|
|
522
|
+
if (this.browserPromise === tracked) this.browserPromise = void 0;
|
|
523
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.BrowserFailed, "Chromium launch failed", "browser_launch", true);
|
|
524
|
+
});
|
|
525
|
+
this.browserPromise = tracked;
|
|
526
|
+
return tracked;
|
|
527
|
+
}
|
|
528
|
+
async finishClose(operation) {
|
|
529
|
+
try {
|
|
530
|
+
await raceWithContext(this.waitForIdle(), operation);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.closeBehavior = "cancel";
|
|
533
|
+
const shutdown = new OperationShutdownError("render");
|
|
534
|
+
for (const pending of this.pendingJobs.splice(0)) pending.reject(shutdown);
|
|
535
|
+
for (const scope of this.activeScopes) scope.abortForShutdown("render");
|
|
536
|
+
await this.forceCloseBrowserOnce();
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
const browserPromise = this.browserPromise;
|
|
540
|
+
if (browserPromise === void 0) return;
|
|
541
|
+
let browser;
|
|
542
|
+
try {
|
|
543
|
+
browser = await raceWithContext(browserPromise, operation);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
if (isOperationError(error) && error.code === OperationErrorCode.Shutdown) return;
|
|
546
|
+
await this.forceCloseBrowserOnce();
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
if (!browser.connected) return;
|
|
550
|
+
try {
|
|
551
|
+
await raceWithContext(cleanupWithDeadline(browser.close(), () => this.forceCloseBrowserOnce(browser)), operation);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
await this.forceCloseBrowserOnce(browser);
|
|
554
|
+
if (isOperationError(error)) throw error;
|
|
555
|
+
throw new NodeRenderError(NodeRenderErrorCode.CloseFailed, "Chromium close failed", {
|
|
556
|
+
stage: "browser_close",
|
|
557
|
+
retryable: true,
|
|
558
|
+
cause: error
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
waitForIdle() {
|
|
563
|
+
if (this.activeJobs === 0 && this.pendingJobs.length === 0) return Promise.resolve();
|
|
564
|
+
return new Promise((resolve) => {
|
|
565
|
+
this.idleResolvers.push(resolve);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
forceCloseBrowserOnce(browser) {
|
|
569
|
+
if (this.launchPending) {
|
|
570
|
+
this.abandonLaunch(new OperationShutdownError("browser_close"));
|
|
571
|
+
return Promise.resolve();
|
|
572
|
+
}
|
|
573
|
+
if (browser !== void 0) return this.forceCloseHandleOnce(browser);
|
|
574
|
+
const browserPromise = this.browserPromise;
|
|
575
|
+
return browserPromise === void 0 ? Promise.resolve() : browserPromise.then((handle) => this.forceCloseHandleOnce(handle), () => void 0);
|
|
576
|
+
}
|
|
577
|
+
abandonLaunch(error) {
|
|
578
|
+
if (!this.launchPending) return;
|
|
579
|
+
this.launchAbandoned = true;
|
|
580
|
+
this.launchError = error;
|
|
581
|
+
this.launchController?.abort(error);
|
|
582
|
+
for (const waiter of this.launchWaiters) waiter.abort(error);
|
|
583
|
+
}
|
|
584
|
+
forceCloseHandleOnce(browser) {
|
|
585
|
+
if (this.currentBrowser === browser) {
|
|
586
|
+
this.currentBrowser = void 0;
|
|
587
|
+
this.browserPromise = void 0;
|
|
588
|
+
}
|
|
589
|
+
if (this.forceCloseBrowser !== browser || this.forceClosePromise === void 0) {
|
|
590
|
+
this.forceCloseBrowser = browser;
|
|
591
|
+
this.forceClosePromise = browser.forceClose().catch((error) => {
|
|
592
|
+
throw new NodeRenderError(NodeRenderErrorCode.CleanupFailed, "Browser cleanup failed", {
|
|
593
|
+
stage: "browser_cleanup",
|
|
594
|
+
retryable: true,
|
|
595
|
+
cause: error
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
return this.forceClosePromise;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
async function importPuppeteerAndLaunch(options) {
|
|
603
|
+
const { launch } = await import("puppeteer");
|
|
604
|
+
return new PuppeteerBrowserHandle(await launch(options));
|
|
605
|
+
}
|
|
606
|
+
var PuppeteerBrowserLauncher = class {
|
|
607
|
+
launchBrowser;
|
|
608
|
+
constructor(launchBrowser = importPuppeteerAndLaunch) {
|
|
609
|
+
this.launchBrowser = launchBrowser;
|
|
610
|
+
}
|
|
611
|
+
async launch(options, operation) {
|
|
612
|
+
const deadlineMs = operation.deadlineMs ?? Date.now() + INTERNAL_LAUNCH_TIMEOUT_MS;
|
|
613
|
+
const timeout = Math.max(1, deadlineMs - Date.now());
|
|
614
|
+
try {
|
|
615
|
+
const launching = this.launchBrowser({
|
|
616
|
+
...options,
|
|
617
|
+
timeout: Math.min(options.timeout ?? timeout, timeout)
|
|
618
|
+
});
|
|
619
|
+
try {
|
|
620
|
+
return await raceWithContext(launching, operation);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
await launching.then((handle) => handle.forceClose().catch(() => void 0), () => void 0);
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (isMissingBrowserError(error)) throw new NodeRenderError(NodeRenderErrorCode.BrowserBinaryMissing, "Chromium executable is unavailable", {
|
|
627
|
+
stage: "browser_launch",
|
|
628
|
+
retryable: false,
|
|
629
|
+
cause: error
|
|
630
|
+
});
|
|
631
|
+
throw normalizeNodeRenderError(error, NodeRenderErrorCode.BrowserFailed, "Chromium launch failed", "browser_launch", true);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
var PuppeteerBrowserHandle = class {
|
|
636
|
+
browser;
|
|
637
|
+
constructor(browser) {
|
|
638
|
+
this.browser = browser;
|
|
639
|
+
}
|
|
640
|
+
get connected() {
|
|
641
|
+
return this.browser.connected;
|
|
642
|
+
}
|
|
643
|
+
createBrowserContext(options) {
|
|
644
|
+
return this.browser.createBrowserContext(options);
|
|
645
|
+
}
|
|
646
|
+
close() {
|
|
647
|
+
return this.browser.close();
|
|
648
|
+
}
|
|
649
|
+
async forceClose() {
|
|
650
|
+
this.browser.process()?.kill("SIGKILL");
|
|
651
|
+
try {
|
|
652
|
+
await this.browser.disconnect();
|
|
653
|
+
} catch {}
|
|
654
|
+
}
|
|
655
|
+
on(event, handler) {
|
|
656
|
+
this.browser.on(event, handler);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
function cleanupWithDeadline(cleanup, forceCleanup) {
|
|
660
|
+
return new Promise((resolve, reject) => {
|
|
661
|
+
let settled = false;
|
|
662
|
+
let closedGracefully = false;
|
|
663
|
+
const timeout = setTimeout(() => {
|
|
664
|
+
if (settled) return;
|
|
665
|
+
settled = true;
|
|
666
|
+
forceCleanup().then(() => {
|
|
667
|
+
if (closedGracefully) {
|
|
668
|
+
resolve();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
reject(new NodeRenderError(NodeRenderErrorCode.CleanupFailed, "Cleanup deadline exceeded", {
|
|
672
|
+
stage: "context_cleanup",
|
|
673
|
+
retryable: true
|
|
674
|
+
}));
|
|
675
|
+
}, reject);
|
|
676
|
+
}, INTERNAL_CLEANUP_TIMEOUT_MS);
|
|
677
|
+
cleanup.then(() => {
|
|
678
|
+
closedGracefully = true;
|
|
679
|
+
if (!settled) {
|
|
680
|
+
settled = true;
|
|
681
|
+
clearTimeout(timeout);
|
|
682
|
+
resolve();
|
|
683
|
+
}
|
|
684
|
+
}, (error) => {
|
|
685
|
+
if (!settled) {
|
|
686
|
+
settled = true;
|
|
687
|
+
clearTimeout(timeout);
|
|
688
|
+
forceCleanup().then(() => {
|
|
689
|
+
reject(new NodeRenderError(NodeRenderErrorCode.CleanupFailed, "Browser context cleanup failed", {
|
|
690
|
+
stage: "context_cleanup",
|
|
691
|
+
retryable: true,
|
|
692
|
+
cause: error
|
|
693
|
+
}));
|
|
694
|
+
}, reject);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
function throwIfContextEnded(operation) {
|
|
700
|
+
if (operation.signal?.aborted === true) throw operationReason(operation.signal);
|
|
701
|
+
if (operation.deadlineMs !== void 0 && Date.now() >= operation.deadlineMs) throw new OperationDeadlineExceededError("render");
|
|
702
|
+
}
|
|
703
|
+
function requireSignal(operation) {
|
|
704
|
+
if (operation.signal === void 0) throw new OperationShutdownError("operation");
|
|
705
|
+
return operation.signal;
|
|
706
|
+
}
|
|
707
|
+
function isBrowserDisconnected(error) {
|
|
708
|
+
return error instanceof Error && (error.message.includes("disconnected") || error.message.includes("Target closed") || error.message.includes("Connection closed"));
|
|
709
|
+
}
|
|
710
|
+
function validatePositiveInteger(value, name) {
|
|
711
|
+
if (!Number.isInteger(value) || value <= 0) throw invalidNodeOptions(`${name} must be a positive integer`);
|
|
712
|
+
}
|
|
713
|
+
function validateNonNegativeInteger(value, name) {
|
|
714
|
+
if (!Number.isInteger(value) || value < 0) throw invalidNodeOptions(`${name} must be a non-negative integer`);
|
|
715
|
+
}
|
|
716
|
+
function isMissingBrowserError(error) {
|
|
717
|
+
if (!(error instanceof Error)) return false;
|
|
718
|
+
return error.message.includes("Could not find Chrome") || error.message.includes("Browser was not found") || error.message.includes("executable doesn't exist");
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
export { LocalKatexAssetLoader, LocalMermaidAssetLoader, PuppeteerBrowserLauncher, PuppeteerRenderRuntime };
|
|
722
|
+
|
|
723
|
+
//# sourceMappingURL=puppeteer.js.map
|