@gscdump/engine-duckdb-wasm 0.32.9 → 0.32.11

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,12 @@ 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
+ * Low-volume diagnostics for the OPFS attach waterfall. This is intentionally
141
+ * phase-level rather than a logger so hosts can join it with their own route
142
+ * timings without parsing console text.
143
+ */
144
+ onTiming?: (info: OpfsAttachTiming) => void;
139
145
  /**
140
146
  * Serializer for the operations that mutate the DuckDB-WASM virtual
141
147
  * filesystem / catalog (`registerFileHandle`, `dropFile`, `CREATE`/`DROP
@@ -178,6 +184,23 @@ interface OpfsFileProgress {
178
184
  bytes: number;
179
185
  /** `'cache-hit'` — already in OPFS, verified; `'downloaded'` — fetched. */
180
186
  outcome: 'cache-hit' | 'downloaded';
187
+ /** Time spent probing/downloading/writing this file before DuckDB registration. */
188
+ materialiseMs?: number;
189
+ /** Time spent inside DuckDB's file-registration mutation lock for this file. */
190
+ registerMs?: number;
191
+ /** End-to-end file phase time, materialise + register + callback overhead. */
192
+ totalMs?: number;
193
+ }
194
+ type OpfsAttachTimingStage = 'persist' | 'root' | 'plan' | 'duckdb-import' | 'sweep' | 'materialise' | 'register' | 'downloads' | 'view' | 'recovery' | 'total';
195
+ interface OpfsAttachTiming {
196
+ stage: OpfsAttachTimingStage;
197
+ durationMs: number;
198
+ table?: string;
199
+ file?: string;
200
+ files?: number;
201
+ tables?: number;
202
+ bytes?: number;
203
+ outcome?: 'cache-hit' | 'downloaded';
181
204
  }
182
205
  /** Handle returned from {@link attachOpfsParquetTables}. */
183
206
  interface OpfsAttachedHandle {
@@ -484,4 +507,4 @@ declare function createBrowserAnalysisRuntime(boot: DuckDBWasmBootResult, option
484
507
  version?: number | string;
485
508
  attachedTables?: readonly string[];
486
509
  }): BrowserAnalysisRuntime;
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 };
510
+ 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 OpfsAttachTiming, type OpfsAttachTimingStage, 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
@@ -567,6 +567,28 @@ var OpfsQuotaExceededError = class extends Error {
567
567
  const DEFAULT_CONCURRENCY = 2;
568
568
  const OPFS_PREFIX = "gscdump-snapshot__";
569
569
  const RECOVER_BUFFER_PREFIX = "gscdump-recover__";
570
+ const MAX_CONTENT_HASH_SLUG_CACHE = 4096;
571
+ const contentHashSlugCache = /* @__PURE__ */ new Map();
572
+ function nowMs() {
573
+ return globalThis.performance?.now?.() ?? Date.now();
574
+ }
575
+ function emitTiming(onTiming, info) {
576
+ try {
577
+ onTiming?.(info);
578
+ } catch {}
579
+ }
580
+ async function timed(onTiming, stage, meta, fn) {
581
+ const started = nowMs();
582
+ try {
583
+ return await fn();
584
+ } finally {
585
+ emitTiming(onTiming, {
586
+ ...meta,
587
+ stage,
588
+ durationMs: nowMs() - started
589
+ });
590
+ }
591
+ }
570
592
  const dbRegistries = /* @__PURE__ */ new WeakMap();
571
593
  function getOpfsRegistry(db, protocol) {
572
594
  let registry = dbRegistries.get(db);
@@ -636,6 +658,20 @@ async function estimateOpfsStorage() {
636
658
  } : {};
637
659
  }
638
660
  async function contentHashSlug(contentHash) {
661
+ const cached = contentHashSlugCache.get(contentHash);
662
+ if (cached) return cached;
663
+ if (contentHashSlugCache.size >= MAX_CONTENT_HASH_SLUG_CACHE) {
664
+ const oldest = contentHashSlugCache.keys().next().value;
665
+ if (oldest !== void 0) contentHashSlugCache.delete(oldest);
666
+ }
667
+ const promise = deriveContentHashSlug(contentHash).catch((err) => {
668
+ contentHashSlugCache.delete(contentHash);
669
+ throw err;
670
+ });
671
+ contentHashSlugCache.set(contentHash, promise);
672
+ return promise;
673
+ }
674
+ async function deriveContentHashSlug(contentHash) {
639
675
  const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(contentHash));
640
676
  const bytes = new Uint8Array(digest);
641
677
  let hex = "";
@@ -724,43 +760,49 @@ async function runWithConcurrency$1(items, concurrency, fn) {
724
760
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
725
761
  }
726
762
  async function attachOpfsParquetTables(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;
728
- await requestPersistentStorage();
729
- const root = await getOpfsRoot();
763
+ 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;
764
+ const totalStarted = nowMs();
765
+ await timed(onTiming, "persist", {}, requestPersistentStorage);
766
+ const root = await timed(onTiming, "root", {}, getOpfsRoot);
730
767
  const flat = [];
731
768
  const expectedByTable = /* @__PURE__ */ new Map();
732
- for (const t of tables) {
733
- const expected = /* @__PURE__ */ new Set();
734
- for (let i = 0; i < t.files.length; i++) {
735
- const file = t.files[i];
736
- const slug = file.contentHash ? await contentHashSlug(file.contentHash) : void 0;
737
- const name = opfsFileName(t.table, slug, i);
738
- flat.push({
739
- table: t.table,
740
- file,
741
- name
742
- });
743
- expected.add(name);
744
- }
745
- if (t.overlay) {
746
- const slug = t.overlay.contentHash ? await contentHashSlug(t.overlay.contentHash) : void 0;
747
- const name = opfsFileName(t.table, slug, t.files.length);
748
- flat.push({
749
- table: t.table,
750
- file: t.overlay,
751
- name,
752
- overlay: true
753
- });
754
- expected.add(name);
769
+ await timed(onTiming, "plan", { tables: tables.length }, async () => {
770
+ for (const t of tables) {
771
+ const expected = /* @__PURE__ */ new Set();
772
+ for (let i = 0; i < t.files.length; i++) {
773
+ const file = t.files[i];
774
+ const slug = file.contentHash ? await contentHashSlug(file.contentHash) : void 0;
775
+ const name = opfsFileName(t.table, slug, i);
776
+ flat.push({
777
+ table: t.table,
778
+ file,
779
+ name
780
+ });
781
+ expected.add(name);
782
+ }
783
+ if (t.overlay) {
784
+ const slug = t.overlay.contentHash ? await contentHashSlug(t.overlay.contentHash) : void 0;
785
+ const name = opfsFileName(t.table, slug, t.files.length);
786
+ flat.push({
787
+ table: t.table,
788
+ file: t.overlay,
789
+ name,
790
+ overlay: true
791
+ });
792
+ expected.add(name);
793
+ }
794
+ expectedByTable.set(t.table, expected);
755
795
  }
756
- expectedByTable.set(t.table, expected);
757
- }
796
+ });
758
797
  const total = flat.length;
759
798
  const overlayNames = /* @__PURE__ */ new Map();
760
799
  const expectedCount = new Map(tables.map((t) => [t.table, t.files.length + (t.overlay ? 1 : 0)]));
761
- const { DuckDBDataProtocol } = await import("@duckdb/duckdb-wasm");
800
+ const { DuckDBDataProtocol } = await timed(onTiming, "duckdb-import", {}, () => import("@duckdb/duckdb-wasm"));
762
801
  const registry = getOpfsRegistry(db, DuckDBDataProtocol.BROWSER_FSACCESS);
763
- await sweepStaleEntries(root, registry, expectedByTable).catch(() => {});
802
+ await timed(onTiming, "sweep", {
803
+ files: total,
804
+ tables: tables.length
805
+ }, () => sweepStaleEntries(root, registry, expectedByTable)).catch(() => {});
764
806
  const tableFiles = /* @__PURE__ */ new Map();
765
807
  const degraded = /* @__PURE__ */ new Set();
766
808
  const degradeReason = /* @__PURE__ */ new Map();
@@ -776,10 +818,23 @@ async function attachOpfsParquetTables(options) {
776
818
  const runDownloads = () => runWithConcurrency$1(flat, Math.max(1, fetchConcurrency), async (item, index) => {
777
819
  if (degraded.has(item.table)) return;
778
820
  signal?.throwIfAborted();
821
+ const fileStarted = nowMs();
779
822
  const name = item.name;
780
823
  let result;
824
+ let materialiseMs = 0;
825
+ let registerMs = 0;
781
826
  try {
827
+ const materialiseStarted = nowMs();
782
828
  result = await materialiseFile(root, name, item.file, fetchImpl, fetchInit, signal);
829
+ materialiseMs = nowMs() - materialiseStarted;
830
+ emitTiming(onTiming, {
831
+ stage: "materialise",
832
+ durationMs: materialiseMs,
833
+ table: item.table,
834
+ file: name,
835
+ bytes: item.file.bytes,
836
+ outcome: result.outcome
837
+ });
783
838
  } catch (err) {
784
839
  if (isAbortError$1(err)) throw err;
785
840
  if (isQuotaError(err)) {
@@ -795,7 +850,17 @@ async function attachOpfsParquetTables(options) {
795
850
  throw err;
796
851
  }
797
852
  try {
853
+ const registerStarted = nowMs();
798
854
  await withDb(() => registry.acquire(name, () => result.handle));
855
+ registerMs = nowMs() - registerStarted;
856
+ emitTiming(onTiming, {
857
+ stage: "register",
858
+ durationMs: registerMs,
859
+ table: item.table,
860
+ file: name,
861
+ bytes: item.file.bytes,
862
+ outcome: result.outcome
863
+ });
799
864
  acquiredNames.add(name);
800
865
  } catch (err) {
801
866
  if (isAbortError$1(err)) throw err;
@@ -820,11 +885,17 @@ async function attachOpfsParquetTables(options) {
820
885
  index,
821
886
  total,
822
887
  bytes: item.file.bytes,
823
- outcome: result.outcome
888
+ outcome: result.outcome,
889
+ materialiseMs,
890
+ registerMs,
891
+ totalMs: nowMs() - fileStarted
824
892
  });
825
893
  });
826
894
  try {
827
- await runDownloads();
895
+ await timed(onTiming, "downloads", {
896
+ files: total,
897
+ tables: tables.length
898
+ }, runDownloads);
828
899
  } catch (err) {
829
900
  await withDb(() => registry.release([...acquiredNames])).catch(() => {});
830
901
  throw err;
@@ -847,7 +918,10 @@ async function attachOpfsParquetTables(options) {
847
918
  const lakeNames = overlayName ? files.map((f) => f.name).filter((n) => n !== overlayName) : files.map((f) => f.name);
848
919
  try {
849
920
  signal?.throwIfAborted();
850
- await withDb(() => conn.query(overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames)));
921
+ await timed(onTiming, "view", {
922
+ table: t.table,
923
+ files: files.length
924
+ }, () => withDb(() => conn.query(overlayName ? readParquetViewWithOverlaySql(schema, t.table, lakeNames, overlayName) : readParquetViewSql$1(schema, t.table, lakeNames))));
851
925
  } catch (err) {
852
926
  if (isAbortError$1(err)) {
853
927
  await withDb(() => detachOpfs(registry, conn, schema, attached, [...acquiredNames])).catch(() => {});
@@ -868,43 +942,48 @@ async function attachOpfsParquetTables(options) {
868
942
  for (const f of files) registeredNames.push(f.name);
869
943
  }
870
944
  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
945
+ return timed(onTiming, "recovery", {
946
+ table: t.table,
947
+ files: t.files.length + (t.overlay ? 1 : 0)
948
+ }, async () => {
949
+ const readOne = async (file, index) => {
950
+ const cached = await readOpfsSnapshotFile(t.table, file.contentHash, index, file.bytes);
951
+ if (cached) return cached;
952
+ signal?.throwIfAborted();
953
+ const resp = await fetchImpl(file.url, {
954
+ ...fetchInit,
955
+ signal
956
+ });
957
+ if (!resp.ok) throw new Error(`[engine-duckdb-wasm/opfs] recover ${file.url} failed: ${resp.status}`);
958
+ return new Uint8Array(await resp.arrayBuffer());
959
+ };
960
+ const lakeNames = [];
961
+ 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));
965
+ bufferFiles.push(name);
966
+ lakeNames.push(name);
967
+ bytesAttached += t.files[i].bytes;
968
+ }
969
+ let overlayName;
970
+ if (t.overlay) {
971
+ 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));
974
+ bufferFiles.push(overlayName);
975
+ bytesAttached += t.overlay.bytes;
976
+ }
977
+ const body = overlayViewBody({
978
+ lakeSelect: lakeNames.length ? lakeSelect(lakeNames) : null,
979
+ overlaySelect: overlayName ? `SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet(['${overlayName.replace(/'/g, "''")}'], union_by_name = true)` : null,
980
+ materializeLake: false
878
981
  });
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
982
+ if (!body) return false;
983
+ await withDb(() => conn.query(`CREATE OR REPLACE VIEW ${schema}.${t.table} AS ${body}`));
984
+ bufferViews.push(t.table);
985
+ return true;
903
986
  });
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
987
  };
909
988
  if (recoverContention && !signal?.aborted) for (const t of tables) {
910
989
  if (!degraded.has(t.table) || degradeReason.get(t.table) !== "contention") continue;
@@ -915,6 +994,13 @@ async function attachOpfsParquetTables(options) {
915
994
  bufferFiles.length = before;
916
995
  }
917
996
  }
997
+ emitTiming(onTiming, {
998
+ stage: "total",
999
+ durationMs: nowMs() - totalStarted,
1000
+ files: total,
1001
+ tables: tables.length,
1002
+ bytes: bytesAttached
1003
+ });
918
1004
  let detached = false;
919
1005
  return {
920
1006
  version,
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.9",
4
+ "version": "0.32.11",
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.32.9",
49
- "@gscdump/contracts": "0.32.9",
50
- "gscdump": "0.32.9"
48
+ "@gscdump/contracts": "0.32.11",
49
+ "@gscdump/engine": "0.32.11",
50
+ "gscdump": "0.32.11"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@duckdb/duckdb-wasm": "^1.32.0",