@gscdump/cloudflare 0.38.1 → 0.39.0

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
@@ -21,6 +21,7 @@ interface AnalyticsEnv {
21
21
  tables?: Record<string, {
22
22
  ipc: Uint8Array;
23
23
  }>;
24
+ deadlineAt?: number;
24
25
  }) => Promise<{
25
26
  rows: unknown[];
26
27
  sql: string;
@@ -61,6 +62,8 @@ declare function useAnalyticsEnv(event: H3Event): AnalyticsEnv;
61
62
  interface AnalyticsEngineHooks {
62
63
  /** Called once per R2 PUT, with the byte size of the payload. */
63
64
  onR2Write?: (byteLength: number) => void;
65
+ /** Override the interactive 22-second DuckDB RPC budget for background work. */
66
+ duckdbRpcTimeoutMs?: number;
64
67
  }
65
68
  interface AnalyticsEngineRuntime {
66
69
  getEngine: (env: AnalyticsEnv, db: any, hooks?: AnalyticsEngineHooks) => ReturnType<typeof createStorageEngine> | null;
@@ -94,6 +97,8 @@ interface DucklingsExecutorOptions {
94
97
  ipcChunkBytes?: number;
95
98
  ipcDirectCallBytes?: number;
96
99
  ipcTotalBytes?: number;
100
+ /** Per-RPC wall/queue budget. Interactive default is 22 seconds. */
101
+ rpcTimeoutMs?: number;
97
102
  rowCache?: DucklingsRowCache;
98
103
  }
99
104
  declare function createDucklingsExecutor(env: AnalyticsEnv, opts?: DucklingsExecutorOptions): QueryExecutor;
package/dist/index.mjs CHANGED
@@ -74,15 +74,32 @@ var DuckDBServiceTimeoutError = class extends Error {
74
74
  function withDuckDBDeadline(op, timeoutMs, signal) {
75
75
  if (signal?.aborted) return Promise.reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
76
76
  return new Promise((resolve, reject) => {
77
- const timer = setTimeout(() => reject(new DuckDBServiceTimeoutError(timeoutMs)), timeoutMs);
78
- const onAbort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
77
+ let settled = false;
78
+ let timer;
79
+ let onAbort;
80
+ const cleanup = () => {
81
+ if (timer !== void 0) clearTimeout(timer);
82
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
83
+ };
84
+ const settle = (complete, value) => {
85
+ if (settled) return;
86
+ settled = true;
87
+ cleanup();
88
+ complete(value);
89
+ };
90
+ onAbort = () => settle(reject, signal.reason ?? new DOMException("Aborted", "AbortError"));
91
+ timer = setTimeout(() => settle(reject, new DuckDBServiceTimeoutError(timeoutMs)), timeoutMs);
79
92
  signal?.addEventListener("abort", onAbort, { once: true });
80
- op.then(resolve, reject).finally(() => {
81
- clearTimeout(timer);
82
- signal?.removeEventListener("abort", onAbort);
83
- });
93
+ op.then((value) => settle(resolve, value), (error) => settle(reject, error));
84
94
  });
85
95
  }
96
+ function runSQLWithDeadline(svc, args, timeoutMs, signal) {
97
+ const deadlineAt = Date.now() + timeoutMs;
98
+ return withDuckDBDeadline(svc.runSQL({
99
+ ...args,
100
+ deadlineAt
101
+ }), timeoutMs, signal);
102
+ }
86
103
  function createDucklingsCodec(_env) {
87
104
  return createHyparquetCodec();
88
105
  }
@@ -95,13 +112,21 @@ async function mapLimit(items, concurrency, fn) {
95
112
  const limit = Math.max(1, Math.floor(concurrency));
96
113
  const out = Array.from({ length: items.length });
97
114
  let next = 0;
115
+ let failed = false;
116
+ let failure;
98
117
  async function worker() {
99
- while (next < items.length) {
118
+ while (!failed && next < items.length) {
100
119
  const index = next++;
101
- out[index] = await fn(items[index], index);
120
+ try {
121
+ out[index] = await fn(items[index], index);
122
+ } catch (error) {
123
+ failed = true;
124
+ failure = error;
125
+ }
102
126
  }
103
127
  }
104
128
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
129
+ if (failed) throw failure;
105
130
  return out;
106
131
  }
107
132
  function assertWorkerReadBudget(opts) {
@@ -247,19 +272,42 @@ function createDucklingsRowCache(maxBytes = ROW_CACHE_MAX_BYTES) {
247
272
  }
248
273
  function createDucklingsExecutor(env, opts = {}) {
249
274
  const rowCache = opts.rowCache ?? createDucklingsRowCache();
275
+ const rpcTimeoutMs = opts.rpcTimeoutMs ?? DUCKDB_RPC_TIMEOUT_MS;
276
+ if (!Number.isFinite(rpcTimeoutMs) || rpcTimeoutMs <= 0) throw new TypeError("createDucklingsExecutor: rpcTimeoutMs must be a positive finite number");
250
277
  return { async execute({ sql, params, fileKeys, placeholderTables, pushdownFilters, dataSource, signal, table }) {
251
278
  signal?.throwIfAborted();
252
279
  const svc = resolveSvc(env);
253
280
  assertWorkerReadBudget({ fileKeys });
254
- if (dataSource.head) {
255
- const uniqueKeys = [...new Set(Object.values(fileKeys).flat())];
281
+ const cachedUnfiltered = /* @__PURE__ */ new Map();
282
+ const scheduledUnfiltered = /* @__PURE__ */ new Set();
283
+ const plannedReadKeys = [];
284
+ for (const [placeholder, keys] of Object.entries(fileKeys)) {
285
+ const filter = pushdownFilters?.[placeholder];
286
+ for (const key of keys) {
287
+ if (filter) {
288
+ plannedReadKeys.push(key);
289
+ continue;
290
+ }
291
+ const cached = cachedUnfiltered.get(key) ?? rowCache.get(key);
292
+ if (cached !== void 0) {
293
+ cachedUnfiltered.set(key, cached);
294
+ continue;
295
+ }
296
+ if (!scheduledUnfiltered.has(key)) {
297
+ scheduledUnfiltered.add(key);
298
+ plannedReadKeys.push(key);
299
+ }
300
+ }
301
+ }
302
+ if (dataSource.head && plannedReadKeys.length > 0) {
303
+ const uniqueKeys = [...new Set(plannedReadKeys)];
256
304
  const sizes = {};
257
305
  await mapLimit(uniqueKeys, WORKER_R2_HEAD_CONCURRENCY, async (key) => {
258
306
  signal?.throwIfAborted();
259
307
  sizes[key] = (await dataSource.head(key))?.bytes;
260
308
  });
261
309
  assertWorkerReadBudget({
262
- fileKeys,
310
+ fileKeys: { READS: plannedReadKeys },
263
311
  sizes
264
312
  });
265
313
  }
@@ -269,20 +317,32 @@ function createDucklingsExecutor(env, opts = {}) {
269
317
  const maxChunkBytes = opts.ipcChunkBytes ?? IPC_CHUNK_BUDGET;
270
318
  const maxDirectCallBytes = opts.ipcDirectCallBytes ?? IPC_DIRECT_CALL_BUDGET;
271
319
  const maxTotalBytes = opts.ipcTotalBytes ?? IPC_STAGED_TOTAL_BUDGET;
320
+ const unfilteredLoads = /* @__PURE__ */ new Map();
321
+ for (const [key, rows] of cachedUnfiltered) unfilteredLoads.set(key, Promise.resolve(rows));
322
+ const loadUnfiltered = (key) => {
323
+ let loading = unfilteredLoads.get(key);
324
+ if (!loading) {
325
+ loading = (async () => {
326
+ signal?.throwIfAborted();
327
+ const bytes = await dataSource.read(key, void 0, signal);
328
+ signal?.throwIfAborted();
329
+ const rows = await decodeParquetToRows(bytes);
330
+ rowCache.put(key, rows);
331
+ return rows;
332
+ })();
333
+ unfilteredLoads.set(key, loading);
334
+ }
335
+ return loading;
336
+ };
272
337
  for (const [placeholder, keys] of Object.entries(fileKeys)) {
273
338
  signal?.throwIfAborted();
274
339
  const filter = pushdownFilters?.[placeholder];
275
340
  const perFile = await mapLimit(keys, WORKER_R2_DECODE_CONCURRENCY, async (key) => {
276
- if (!filter) {
277
- const cached = rowCache.get(key);
278
- if (cached) return cached;
279
- }
341
+ if (!filter) return loadUnfiltered(key);
280
342
  signal?.throwIfAborted();
281
343
  const bytes = await dataSource.read(key, void 0, signal);
282
344
  signal?.throwIfAborted();
283
- const rows = await decodeParquetToRows(bytes, filter ? { filter } : {});
284
- if (!filter) rowCache.put(key, rows);
285
- return rows;
345
+ return await decodeParquetToRows(bytes, filter ? { filter } : {});
286
346
  });
287
347
  const merged = [];
288
348
  for (const rows of perFile) merged.push(...rows);
@@ -308,10 +368,10 @@ function createDucklingsExecutor(env, opts = {}) {
308
368
  if (canInlineTables) {
309
369
  const tables = {};
310
370
  for (const [name, chunks] of Object.entries(tableChunks)) tables[name] = { ipc: chunks[0].ipc };
311
- result = await withDuckDBDeadline(svc.runSQL({
371
+ result = await runSQLWithDeadline(svc, {
312
372
  sql: finalSql,
313
373
  tables
314
- }), DUCKDB_RPC_TIMEOUT_MS, signal);
374
+ }, rpcTimeoutMs, signal);
315
375
  } else {
316
376
  if (!svc.stageArrowTable || !svc.dropTables) throw new Error("createDucklingsExecutor: DUCKDB_SVC does not support chunked Arrow IPC staging. Deploy the gscdump-duckdb worker with stageArrowTable/dropTables support.");
317
377
  const staged = /* @__PURE__ */ new Set();
@@ -324,14 +384,14 @@ function createDucklingsExecutor(env, opts = {}) {
324
384
  await withDuckDBDeadline(svc.stageArrowTable({
325
385
  table: name,
326
386
  ipc: chunk.ipc
327
- }), DUCKDB_RPC_TIMEOUT_MS, signal);
387
+ }), rpcTimeoutMs, signal);
328
388
  }
329
- result = await withDuckDBDeadline(svc.runSQL({ sql: finalSql }), DUCKDB_RPC_TIMEOUT_MS, signal);
389
+ result = await runSQLWithDeadline(svc, { sql: finalSql }, rpcTimeoutMs, signal);
330
390
  } catch (error) {
331
391
  primaryError = error;
332
392
  } finally {
333
393
  if (staged.size > 0) try {
334
- await withDuckDBDeadline(svc.dropTables({ tables: [...staged] }), DUCKDB_RPC_TIMEOUT_MS);
394
+ await withDuckDBDeadline(svc.dropTables({ tables: [...staged] }), rpcTimeoutMs);
335
395
  } catch (error) {
336
396
  cleanupError = error;
337
397
  }
@@ -364,7 +424,10 @@ function createAnalyticsEngineRuntime() {
364
424
  } : baseDataSource,
365
425
  manifestStore: createD1ManifestStore(db),
366
426
  codec: createDucklingsCodec(env),
367
- executor: createDucklingsExecutor(env, { rowCache })
427
+ executor: createDucklingsExecutor(env, {
428
+ rowCache,
429
+ ...hooks.duckdbRpcTimeoutMs !== void 0 ? { rpcTimeoutMs: hooks.duckdbRpcTimeoutMs } : {}
430
+ })
368
431
  });
369
432
  } };
370
433
  }
@@ -47,6 +47,7 @@ type DuckDbIcebergRow = Record<string, string | number | null>;
47
47
  interface DuckDbSvc {
48
48
  runSQL: (args: {
49
49
  sql: string;
50
+ deadlineAt?: number;
50
51
  }) => Promise<{
51
52
  rows: unknown[];
52
53
  sql: string;
@@ -460,7 +460,11 @@ function createDuckDbIcebergExecutor(config) {
460
460
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
461
461
  async function sendResult(sql) {
462
462
  const started = Date.now();
463
- const raced = await withDeadline(config.svc.runSQL({ sql }), timeoutMs).then((value) => ok(value)).catch((error) => error instanceof DuckDbIcebergTimeoutError ? err(error) : err(new DuckDbIcebergError(`DUCKDB_SVC.runSQL failed: ${error.message}`)));
463
+ const deadlineAt = Date.now() + timeoutMs;
464
+ const raced = await withDeadline(config.svc.runSQL({
465
+ sql,
466
+ deadlineAt
467
+ }), timeoutMs).then((value) => ok(value)).catch((error) => error instanceof DuckDbIcebergTimeoutError ? err(error) : err(new DuckDbIcebergError(`DUCKDB_SVC.runSQL failed: ${error.message}`)));
464
468
  if (!raced.ok) return raced;
465
469
  const result = raced.value;
466
470
  return ok({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cloudflare",
3
3
  "type": "module",
4
- "version": "0.38.1",
4
+ "version": "0.39.0",
5
5
  "description": "Cloudflare-Workers-flavored helpers for the gscdump analytics stack: AnalyticsEnv binding contract, R2 SigV4 presigner, size-hint HMAC, DuckDB Workers shims, engine factory.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -46,10 +46,10 @@
46
46
  "dependencies": {
47
47
  "@uwdata/flechette": "^2.5.0",
48
48
  "aws4fetch": "^1.0.20",
49
- "@gscdump/contracts": "0.38.1",
50
- "@gscdump/engine": "0.38.1",
51
- "@gscdump/engine-sqlite": "0.38.1",
52
- "gscdump": "0.38.1"
49
+ "@gscdump/engine": "0.39.0",
50
+ "@gscdump/contracts": "0.39.0",
51
+ "gscdump": "0.39.0",
52
+ "@gscdump/engine-sqlite": "0.39.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@cloudflare/vitest-pool-workers": "^0.18.4",