duckfn-docs-kit 0.2.1 → 0.3.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/AGENTS.md +14 -4
- package/bin/sql-verify.mjs +12 -12
- package/dist/index.js +1 -1
- package/dist/{register-DKLiYs-F.js → register-CALCwFBv.js} +32 -23
- package/dist/remark.d.ts +1 -1
- package/dist/sql/browserRunner.d.ts +41 -0
- package/dist/sql/browserRunner.js +186 -0
- package/dist/sql/client.js +1 -1
- package/dist/sql/harness.d.ts +59 -0
- package/dist/sql/harness.js +8328 -0
- package/dist/sql/runtime.d.ts +20 -0
- package/dist/sql/verify.d.ts +4 -5
- package/dist/sql/verify.js +36 -49
- package/package.json +3 -2
- package/src/remark.ts +1 -1
- package/src/sql/browserRunner.ts +402 -0
- package/src/sql/harness.ts +134 -0
- package/src/sql/runtime.ts +44 -0
- package/src/sql/verify.ts +23 -41
- package/dist/sql/nodeRunner.d.ts +0 -51
- package/dist/sql/nodeRunner.js +0 -115
- package/src/sql/nodeRunner.ts +0 -298
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `duckfn-docs-kit/sql/harness` — the browser page the offline SQL verifier
|
|
3
|
+
* (`sql/browserRunner`) drives with Playwright.
|
|
4
|
+
*
|
|
5
|
+
* It is deliberately thin: it reuses the site's own runtime (`sql/runtime`) so
|
|
6
|
+
* a verified block runs through exactly the code path a reader's **Run** click
|
|
7
|
+
* does — same `AsyncDuckDB` wiring, same ordered extension preload, same
|
|
8
|
+
* "multi-statement returns the last result" `execute()`. The only difference is
|
|
9
|
+
* where the engine comes from: instead of the jsDelivr CDN, the runner serves
|
|
10
|
+
* `duckdb-*.wasm` and its worker script same-origin and hands those URLs in
|
|
11
|
+
* through {@link window.DFK_HARNESS_BUNDLE}, which `runtime.init` accepts as a
|
|
12
|
+
* {@link LocalBundle}.
|
|
13
|
+
*
|
|
14
|
+
* The two contracts the page reads are injected by the served `harness.html`:
|
|
15
|
+
*
|
|
16
|
+
* - `window.DFK_HARNESS_BUNDLE` — `{mainModule, mainWorker, pthreadWorker?}`,
|
|
17
|
+
* the locally-served engine bundle (an absolute same-origin URL each).
|
|
18
|
+
* - the `<script id="dfk-sql-runtime">` JSON tag — the preload list and
|
|
19
|
+
* `allowUnsignedExtensions`, in the very shape the site plugin injects
|
|
20
|
+
* (see `sql/runtimeConfig`), so nothing here re-implements extension loading.
|
|
21
|
+
*
|
|
22
|
+
* The runner communicates through `window.__dfkReady`, `window.__dfkRun` and
|
|
23
|
+
* `window.__dfkQuery`, each reached with a Playwright `page.evaluate` (which
|
|
24
|
+
* awaits the returned promise and marshals the value back to Node). Page reload
|
|
25
|
+
* per content file is what gives "one instance per page": a fresh document
|
|
26
|
+
* resets the module-level singleton, so pages never share a DuckDB instance
|
|
27
|
+
* while the blocks of one page keep sharing its connection (via
|
|
28
|
+
* `DuckDBRuntime.execute`).
|
|
29
|
+
*/
|
|
30
|
+
import {DuckDBRuntime, type LocalBundle} from './runtime';
|
|
31
|
+
|
|
32
|
+
declare global {
|
|
33
|
+
interface Window {
|
|
34
|
+
/** The locally-served engine bundle, injected by the runner's harness.html. */
|
|
35
|
+
DFK_HARNESS_BUNDLE?: LocalBundle;
|
|
36
|
+
/** Resolves once the instance is up and the site preloads have loaded. */
|
|
37
|
+
__dfkReady?: Promise<void>;
|
|
38
|
+
/** Runs one block and reports rows/columns, or a captured error message. */
|
|
39
|
+
__dfkRun?: (sql: string) => Promise<HarnessRunResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Runs one statement and returns its actual result — the rows, not just a
|
|
42
|
+
* count. Used by the vfs browser probe (`browserRunner.query`) to read the
|
|
43
|
+
* value each statement produced; the docs verifier never needs it.
|
|
44
|
+
*/
|
|
45
|
+
__dfkQuery?: (sql: string) => Promise<HarnessQueryResult>;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What {@link window.__dfkRun} resolves with; mirrors the runner's `RunResult`. */
|
|
50
|
+
export interface HarnessRunResult {
|
|
51
|
+
rows: number;
|
|
52
|
+
columns: number;
|
|
53
|
+
/** Set when the block failed; `rows`/`columns` are then 0. */
|
|
54
|
+
error?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What {@link window.__dfkQuery} resolves with: the marshalled result rows. */
|
|
58
|
+
export interface HarnessQueryResult {
|
|
59
|
+
columns: string[];
|
|
60
|
+
rows: Record<string, unknown>[];
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ready(): Promise<void> {
|
|
65
|
+
const bundle = window.DFK_HARNESS_BUNDLE;
|
|
66
|
+
if (!bundle) {
|
|
67
|
+
throw new Error('sql/harness: window.DFK_HARNESS_BUNDLE is not set');
|
|
68
|
+
}
|
|
69
|
+
const runtime = DuckDBRuntime.getInstance();
|
|
70
|
+
const allowUnsigned = readAllowUnsigned();
|
|
71
|
+
await runtime.init({bundle, allowUnsignedExtensions: allowUnsigned});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `allowUnsignedExtensions` is fixed at `open()` time, so the harness has to
|
|
76
|
+
* hand it to `init()` directly rather than let the runtime read it lazily from
|
|
77
|
+
* the config tag. Reading the same tag here (idempotently) keeps the injected
|
|
78
|
+
* config the single source of truth — the runtime still reads the preload list
|
|
79
|
+
* from it inside `#create()`.
|
|
80
|
+
*/
|
|
81
|
+
function readAllowUnsigned(): boolean {
|
|
82
|
+
const text = document
|
|
83
|
+
.getElementById('dfk-sql-runtime')
|
|
84
|
+
?.textContent?.trim();
|
|
85
|
+
if (!text) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(text) as {allowUnsignedExtensions?: boolean};
|
|
90
|
+
return parsed.allowUnsignedExtensions === true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function run(sql: string): Promise<HarnessRunResult> {
|
|
97
|
+
const result = await DuckDBRuntime.getInstance().execute(sql);
|
|
98
|
+
if (result.error) {
|
|
99
|
+
return {rows: 0, columns: 0, error: result.error};
|
|
100
|
+
}
|
|
101
|
+
return {rows: result.rows.length, columns: result.columns.length};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function query(sql: string): Promise<HarnessQueryResult> {
|
|
105
|
+
const result = await DuckDBRuntime.getInstance().execute(sql);
|
|
106
|
+
if (result.error) {
|
|
107
|
+
return {columns: [], rows: [], error: result.error};
|
|
108
|
+
}
|
|
109
|
+
// Rebuild each row as a plain, own-enumerable object. `execute()` hands back
|
|
110
|
+
// Apache-Arrow row objects whose values sit behind getters, so Playwright's
|
|
111
|
+
// `page.evaluate` serializer would marshal them to `{}`; reading the getters
|
|
112
|
+
// here, in-page, and copying to a literal survives the hop. BigInt values
|
|
113
|
+
// (DuckDB integers) become strings for the same reason.
|
|
114
|
+
const rows = result.rows.map((row) => {
|
|
115
|
+
const plain: Record<string, unknown> = {};
|
|
116
|
+
for (const column of result.columns) {
|
|
117
|
+
const value = row[column];
|
|
118
|
+
plain[column] =
|
|
119
|
+
typeof value === 'bigint'
|
|
120
|
+
? value.toString()
|
|
121
|
+
: value === null || value === undefined
|
|
122
|
+
? null
|
|
123
|
+
: typeof value === 'object'
|
|
124
|
+
? JSON.stringify(value)
|
|
125
|
+
: value;
|
|
126
|
+
}
|
|
127
|
+
return plain;
|
|
128
|
+
});
|
|
129
|
+
return {columns: result.columns, rows};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
window.__dfkReady = ready();
|
|
133
|
+
window.__dfkRun = run;
|
|
134
|
+
window.__dfkQuery = query;
|
package/src/sql/runtime.ts
CHANGED
|
@@ -65,6 +65,27 @@ export type RuntimeState = 'idle' | 'loading' | 'ready' | 'error';
|
|
|
65
65
|
export interface RuntimeOptions {
|
|
66
66
|
/** Let `LOAD` accept an extension whose signature does not verify. */
|
|
67
67
|
allowUnsignedExtensions?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Serve the DuckDB-Wasm engine from explicit same-origin URLs instead of the
|
|
70
|
+
* jsDelivr CDN. Used by the offline SQL verifier (`sql/browserRunner`): the
|
|
71
|
+
* harness passes the locally-served `duckdb-*.wasm` / worker script so a CI
|
|
72
|
+
* run never reaches the network. When set, `#create()` skips
|
|
73
|
+
* `getJsDelivrBundles()`/`selectBundle()` and the cross-origin blob-worker
|
|
74
|
+
* wrapper (the worker is same-origin here, so it is constructed directly).
|
|
75
|
+
*/
|
|
76
|
+
bundle?: LocalBundle;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A locally-served DuckDB-Wasm bundle: absolute same-origin URLs for the
|
|
81
|
+
* engine wasm and its worker script. `pthreadWorker` is only needed for the
|
|
82
|
+
* cross-origin-isolated (COI) bundle; the default non-COI `eh`/`mvp` bundles run
|
|
83
|
+
* single-threaded and leave it unset.
|
|
84
|
+
*/
|
|
85
|
+
export interface LocalBundle {
|
|
86
|
+
mainModule: string;
|
|
87
|
+
mainWorker: string;
|
|
88
|
+
pthreadWorker?: string;
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
/** Options for {@link DuckDBRuntime.loadExtension}. */
|
|
@@ -96,6 +117,8 @@ export class DuckDBRuntime {
|
|
|
96
117
|
#db: DuckdbWasm.AsyncDuckDB | null = null;
|
|
97
118
|
#conn: DuckdbWasm.AsyncDuckDBConnection | null = null;
|
|
98
119
|
#allowUnsigned = false;
|
|
120
|
+
/** A locally-served engine bundle (offline verifier); `null` means CDN. */
|
|
121
|
+
#bundle: LocalBundle | null = null;
|
|
99
122
|
/** The injected site config; read (and validated) once on first init. */
|
|
100
123
|
#site: SiteRuntimeConfig | null = null;
|
|
101
124
|
/** Loaded / in-flight extensions, keyed by repository + name, or by URL. */
|
|
@@ -125,6 +148,9 @@ export class DuckDBRuntime {
|
|
|
125
148
|
if (options.allowUnsignedExtensions) {
|
|
126
149
|
this.#allowUnsigned = true;
|
|
127
150
|
}
|
|
151
|
+
if (options.bundle) {
|
|
152
|
+
this.#bundle = options.bundle;
|
|
153
|
+
}
|
|
128
154
|
if (this.#state === 'ready') {
|
|
129
155
|
return Promise.resolve();
|
|
130
156
|
}
|
|
@@ -154,6 +180,24 @@ export class DuckDBRuntime {
|
|
|
154
180
|
|
|
155
181
|
async #create(preload: readonly PreloadEntry[]): Promise<void> {
|
|
156
182
|
const duckdb = await import('@duckdb/duckdb-wasm');
|
|
183
|
+
|
|
184
|
+
// Offline verifier path: the engine and its worker are served same-origin
|
|
185
|
+
// by the runner, so the worker is constructed directly (no cross-origin
|
|
186
|
+
// blob wrapper) and no CDN bundle is selected.
|
|
187
|
+
if (this.#bundle) {
|
|
188
|
+
const worker = new Worker(this.#bundle.mainWorker);
|
|
189
|
+
this.#db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
|
|
190
|
+
await this.#db.instantiate(this.#bundle.mainModule, this.#bundle.pthreadWorker ?? null);
|
|
191
|
+
await this.#db.open({allowUnsignedExtensions: this.#allowUnsigned});
|
|
192
|
+
this.#conn = await this.#db.connect();
|
|
193
|
+
for (const entry of preload) {
|
|
194
|
+
await this.#loadEntry(entry);
|
|
195
|
+
}
|
|
196
|
+
this.#state = 'ready';
|
|
197
|
+
this.#message = '';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
157
201
|
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
|
|
158
202
|
if (!bundle.mainWorker) {
|
|
159
203
|
throw new Error('The selected DuckDB-Wasm bundle has no worker script');
|
package/src/sql/verify.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* publishes, so a broken example is caught by CI instead of by a reader
|
|
4
4
|
* clicking **Run**.
|
|
5
5
|
*
|
|
6
|
-
* It is the same environment the site gives a block: DuckDB-Wasm in a
|
|
6
|
+
* It is the same environment the site gives a block: DuckDB-Wasm in a browser,
|
|
7
7
|
* the site's extension preloaded, one instance per page and the page's blocks
|
|
8
|
-
* sharing a connection (see `sql/
|
|
9
|
-
* not the
|
|
8
|
+
* sharing a connection (see `sql/browserRunner.ts` for why this is a real
|
|
9
|
+
* browser and not the Node worker the kit used to run).
|
|
10
10
|
*
|
|
11
11
|
* A block may fail *on purpose* — half the guide ends on a statement that
|
|
12
12
|
* demonstrates an error. Such a block says so in its own metadata
|
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
* on has to be data rather than a string match on a comment. Only blocks that
|
|
16
16
|
* did not behave as declared make the command exit non-zero.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
19
|
-
import {tmpdir} from 'node:os';
|
|
18
|
+
import {readdirSync, statSync, writeFileSync} from 'node:fs';
|
|
20
19
|
import {join, resolve} from 'node:path';
|
|
21
20
|
|
|
22
21
|
import {collectRunnableSql, expectsError, type RunnableSqlBlock} from './collect';
|
|
23
|
-
import {
|
|
22
|
+
import {BrowserSqlRunner, type WasmPlatform} from './browserRunner';
|
|
24
23
|
|
|
25
24
|
export interface VerifyOptions {
|
|
26
25
|
/** Docs site root; defaults to the working directory. */
|
|
@@ -39,11 +38,10 @@ export interface VerifyOptions {
|
|
|
39
38
|
/** Per-block timeout in milliseconds; a hang is reported instead of blocking CI. */
|
|
40
39
|
timeoutMs?: number;
|
|
41
40
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* DuckDB's file system is the real one, relative to the working directory.
|
|
41
|
+
* The browser executable that runs the blocks. Defaults to a detected
|
|
42
|
+
* Chrome/Edge; the `DFK_BROWSER` environment variable is the same override.
|
|
45
43
|
*/
|
|
46
|
-
|
|
44
|
+
browser?: string;
|
|
47
45
|
/** Write the full result list here as JSON. */
|
|
48
46
|
reportFile?: string;
|
|
49
47
|
}
|
|
@@ -79,8 +77,6 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
79
77
|
|
|
80
78
|
/** Runs every runnable block of the site and returns the outcome of each. */
|
|
81
79
|
export async function verifySqlDocs(options: VerifyOptions = {}): Promise<VerifyReport> {
|
|
82
|
-
// Everything is resolved to absolute paths first: the run changes the working
|
|
83
|
-
// directory (see below).
|
|
84
80
|
const siteDir = resolve(options.siteDir ?? process.cwd());
|
|
85
81
|
const blocks = collectRunnableSql({
|
|
86
82
|
siteDir,
|
|
@@ -94,26 +90,10 @@ export async function verifySqlDocs(options: VerifyOptions = {}): Promise<Verify
|
|
|
94
90
|
: defaultExtension(siteDir);
|
|
95
91
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
96
92
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
const scratch = options.workingDir
|
|
102
|
-
? resolve(options.workingDir)
|
|
103
|
-
: mkdtempSync(join(tmpdir(), 'duckfn-sql-verify-'));
|
|
104
|
-
mkdirSync(scratch, {recursive: true});
|
|
105
|
-
const previousCwd = process.cwd();
|
|
106
|
-
process.chdir(scratch);
|
|
107
|
-
|
|
108
|
-
let results: BlockResult[];
|
|
109
|
-
try {
|
|
110
|
-
results = await runPages(blocks, extension, options, timeoutMs);
|
|
111
|
-
} finally {
|
|
112
|
-
process.chdir(previousCwd);
|
|
113
|
-
if (!options.workingDir) {
|
|
114
|
-
rmSync(scratch, {recursive: true, force: true});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
93
|
+
// No working directory: in a browser DuckDB's file system is the instance's
|
|
94
|
+
// own memory, so `COPY … TO` / `dfn_file_write_*` never touch the docs tree —
|
|
95
|
+
// they land in the page and vanish on the next `newPage()`.
|
|
96
|
+
const results = await runPages(blocks, extension, options, timeoutMs);
|
|
117
97
|
|
|
118
98
|
const report: VerifyReport = {
|
|
119
99
|
blocks: results,
|
|
@@ -135,10 +115,11 @@ async function runPages(
|
|
|
135
115
|
options: VerifyOptions,
|
|
136
116
|
timeoutMs: number,
|
|
137
117
|
): Promise<BlockResult[]> {
|
|
138
|
-
const runner = await
|
|
118
|
+
const runner = await BrowserSqlRunner.create({
|
|
139
119
|
extension,
|
|
140
120
|
platform: options.platform,
|
|
141
121
|
engine: options.engine,
|
|
122
|
+
browser: options.browser,
|
|
142
123
|
});
|
|
143
124
|
const results: BlockResult[] = [];
|
|
144
125
|
try {
|
|
@@ -158,7 +139,7 @@ async function runPages(
|
|
|
158
139
|
|
|
159
140
|
/** Runs one block and judges the result against the block's own declaration. */
|
|
160
141
|
async function runBlock(
|
|
161
|
-
runner:
|
|
142
|
+
runner: BrowserSqlRunner,
|
|
162
143
|
block: RunnableSqlBlock,
|
|
163
144
|
timeoutMs: number,
|
|
164
145
|
): Promise<BlockResult> {
|
|
@@ -278,9 +259,9 @@ export async function cliMain(argv: readonly string[]): Promise<void> {
|
|
|
278
259
|
|
|
279
260
|
const USAGE = `Usage: duckfn-sql-verify [options]
|
|
280
261
|
|
|
281
|
-
Runs every runnable SQL block of a duckfn docs site in
|
|
282
|
-
that each one behaves as its own metadata declares
|
|
283
|
-
block that demonstrates a failure).
|
|
262
|
+
Runs every runnable SQL block of a duckfn docs site in a headless browser
|
|
263
|
+
(DuckDB-Wasm), and checks that each one behaves as its own metadata declares
|
|
264
|
+
("expect": "error" for a block that demonstrates a failure).
|
|
284
265
|
|
|
285
266
|
--site <dir> Docs site root (default: the working directory)
|
|
286
267
|
--content <dir> Content directory, relative to the site root (repeatable;
|
|
@@ -291,8 +272,9 @@ block that demonstrates a failure).
|
|
|
291
272
|
--platform <eh|mvp> DuckDB-Wasm bundle, which must match the extension build
|
|
292
273
|
(default: eh)
|
|
293
274
|
--engine <path> Engine wasm override
|
|
275
|
+
--browser <path> Browser executable (default: a detected Chrome/Edge,
|
|
276
|
+
or the DFK_BROWSER environment variable)
|
|
294
277
|
--timeout <ms> Per-block timeout (default: 30000)
|
|
295
|
-
--working-dir <dir> Directory the blocks run in (default: a temporary one)
|
|
296
278
|
--report <file> Write the full result list as JSON
|
|
297
279
|
--quiet Only report unexpected behaviour
|
|
298
280
|
--help Show this help
|
|
@@ -330,12 +312,12 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
|
330
312
|
case '--engine':
|
|
331
313
|
options.engine = next();
|
|
332
314
|
break;
|
|
315
|
+
case '--browser':
|
|
316
|
+
options.browser = next();
|
|
317
|
+
break;
|
|
333
318
|
case '--timeout':
|
|
334
319
|
options.timeoutMs = Number(next());
|
|
335
320
|
break;
|
|
336
|
-
case '--working-dir':
|
|
337
|
-
options.workingDir = next();
|
|
338
|
-
break;
|
|
339
321
|
case '--report':
|
|
340
322
|
options.reportFile = next();
|
|
341
323
|
break;
|
package/dist/sql/nodeRunner.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export interface DuckdbQueryResult {
|
|
2
|
-
numRows: number;
|
|
3
|
-
schema: {
|
|
4
|
-
fields: {
|
|
5
|
-
name: string;
|
|
6
|
-
}[];
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export interface DuckdbConnection {
|
|
10
|
-
query(sql: string): Promise<DuckdbQueryResult>;
|
|
11
|
-
}
|
|
12
|
-
export type WasmPlatform = 'eh' | 'mvp';
|
|
13
|
-
export interface RunnerOptions {
|
|
14
|
-
/**
|
|
15
|
-
* The extension to `LOAD`: a local `.duckdb_extension.wasm` path (served to
|
|
16
|
-
* the worker over a loopback http server) or an absolute `http(s)` URL.
|
|
17
|
-
*/
|
|
18
|
-
extension: string;
|
|
19
|
-
/**
|
|
20
|
-
* DuckDB-Wasm platform. Must match how the extension was built — a site
|
|
21
|
-
* serving `duckfn-wasm_eh.duckdb_extension.wasm` runs the `eh` bundle.
|
|
22
|
-
*/
|
|
23
|
-
platform?: WasmPlatform;
|
|
24
|
-
/** The engine wasm; defaults to the one shipped beside the worker bundle. */
|
|
25
|
-
engine?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface RunResult {
|
|
28
|
-
rows: number;
|
|
29
|
-
columns: number;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* One DuckDB-Wasm instance at a time, driven from Node.
|
|
33
|
-
*
|
|
34
|
-
* `newPage()` is what a docs site does per page load: a fresh instance, a fresh
|
|
35
|
-
* connection, the extension loaded again. Blocks of one page then share state
|
|
36
|
-
* (a table created in one block is visible to the next), while pages stay
|
|
37
|
-
* isolated — which is why the runner is used one page at a time rather than
|
|
38
|
-
* over a single long-lived connection.
|
|
39
|
-
*/
|
|
40
|
-
export declare class WasmSqlRunner {
|
|
41
|
-
#private;
|
|
42
|
-
private constructor();
|
|
43
|
-
static create(options: RunnerOptions): Promise<WasmSqlRunner>;
|
|
44
|
-
/** Drops the current instance and starts a fresh page: new instance, new connection. */
|
|
45
|
-
newPage(): Promise<void>;
|
|
46
|
-
/** Runs one block; the caller decides whether a failure is expected. */
|
|
47
|
-
run(sql: string): Promise<RunResult>;
|
|
48
|
-
/** Where the fetched extension was staged, for diagnostics. */
|
|
49
|
-
get stagingDir(): string | null;
|
|
50
|
-
close(): Promise<void>;
|
|
51
|
-
}
|
package/dist/sql/nodeRunner.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { createRequire as e } from "node:module";
|
|
2
|
-
import { basename as t, dirname as n, join as r } from "node:path";
|
|
3
|
-
import { mkdirSync as i, readFileSync as a, rmSync as o } from "node:fs";
|
|
4
|
-
import { createServer as s } from "node:http";
|
|
5
|
-
import { Worker as c } from "node:worker_threads";
|
|
6
|
-
import { homedir as l } from "node:os";
|
|
7
|
-
//#region src/sql/nodeRunner.ts
|
|
8
|
-
var u = e(import.meta.url), d = "@duckdb/duckdb-wasm/dist/duckdb-node.cjs", f = class {
|
|
9
|
-
#e;
|
|
10
|
-
#t = /* @__PURE__ */ new Map();
|
|
11
|
-
onmessage = null;
|
|
12
|
-
onerror = null;
|
|
13
|
-
onclose = null;
|
|
14
|
-
constructor(e) {
|
|
15
|
-
this.#e = new c(u.resolve(d), { workerData: {
|
|
16
|
-
mod: e,
|
|
17
|
-
name: "",
|
|
18
|
-
type: ""
|
|
19
|
-
} }), this.#e.on("message", (e) => this.#n("message", e)), this.#e.on("error", (e) => this.#n("error", e)), this.#e.on("exit", () => this.#n("close"));
|
|
20
|
-
}
|
|
21
|
-
#n(e, t) {
|
|
22
|
-
let n = {
|
|
23
|
-
type: e,
|
|
24
|
-
data: t,
|
|
25
|
-
target: this,
|
|
26
|
-
currentTarget: this
|
|
27
|
-
}, r = this[`on${e}`];
|
|
28
|
-
typeof r == "function" && r(n);
|
|
29
|
-
for (let t of this.#t.get(e) ?? []) t(n);
|
|
30
|
-
}
|
|
31
|
-
addEventListener(e, t) {
|
|
32
|
-
this.#t.set(e, [...this.#t.get(e) ?? [], t]);
|
|
33
|
-
}
|
|
34
|
-
removeEventListener(e, t) {
|
|
35
|
-
this.#t.set(e, (this.#t.get(e) ?? []).filter((e) => e !== t));
|
|
36
|
-
}
|
|
37
|
-
postMessage(e, t) {
|
|
38
|
-
this.#e.postMessage(e, t ?? []);
|
|
39
|
-
}
|
|
40
|
-
terminate() {
|
|
41
|
-
return this.#e.terminate();
|
|
42
|
-
}
|
|
43
|
-
}, p = class e {
|
|
44
|
-
#e = null;
|
|
45
|
-
#t = null;
|
|
46
|
-
#n = null;
|
|
47
|
-
#r = null;
|
|
48
|
-
#i;
|
|
49
|
-
#a;
|
|
50
|
-
#o;
|
|
51
|
-
constructor(e, t, n) {
|
|
52
|
-
this.#i = e, this.#a = t, this.#o = n;
|
|
53
|
-
}
|
|
54
|
-
static async create(t) {
|
|
55
|
-
let i = t.platform ?? "eh", a = u.resolve(`@duckdb/duckdb-wasm/dist/duckdb-node-${i}.worker.cjs`), o = t.engine ?? r(n(a), `duckdb-${i}.wasm`), s = /^https?:\/\//i.test(t.extension) ? null : await m(t.extension), c = s ? s.url : t.extension, l = new e(c, o, a);
|
|
56
|
-
return l.#n = s ? s.server : null, l.#r = s ? h(s.stagingHost, s.stagingSegment) : null, l;
|
|
57
|
-
}
|
|
58
|
-
async newPage() {
|
|
59
|
-
this.#e && (await this.#e.terminate(), this.#e = null, this.#t = null);
|
|
60
|
-
let e = await import(
|
|
61
|
-
/* @vite-ignore */
|
|
62
|
-
d
|
|
63
|
-
), t = new e.AsyncDuckDB(new e.ConsoleLogger(), new f(this.#o));
|
|
64
|
-
await t.instantiate(this.#a, null), await t.open({ allowUnsignedExtensions: !0 });
|
|
65
|
-
let n = await t.connect();
|
|
66
|
-
await n.query(`LOAD '${this.#i}'`), this.#e = t, this.#t = n;
|
|
67
|
-
}
|
|
68
|
-
async run(e) {
|
|
69
|
-
let t = this.#t;
|
|
70
|
-
if (!t) throw Error("sql/verify: no page is open — call newPage() first");
|
|
71
|
-
let n = await t.query(e);
|
|
72
|
-
return {
|
|
73
|
-
rows: n.numRows,
|
|
74
|
-
columns: n.schema.fields.length
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
get stagingDir() {
|
|
78
|
-
return this.#r;
|
|
79
|
-
}
|
|
80
|
-
async close() {
|
|
81
|
-
this.#e && (await this.#e.terminate(), this.#e = null, this.#t = null), this.#n &&= (await new Promise((e) => this.#n?.close(() => e())), null);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
async function m(e) {
|
|
85
|
-
let n = a(e), r = t(e), i = g(e), o = s((e, t) => {
|
|
86
|
-
t.writeHead(200, {
|
|
87
|
-
"content-type": "application/octet-stream",
|
|
88
|
-
"content-length": n.length
|
|
89
|
-
}), t.end(n);
|
|
90
|
-
}), c = process.platform === "win32" ? 80 : 0;
|
|
91
|
-
await new Promise((e, t) => {
|
|
92
|
-
o.once("error", (e) => {
|
|
93
|
-
t(/* @__PURE__ */ Error(`sql/verify: cannot serve the extension on port ${c} (${e.code}): a port in the URL would put a colon into DuckDB's staging path, which is not a legal Windows path — free port 80, or load the extension from an http(s) URL with --extension`));
|
|
94
|
-
}), o.listen(c, "localhost", e);
|
|
95
|
-
});
|
|
96
|
-
let l = o.address(), u = typeof l == "object" && l ? l.port : c, d = u === 80 ? "localhost" : `localhost:${u}`;
|
|
97
|
-
return {
|
|
98
|
-
server: o,
|
|
99
|
-
url: `http://${d}/${i}/${r}`,
|
|
100
|
-
stagingHost: d,
|
|
101
|
-
stagingSegment: i
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function h(e, t) {
|
|
105
|
-
let n = r(l(), ".duckdb", "extensions", e, t);
|
|
106
|
-
return o(n, {
|
|
107
|
-
recursive: !0,
|
|
108
|
-
force: !0
|
|
109
|
-
}), i(n, { recursive: !0 }), n;
|
|
110
|
-
}
|
|
111
|
-
function g(e) {
|
|
112
|
-
return t(e).split(".")[0] ?? "";
|
|
113
|
-
}
|
|
114
|
-
//#endregion
|
|
115
|
-
export { p as WasmSqlRunner };
|