@gscdump/engine 0.33.0 → 0.33.3

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.
@@ -304,6 +304,12 @@ function txLeaf(leaf, columns) {
304
304
  if (!column || !columns.has(column)) return null;
305
305
  return { [column]: { $eq: leaf.expression } };
306
306
  }
307
+ function combineFilters(parts, groupType) {
308
+ const first = parts[0];
309
+ if (!first) return null;
310
+ if (parts.length === 1) return first;
311
+ return groupType === "or" ? { $or: [...parts] } : { $and: [...parts] };
312
+ }
307
313
  function txExact(node, columns) {
308
314
  const groupType = node._groupType ?? "and";
309
315
  const leafParts = [];
@@ -314,7 +320,7 @@ function txExact(node, columns) {
314
320
  }
315
321
  if (groupType === "or") {
316
322
  if (node._nestedGroups?.length || leafParts.length === 0) return null;
317
- return leafParts.length === 1 ? leafParts[0] : { $or: leafParts };
323
+ return combineFilters(leafParts, "or");
318
324
  }
319
325
  const parts = leafParts;
320
326
  for (const group of node._nestedGroups ?? []) {
@@ -323,7 +329,7 @@ function txExact(node, columns) {
323
329
  parts.push(t);
324
330
  }
325
331
  if (parts.length === 0) return null;
326
- return parts.length === 1 ? parts[0] : { $and: parts };
332
+ return combineFilters(parts, "and");
327
333
  }
328
334
  function extractParquetPushdown(state, table) {
329
335
  const filter = state?.filter;
@@ -341,7 +347,7 @@ function extractParquetPushdown(state, table) {
341
347
  if (t) parts.push(t);
342
348
  }
343
349
  if (parts.length === 0) return void 0;
344
- return parts.length === 1 ? parts[0] : { $and: parts };
350
+ return combineFilters(parts, "and") ?? void 0;
345
351
  }
346
352
  const URL_PURGE_TABLES = ["pages", "page_queries"];
347
353
  const MAX_DAY_BYTES = 100 * 1024 * 1024;
@@ -737,6 +737,7 @@ function createSitemapStore(opts) {
737
737
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
738
738
  if (!m) continue;
739
739
  const date = m[1];
740
+ if (!date) continue;
740
741
  if (from && date < from) continue;
741
742
  if (to && date > to) continue;
742
743
  const bytes = await readOptional(ds, key);
@@ -763,9 +764,11 @@ function createSitemapStore(opts) {
763
764
  for (const key of deltaKeys) {
764
765
  const m = SITEMAP_URLS_DELTA_PREFIX_RE.exec(key);
765
766
  if (!m) continue;
766
- const list = deltasByFeed.get(m[2]) ?? [];
767
+ const feedpathHash = m[2];
768
+ if (!feedpathHash) continue;
769
+ const list = deltasByFeed.get(feedpathHash) ?? [];
767
770
  list.push(key);
768
- deltasByFeed.set(m[2], list);
771
+ deltasByFeed.set(feedpathHash, list);
769
772
  }
770
773
  for (const [fpHash, feedDeltaKeys] of deltasByFeed) {
771
774
  const indexKey = sitemapUrlsIndexKey(ctx, fpHash);
@@ -0,0 +1,56 @@
1
+ import { inferLegacyTier, inferSearchType } from "./layout.mjs";
2
+ function manifestEntryKey(entry) {
3
+ return entry.objectKey;
4
+ }
5
+ function watermarkKey(watermark) {
6
+ return `${watermark.userId}|${watermark.siteId ?? ""}|${watermark.table}`;
7
+ }
8
+ function syncStateKey(state) {
9
+ return `${state.userId}|${state.siteId ?? ""}|${state.table}|${state.date}|${inferSearchType(state)}`;
10
+ }
11
+ function matchesManifestEntryFilter(entry, filter, options = {}) {
12
+ if (!options.ignoreUserId && entry.userId !== filter.userId) return false;
13
+ if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
14
+ if (filter.table !== void 0 && entry.table !== filter.table) return false;
15
+ if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
16
+ if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
17
+ if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
18
+ return true;
19
+ }
20
+ function matchesWatermarkFilter(watermark, filter, options = {}) {
21
+ if (!options.ignoreUserId && watermark.userId !== filter.userId) return false;
22
+ if (filter.siteId !== void 0 && watermark.siteId !== filter.siteId) return false;
23
+ if (filter.table !== void 0 && watermark.table !== filter.table) return false;
24
+ return true;
25
+ }
26
+ function matchesSyncStateFilter(state, filter, options = {}) {
27
+ if (!options.ignoreUserId && state.userId !== filter.userId) return false;
28
+ if (filter.siteId !== void 0 && state.siteId !== filter.siteId) return false;
29
+ if (filter.table !== void 0 && state.table !== filter.table) return false;
30
+ if (filter.state !== void 0 && state.state !== filter.state) return false;
31
+ if (filter.searchType !== void 0 && inferSearchType(state) !== filter.searchType) return false;
32
+ return true;
33
+ }
34
+ function mergeSyncState(existing, scope, state, detail) {
35
+ const at = detail?.at ?? Date.now();
36
+ const attemptsBump = state === "inflight" ? 1 : 0;
37
+ if (!existing) return {
38
+ userId: scope.userId,
39
+ siteId: scope.siteId,
40
+ table: scope.table,
41
+ date: scope.date,
42
+ state,
43
+ updatedAt: at,
44
+ attempts: attemptsBump,
45
+ error: detail?.error,
46
+ ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
47
+ };
48
+ return {
49
+ ...existing,
50
+ state,
51
+ updatedAt: at,
52
+ attempts: existing.attempts + attemptsBump,
53
+ error: state === "done" ? void 0 : detail?.error ?? existing.error
54
+ };
55
+ }
56
+ export { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter, mergeSyncState, syncStateKey, watermarkKey };
@@ -1,4 +1,4 @@
1
- import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
1
+ import { manifestEntryKey, matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter, mergeSyncState, syncStateKey, watermarkKey } from "../_chunks/manifest-store-utils.mjs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { randomBytes } from "node:crypto";
@@ -29,13 +29,13 @@ function createFilesystemDataSource(opts) {
29
29
  }));
30
30
  },
31
31
  async list(prefix) {
32
- const full = resolve(root, prefix);
32
+ const full = pathFor(prefix);
33
33
  const out = [];
34
34
  await walk(full, out);
35
35
  return out.map((p) => p.slice(root.length + 1));
36
36
  },
37
37
  async *streamList(prefix) {
38
- const full = resolve(root, prefix);
38
+ const full = pathFor(prefix);
39
39
  for await (const p of walkStream(full)) yield p.slice(root.length + 1);
40
40
  },
41
41
  async head(key) {
@@ -71,57 +71,6 @@ async function walk(dir, out) {
71
71
  else out.push(p);
72
72
  }
73
73
  }
74
- function watermarkKey(w) {
75
- return `${w.userId}|${w.siteId ?? ""}|${w.table}`;
76
- }
77
- function matchesWatermarkFilter(w, filter) {
78
- if (w.userId !== filter.userId) return false;
79
- if (filter.siteId !== void 0 && w.siteId !== filter.siteId) return false;
80
- if (filter.table !== void 0 && w.table !== filter.table) return false;
81
- return true;
82
- }
83
- function syncStateKey(s) {
84
- return `${s.userId}|${s.siteId ?? ""}|${s.table}|${s.date}|${inferSearchType(s)}`;
85
- }
86
- function matchesSyncStateFilter(s, filter) {
87
- if (s.userId !== filter.userId) return false;
88
- if (filter.siteId !== void 0 && s.siteId !== filter.siteId) return false;
89
- if (filter.table !== void 0 && s.table !== filter.table) return false;
90
- if (filter.state !== void 0 && s.state !== filter.state) return false;
91
- if (filter.searchType !== void 0 && inferSearchType(s) !== filter.searchType) return false;
92
- return true;
93
- }
94
- function mergeSyncState(existing, scope, state, detail) {
95
- const at = detail?.at ?? Date.now();
96
- const attemptsBump = state === "inflight" ? 1 : 0;
97
- if (!existing) return {
98
- userId: scope.userId,
99
- siteId: scope.siteId,
100
- table: scope.table,
101
- date: scope.date,
102
- state,
103
- updatedAt: at,
104
- attempts: attemptsBump,
105
- error: detail?.error,
106
- ...scope.searchType !== void 0 ? { searchType: scope.searchType } : {}
107
- };
108
- return {
109
- ...existing,
110
- state,
111
- updatedAt: at,
112
- attempts: existing.attempts + attemptsBump,
113
- error: state === "done" ? void 0 : detail?.error ?? existing.error
114
- };
115
- }
116
- function matchesFilter(entry, filter) {
117
- if (entry.userId !== filter.userId) return false;
118
- if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
119
- if (filter.table !== void 0 && entry.table !== filter.table) return false;
120
- if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
121
- if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
122
- if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
123
- return true;
124
- }
125
74
  function lockFileFor(locksDir, scope) {
126
75
  return join(locksDir, `${`${scope.userId}|${scope.siteId ?? ""}|${scope.table}|${scope.partition}`.replace(/[^\w.-]/g, "_")}.lock`);
127
76
  }
@@ -166,30 +115,27 @@ function createFilesystemManifestStore(opts) {
166
115
  while (queue.length > 0) await queue.shift()().catch(() => {});
167
116
  running = false;
168
117
  }
169
- function entryKey(e) {
170
- return e.objectKey;
171
- }
172
118
  async function registerVersionsImpl(newEntries, superseding) {
173
119
  const data = await load();
174
120
  const supersededAt = newEntries[0]?.createdAt ?? Date.now();
175
- const byKey = new Map(data.entries.map((e) => [entryKey(e), e]));
121
+ const byKey = new Map(data.entries.map((e) => [manifestEntryKey(e), e]));
176
122
  if (superseding) for (const s of superseding) {
177
- const existing = byKey.get(entryKey(s));
178
- if (existing && existing.retiredAt === void 0) byKey.set(entryKey(s), {
123
+ const existing = byKey.get(manifestEntryKey(s));
124
+ if (existing && existing.retiredAt === void 0) byKey.set(manifestEntryKey(s), {
179
125
  ...existing,
180
126
  retiredAt: supersededAt
181
127
  });
182
128
  }
183
- for (const e of newEntries) byKey.set(entryKey(e), e);
129
+ for (const e of newEntries) byKey.set(manifestEntryKey(e), e);
184
130
  data.entries = Array.from(byKey.values());
185
131
  await save(data);
186
132
  }
187
133
  return {
188
134
  async listLive(filter) {
189
- return (await load()).entries.filter((e) => e.retiredAt === void 0 && matchesFilter(e, filter));
135
+ return (await load()).entries.filter((e) => e.retiredAt === void 0 && matchesManifestEntryFilter(e, filter));
190
136
  },
191
137
  async listAll(filter) {
192
- return (await load()).entries.filter((e) => matchesFilter(e, filter));
138
+ return (await load()).entries.filter((e) => matchesManifestEntryFilter(e, filter));
193
139
  },
194
140
  async registerVersion(entry, superseding) {
195
141
  return enqueue(() => registerVersionsImpl([entry], superseding));
@@ -203,8 +149,8 @@ function createFilesystemManifestStore(opts) {
203
149
  async delete(toDelete) {
204
150
  return enqueue(async () => {
205
151
  const data = await load();
206
- const toDeleteKeys = new Set(toDelete.map(entryKey));
207
- data.entries = data.entries.filter((e) => !toDeleteKeys.has(entryKey(e)));
152
+ const toDeleteKeys = new Set(toDelete.map(manifestEntryKey));
153
+ data.entries = data.entries.filter((e) => !toDeleteKeys.has(manifestEntryKey(e)));
208
154
  await save(data);
209
155
  });
210
156
  },
@@ -124,8 +124,11 @@ function naturalKeyFor(table, row) {
124
124
  }
125
125
  function encodeOrderedRows(rows, columns, rowGroupSize) {
126
126
  const schema = buildWriteSchema(columns);
127
- const isDate = columns.map((col) => col.type === "DATE");
128
- const types = columns.map((col) => basicTypeFor(col.type));
127
+ const codecs = columns.map((col) => ({
128
+ name: col.name,
129
+ isDate: col.type === "DATE",
130
+ type: basicTypeFor(col.type)
131
+ }));
129
132
  const columnSpecs = columns.map((col) => ({
130
133
  name: col.name,
131
134
  nullable: col.nullable,
@@ -134,10 +137,7 @@ function encodeOrderedRows(rows, columns, rowGroupSize) {
134
137
  function* coercedRows() {
135
138
  for (const r of rows) {
136
139
  const out = {};
137
- for (let c = 0; c < columns.length; c++) {
138
- const name = columns[c].name;
139
- out[name] = isDate[c] ? toEpochDays(r[name]) : coerceValue(r[name], types[c]);
140
- }
140
+ for (const codec of codecs) out[codec.name] = codec.isDate ? toEpochDays(r[codec.name]) : coerceValue(r[codec.name], codec.type);
141
141
  yield out;
142
142
  }
143
143
  }
@@ -1,5 +1,6 @@
1
- import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
1
+ import { inferSearchType } from "../_chunks/layout.mjs";
2
2
  import { engineErrorToException, engineErrors } from "../errors.mjs";
3
+ import { matchesManifestEntryFilter, matchesSyncStateFilter, matchesWatermarkFilter } from "../_chunks/manifest-store-utils.mjs";
3
4
  import { err, ok, unwrapResult } from "gscdump/result";
4
5
  const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
5
6
  const CAS_BACKOFF_BASE_MS = 5;
@@ -52,26 +53,6 @@ function shardScopesFromEntries(entries) {
52
53
  }
53
54
  return out;
54
55
  }
55
- function matchesEntryFilter(entry, filter) {
56
- if (filter.siteId !== void 0 && entry.siteId !== filter.siteId) return false;
57
- if (filter.table !== void 0 && entry.table !== filter.table) return false;
58
- if (filter.partitions && !filter.partitions.includes(entry.partition)) return false;
59
- if (filter.tier !== void 0 && inferLegacyTier(entry) !== filter.tier) return false;
60
- if (filter.searchType !== void 0 && inferSearchType(entry) !== filter.searchType) return false;
61
- return true;
62
- }
63
- function matchesWatermarkFilter(w, filter) {
64
- if (filter.siteId !== void 0 && w.siteId !== filter.siteId) return false;
65
- if (filter.table !== void 0 && w.table !== filter.table) return false;
66
- return true;
67
- }
68
- function matchesSyncStateFilter(s, filter) {
69
- if (filter.siteId !== void 0 && s.siteId !== filter.siteId) return false;
70
- if (filter.table !== void 0 && s.table !== filter.table) return false;
71
- if (filter.state !== void 0 && s.state !== filter.state) return false;
72
- if (filter.searchType !== void 0 && inferSearchType(s) !== filter.searchType) return false;
73
- return true;
74
- }
75
56
  function createR2ManifestStore(opts) {
76
57
  const { bucket, userId } = opts;
77
58
  const newSnapshotId = opts.newSnapshotId ?? defaultSnapshotId;
@@ -170,13 +151,17 @@ function createR2ManifestStore(opts) {
170
151
  }];
171
152
  return (await listShards()).filter((s) => (filter.siteId === void 0 || s.siteId === filter.siteId) && (filter.table === void 0 || s.table === filter.table));
172
153
  }
154
+ function assertScopedUser(got, op) {
155
+ if (got !== userId) throw new Error(`${op}: R2 manifest store is scoped to userId=${userId}, got ${got}`);
156
+ }
173
157
  async function readEntriesAcrossShards(filter, includeRetired) {
158
+ assertScopedUser(filter.userId, includeRetired ? "listAll" : "listLive");
174
159
  return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
175
160
  const { snapshot } = await readShard(siteId, table);
176
161
  const entries = [];
177
162
  for (const entry of snapshot.entries) {
178
163
  if (!includeRetired && entry.retiredAt !== void 0) continue;
179
- if (matchesEntryFilter(entry, filter)) entries.push(entry);
164
+ if (matchesManifestEntryFilter(entry, filter, { ignoreUserId: true })) entries.push(entry);
180
165
  }
181
166
  return entries;
182
167
  })).flat();
@@ -195,6 +180,7 @@ function createR2ManifestStore(opts) {
195
180
  const supersededAt = newEntries[0]?.createdAt ?? now();
196
181
  const byShard = /* @__PURE__ */ new Map();
197
182
  function bucket(entry, kind) {
183
+ assertScopedUser(entry.userId, "registerVersions");
198
184
  if (entry.siteId === void 0) throw new Error("R2 manifest store requires entries to carry siteId");
199
185
  const key = `${entry.siteId}\0${entry.table}`;
200
186
  let bag = byShard.get(key);
@@ -258,14 +244,16 @@ function createR2ManifestStore(opts) {
258
244
  });
259
245
  },
260
246
  async getWatermarks(filter) {
247
+ assertScopedUser(filter.userId, "getWatermarks");
261
248
  return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
262
249
  const { snapshot } = await readShard(siteId, table);
263
250
  const watermarks = [];
264
- for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter)) watermarks.push(w);
251
+ for (const w of snapshot.watermarks) if (matchesWatermarkFilter(w, filter, { ignoreUserId: true })) watermarks.push(w);
265
252
  return watermarks;
266
253
  })).flat();
267
254
  },
268
255
  async bumpWatermark(scope, date, at) {
256
+ assertScopedUser(scope.userId, "bumpWatermark");
269
257
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires watermarks to carry siteId");
270
258
  const ts = at ?? now();
271
259
  await mutateShard(scope.siteId, scope.table, (snap) => {
@@ -294,14 +282,16 @@ function createR2ManifestStore(opts) {
294
282
  });
295
283
  },
296
284
  async getSyncStates(filter) {
285
+ assertScopedUser(filter.userId, "getSyncStates");
297
286
  return (await mapWithConcurrency(await shardsForFilter(filter), SHARD_IO_CONCURRENCY, async ({ siteId, table }) => {
298
287
  const { snapshot } = await readShard(siteId, table);
299
288
  const states = [];
300
- for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter)) states.push(s);
289
+ for (const s of snapshot.syncStates) if (matchesSyncStateFilter(s, filter, { ignoreUserId: true })) states.push(s);
301
290
  return states;
302
291
  })).flat();
303
292
  },
304
293
  async setSyncState(scope, state, detail) {
294
+ assertScopedUser(scope.userId, "setSyncState");
305
295
  if (scope.siteId === void 0) throw new Error("R2 manifest store requires sync states to carry siteId");
306
296
  const at = detail?.at ?? now();
307
297
  const scopeSearchType = inferSearchType(scope);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "0.33.0",
4
+ "version": "0.33.3",
5
5
  "description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -181,8 +181,8 @@
181
181
  "dependencies": {
182
182
  "drizzle-orm": "1.0.0-rc.3",
183
183
  "proper-lockfile": "^4.1.2",
184
- "@gscdump/contracts": "0.33.0",
185
- "gscdump": "0.33.0"
184
+ "@gscdump/contracts": "0.33.3",
185
+ "gscdump": "0.33.3"
186
186
  },
187
187
  "devDependencies": {
188
188
  "@duckdb/duckdb-wasm": "^1.32.0",