@gscdump/engine-duckdb-wasm 1.3.2 → 1.4.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/dist/archetype-sql.d.mts +15 -0
- package/dist/archetype-sql.mjs +373 -0
- package/dist/drizzle-adapter/client.d.mts +9 -0
- package/dist/drizzle-adapter/client.mjs +20 -0
- package/dist/drizzle-adapter/driver.d.mts +14 -0
- package/dist/drizzle-adapter/driver.mjs +21 -0
- package/dist/drizzle-adapter/index.d.mts +3 -0
- package/dist/drizzle-adapter/index.mjs +3 -0
- package/dist/drizzle-adapter/session.d.mts +8 -0
- package/dist/drizzle-adapter/session.mjs +28 -0
- package/dist/index.d.mts +9 -493
- package/dist/index.mjs +9 -1555
- package/dist/opfs-registry.mjs +89 -0
- package/dist/opfs.d.mts +195 -0
- package/dist/opfs.mjs +584 -0
- package/dist/overlay-view.d.mts +33 -0
- package/dist/overlay-view.mjs +10 -0
- package/dist/runner.d.mts +28 -0
- package/dist/runner.mjs +19 -0
- package/dist/runtime.d.mts +210 -0
- package/dist/runtime.mjs +429 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
function createOpfsHandleRegistry(backend) {
|
|
2
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4
|
+
function reportCleanupFailure(operation, error) {
|
|
5
|
+
console.warn(`[gscdump/engine-duckdb-wasm] ${operation} failed`, error);
|
|
6
|
+
}
|
|
7
|
+
async function acquire(name, openHandle) {
|
|
8
|
+
const existing = entries.get(name);
|
|
9
|
+
if (existing) {
|
|
10
|
+
existing.refs++;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const inflight = pending.get(name);
|
|
14
|
+
if (inflight) {
|
|
15
|
+
await inflight.then(() => void 0, () => void 0);
|
|
16
|
+
const settled = entries.get(name);
|
|
17
|
+
if (settled) {
|
|
18
|
+
settled.refs++;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return acquire(name, openHandle);
|
|
22
|
+
}
|
|
23
|
+
const registration = (async () => {
|
|
24
|
+
const handle = await openHandle();
|
|
25
|
+
await backend.register(name, handle);
|
|
26
|
+
entries.set(name, {
|
|
27
|
+
handle,
|
|
28
|
+
refs: 1
|
|
29
|
+
});
|
|
30
|
+
})();
|
|
31
|
+
pending.set(name, registration);
|
|
32
|
+
try {
|
|
33
|
+
await registration;
|
|
34
|
+
} finally {
|
|
35
|
+
pending.delete(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function release(names) {
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const entry = entries.get(name);
|
|
41
|
+
if (!entry) continue;
|
|
42
|
+
entry.refs--;
|
|
43
|
+
if (entry.refs > 0) continue;
|
|
44
|
+
entries.delete(name);
|
|
45
|
+
await backend.drop(name).catch((error) => {
|
|
46
|
+
reportCleanupFailure(`dropping OPFS handle ${name}`, error);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const viewRefs = /* @__PURE__ */ new Map();
|
|
51
|
+
function acquireView(key, signature) {
|
|
52
|
+
const existing = viewRefs.get(key);
|
|
53
|
+
if (existing) {
|
|
54
|
+
if (existing.signature !== signature) throw new Error(`view ${key} is already attached with a different signature`);
|
|
55
|
+
existing.refs++;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
viewRefs.set(key, {
|
|
59
|
+
refs: 1,
|
|
60
|
+
signature
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function releaseView(key, drop) {
|
|
64
|
+
const existing = viewRefs.get(key);
|
|
65
|
+
const next = (existing?.refs ?? 0) - 1;
|
|
66
|
+
if (next > 0) {
|
|
67
|
+
viewRefs.set(key, {
|
|
68
|
+
refs: next,
|
|
69
|
+
signature: existing?.signature
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
viewRefs.delete(key);
|
|
74
|
+
await drop().catch((error) => {
|
|
75
|
+
reportCleanupFailure(`dropping view ${key}`, error);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
acquire,
|
|
80
|
+
release,
|
|
81
|
+
size: () => entries.size,
|
|
82
|
+
refs: (name) => entries.get(name)?.refs ?? 0,
|
|
83
|
+
acquireView,
|
|
84
|
+
releaseView,
|
|
85
|
+
viewRefs: (key) => viewRefs.get(key)?.refs ?? 0,
|
|
86
|
+
viewSignature: (key) => viewRefs.get(key)?.signature
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export { createOpfsHandleRegistry };
|
package/dist/opfs.d.mts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { AsyncDuckDB, AsyncDuckDBConnection } from "@duckdb/duckdb-wasm";
|
|
2
|
+
/** A parquet data file to materialise into OPFS. */
|
|
3
|
+
interface OpfsParquetFile {
|
|
4
|
+
/** Same-origin URL carrying a signed size hint + short-lived access token. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Expected byte size — drives progress + a cheap pre-verify shortcut. */
|
|
7
|
+
bytes: number;
|
|
8
|
+
/**
|
|
9
|
+
* Opaque content-stable identifier for this file (e.g. the Iceberg data-
|
|
10
|
+
* file object key). Encoded into the OPFS filename so the same hash is the
|
|
11
|
+
* same cache entry. NOT required to be a SHA-256. When omitted, the cache
|
|
12
|
+
* key falls back to `(table, index)` and only the byte size is verified
|
|
13
|
+
* (degraded — stale entries can survive a content change).
|
|
14
|
+
*/
|
|
15
|
+
contentHash?: string;
|
|
16
|
+
/** Row count — diagnostics only. */
|
|
17
|
+
rowCount?: number;
|
|
18
|
+
}
|
|
19
|
+
/** One logical table and the parquet files that compose it. */
|
|
20
|
+
interface OpfsParquetTable {
|
|
21
|
+
/** Iceberg table name — becomes the DuckDB view name. */
|
|
22
|
+
table: string;
|
|
23
|
+
files: OpfsParquetFile[];
|
|
24
|
+
/**
|
|
25
|
+
* Recent-window overlay parquet (the non-stable tail the lake excludes). When
|
|
26
|
+
* present it is materialised into OPFS alongside `files` and the view unions
|
|
27
|
+
* it with an anti-join dedup: the lake (`files`) serves every day it has, the
|
|
28
|
+
* overlay serves ONLY days the lake lacks. So a stale overlay whose days have
|
|
29
|
+
* since landed in the lake can't double-count, and a day that stabilised but
|
|
30
|
+
* isn't yet in the lake still serves from the overlay. Its `contentHash` must
|
|
31
|
+
* change when the overlay bytes change (it is overwritten in place) so the
|
|
32
|
+
* cache re-downloads.
|
|
33
|
+
*/
|
|
34
|
+
overlay?: OpfsParquetFile;
|
|
35
|
+
}
|
|
36
|
+
interface AttachOpfsTablesOptions {
|
|
37
|
+
db: AsyncDuckDB;
|
|
38
|
+
conn: AsyncDuckDBConnection;
|
|
39
|
+
tables: OpfsParquetTable[];
|
|
40
|
+
/** DuckDB schema the views are created in. Default `main`. */
|
|
41
|
+
schema?: string;
|
|
42
|
+
/** `fetch` override (tests). Default `globalThis.fetch`. */
|
|
43
|
+
fetch?: typeof fetch;
|
|
44
|
+
/** Request init for the parquet downloads (auth headers, credentials mode). */
|
|
45
|
+
fetchInit?: RequestInit;
|
|
46
|
+
/** Caps simultaneous downloads. Default 2. */
|
|
47
|
+
fetchConcurrency?: number;
|
|
48
|
+
/** Abort signal threaded through downloads + registration. */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
/** Snapshot version associated with this file set — echoed on the handle. */
|
|
51
|
+
version?: string;
|
|
52
|
+
/** Ticks once per file as it lands in OPFS + registers. UI progress. */
|
|
53
|
+
onFileProgress?: (info: OpfsFileProgress) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Low-volume diagnostics for the OPFS attach waterfall. This is intentionally
|
|
56
|
+
* phase-level rather than a logger so hosts can join it with their own route
|
|
57
|
+
* timings without parsing console text.
|
|
58
|
+
*/
|
|
59
|
+
onTiming?: (info: OpfsAttachTiming) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Serializer for the operations that mutate the DuckDB-WASM virtual
|
|
62
|
+
* filesystem / catalog (`registerFileHandle`, `dropFile`, `CREATE`/`DROP
|
|
63
|
+
* VIEW`) — those are not concurrency-safe across consumers sharing one
|
|
64
|
+
* `AsyncDuckDB`. Callers with a shared DB pass their global attach mutex
|
|
65
|
+
* here so ONLY these mutations serialize; the parquet downloads (network +
|
|
66
|
+
* OPFS writes, no DB interaction) run outside the lock and overlap across
|
|
67
|
+
* concurrent attaches. Wrapping the whole `attachOpfsParquetTables` call in
|
|
68
|
+
* the mutex instead serializes every table's downloads end-to-end — the
|
|
69
|
+
* dominant cold-load cost.
|
|
70
|
+
*
|
|
71
|
+
* The returned handle's `detach()` is NOT routed through this — callers
|
|
72
|
+
* that pass a lock typically already hold it around teardown, and the lock
|
|
73
|
+
* is not reentrant.
|
|
74
|
+
*
|
|
75
|
+
* Default: run inline (caller owns the DB exclusively).
|
|
76
|
+
*/
|
|
77
|
+
withDb?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
78
|
+
/**
|
|
79
|
+
* Recover a table degraded by an OPFS sync-access-handle / write conflict
|
|
80
|
+
* (the multi-tab case — another tab holds the file's exclusive handle) by
|
|
81
|
+
* reading the already-cached bytes via the lock-free File API (or HTTP on a
|
|
82
|
+
* genuine cache miss) and registering them as in-memory buffers, so the table
|
|
83
|
+
* attaches from the shared cache instead of being pushed back to the caller.
|
|
84
|
+
* Quota / incomplete degradations are NEVER recovered this way — buffering a
|
|
85
|
+
* quota-exceeding set risks OOM, and the caller routes those to its tail.
|
|
86
|
+
* Default true.
|
|
87
|
+
*/
|
|
88
|
+
recoverContention?: boolean;
|
|
89
|
+
}
|
|
90
|
+
interface OpfsFileProgress {
|
|
91
|
+
table: string;
|
|
92
|
+
/** OPFS file name. */
|
|
93
|
+
file: string;
|
|
94
|
+
/** Index within the flat file list. */
|
|
95
|
+
index: number;
|
|
96
|
+
/** Total files across all tables. */
|
|
97
|
+
total: number;
|
|
98
|
+
/** Bytes of this file (cumulative caller-side). */
|
|
99
|
+
bytes: number;
|
|
100
|
+
/** `'cache-hit'` — already in OPFS, verified; `'downloaded'` — fetched. */
|
|
101
|
+
outcome: 'cache-hit' | 'downloaded';
|
|
102
|
+
/** Time spent probing/downloading/writing this file before DuckDB registration. */
|
|
103
|
+
materialiseMs?: number;
|
|
104
|
+
/** Time spent inside DuckDB's file-registration mutation lock for this file. */
|
|
105
|
+
registerMs?: number;
|
|
106
|
+
/** End-to-end file phase time, materialise + register + callback overhead. */
|
|
107
|
+
totalMs?: number;
|
|
108
|
+
}
|
|
109
|
+
type OpfsAttachTimingStage = 'persist' | 'root' | 'plan' | 'duckdb-import' | 'sweep' | 'materialise' | 'register' | 'downloads' | 'view' | 'recovery' | 'total';
|
|
110
|
+
interface OpfsAttachTiming {
|
|
111
|
+
stage: OpfsAttachTimingStage;
|
|
112
|
+
durationMs: number;
|
|
113
|
+
table?: string;
|
|
114
|
+
file?: string;
|
|
115
|
+
files?: number;
|
|
116
|
+
tables?: number;
|
|
117
|
+
bytes?: number;
|
|
118
|
+
outcome?: 'cache-hit' | 'downloaded';
|
|
119
|
+
}
|
|
120
|
+
/** Handle returned from {@link attachOpfsParquetTables}. */
|
|
121
|
+
interface OpfsAttachedHandle {
|
|
122
|
+
version: string | undefined;
|
|
123
|
+
/** Tables that successfully attached (a per-table failure drops only that table). */
|
|
124
|
+
tables: string[];
|
|
125
|
+
schema: string;
|
|
126
|
+
/** Total bytes materialised into OPFS for this attach. */
|
|
127
|
+
bytesAttached: number;
|
|
128
|
+
/**
|
|
129
|
+
* Tables that could NOT be attached because OPFS ran out of quota. The
|
|
130
|
+
* caller routes these to the server tail. Empty on a clean attach.
|
|
131
|
+
*/
|
|
132
|
+
degradedTables: string[];
|
|
133
|
+
/**
|
|
134
|
+
* Why each degraded table degraded. 'quota' is actionable (the caller can
|
|
135
|
+
* reclaim space via `clearOpfsSnapshotCache` and retry once); 'contention'
|
|
136
|
+
* and 'incomplete' are transient multi-tab races the buffer fallback
|
|
137
|
+
* already absorbs.
|
|
138
|
+
*/
|
|
139
|
+
degradeReasons: Partial<Record<string, 'quota' | 'contention' | 'incomplete'>>;
|
|
140
|
+
/** Detach the created views + release the registered OPFS file handles. */
|
|
141
|
+
detach: () => Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Raised when OPFS cannot hold the file set. Carries the partial state so the
|
|
145
|
+
* caller can degrade — attach what fit, route the rest server-side.
|
|
146
|
+
*/
|
|
147
|
+
declare class OpfsQuotaExceededError extends Error {
|
|
148
|
+
name: string;
|
|
149
|
+
/** Tables that did not fit. */
|
|
150
|
+
readonly degradedTables: string[];
|
|
151
|
+
constructor(message: string, degradedTables: string[]);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Request persistent storage so the browser is less likely to evict the OPFS
|
|
155
|
+
* cache under pressure. Returns the granted state — `false` is normal for an
|
|
156
|
+
* un-engaged origin and is NOT an error; it just means eviction is possible.
|
|
157
|
+
*/
|
|
158
|
+
declare function requestPersistentStorage(): Promise<boolean>;
|
|
159
|
+
/** Best-effort `{ usageBytes, quotaBytes }` from the Storage API. */
|
|
160
|
+
declare function estimateOpfsStorage(): Promise<{
|
|
161
|
+
usageBytes?: number;
|
|
162
|
+
quotaBytes?: number;
|
|
163
|
+
}>;
|
|
164
|
+
/**
|
|
165
|
+
* Read a content-addressed OPFS snapshot file via the async File API and return
|
|
166
|
+
* its bytes, or null when absent / size-mismatched (partial / stale write).
|
|
167
|
+
*
|
|
168
|
+
* `getFile()` takes NO lock — unlike DuckDB's `BROWSER_FSACCESS` sync access
|
|
169
|
+
* handle — so it reads cleanly while ANOTHER tab holds the same file open. That
|
|
170
|
+
* is the basis for cross-tab cache sharing: a tab that loses the exclusive-handle
|
|
171
|
+
* race reads the shared cached bytes here instead of re-downloading. The
|
|
172
|
+
* filename derivation is the SAME `opfsFileName` + `contentHashSlug` the attach
|
|
173
|
+
* path writes, so consumers reuse the engine's naming with no replicated slug
|
|
174
|
+
* logic to drift out of lock-step. `index` is only the disambiguator for the
|
|
175
|
+
* degraded no-`contentHash` fallback (mirrors `attachOpfsParquetTables`).
|
|
176
|
+
*/
|
|
177
|
+
declare function readOpfsSnapshotFile(table: string, contentHash: string | undefined, index: number, expectedBytes: number): Promise<Uint8Array | null>;
|
|
178
|
+
/**
|
|
179
|
+
* Download every parquet file in `tables` into OPFS, content-hash verify, and
|
|
180
|
+
* attach them as DuckDB-WASM views. Attach-once: call this once per
|
|
181
|
+
* `(site, table)` span; re-query without re-attaching for filter / range
|
|
182
|
+
* changes inside the span.
|
|
183
|
+
*
|
|
184
|
+
* Quota handling: a `QuotaExceededError` while writing a table's files does
|
|
185
|
+
* NOT throw — that table is recorded in `degradedTables` and skipped; the
|
|
186
|
+
* caller routes it to the server tail. The remaining tables still attach.
|
|
187
|
+
*/
|
|
188
|
+
declare function attachOpfsParquetTables(options: AttachOpfsTablesOptions): Promise<OpfsAttachedHandle>;
|
|
189
|
+
/**
|
|
190
|
+
* Delete every OPFS entry this module created. Used to reclaim space after a
|
|
191
|
+
* quota error, or to force a clean re-download. Best-effort — missing entries
|
|
192
|
+
* are ignored.
|
|
193
|
+
*/
|
|
194
|
+
declare function clearOpfsSnapshotCache(): Promise<void>;
|
|
195
|
+
export { AttachOpfsTablesOptions, OpfsAttachTiming, OpfsAttachTimingStage, OpfsAttachedHandle, OpfsFileProgress, OpfsParquetFile, OpfsParquetTable, OpfsQuotaExceededError, attachOpfsParquetTables, clearOpfsSnapshotCache, estimateOpfsStorage, readOpfsSnapshotFile, requestPersistentStorage };
|