@tlockney/cdp-kernel 0.7.0 → 0.7.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 +4 -2
- package/dist/browser/kernel.js +136 -10
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/mod.d.ts +6 -1
- package/dist/browser/mod.js +4 -1
- package/dist/browser/target.d.ts +23 -0
- package/dist/browser/target.js +62 -4
- package/dist/browser/worker.js +83 -12
- package/dist/browser/worker.js.map +3 -3
- package/dist/bun-target.d.ts +2 -0
- package/dist/bun-target.js +15 -2
- package/dist/cdp.d.ts +2 -0
- package/dist/cdp.js +34 -7
- package/dist/celld-target.js +3 -2
- package/dist/debugger.js +51 -5
- package/dist/kernel-core.d.ts +9 -0
- package/dist/kernel-core.js +73 -3
- package/dist/kernel.js +12 -0
- package/dist/main.js +11 -4
- package/dist/package.d.ts +1 -1
- package/dist/package.js +1 -1
- package/dist/target-api.d.ts +12 -0
- package/dist/target-api.js +26 -0
- package/dist/target.d.ts +3 -2
- package/dist/target.js +25 -13
- package/dist/workerd-target.d.ts +6 -1
- package/dist/workerd-target.js +56 -12
- package/dist/workers-project.d.ts +1 -1
- package/dist/workers-project.js +4 -3
- package/package.json +3 -1
- package/runtime/bootstrap.mjs +2 -0
- package/runtime/browser-worker.mjs +83 -0
- package/runtime/evaluator.mjs +2 -0
- package/runtime/workerd-worker.mjs +2 -0
package/dist/workerd-target.js
CHANGED
|
@@ -18,7 +18,7 @@ import { Buffer } from "node:buffer";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
20
20
|
import { randomBytes } from "node:crypto";
|
|
21
|
-
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { copyFileSync, existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { builtinModules } from "node:module";
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
@@ -28,7 +28,8 @@ import { asEvaluateResult } from "./cdp.js";
|
|
|
28
28
|
import { transformCellForEval } from "./transform.js";
|
|
29
29
|
import { runtimeDir } from "./package.js";
|
|
30
30
|
import { killOnExit } from "./children.js";
|
|
31
|
-
import { Interrupted, isTargetMessage } from "./target.js";
|
|
31
|
+
import { Interrupted, isTargetMessage, parseBundle, parseJson } from "./target.js";
|
|
32
|
+
import { withStartTimeout } from "./target-api.js";
|
|
32
33
|
import { connectUnixWebSocket } from "./ws-client.js";
|
|
33
34
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
34
35
|
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
@@ -188,8 +189,30 @@ function isWorkerdIn(pid, dir) {
|
|
|
188
189
|
try {
|
|
189
190
|
if (process.platform === "linux")
|
|
190
191
|
return readlinkSync(`/proc/${pid}/cwd`) === dir;
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
// No cwd to compare elsewhere: the process must at least be ours and
|
|
193
|
+
// look like a workerd (#335).
|
|
194
|
+
const [uid, ...args] = execFileSync("ps", ["-o", "uid=,args=", "-p", String(pid)], { encoding: "utf8" }).trim().split(/\s+/);
|
|
195
|
+
const command = args.join(" ");
|
|
196
|
+
return Number(uid) === process.getuid?.() && /workerd/.test(command) && command.includes("config.capnp");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether `dir` is a directory mkdtemp made for this user: not a symlink,
|
|
204
|
+
* owned by us, and closed to everyone else. Anyone can create entries in
|
|
205
|
+
* tmpdir(), so nothing in a directory that fails this is trusted (#335).
|
|
206
|
+
* Windows has no uids; its tmpdir() is per-user.
|
|
207
|
+
*/
|
|
208
|
+
function isOwnPrivateDir(dir) {
|
|
209
|
+
try {
|
|
210
|
+
const stat = lstatSync(dir);
|
|
211
|
+
if (!stat.isDirectory())
|
|
212
|
+
return false;
|
|
213
|
+
if (process.getuid === undefined)
|
|
214
|
+
return true;
|
|
215
|
+
return stat.uid === process.getuid() && (stat.mode & 0o077) === 0;
|
|
193
216
|
}
|
|
194
217
|
catch {
|
|
195
218
|
return false;
|
|
@@ -210,12 +233,14 @@ export function sweepStaleWorkerdDirs(root = tmpdir()) {
|
|
|
210
233
|
}
|
|
211
234
|
for (const name of names) {
|
|
212
235
|
const dir = join(root, name);
|
|
236
|
+
if (!isOwnPrivateDir(dir))
|
|
237
|
+
continue;
|
|
213
238
|
let owner;
|
|
214
239
|
try {
|
|
215
240
|
owner = JSON.parse(readFileSync(join(dir, OWNER_FILE), "utf8"));
|
|
216
241
|
}
|
|
217
242
|
catch {
|
|
218
|
-
continue; //
|
|
243
|
+
continue; // From before owner files, or being set up.
|
|
219
244
|
}
|
|
220
245
|
if (typeof owner.host !== "number" || pidAlive(owner.host))
|
|
221
246
|
continue;
|
|
@@ -227,7 +252,12 @@ export function sweepStaleWorkerdDirs(root = tmpdir()) {
|
|
|
227
252
|
// Gone already.
|
|
228
253
|
}
|
|
229
254
|
}
|
|
230
|
-
|
|
255
|
+
try {
|
|
256
|
+
rmSync(dir, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Cleanup is best effort; it must not stop this kernel starting.
|
|
260
|
+
}
|
|
231
261
|
}
|
|
232
262
|
}
|
|
233
263
|
/** The driver on a workerd of its own, behind a Unix socket. */
|
|
@@ -295,7 +325,7 @@ export class RawWorkerd {
|
|
|
295
325
|
}
|
|
296
326
|
})();
|
|
297
327
|
try {
|
|
298
|
-
const host = new RawWorkerd(child, await Promise.race([ready, exited]), compatibilityDate);
|
|
328
|
+
const host = new RawWorkerd(child, await withStartTimeout(Promise.race([ready, exited]), "workerd"), compatibilityDate);
|
|
299
329
|
starting = false;
|
|
300
330
|
early = "";
|
|
301
331
|
return host;
|
|
@@ -360,7 +390,7 @@ export class WorkerdTarget {
|
|
|
360
390
|
let notice;
|
|
361
391
|
if (project !== null) {
|
|
362
392
|
try {
|
|
363
|
-
host = await startMiniflare(project, cwd);
|
|
393
|
+
host = await startMiniflare(project, cwd, options.startTimeoutMs);
|
|
364
394
|
}
|
|
365
395
|
catch (err) {
|
|
366
396
|
// Like a broken deno.json (#13): say why in the first cell, and run
|
|
@@ -370,16 +400,18 @@ export class WorkerdTarget {
|
|
|
370
400
|
}
|
|
371
401
|
}
|
|
372
402
|
host ??= await RawWorkerd.start(cwd, options.executable);
|
|
373
|
-
return await WorkerdTarget.open(host, cwd, events, "workerd", notice);
|
|
403
|
+
return await WorkerdTarget.open(host, cwd, events, "workerd", { notice, startTimeoutMs: options.startTimeoutMs });
|
|
374
404
|
}
|
|
375
405
|
/**
|
|
376
406
|
* A target over a started host's session. `notice` (a project that
|
|
377
407
|
* failed to load) is replaced by the bindings when the host has a project.
|
|
378
408
|
*/
|
|
379
|
-
static async open(host, cwd, events, runtime,
|
|
409
|
+
static async open(host, cwd, events, runtime, options = {}) {
|
|
380
410
|
const target = new WorkerdTarget(host, cwd, events, runtime);
|
|
411
|
+
let notice = options.notice;
|
|
381
412
|
try {
|
|
382
|
-
|
|
413
|
+
// A driver that never answers is stopped (#336).
|
|
414
|
+
const { v8, bindings, worker } = await withStartTimeout(target.#request("info"), `the ${runtime} driver`, options.startTimeoutMs);
|
|
383
415
|
if (typeof v8 === "string" && v8)
|
|
384
416
|
target.versions.engine = `V8 ${v8}`;
|
|
385
417
|
if (host.project !== undefined) {
|
|
@@ -396,7 +428,15 @@ export class WorkerdTarget {
|
|
|
396
428
|
return target;
|
|
397
429
|
}
|
|
398
430
|
#receive(text) {
|
|
399
|
-
const msg =
|
|
431
|
+
const msg = parseJson(text);
|
|
432
|
+
if (msg === undefined) {
|
|
433
|
+
// The driver only sends JSON; a frame that isn't means the session is
|
|
434
|
+
// broken, which ends the target rather than the kernel (#337).
|
|
435
|
+
this.#stopNote = `[cdp-kernel] ${this.runtime} sent a message that isn't JSON, so it was stopped; ` +
|
|
436
|
+
"it restarts on the next cell, without the earlier cells' state";
|
|
437
|
+
this.kill();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
400
440
|
if (isRecord(msg) && msg.kind === "stream") {
|
|
401
441
|
this.#events.stream(msg.name === "stderr" ? "stderr" : "stdout", String(msg.text));
|
|
402
442
|
}
|
|
@@ -519,6 +559,10 @@ export class WorkerdTarget {
|
|
|
519
559
|
}
|
|
520
560
|
return { handled: reply.handled === true };
|
|
521
561
|
}
|
|
562
|
+
async format(value) {
|
|
563
|
+
const reply = await this.#request("format", { handle: value.objectId });
|
|
564
|
+
return parseBundle(reply.value);
|
|
565
|
+
}
|
|
522
566
|
async describeError(exception) {
|
|
523
567
|
if (exception.objectId === undefined)
|
|
524
568
|
return { name: "Error", message: exception.description ?? "", stack: "" };
|
|
@@ -24,4 +24,4 @@ export declare function bundleProject(main: string, define?: {
|
|
|
24
24
|
[name: string]: string;
|
|
25
25
|
}): Promise<string>;
|
|
26
26
|
/** Starts the driver on the project's Miniflare with its bindings. */
|
|
27
|
-
export declare function startMiniflare(configPath: string, cwd: string): Promise<WorkerHost>;
|
|
27
|
+
export declare function startMiniflare(configPath: string, cwd: string, startTimeoutMs?: number): Promise<WorkerHost>;
|
package/dist/workers-project.js
CHANGED
|
@@ -17,6 +17,7 @@ import { dirname, join } from "node:path";
|
|
|
17
17
|
import { build } from "esbuild";
|
|
18
18
|
import { childPids, reap } from "./children.js";
|
|
19
19
|
import { runtimeDir } from "./package.js";
|
|
20
|
+
import { withStartTimeout } from "./target-api.js";
|
|
20
21
|
import { cellMeta, WORKER_MODULES } from "./workerd-target.js";
|
|
21
22
|
export const WRANGLER_CONFIGS = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
22
23
|
/** The nearest wrangler config at or above `cwd`, or null. */
|
|
@@ -157,7 +158,7 @@ class MiniflareHost {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
/** Starts the driver on the project's Miniflare with its bindings. */
|
|
160
|
-
export async function startMiniflare(configPath, cwd) {
|
|
161
|
+
export async function startMiniflare(configPath, cwd, startTimeoutMs) {
|
|
161
162
|
const { wrangler, miniflare, version } = loadTooling(configPath);
|
|
162
163
|
const projectDir = dirname(configPath);
|
|
163
164
|
const { workerOptions, main, define } = wrangler.unstable_getMiniflareWorkerOptions(configPath);
|
|
@@ -190,10 +191,10 @@ export async function startMiniflare(configPath, cwd) {
|
|
|
190
191
|
const before = new Set(childPids(process.pid, "workerd"));
|
|
191
192
|
const mf = new miniflare.Miniflare(v5 && miniflare.convertV4MiniflareOptions ? miniflare.convertV4MiniflareOptions(options) : options);
|
|
192
193
|
try {
|
|
193
|
-
await mf.ready;
|
|
194
194
|
// Miniflare listens on a loopback TCP port; the token is what keeps
|
|
195
195
|
// other local processes from the driver.
|
|
196
|
-
const
|
|
196
|
+
const connect = mf.ready.then(() => mf.dispatchFetch("http://cdpk/", { headers: { Upgrade: "websocket", "x-cdpk-token": token } }));
|
|
197
|
+
const res = await withStartTimeout(connect, "Miniflare", startTimeoutMs);
|
|
197
198
|
if (res.status !== 101 || res.webSocket === null)
|
|
198
199
|
throw new Error(`the driver answered ${res.status}: ${await res.text()}`);
|
|
199
200
|
const workerd = childPids(process.pid, "workerd").filter((pid) => !before.has(pid));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tlockney/cdp-kernel",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Deno's Jupyter kernel, extracted to run notebooks on Node, Deno, Bun, workerd and celld",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Thomas Lockney <thomas@lockney.net>",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"docs:dev": "vitepress dev docs",
|
|
60
60
|
"docs:build": "vitepress build docs",
|
|
61
61
|
"docs:notebooks": "node docs/.vitepress/run-notebooks.ts",
|
|
62
|
+
"docs:debugger": "uv run --with jupyterlab --with playwright --with nbformat python test/debugger_screenshot.py",
|
|
62
63
|
"docs:widgets": "uv run --with jupyterlab --with anywidget --with playwright --with nbformat python test/widget_screenshots.py",
|
|
63
64
|
"install:node": "node src/main.ts install --runtime node",
|
|
64
65
|
"install:deno": "node src/main.ts install --runtime deno",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@types/node": "^24.19.2",
|
|
85
|
+
"medium-zoom": "^1.1.0",
|
|
84
86
|
"vitepress": "^1.6.4",
|
|
85
87
|
"workerd": "1.20261015.1",
|
|
86
88
|
"wrangler": "4.151.0"
|
package/runtime/bootstrap.mjs
CHANGED
|
@@ -215,6 +215,8 @@ globalThis.__cdpk = {
|
|
|
215
215
|
inspectPath: (path, detail) => inspectPath(path, { inspect: inspectPlain, detail }),
|
|
216
216
|
resolvePath,
|
|
217
217
|
broadcastResult: jupyter.broadcastResult,
|
|
218
|
+
// execute_request's user_expressions (#340).
|
|
219
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
218
220
|
// The debugger's rich variable viewer (#18), synchronously: it runs while
|
|
219
221
|
// the target is paused. A value whose $display is async shows as text.
|
|
220
222
|
richBundle(value) {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
//
|
|
9
9
|
// A Worker has no module resolution beyond URLs, so `npm:` and `jsr:`
|
|
10
10
|
// specifiers, and bare package names, load from a CDN (esm.sh by default).
|
|
11
|
+
//
|
|
12
|
+
// The page can also offer cells its own features (#384): `host` requests go
|
|
13
|
+
// to the page's handler, and `preload` modules run before the first cell.
|
|
11
14
|
|
|
12
15
|
import { createJupyter } from "./display.mjs";
|
|
13
16
|
import { createHandler } from "./evaluator.mjs";
|
|
@@ -68,6 +71,82 @@ const jupyter = createJupyter({
|
|
|
68
71
|
let cdn = DEFAULT_CDN;
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
// --- The host page (#384) -----------------------------------------------------
|
|
75
|
+
// Requests to the page's `host` handler (src/browser/target.ts), and events
|
|
76
|
+
// it sends. Preload modules get this object; cells only through them.
|
|
77
|
+
const hostCalls = new Map();
|
|
78
|
+
const hostListeners = new Set();
|
|
79
|
+
let nextHostId = 0;
|
|
80
|
+
|
|
81
|
+
function hostError(error) {
|
|
82
|
+
const err = new Error(String(error?.message ?? "the host request failed"));
|
|
83
|
+
if (typeof error?.name === "string") err.name = error.name;
|
|
84
|
+
if (error?.code !== undefined) err.code = error.code;
|
|
85
|
+
return err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const host = {
|
|
89
|
+
/** Resolves with the page's answer; aborting `signal` cancels the call on the page too. */
|
|
90
|
+
request(method, params, { signal, transfer } = {}) {
|
|
91
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
92
|
+
const id = ++nextHostId;
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const onAbort = () => {
|
|
95
|
+
hostCalls.delete(id);
|
|
96
|
+
post({ kind: "host", id, cancel: true });
|
|
97
|
+
reject(signal.reason);
|
|
98
|
+
};
|
|
99
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
100
|
+
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
101
|
+
try {
|
|
102
|
+
globalThis.postMessage({ kind: "host", id, method, params }, transfer ?? []);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
done(); // params structured clone can't copy
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
hostCalls.set(id, { resolve, reject, done });
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
/** Calls `listener(event, value)` for each event the page sends; returns a function that stops it. */
|
|
111
|
+
onEvent(listener) {
|
|
112
|
+
hostListeners.add(listener);
|
|
113
|
+
return () => hostListeners.delete(listener);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function receiveHost(msg) {
|
|
118
|
+
if (msg.id === undefined) {
|
|
119
|
+
for (const listener of hostListeners) {
|
|
120
|
+
try {
|
|
121
|
+
listener(msg.event, msg.value);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reportUncaught("Uncaught", err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const call = hostCalls.get(msg.id);
|
|
129
|
+
if (!call) return; // cancelled
|
|
130
|
+
hostCalls.delete(msg.id);
|
|
131
|
+
call.done();
|
|
132
|
+
if (msg.error !== undefined) call.reject(hostError(msg.error));
|
|
133
|
+
else call.resolve(msg.result);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Each module is imported in order, and a default export that is a function
|
|
137
|
+
// is called with the host. A module that fails is reported, and the kernel
|
|
138
|
+
// runs without it.
|
|
139
|
+
async function preload(urls, base) {
|
|
140
|
+
for (const specifier of Array.isArray(urls) ? urls : []) {
|
|
141
|
+
try {
|
|
142
|
+
const mod = await import(new URL(specifier, base).href);
|
|
143
|
+
if (typeof mod.default === "function") await mod.default({ host, jupyter: jupyter.namespace });
|
|
144
|
+
} catch (err) {
|
|
145
|
+
reportUncaught(`Preloading ${specifier} failed`, err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
// --- The shim cells and the evaluator use -----------------------------------
|
|
72
151
|
let handle = null;
|
|
73
152
|
|
|
@@ -83,6 +162,8 @@ function install({ base, cdn: cdnBase }) {
|
|
|
83
162
|
resolvePath,
|
|
84
163
|
barrier: () => 0,
|
|
85
164
|
broadcastResult: jupyter.broadcastResult,
|
|
165
|
+
// execute_request's user_expressions (#340).
|
|
166
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
86
167
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
87
168
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
88
169
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
@@ -104,6 +185,7 @@ function install({ base, cdn: cdnBase }) {
|
|
|
104
185
|
async function answer(req) {
|
|
105
186
|
if (req.op === "info") {
|
|
106
187
|
install(req);
|
|
188
|
+
await preload(req.preload, req.base);
|
|
107
189
|
return { userAgent: globalThis.navigator?.userAgent ?? "" };
|
|
108
190
|
}
|
|
109
191
|
if (handle === null) throw new Error("the driver wasn't sent info first");
|
|
@@ -112,6 +194,7 @@ async function answer(req) {
|
|
|
112
194
|
|
|
113
195
|
globalThis.addEventListener("message", (event) => {
|
|
114
196
|
const req = event.data;
|
|
197
|
+
if (req?.op === "host") return receiveHost(req);
|
|
115
198
|
answer(req).then(
|
|
116
199
|
(reply) => post({ kind: "reply", id: req.id, ...reply }),
|
|
117
200
|
(err) => post({ kind: "reply", id: req.id, failure: String(err?.stack ?? err) }),
|
package/runtime/evaluator.mjs
CHANGED
|
@@ -56,6 +56,8 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
56
56
|
case "broadcast":
|
|
57
57
|
await shim.broadcastResult(request.count, take(request.handle));
|
|
58
58
|
return {};
|
|
59
|
+
case "format":
|
|
60
|
+
return { value: await shim.formatJson(request.handle === undefined ? undefined : take(request.handle)) };
|
|
59
61
|
case "describe":
|
|
60
62
|
return { value: shim.describeError(take(request.handle)) };
|
|
61
63
|
case "comm":
|
|
@@ -141,6 +141,8 @@ function install(env) {
|
|
|
141
141
|
resolvePath,
|
|
142
142
|
barrier: () => 0,
|
|
143
143
|
broadcastResult: jupyter.broadcastResult,
|
|
144
|
+
// execute_request's user_expressions (#340).
|
|
145
|
+
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
144
146
|
async commDispatch(msgType, content, metadata, buffers64) {
|
|
145
147
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
146
148
|
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|