@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 +5 -0
- package/dist/index.mjs +87 -24
- package/dist/server-tail/index.d.mts +1 -0
- package/dist/server-tail/index.mjs +5 -1
- package/package.json +5 -5
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
|
-
|
|
78
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
371
|
+
result = await runSQLWithDeadline(svc, {
|
|
312
372
|
sql: finalSql,
|
|
313
373
|
tables
|
|
314
|
-
}
|
|
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
|
-
}),
|
|
387
|
+
}), rpcTimeoutMs, signal);
|
|
328
388
|
}
|
|
329
|
-
result = await
|
|
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] }),
|
|
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, {
|
|
427
|
+
executor: createDucklingsExecutor(env, {
|
|
428
|
+
rowCache,
|
|
429
|
+
...hooks.duckdbRpcTimeoutMs !== void 0 ? { rpcTimeoutMs: hooks.duckdbRpcTimeoutMs } : {}
|
|
430
|
+
})
|
|
368
431
|
});
|
|
369
432
|
} };
|
|
370
433
|
}
|
|
@@ -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
|
|
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.
|
|
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/
|
|
50
|
-
"@gscdump/
|
|
51
|
-
"
|
|
52
|
-
"gscdump": "0.
|
|
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",
|