@gscdump/engine-duckdb-wasm 0.31.10 → 0.32.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/index.d.mts +26 -1
- package/dist/index.mjs +73 -3
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -154,6 +154,17 @@ interface AttachOpfsTablesOptions {
|
|
|
154
154
|
* Default: run inline (caller owns the DB exclusively).
|
|
155
155
|
*/
|
|
156
156
|
withDb?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
157
|
+
/**
|
|
158
|
+
* Recover a table degraded by an OPFS sync-access-handle / write conflict
|
|
159
|
+
* (the multi-tab case — another tab holds the file's exclusive handle) by
|
|
160
|
+
* reading the already-cached bytes via the lock-free File API (or HTTP on a
|
|
161
|
+
* genuine cache miss) and registering them as in-memory buffers, so the table
|
|
162
|
+
* attaches from the shared cache instead of being pushed back to the caller.
|
|
163
|
+
* Quota / incomplete degradations are NEVER recovered this way — buffering a
|
|
164
|
+
* quota-exceeding set risks OOM, and the caller routes those to its tail.
|
|
165
|
+
* Default true.
|
|
166
|
+
*/
|
|
167
|
+
recoverContention?: boolean;
|
|
157
168
|
}
|
|
158
169
|
interface OpfsFileProgress {
|
|
159
170
|
table: string;
|
|
@@ -205,6 +216,20 @@ declare function estimateOpfsStorage(): Promise<{
|
|
|
205
216
|
usageBytes?: number;
|
|
206
217
|
quotaBytes?: number;
|
|
207
218
|
}>;
|
|
219
|
+
/**
|
|
220
|
+
* Read a content-addressed OPFS snapshot file via the async File API and return
|
|
221
|
+
* its bytes, or null when absent / size-mismatched (partial / stale write).
|
|
222
|
+
*
|
|
223
|
+
* `getFile()` takes NO lock — unlike DuckDB's `BROWSER_FSACCESS` sync access
|
|
224
|
+
* handle — so it reads cleanly while ANOTHER tab holds the same file open. That
|
|
225
|
+
* is the basis for cross-tab cache sharing: a tab that loses the exclusive-handle
|
|
226
|
+
* race reads the shared cached bytes here instead of re-downloading. The
|
|
227
|
+
* filename derivation is the SAME `opfsFileName` + `contentHashSlug` the attach
|
|
228
|
+
* path writes, so consumers reuse the engine's naming with no replicated slug
|
|
229
|
+
* logic to drift out of lock-step. `index` is only the disambiguator for the
|
|
230
|
+
* degraded no-`contentHash` fallback (mirrors `attachOpfsParquetTables`).
|
|
231
|
+
*/
|
|
232
|
+
declare function readOpfsSnapshotFile(table: string, contentHash: string | undefined, index: number, expectedBytes: number): Promise<Uint8Array | null>;
|
|
208
233
|
/**
|
|
209
234
|
* Download every parquet file in `tables` into OPFS, content-hash verify, and
|
|
210
235
|
* attach them as DuckDB-WASM views. Attach-once: call this once per
|
|
@@ -459,4 +484,4 @@ declare function createBrowserAnalysisRuntime(boot: DuckDBWasmBootResult, option
|
|
|
459
484
|
version?: number | string;
|
|
460
485
|
attachedTables?: readonly string[];
|
|
461
486
|
}): BrowserAnalysisRuntime;
|
|
462
|
-
export { type AnalyzeResult, type AttachOpfsTablesOptions, type AttachParquetTablesOptions, type AttachParquetUrlTablesOptions, type AttachedTablesHandle, type BootDuckDBWasmOptions, type BrowserAnalysisRuntime, BrowserAttachBudgetExceededError, type BrowserAttachError, type BrowserParquetFile, type BrowserParquetTable, type BrowserParquetUrlTable, type ComparisonMode, type CompiledArchetypeSql, type DuckDBWasmBootResult, type DuckDBWasmClient, DuckDBWasmDatabase, type DuckDBWasmDrizzleDatabase, type InsightRunner, type InsightRunnerOptions, type OpfsAttachedHandle, type OpfsFileProgress, type OpfsParquetFile, type OpfsParquetTable, OpfsQuotaExceededError, type QueryResult, type ResolveWindowOptions, type ResolvedWindow, type Schema, type ScopedRunnerOptions, type StrikingMomentumOptions, type StrikingMomentumRow, type TableScope, type WindowPreset, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, attachParquetUrlTablesResult, bootDuckDBWasm, browserAttachErrors, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, isBrowserAttachError, mergeScope, overlayViewBody, page_queries, pages, queries, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
|
|
487
|
+
export { type AnalyzeResult, type AttachOpfsTablesOptions, type AttachParquetTablesOptions, type AttachParquetUrlTablesOptions, type AttachedTablesHandle, type BootDuckDBWasmOptions, type BrowserAnalysisRuntime, BrowserAttachBudgetExceededError, type BrowserAttachError, type BrowserParquetFile, type BrowserParquetTable, type BrowserParquetUrlTable, type ComparisonMode, type CompiledArchetypeSql, type DuckDBWasmBootResult, type DuckDBWasmClient, DuckDBWasmDatabase, type DuckDBWasmDrizzleDatabase, type InsightRunner, type InsightRunnerOptions, type OpfsAttachedHandle, type OpfsFileProgress, type OpfsParquetFile, type OpfsParquetTable, OpfsQuotaExceededError, type QueryResult, type ResolveWindowOptions, type ResolvedWindow, type Schema, type ScopedRunnerOptions, type StrikingMomentumOptions, type StrikingMomentumRow, type TableScope, type WindowPreset, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, attachParquetUrlTablesResult, bootDuckDBWasm, browserAttachErrors, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, isBrowserAttachError, mergeScope, overlayViewBody, page_queries, pages, queries, readOpfsSnapshotFile, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
|
package/dist/index.mjs
CHANGED
|
@@ -566,6 +566,7 @@ var OpfsQuotaExceededError = class extends Error {
|
|
|
566
566
|
};
|
|
567
567
|
const DEFAULT_CONCURRENCY = 2;
|
|
568
568
|
const OPFS_PREFIX = "gscdump-snapshot__";
|
|
569
|
+
const RECOVER_BUFFER_PREFIX = "gscdump-recover__";
|
|
569
570
|
const dbRegistries = /* @__PURE__ */ new WeakMap();
|
|
570
571
|
function getOpfsRegistry(db, protocol) {
|
|
571
572
|
let registry = dbRegistries.get(db);
|
|
@@ -646,6 +647,17 @@ async function getOpfsRoot() {
|
|
|
646
647
|
if (!dir) throw new Error("[engine-duckdb-wasm/opfs] OPFS unavailable: navigator.storage.getDirectory missing");
|
|
647
648
|
return dir.call(globalThis.navigator.storage);
|
|
648
649
|
}
|
|
650
|
+
async function readOpfsSnapshotFile(table, contentHash, index, expectedBytes) {
|
|
651
|
+
try {
|
|
652
|
+
const root = await getOpfsRoot();
|
|
653
|
+
const name = opfsFileName(table, contentHash ? await contentHashSlug(contentHash) : void 0, index);
|
|
654
|
+
const file = await (await root.getFileHandle(name)).getFile();
|
|
655
|
+
if (file.size !== expectedBytes) return null;
|
|
656
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
649
661
|
async function materialiseFile(root, name, file, fetchImpl, fetchInit, signal) {
|
|
650
662
|
signal?.throwIfAborted();
|
|
651
663
|
let handle;
|
|
@@ -712,7 +724,7 @@ async function runWithConcurrency$1(items, concurrency, fn) {
|
|
|
712
724
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
713
725
|
}
|
|
714
726
|
async function attachOpfsParquetTables(options) {
|
|
715
|
-
const { db, conn, tables, schema = "main", fetch: fetchImpl = globalThis.fetch.bind(globalThis), fetchInit, fetchConcurrency = DEFAULT_CONCURRENCY, signal, version, onFileProgress, withDb = (fn) => fn() } = options;
|
|
727
|
+
const { db, conn, tables, schema = "main", fetch: fetchImpl = globalThis.fetch.bind(globalThis), fetchInit, fetchConcurrency = DEFAULT_CONCURRENCY, signal, version, onFileProgress, withDb = (fn) => fn(), recoverContention = true } = options;
|
|
716
728
|
await requestPersistentStorage();
|
|
717
729
|
const root = await getOpfsRoot();
|
|
718
730
|
const flat = [];
|
|
@@ -751,7 +763,10 @@ async function attachOpfsParquetTables(options) {
|
|
|
751
763
|
await sweepStaleEntries(root, registry, expectedByTable).catch(() => {});
|
|
752
764
|
const tableFiles = /* @__PURE__ */ new Map();
|
|
753
765
|
const degraded = /* @__PURE__ */ new Set();
|
|
766
|
+
const degradeReason = /* @__PURE__ */ new Map();
|
|
754
767
|
const acquiredNames = /* @__PURE__ */ new Set();
|
|
768
|
+
const bufferFiles = [];
|
|
769
|
+
const bufferViews = [];
|
|
755
770
|
let bytesAttached = 0;
|
|
756
771
|
const releaseTable = async (table) => {
|
|
757
772
|
const names = (tableFiles.get(table) ?? []).map((f) => f.name);
|
|
@@ -769,10 +784,12 @@ async function attachOpfsParquetTables(options) {
|
|
|
769
784
|
if (isAbortError$1(err)) throw err;
|
|
770
785
|
if (isQuotaError(err)) {
|
|
771
786
|
degraded.add(item.table);
|
|
787
|
+
degradeReason.set(item.table, "quota");
|
|
772
788
|
return;
|
|
773
789
|
}
|
|
774
790
|
if (isOpfsWriteConflict(err)) {
|
|
775
791
|
degraded.add(item.table);
|
|
792
|
+
degradeReason.set(item.table, "contention");
|
|
776
793
|
return;
|
|
777
794
|
}
|
|
778
795
|
throw err;
|
|
@@ -784,6 +801,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
784
801
|
if (isAbortError$1(err)) throw err;
|
|
785
802
|
if (isOpfsAccessHandleConflict(err)) {
|
|
786
803
|
degraded.add(item.table);
|
|
804
|
+
degradeReason.set(item.table, "contention");
|
|
787
805
|
return;
|
|
788
806
|
}
|
|
789
807
|
throw err;
|
|
@@ -821,6 +839,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
821
839
|
const files = tableFiles.get(t.table) ?? [];
|
|
822
840
|
if (files.length !== (expectedCount.get(t.table) ?? t.files.length)) {
|
|
823
841
|
degraded.add(t.table);
|
|
842
|
+
degradeReason.set(t.table, degradeReason.get(t.table) ?? "incomplete");
|
|
824
843
|
await releaseTable(t.table);
|
|
825
844
|
continue;
|
|
826
845
|
}
|
|
@@ -837,6 +856,7 @@ async function attachOpfsParquetTables(options) {
|
|
|
837
856
|
if (isOpfsAccessHandleConflict(err)) {
|
|
838
857
|
if (registry.viewRefs(`${schema}.${t.table}`) === 0) await withDb(() => conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`)).catch(() => {});
|
|
839
858
|
degraded.add(t.table);
|
|
859
|
+
degradeReason.set(t.table, "contention");
|
|
840
860
|
await releaseTable(t.table);
|
|
841
861
|
continue;
|
|
842
862
|
}
|
|
@@ -847,10 +867,58 @@ async function attachOpfsParquetTables(options) {
|
|
|
847
867
|
registry.acquireView(`${schema}.${t.table}`);
|
|
848
868
|
for (const f of files) registeredNames.push(f.name);
|
|
849
869
|
}
|
|
870
|
+
const recoverTableToBuffers = async (t) => {
|
|
871
|
+
const readOne = async (file, index) => {
|
|
872
|
+
const cached = await readOpfsSnapshotFile(t.table, file.contentHash, index, file.bytes);
|
|
873
|
+
if (cached) return cached;
|
|
874
|
+
signal?.throwIfAborted();
|
|
875
|
+
const resp = await fetchImpl(file.url, {
|
|
876
|
+
...fetchInit,
|
|
877
|
+
signal
|
|
878
|
+
});
|
|
879
|
+
if (!resp.ok) throw new Error(`[engine-duckdb-wasm/opfs] recover ${file.url} failed: ${resp.status}`);
|
|
880
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
881
|
+
};
|
|
882
|
+
const lakeNames = [];
|
|
883
|
+
for (let i = 0; i < t.files.length; i++) {
|
|
884
|
+
const buf = await readOne(t.files[i], i);
|
|
885
|
+
const name = `${RECOVER_BUFFER_PREFIX}${t.table}_${i}.parquet`;
|
|
886
|
+
await withDb(() => db.registerFileBuffer(name, buf));
|
|
887
|
+
bufferFiles.push(name);
|
|
888
|
+
lakeNames.push(name);
|
|
889
|
+
bytesAttached += t.files[i].bytes;
|
|
890
|
+
}
|
|
891
|
+
let overlayName;
|
|
892
|
+
if (t.overlay) {
|
|
893
|
+
const buf = await readOne(t.overlay, t.files.length);
|
|
894
|
+
overlayName = `${RECOVER_BUFFER_PREFIX}${t.table}_overlay.parquet`;
|
|
895
|
+
await withDb(() => db.registerFileBuffer(overlayName, buf));
|
|
896
|
+
bufferFiles.push(overlayName);
|
|
897
|
+
bytesAttached += t.overlay.bytes;
|
|
898
|
+
}
|
|
899
|
+
const body = overlayViewBody({
|
|
900
|
+
lakeSelect: lakeNames.length ? lakeSelect(lakeNames) : null,
|
|
901
|
+
overlaySelect: overlayName ? `SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet(['${overlayName.replace(/'/g, "''")}'], union_by_name = true)` : null,
|
|
902
|
+
materializeLake: false
|
|
903
|
+
});
|
|
904
|
+
if (!body) return false;
|
|
905
|
+
await withDb(() => conn.query(`CREATE OR REPLACE VIEW ${schema}.${t.table} AS ${body}`));
|
|
906
|
+
bufferViews.push(t.table);
|
|
907
|
+
return true;
|
|
908
|
+
};
|
|
909
|
+
if (recoverContention && !signal?.aborted) for (const t of tables) {
|
|
910
|
+
if (!degraded.has(t.table) || degradeReason.get(t.table) !== "contention") continue;
|
|
911
|
+
const before = bufferFiles.length;
|
|
912
|
+
if (await recoverTableToBuffers(t).catch(() => false)) degraded.delete(t.table);
|
|
913
|
+
else {
|
|
914
|
+
for (const n of bufferFiles.slice(before)) await withDb(() => db.dropFile(n)).catch(() => {});
|
|
915
|
+
bufferFiles.length = before;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
850
918
|
let detached = false;
|
|
851
919
|
return {
|
|
852
920
|
version,
|
|
853
|
-
tables: attached,
|
|
921
|
+
tables: [...attached, ...bufferViews],
|
|
854
922
|
schema,
|
|
855
923
|
bytesAttached,
|
|
856
924
|
degradedTables: [...degraded],
|
|
@@ -858,6 +926,8 @@ async function attachOpfsParquetTables(options) {
|
|
|
858
926
|
if (detached) return;
|
|
859
927
|
detached = true;
|
|
860
928
|
await detachOpfs(registry, conn, schema, attached, registeredNames);
|
|
929
|
+
for (const v of bufferViews) await conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).catch(() => {});
|
|
930
|
+
for (const n of bufferFiles) await db.dropFile(n).catch(() => {});
|
|
861
931
|
}
|
|
862
932
|
};
|
|
863
933
|
}
|
|
@@ -1307,4 +1377,4 @@ function createBrowserAnalysisRuntime(boot, options = {}) {
|
|
|
1307
1377
|
}
|
|
1308
1378
|
};
|
|
1309
1379
|
}
|
|
1310
|
-
export { BrowserAttachBudgetExceededError, DuckDBWasmDatabase, OpfsQuotaExceededError, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, attachParquetUrlTablesResult, bootDuckDBWasm, browserAttachErrors, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, isBrowserAttachError, mergeScope, overlayViewBody, page_queries, pages, queries, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
|
|
1380
|
+
export { BrowserAttachBudgetExceededError, DuckDBWasmDatabase, OpfsQuotaExceededError, attachOpfsParquetTables, attachParquetTables, attachParquetUrlTables, attachParquetUrlTablesResult, bootDuckDBWasm, browserAttachErrors, clearOpfsSnapshotCache, compileArchetypeSql, countries, createBrowserAnalysisRuntime, createClient, createInsightRunner, dates, drizzle, estimateOpfsStorage, hourly_pages, isBrowserAttachError, mergeScope, overlayViewBody, page_queries, pages, queries, readOpfsSnapshotFile, requestPersistentStorage, resolveWindow, schema, scopeFor, strikingMomentum, tableForArchetype };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/engine-duckdb-wasm",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.32.1",
|
|
5
5
|
"description": "DuckDB-WASM engine adapter for @gscdump/analysis — typed browser analytics against parquet via R2.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"drizzle-orm": "1.0.0-rc.3",
|
|
48
|
-
"@gscdump/
|
|
49
|
-
"@gscdump/
|
|
50
|
-
"gscdump": "0.
|
|
48
|
+
"@gscdump/engine": "0.32.1",
|
|
49
|
+
"@gscdump/contracts": "0.32.1",
|
|
50
|
+
"gscdump": "0.32.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@duckdb/duckdb-wasm": "^1.32.0",
|
|
54
54
|
"@vitest/browser": "^4.1.9",
|
|
55
55
|
"@vitest/browser-playwright": "^4.1.9",
|
|
56
|
-
"playwright": "^1.61.
|
|
56
|
+
"playwright": "^1.61.1",
|
|
57
57
|
"vitest": "^4.1.9"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|