@gscdump/engine-duckdb-wasm 0.26.6 → 0.26.7

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 CHANGED
@@ -136,6 +136,24 @@ interface AttachOpfsTablesOptions {
136
136
  version?: string;
137
137
  /** Ticks once per file as it lands in OPFS + registers. UI progress. */
138
138
  onFileProgress?: (info: OpfsFileProgress) => void;
139
+ /**
140
+ * Serializer for the operations that mutate the DuckDB-WASM virtual
141
+ * filesystem / catalog (`registerFileHandle`, `dropFile`, `CREATE`/`DROP
142
+ * VIEW`) — those are not concurrency-safe across consumers sharing one
143
+ * `AsyncDuckDB`. Callers with a shared DB pass their global attach mutex
144
+ * here so ONLY these mutations serialize; the parquet downloads (network +
145
+ * OPFS writes, no DB interaction) run outside the lock and overlap across
146
+ * concurrent attaches. Wrapping the whole `attachOpfsParquetTables` call in
147
+ * the mutex instead serializes every table's downloads end-to-end — the
148
+ * dominant cold-load cost.
149
+ *
150
+ * The returned handle's `detach()` is NOT routed through this — callers
151
+ * that pass a lock typically already hold it around teardown, and the lock
152
+ * is not reentrant.
153
+ *
154
+ * Default: run inline (caller owns the DB exclusively).
155
+ */
156
+ withDb?: <T>(fn: () => Promise<T>) => Promise<T>;
139
157
  }
140
158
  interface OpfsFileProgress {
141
159
  table: string;
package/dist/index.mjs CHANGED
@@ -702,7 +702,7 @@ async function runWithConcurrency$1(items, concurrency, fn) {
702
702
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
703
703
  }
704
704
  async function attachOpfsParquetTables(options) {
705
- const { db, conn, tables, schema = "main", fetch: fetchImpl = globalThis.fetch.bind(globalThis), fetchInit, fetchConcurrency = DEFAULT_CONCURRENCY, signal, version, onFileProgress } = options;
705
+ const { db, conn, tables, schema = "main", fetch: fetchImpl = globalThis.fetch.bind(globalThis), fetchInit, fetchConcurrency = DEFAULT_CONCURRENCY, signal, version, onFileProgress, withDb = (fn) => fn() } = options;
706
706
  await requestPersistentStorage();
707
707
  const root = await getOpfsRoot();
708
708
  const flat = [];
@@ -746,7 +746,7 @@ async function attachOpfsParquetTables(options) {
746
746
  const releaseTable = async (table) => {
747
747
  const names = (tableFiles.get(table) ?? []).map((f) => f.name);
748
748
  for (const n of names) acquiredNames.delete(n);
749
- await registry.release(names);
749
+ await withDb(() => registry.release(names));
750
750
  };
751
751
  const runDownloads = () => runWithConcurrency$1(flat, Math.max(1, fetchConcurrency), async (item, index) => {
752
752
  if (degraded.has(item.table)) return;
@@ -764,7 +764,7 @@ async function attachOpfsParquetTables(options) {
764
764
  throw err;
765
765
  }
766
766
  try {
767
- await registry.acquire(name, () => result.handle);
767
+ await withDb(() => registry.acquire(name, () => result.handle));
768
768
  acquiredNames.add(name);
769
769
  } catch (err) {
770
770
  if (isAbortError$1(err)) throw err;
@@ -794,7 +794,7 @@ async function attachOpfsParquetTables(options) {
794
794
  try {
795
795
  await runDownloads();
796
796
  } catch (err) {
797
- await registry.release([...acquiredNames]).catch(() => {});
797
+ await withDb(() => registry.release([...acquiredNames])).catch(() => {});
798
798
  throw err;
799
799
  }
800
800
  const attached = [];
@@ -814,19 +814,19 @@ async function attachOpfsParquetTables(options) {
814
814
  const lakeNames = overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name);
815
815
  try {
816
816
  signal?.throwIfAborted();
817
- await conn.query(overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames));
817
+ await withDb(() => conn.query(overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames)));
818
818
  } catch (err) {
819
819
  if (isAbortError$1(err)) {
820
- await detachOpfs(registry, conn, schema, attached, [...acquiredNames]).catch(() => {});
820
+ await withDb(() => detachOpfs(registry, conn, schema, attached, [...acquiredNames])).catch(() => {});
821
821
  throw err;
822
822
  }
823
823
  if (isOpfsAccessHandleConflict(err)) {
824
- if (registry.viewRefs(`${schema}.${t.table}`) === 0) await conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`).catch(() => {});
824
+ if (registry.viewRefs(`${schema}.${t.table}`) === 0) await withDb(() => conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`)).catch(() => {});
825
825
  degraded.add(t.table);
826
826
  await releaseTable(t.table);
827
827
  continue;
828
828
  }
829
- await detachOpfs(registry, conn, schema, attached, [...acquiredNames]).catch(() => {});
829
+ await withDb(() => detachOpfs(registry, conn, schema, attached, [...acquiredNames])).catch(() => {});
830
830
  throw err;
831
831
  }
832
832
  attached.push(t.table);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-duckdb-wasm",
3
3
  "type": "module",
4
- "version": "0.26.6",
4
+ "version": "0.26.7",
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,9 +45,9 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "drizzle-orm": "1.0.0-rc.3",
48
- "@gscdump/engine": "0.26.6",
49
- "gscdump": "0.26.6",
50
- "@gscdump/sdk": "0.26.6"
48
+ "@gscdump/engine": "0.26.7",
49
+ "@gscdump/sdk": "0.26.7",
50
+ "gscdump": "0.26.7"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@duckdb/duckdb-wasm": "^1.32.0",