@gscdump/cloudflare 0.37.2 → 0.37.4

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
@@ -1,4 +1,4 @@
1
- import { ParquetCodec, QueryExecutor, createStorageEngine } from "@gscdump/engine";
1
+ import { ParquetCodec, QueryExecutor, Row, createStorageEngine } from "@gscdump/engine";
2
2
  import { H3Event } from "h3";
3
3
  interface AnalyticsEnv {
4
4
  /** R2 bucket holding parquet + rollup + entity data. Required in origin mode. */
@@ -62,6 +62,10 @@ interface AnalyticsEngineHooks {
62
62
  /** Called once per R2 PUT, with the byte size of the payload. */
63
63
  onR2Write?: (byteLength: number) => void;
64
64
  }
65
+ interface AnalyticsEngineRuntime {
66
+ getEngine: (env: AnalyticsEnv, db: any, hooks?: AnalyticsEngineHooks) => ReturnType<typeof createStorageEngine> | null;
67
+ }
68
+ declare function createAnalyticsEngineRuntime(): AnalyticsEngineRuntime;
65
69
  declare function getAnalyticsEngine(env: AnalyticsEnv, db: any, hooks?: AnalyticsEngineHooks): ReturnType<typeof createStorageEngine> | null;
66
70
  interface InflightDedupe<T> {
67
71
  dedupe: (key: string, run: () => Promise<T>) => Promise<T>;
@@ -80,10 +84,17 @@ declare function getHostedR2QueryKey(input: HostedR2QueryKeyInput): string;
80
84
  declare function signSizeHint(env: AnalyticsEnv, key: string, bytes: number): Promise<string>;
81
85
  declare function verifySizeHint(env: AnalyticsEnv, key: string, bytes: number, providedHex: string): Promise<boolean>;
82
86
  declare function createDucklingsCodec(_env: AnalyticsEnv): ParquetCodec;
87
+ interface DucklingsRowCache {
88
+ clear: () => void;
89
+ get: (key: string) => Row[] | undefined;
90
+ put: (key: string, rows: Row[]) => void;
91
+ }
92
+ declare function createDucklingsRowCache(maxBytes?: number): DucklingsRowCache;
83
93
  interface DucklingsExecutorOptions {
84
94
  ipcChunkBytes?: number;
85
95
  ipcDirectCallBytes?: number;
86
96
  ipcTotalBytes?: number;
97
+ rowCache?: DucklingsRowCache;
87
98
  }
88
99
  declare function createDucklingsExecutor(env: AnalyticsEnv, opts?: DucklingsExecutorOptions): QueryExecutor;
89
- export { type AnalyticsEngineHooks, type AnalyticsEnv, type HostedR2QueryKeyInput, type InflightDedupe, createDucklingsCodec, createDucklingsExecutor, createInflightDedupe, getAnalyticsEngine, getHostedR2QueryKey, signSizeHint, useAnalyticsEnv, verifySizeHint };
100
+ export { type AnalyticsEngineHooks, type AnalyticsEngineRuntime, type AnalyticsEnv, type DucklingsRowCache, type HostedR2QueryKeyInput, type InflightDedupe, createAnalyticsEngineRuntime, createDucklingsCodec, createDucklingsExecutor, createDucklingsRowCache, createInflightDedupe, getAnalyticsEngine, getHostedR2QueryKey, signSizeHint, useAnalyticsEnv, verifySizeHint };
package/dist/index.mjs CHANGED
@@ -119,55 +119,54 @@ function assertWorkerReadBudget(opts) {
119
119
  if (totalBytes > maxBytes) throw new Error(`createDucklingsExecutor: planned read spans ${totalBytes} bytes, exceeding the ${maxBytes} byte Worker budget. Narrow the date range or route through a background/windowed query.`);
120
120
  }
121
121
  const ROW_CACHE_MAX_BYTES = 16 * 1024 * 1024;
122
- let rowCacheBytes = 0;
123
- const rowCache = /* @__PURE__ */ new Map();
124
122
  function estimateRowsBytes(rows) {
125
123
  if (rows.length === 0) return 0;
126
124
  const cols = Object.keys(rows[0]).length;
127
125
  return rows.length * cols * 64;
128
126
  }
129
- function inferExtraColumnType(values) {
130
- let hasValue = false;
131
- let hasString = false;
132
- let hasFloat = false;
133
- let hasBigInt = false;
134
- for (const value of values) {
135
- if (value === null || value === void 0) continue;
136
- hasValue = true;
137
- if (typeof value === "string") {
138
- hasString = true;
139
- break;
140
- }
141
- if (typeof value === "bigint") {
142
- hasBigInt = true;
143
- continue;
144
- }
145
- if (typeof value === "number") {
146
- if (!Number.isInteger(value)) hasFloat = true;
147
- if (value > 2147483647 || value < -2147483648) hasBigInt = true;
148
- }
127
+ function addInferredValue(inference, value) {
128
+ if (value === null || value === void 0) return;
129
+ inference.hasValue = true;
130
+ if (typeof value === "string") {
131
+ inference.hasString = true;
132
+ return;
149
133
  }
150
- if (!hasValue || hasString) return "VARCHAR";
151
- if (hasFloat) return "DOUBLE";
152
- return hasBigInt ? "BIGINT" : "INTEGER";
134
+ if (typeof value === "bigint") {
135
+ inference.hasBigInt = true;
136
+ return;
137
+ }
138
+ if (typeof value === "number") {
139
+ if (!Number.isInteger(value)) inference.hasFloat = true;
140
+ if (value > 2147483647 || value < -2147483648) inference.hasBigInt = true;
141
+ }
142
+ }
143
+ function inferredColumnType(inference) {
144
+ if (!inference.hasValue || inference.hasString) return "VARCHAR";
145
+ if (inference.hasFloat) return "DOUBLE";
146
+ return inference.hasBigInt ? "BIGINT" : "INTEGER";
153
147
  }
154
148
  function chunkSchemaColumns(rows, schemaColumns) {
155
149
  const columns = schemaColumns ? [...schemaColumns] : [];
156
150
  const seen = new Set(columns.map((c) => c.name));
157
- const extraValues = /* @__PURE__ */ new Map();
151
+ const extraColumns = /* @__PURE__ */ new Map();
158
152
  for (const row of rows) for (const key in row) {
159
153
  if (seen.has(key)) continue;
160
- let values = extraValues.get(key);
161
- if (!values) {
162
- values = [];
163
- extraValues.set(key, values);
154
+ let inference = extraColumns.get(key);
155
+ if (!inference) {
156
+ inference = {
157
+ hasValue: false,
158
+ hasString: false,
159
+ hasFloat: false,
160
+ hasBigInt: false
161
+ };
162
+ extraColumns.set(key, inference);
164
163
  }
165
- values.push(row[key]);
164
+ addInferredValue(inference, row[key]);
166
165
  }
167
- if (extraValues.size === 0) return schemaColumns ? columns : void 0;
168
- for (const [name, values] of extraValues) columns.push({
166
+ if (extraColumns.size === 0) return schemaColumns ? columns : void 0;
167
+ for (const [name, inference] of extraColumns) columns.push({
169
168
  name,
170
- type: inferExtraColumnType(values),
169
+ type: inferredColumnType(inference),
171
170
  nullable: true
172
171
  });
173
172
  return columns;
@@ -208,30 +207,46 @@ function rowsToArrowIPCChunks(rows, schemaColumns, opts = {}) {
208
207
  }
209
208
  return chunks;
210
209
  }
211
- function rowCacheGet(key) {
212
- const hit = rowCache.get(key);
213
- if (!hit) return void 0;
214
- rowCache.delete(key);
215
- rowCache.set(key, hit);
216
- return hit.rows;
217
- }
218
- function rowCachePut(key, rows) {
219
- const bytes = estimateRowsBytes(rows);
220
- if (bytes > ROW_CACHE_MAX_BYTES) return;
221
- while (rowCacheBytes + bytes > ROW_CACHE_MAX_BYTES) {
222
- const oldest = rowCache.keys().next().value;
223
- if (oldest === void 0) break;
224
- const evicted = rowCache.get(oldest);
225
- rowCache.delete(oldest);
226
- rowCacheBytes -= evicted.bytes;
227
- }
228
- rowCache.set(key, {
229
- rows,
230
- bytes
231
- });
232
- rowCacheBytes += bytes;
210
+ function createDucklingsRowCache(maxBytes = ROW_CACHE_MAX_BYTES) {
211
+ let totalBytes = 0;
212
+ const entries = /* @__PURE__ */ new Map();
213
+ return {
214
+ clear() {
215
+ entries.clear();
216
+ totalBytes = 0;
217
+ },
218
+ get(key) {
219
+ const hit = entries.get(key);
220
+ if (!hit) return void 0;
221
+ entries.delete(key);
222
+ entries.set(key, hit);
223
+ return hit.rows;
224
+ },
225
+ put(key, rows) {
226
+ const bytes = estimateRowsBytes(rows);
227
+ if (bytes > maxBytes) return;
228
+ const existing = entries.get(key);
229
+ if (existing) {
230
+ entries.delete(key);
231
+ totalBytes -= existing.bytes;
232
+ }
233
+ while (totalBytes + bytes > maxBytes) {
234
+ const oldest = entries.keys().next().value;
235
+ if (oldest === void 0) break;
236
+ const evicted = entries.get(oldest);
237
+ entries.delete(oldest);
238
+ totalBytes -= evicted.bytes;
239
+ }
240
+ entries.set(key, {
241
+ rows,
242
+ bytes
243
+ });
244
+ totalBytes += bytes;
245
+ }
246
+ };
233
247
  }
234
248
  function createDucklingsExecutor(env, opts = {}) {
249
+ const rowCache = opts.rowCache ?? createDucklingsRowCache();
235
250
  return { async execute({ sql, params, fileKeys, placeholderTables, pushdownFilters, dataSource, signal, table }) {
236
251
  signal?.throwIfAborted();
237
252
  const svc = resolveSvc(env);
@@ -259,14 +274,14 @@ function createDucklingsExecutor(env, opts = {}) {
259
274
  const filter = pushdownFilters?.[placeholder];
260
275
  const perFile = await mapLimit(keys, WORKER_R2_DECODE_CONCURRENCY, async (key) => {
261
276
  if (!filter) {
262
- const cached = rowCacheGet(key);
277
+ const cached = rowCache.get(key);
263
278
  if (cached) return cached;
264
279
  }
265
280
  signal?.throwIfAborted();
266
281
  const bytes = await dataSource.read(key, void 0, signal);
267
282
  signal?.throwIfAborted();
268
283
  const rows = await decodeParquetToRows(bytes, filter ? { filter } : {});
269
- if (!filter) rowCachePut(key, rows);
284
+ if (!filter) rowCache.put(key, rows);
270
285
  return rows;
271
286
  });
272
287
  const merged = [];
@@ -331,24 +346,32 @@ function createDucklingsExecutor(env, opts = {}) {
331
346
  };
332
347
  } };
333
348
  }
349
+ function createAnalyticsEngineRuntime() {
350
+ const rowCache = createDucklingsRowCache();
351
+ return { getEngine(env, db, hooks = {}) {
352
+ if (!env.R2_DATA) return null;
353
+ const baseDataSource = createR2DataSource({
354
+ bucket: env.R2_DATA,
355
+ bucketName: env.R2_BUCKET_NAME
356
+ });
357
+ return createStorageEngine({
358
+ dataSource: hooks.onR2Write ? {
359
+ ...baseDataSource,
360
+ async write(key, bytes) {
361
+ hooks.onR2Write(bytes.byteLength);
362
+ return baseDataSource.write(key, bytes);
363
+ }
364
+ } : baseDataSource,
365
+ manifestStore: createD1ManifestStore(db),
366
+ codec: createDucklingsCodec(env),
367
+ executor: createDucklingsExecutor(env, { rowCache })
368
+ });
369
+ } };
370
+ }
371
+ let sharedRuntime;
334
372
  function getAnalyticsEngine(env, db, hooks = {}) {
335
- if (!env.R2_DATA) return null;
336
- const baseDataSource = createR2DataSource({
337
- bucket: env.R2_DATA,
338
- bucketName: env.R2_BUCKET_NAME
339
- });
340
- return createStorageEngine({
341
- dataSource: hooks.onR2Write ? {
342
- ...baseDataSource,
343
- async write(key, bytes) {
344
- hooks.onR2Write(bytes.byteLength);
345
- return baseDataSource.write(key, bytes);
346
- }
347
- } : baseDataSource,
348
- manifestStore: createD1ManifestStore(db),
349
- codec: createDucklingsCodec(env),
350
- executor: createDucklingsExecutor(env)
351
- });
373
+ sharedRuntime ??= createAnalyticsEngineRuntime();
374
+ return sharedRuntime.getEngine(env, db, hooks);
352
375
  }
353
376
  function useAnalyticsEnv(event) {
354
377
  const fromCtx = event.context.analyticsEnv;
@@ -436,4 +459,4 @@ async function verifySizeHint(env, key, bytes, providedHex) {
436
459
  for (let i = 0; i < SIG_HEX_LEN; i++) diff |= expected.charCodeAt(i) ^ providedHex.charCodeAt(i);
437
460
  return diff === 0;
438
461
  }
439
- export { createDucklingsCodec, createDucklingsExecutor, createInflightDedupe, getAnalyticsEngine, getHostedR2QueryKey, signSizeHint, useAnalyticsEnv, verifySizeHint };
462
+ export { createAnalyticsEngineRuntime, createDucklingsCodec, createDucklingsExecutor, createDucklingsRowCache, createInflightDedupe, getAnalyticsEngine, getHostedR2QueryKey, signSizeHint, useAnalyticsEnv, verifySizeHint };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cloudflare",
3
3
  "type": "module",
4
- "version": "0.37.2",
4
+ "version": "0.37.4",
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.37.2",
50
- "gscdump": "0.37.2",
51
- "@gscdump/engine-sqlite": "0.37.2",
52
- "@gscdump/engine": "0.37.2"
49
+ "@gscdump/contracts": "0.37.4",
50
+ "@gscdump/engine-sqlite": "0.37.4",
51
+ "@gscdump/engine": "0.37.4",
52
+ "gscdump": "0.37.4"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@cloudflare/vitest-pool-workers": "^0.18.0",