@gscdump/engine-duckdb-wasm 0.32.11 → 0.32.12

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.
Files changed (2) hide show
  1. package/dist/index.mjs +80 -20
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -525,13 +525,26 @@ function createOpfsHandleRegistry(backend) {
525
525
  }
526
526
  }
527
527
  const viewRefs = /* @__PURE__ */ new Map();
528
- function acquireView(key) {
529
- viewRefs.set(key, (viewRefs.get(key) ?? 0) + 1);
528
+ function acquireView(key, signature) {
529
+ const existing = viewRefs.get(key);
530
+ if (existing) {
531
+ if (existing.signature !== signature) throw new Error(`view ${key} is already attached with a different signature`);
532
+ existing.refs++;
533
+ return;
534
+ }
535
+ viewRefs.set(key, {
536
+ refs: 1,
537
+ signature
538
+ });
530
539
  }
531
540
  async function releaseView(key, drop) {
532
- const next = (viewRefs.get(key) ?? 0) - 1;
541
+ const existing = viewRefs.get(key);
542
+ const next = (existing?.refs ?? 0) - 1;
533
543
  if (next > 0) {
534
- viewRefs.set(key, next);
544
+ viewRefs.set(key, {
545
+ refs: next,
546
+ signature: existing?.signature
547
+ });
535
548
  return;
536
549
  }
537
550
  viewRefs.delete(key);
@@ -544,7 +557,8 @@ function createOpfsHandleRegistry(backend) {
544
557
  refs: (name) => entries.get(name)?.refs ?? 0,
545
558
  acquireView,
546
559
  releaseView,
547
- viewRefs: (key) => viewRefs.get(key) ?? 0
560
+ viewRefs: (key) => viewRefs.get(key)?.refs ?? 0,
561
+ viewSignature: (key) => viewRefs.get(key)?.signature
548
562
  };
549
563
  }
550
564
  function overlayViewBody(args) {
@@ -569,6 +583,7 @@ const OPFS_PREFIX = "gscdump-snapshot__";
569
583
  const RECOVER_BUFFER_PREFIX = "gscdump-recover__";
570
584
  const MAX_CONTENT_HASH_SLUG_CACHE = 4096;
571
585
  const contentHashSlugCache = /* @__PURE__ */ new Map();
586
+ const IDENT_RE = /^[A-Z_][\w$]*$/i;
572
587
  function nowMs() {
573
588
  return globalThis.performance?.now?.() ?? Date.now();
574
589
  }
@@ -603,6 +618,23 @@ function getOpfsRegistry(db, protocol) {
603
618
  }
604
619
  return registry;
605
620
  }
621
+ const dbBufferRegistries = /* @__PURE__ */ new WeakMap();
622
+ function getBufferRegistry(db) {
623
+ let registry = dbBufferRegistries.get(db);
624
+ if (!registry) {
625
+ registry = createOpfsHandleRegistry({
626
+ register: (name, handle) => db.registerFileBuffer(name, handle),
627
+ drop: async (name) => {
628
+ await db.dropFile(name);
629
+ }
630
+ });
631
+ dbBufferRegistries.set(db, registry);
632
+ }
633
+ return registry;
634
+ }
635
+ function assertSqlIdentifier(kind, value) {
636
+ if (!IDENT_RE.test(value)) throw new TypeError(`[engine-duckdb-wasm/opfs] invalid ${kind} identifier ${JSON.stringify(value)}`);
637
+ }
606
638
  function isQuotaError(err) {
607
639
  if (typeof err !== "object" || err === null) return false;
608
640
  return err.name === "QuotaExceededError" || err.code === 22;
@@ -743,6 +775,28 @@ function readParquetViewWithOverlaySql(schema, table, lakeFiles, overlayFile) {
743
775
  materializeLake: true
744
776
  })}`;
745
777
  }
778
+ function viewKey(schema, table) {
779
+ return `${schema}.${table}`;
780
+ }
781
+ function viewSignature(lakeFiles, overlayFile) {
782
+ return JSON.stringify({
783
+ lake: [...lakeFiles].sort(),
784
+ overlay: overlayFile ?? null
785
+ });
786
+ }
787
+ async function ensureView(registry, conn, schema, table, signature, sql) {
788
+ const key = viewKey(schema, table);
789
+ if (registry.viewSignature(key) === signature) {
790
+ registry.acquireView(key, signature);
791
+ return;
792
+ }
793
+ if (registry.viewRefs(key) > 0) throw new Error(`[engine-duckdb-wasm/opfs] view ${key} is already attached with a different fileset`);
794
+ await conn.query(sql);
795
+ registry.acquireView(key, signature);
796
+ }
797
+ async function recoveredBufferName(table, file, index) {
798
+ return `${RECOVER_BUFFER_PREFIX}${table}_${file.contentHash ? await contentHashSlug(file.contentHash) : String(index)}.parquet`;
799
+ }
746
800
  async function runWithConcurrency$1(items, concurrency, fn) {
747
801
  let next = 0;
748
802
  let failed;
@@ -757,10 +811,13 @@ async function runWithConcurrency$1(items, concurrency, fn) {
757
811
  }
758
812
  }
759
813
  }
760
- await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
814
+ const rejected = (await Promise.allSettled(Array.from({ length: Math.min(concurrency, items.length) }, worker))).find((r) => r.status === "rejected");
815
+ if (rejected) throw rejected.reason;
761
816
  }
762
817
  async function attachOpfsParquetTables(options) {
763
818
  const { db, conn, tables, schema = "main", fetch: fetchImpl = globalThis.fetch.bind(globalThis), fetchInit, fetchConcurrency = DEFAULT_CONCURRENCY, signal, version, onFileProgress, onTiming, withDb = (fn) => fn(), recoverContention = true } = options;
819
+ assertSqlIdentifier("schema", schema);
820
+ for (const table of tables) assertSqlIdentifier("table", table.table);
764
821
  const totalStarted = nowMs();
765
822
  await timed(onTiming, "persist", {}, requestPersistentStorage);
766
823
  const root = await timed(onTiming, "root", {}, getOpfsRoot);
@@ -799,6 +856,7 @@ async function attachOpfsParquetTables(options) {
799
856
  const expectedCount = new Map(tables.map((t) => [t.table, t.files.length + (t.overlay ? 1 : 0)]));
800
857
  const { DuckDBDataProtocol } = await timed(onTiming, "duckdb-import", {}, () => import("@duckdb/duckdb-wasm"));
801
858
  const registry = getOpfsRegistry(db, DuckDBDataProtocol.BROWSER_FSACCESS);
859
+ const bufferRegistry = getBufferRegistry(db);
802
860
  await timed(onTiming, "sweep", {
803
861
  files: total,
804
862
  tables: tables.length
@@ -915,20 +973,21 @@ async function attachOpfsParquetTables(options) {
915
973
  continue;
916
974
  }
917
975
  const overlayName = overlayNames.get(t.table);
918
- const lakeNames = overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name);
976
+ const lakeNames = (overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name)).sort();
977
+ const signature = viewSignature(lakeNames, overlayName);
919
978
  try {
920
979
  signal?.throwIfAborted();
921
980
  await timed(onTiming, "view", {
922
981
  table: t.table,
923
982
  files: files.length
924
- }, () => withDb(() => conn.query(overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames))));
983
+ }, () => withDb(() => ensureView(registry, conn, schema, t.table, signature, overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames))));
925
984
  } catch (err) {
926
985
  if (isAbortError$1(err)) {
927
986
  await withDb(() => detachOpfs(registry, conn, schema, attached, [...acquiredNames])).catch(() => {});
928
987
  throw err;
929
988
  }
930
989
  if (isOpfsAccessHandleConflict(err)) {
931
- if (registry.viewRefs(`${schema}.${t.table}`) === 0) await withDb(() => conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`)).catch(() => {});
990
+ if (registry.viewRefs(viewKey(schema, t.table)) === 0) await withDb(() => conn.query(`DROP VIEW IF EXISTS ${schema}.${t.table}`)).catch(() => {});
932
991
  degraded.add(t.table);
933
992
  degradeReason.set(t.table, "contention");
934
993
  await releaseTable(t.table);
@@ -938,7 +997,6 @@ async function attachOpfsParquetTables(options) {
938
997
  throw err;
939
998
  }
940
999
  attached.push(t.table);
941
- registry.acquireView(`${schema}.${t.table}`);
942
1000
  for (const f of files) registeredNames.push(f.name);
943
1001
  }
944
1002
  const recoverTableToBuffers = async (t) => {
@@ -959,28 +1017,30 @@ async function attachOpfsParquetTables(options) {
959
1017
  };
960
1018
  const lakeNames = [];
961
1019
  for (let i = 0; i < t.files.length; i++) {
962
- const buf = await readOne(t.files[i], i);
963
- const name = `${RECOVER_BUFFER_PREFIX}${t.table}_${i}.parquet`;
964
- await withDb(() => db.registerFileBuffer(name, buf));
1020
+ const file = t.files[i];
1021
+ const name = await recoveredBufferName(t.table, file, i);
1022
+ const buf = await readOne(file, i);
1023
+ await withDb(() => bufferRegistry.acquire(name, () => buf));
965
1024
  bufferFiles.push(name);
966
1025
  lakeNames.push(name);
967
- bytesAttached += t.files[i].bytes;
1026
+ bytesAttached += file.bytes;
968
1027
  }
969
1028
  let overlayName;
970
1029
  if (t.overlay) {
1030
+ overlayName = await recoveredBufferName(t.table, t.overlay, t.files.length);
971
1031
  const buf = await readOne(t.overlay, t.files.length);
972
- overlayName = `${RECOVER_BUFFER_PREFIX}${t.table}_overlay.parquet`;
973
- await withDb(() => db.registerFileBuffer(overlayName, buf));
1032
+ await withDb(() => bufferRegistry.acquire(overlayName, () => buf));
974
1033
  bufferFiles.push(overlayName);
975
1034
  bytesAttached += t.overlay.bytes;
976
1035
  }
1036
+ lakeNames.sort();
977
1037
  const body = overlayViewBody({
978
1038
  lakeSelect: lakeNames.length ? lakeSelect(lakeNames) : null,
979
1039
  overlaySelect: overlayName ? `SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet(['${overlayName.replace(/'/g, "''")}'], union_by_name = true)` : null,
980
1040
  materializeLake: false
981
1041
  });
982
1042
  if (!body) return false;
983
- await withDb(() => conn.query(`CREATE OR REPLACE VIEW ${schema}.${t.table} AS ${body}`));
1043
+ await withDb(() => ensureView(registry, conn, schema, t.table, viewSignature(lakeNames, overlayName), `CREATE OR REPLACE VIEW ${schema}.${t.table} AS ${body}`));
984
1044
  bufferViews.push(t.table);
985
1045
  return true;
986
1046
  });
@@ -990,7 +1050,7 @@ async function attachOpfsParquetTables(options) {
990
1050
  const before = bufferFiles.length;
991
1051
  if (await recoverTableToBuffers(t).catch(() => false)) degraded.delete(t.table);
992
1052
  else {
993
- for (const n of bufferFiles.slice(before)) await withDb(() => db.dropFile(n)).catch(() => {});
1053
+ await withDb(() => bufferRegistry.release(bufferFiles.slice(before))).catch(() => {});
994
1054
  bufferFiles.length = before;
995
1055
  }
996
1056
  }
@@ -1012,8 +1072,8 @@ async function attachOpfsParquetTables(options) {
1012
1072
  if (detached) return;
1013
1073
  detached = true;
1014
1074
  await detachOpfs(registry, conn, schema, attached, registeredNames);
1015
- for (const v of bufferViews) await conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).catch(() => {});
1016
- for (const n of bufferFiles) await db.dropFile(n).catch(() => {});
1075
+ for (const v of bufferViews) await registry.releaseView(viewKey(schema, v), () => conn.query(`DROP VIEW IF EXISTS ${schema}.${v}`).then(() => {}));
1076
+ await bufferRegistry.release(bufferFiles);
1017
1077
  }
1018
1078
  };
1019
1079
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-duckdb-wasm",
3
3
  "type": "module",
4
- "version": "0.32.11",
4
+ "version": "0.32.12",
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/contracts": "0.32.11",
49
- "@gscdump/engine": "0.32.11",
50
- "gscdump": "0.32.11"
48
+ "@gscdump/engine": "0.32.12",
49
+ "@gscdump/contracts": "0.32.12",
50
+ "gscdump": "0.32.12"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@duckdb/duckdb-wasm": "^1.32.0",