@gscdump/engine 1.4.3 → 1.4.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.
@@ -1,6 +1,6 @@
1
1
  import { DataSource, StorageEngine } from "../storage.mjs";
2
2
  import { SearchType } from "gscdump/query";
3
- import { Row, TableName } from "@gscdump/contracts";
3
+ import { Row, TableName, TenantCtx } from "@gscdump/contracts";
4
4
  interface NodeHarnessOptions {
5
5
  dataDir: string;
6
6
  /** Tenant user id. Defaults to `'local'` for single-user CLI installs. */
@@ -18,6 +18,7 @@ interface NodeHarness {
18
18
  dataDir: string;
19
19
  userId: string;
20
20
  siteIdFor: (siteUrl: string) => string;
21
+ withSitemapMutation: <T>(ctx: TenantCtx, mutate: () => Promise<T>) => Promise<T>;
21
22
  runRawSql: (opts: {
22
23
  sql: string;
23
24
  siteUrl: string;
@@ -11,9 +11,10 @@ function createNodeHarness(opts) {
11
11
  const handle = createNodeDuckDBHandle();
12
12
  const factory = { getDuckDB: async () => handle };
13
13
  const dataSource = createFilesystemDataSource({ rootDir: dataDir });
14
+ const manifestStore = createFilesystemManifestStore({ path: path.join(dataDir, manifestFilename) });
14
15
  const engine = createStorageEngine({
15
16
  dataSource,
16
- manifestStore: createFilesystemManifestStore({ path: path.join(dataDir, manifestFilename) }),
17
+ manifestStore,
17
18
  codec: createDuckDBCodec(factory),
18
19
  executor: createDuckDBExecutor(factory)
19
20
  });
@@ -41,6 +42,16 @@ function createNodeHarness(opts) {
41
42
  dataDir,
42
43
  userId,
43
44
  siteIdFor: encodeSiteId,
45
+ withSitemapMutation(ctx, mutate) {
46
+ if (ctx.userId !== userId) throw new Error(`Sitemap mutation user scope mismatch: expected ${userId}, got ${ctx.userId}`);
47
+ if (!ctx.siteId) throw new Error("Sitemap mutation requires a site id");
48
+ return manifestStore.withLock({
49
+ userId,
50
+ siteId: ctx.siteId,
51
+ table: "pages",
52
+ partition: "__sitemap_entities__"
53
+ }, mutate);
54
+ },
44
55
  runRawSql
45
56
  };
46
57
  }
@@ -1,6 +1,7 @@
1
1
  import { DataSource } from "./storage.mjs";
2
2
  import { ScheduleState } from "./schedule.mjs";
3
- import { emptyTypesKey, hashSortedUrlList, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionIndexKey, inspectionParquetKey, queryDimMetaKey, queryDimParquetKey, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix, sitemapUrlsPrefix } from "./entity-keys.mjs";
3
+ import { SitemapGenerationKey, emptyTypesKey, hashSortedUrlList, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionIndexKey, inspectionParquetKey, parseSitemapUrlsDeltaKey, queryDimMetaKey, queryDimParquetKey, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsEventKey, sitemapUrlsEventSeedKey, sitemapUrlsEventsPrefix, sitemapUrlsGenerationKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix, sitemapUrlsPendingGenerationKey, sitemapUrlsPendingGenerationsPrefix, sitemapUrlsPrefix, sitemapUrlsProjectionManifestKey, sitemapUrlsReconcileGenerationKey } from "./entity-keys.mjs";
4
+ import { SITEMAP_PROJECTION_GRACE_MS, SitemapProjectionFeed, SitemapProjectionFiles, SitemapProjectionManifest, decodeSitemapProjectionManifest, emptySitemapProjectionManifest, parseSitemapProjectionManifest, selectSitemapProjectionFiles, withSitemapProjectionFeed } from "./sitemap-projection.mjs";
4
5
  import { QueryDimDeps, QueryDimMeta, QueryDimRecord, QueryDimStore, buildQueryDimRecords, createQueryDimStore } from "./query-dim.mjs";
5
6
  import { TenantCtx } from "@gscdump/contracts";
6
7
  /**
@@ -284,9 +285,15 @@ interface SnapshotUrlsResult {
284
285
  removed: number;
285
286
  kept: number;
286
287
  contentHash: string;
287
- /** True when contentHash matched prior; the call performed zero writes. */
288
+ /** True when current membership did not change. Initial history seeding may write. */
288
289
  unchanged: boolean;
289
290
  }
291
+ interface CompleteSitemapGeneration {
292
+ _tag: 'complete';
293
+ id: string;
294
+ /** Unix epoch milliseconds. */
295
+ observedAt: number;
296
+ }
290
297
  interface ReconcileResult {
291
298
  /** Feedpaths that were absent from the live set and had their live URLs pruned. */
292
299
  feedpathsPruned: number;
@@ -307,12 +314,12 @@ interface CompactUrlsOptions {
307
314
  maxFeedpaths?: number;
308
315
  }
309
316
  interface CompactUrlsResult {
310
- /** Feedpaths whose deltas were folded and deleted by this call. */
317
+ /** Feedpaths whose active deltas were folded and retired by this call. */
311
318
  compactedFeedpaths: number;
312
319
  /**
313
320
  * Feedpaths that still hold outstanding deltas. Call `compactUrls` again to
314
- * continue no cursor is needed, because a compacted feedpath's deltas are
315
- * deleted, so the next call simply doesn't see it.
321
+ * continue. No cursor is needed because the projection watermark excludes a
322
+ * compacted feedpath's grace-retained deltas from the next call.
316
323
  */
317
324
  remainingFeedpaths: number;
318
325
  }
@@ -325,6 +332,21 @@ interface DeltaEntry {
325
332
  lastmod?: string;
326
333
  at: number;
327
334
  }
335
+ interface SitemapMembershipEvent {
336
+ feedpath: string;
337
+ feedpathHash: string;
338
+ urlHash: string;
339
+ op: 'added' | 'removed';
340
+ loc: string;
341
+ lastmod?: string;
342
+ generationId: string;
343
+ /** Unix epoch milliseconds. */
344
+ observedAt: number;
345
+ /** Stable ordering within one generation. */
346
+ sequence: number;
347
+ /** Whether this row also mutates the disposable current-state projection. */
348
+ projectsState: boolean;
349
+ }
328
350
  interface DateRange {
329
351
  /** YYYY-MM-DD inclusive. */
330
352
  from?: string;
@@ -334,27 +356,29 @@ interface DateRange {
334
356
  interface LoadUrlsOptions {
335
357
  includeRemoved?: boolean;
336
358
  }
337
- interface SitemapStore {
338
- /**
339
- * Persist a snapshot run. Updates the index + writes one immutable
340
- * history doc per record under `history/<feedpathHash>__<capturedAtMs>.json`.
341
- */
342
- writeSnapshot: (ctx: TenantCtx, records: readonly SitemapRecord[]) => Promise<void>;
359
+ interface SitemapReadStore {
343
360
  /** Load the full site index (latest record per feedpath). */
344
361
  loadIndex: (ctx: TenantCtx) => Promise<SitemapIndex>;
345
362
  /** Fetch the latest snapshot for a feedpath, or undefined. */
346
363
  getLatest: (ctx: TenantCtx, path: string) => Promise<SitemapRecord | undefined>;
347
- /**
348
- * Diff incoming URLs against the prior `urls/index.parquet` partition for
349
- * `feedpath`; on change, writes a single delta parquet under
350
- * `urls/deltas/YYYY-MM-DD__{feedpathHash}.parquet`. Skipped (0 PUTs) when
351
- * `contentHash` matches prior.
352
- */
353
- snapshotUrls: (ctx: TenantCtx, feedpath: string, urls: readonly ParsedUrl[]) => Promise<SnapshotUrlsResult>;
354
364
  /** Stream live (and optionally removed) URL rows for a feedpath. */
355
365
  loadUrls: (ctx: TenantCtx, feedpath: string, opts?: LoadUrlsOptions) => AsyncIterable<SitemapUrlRecord>;
356
366
  /** Stream all delta entries within `[from, to]` (YYYY-MM-DD inclusive). */
357
367
  loadDeltas: (ctx: TenantCtx, dateRange?: DateRange) => AsyncIterable<DeltaEntry>;
368
+ /** Stream immutable membership events within `[from, to]`. */
369
+ loadEvents: (ctx: TenantCtx, dateRange?: DateRange) => AsyncIterable<SitemapMembershipEvent>;
370
+ }
371
+ interface SitemapStore extends SitemapReadStore {
372
+ /**
373
+ * Persist a snapshot run. Updates the index + writes one immutable
374
+ * history doc per record under `history/<feedpathHash>__<capturedAtMs>.json`.
375
+ */
376
+ writeSnapshot: (ctx: TenantCtx, records: readonly SitemapRecord[]) => Promise<void>;
377
+ /**
378
+ * Diff a complete sitemap generation against current state. The immutable
379
+ * membership event lands before its disposable state delta.
380
+ */
381
+ snapshotUrls: (ctx: TenantCtx, generation: CompleteSitemapGeneration, feedpath: string, urls: readonly ParsedUrl[]) => Promise<SnapshotUrlsResult>;
358
382
  /**
359
383
  * Fold accumulated deltas into the prior index, one feedpath at a time:
360
384
  * rewrites each touched feedpath's `by-feed/<hash>/index.parquet` and deletes
@@ -362,9 +386,9 @@ interface SitemapStore {
362
386
  * regardless of total site URL count.
363
387
  *
364
388
  * Optionally bounded in TIME too (`opts.deadlineMs` / `opts.maxFeedpaths`).
365
- * A bounded call is safe to stop mid-way: each feedpath's deltas are deleted
366
- * as soon as its index is rewritten, so the remainder is simply what the next
367
- * call finds. Callers drive this from `remainingFeedpaths`, not a cursor.
389
+ * A bounded call is safe to stop mid-way: each rewritten feedpath advances
390
+ * the projection watermark, so the remainder is simply what the next call
391
+ * finds. Callers drive this from `remainingFeedpaths`, not a cursor.
368
392
  */
369
393
  compactUrls: (ctx: TenantCtx, opts?: CompactUrlsOptions) => Promise<CompactUrlsResult>;
370
394
  /**
@@ -373,22 +397,26 @@ interface SitemapStore {
373
397
  * prune URLs *inside* a feedpath that was re-observed; a whole feed dropped
374
398
  * from the sitemap list (no `snapshotUrls` call) leaves its URLs frozen-live
375
399
  * forever. This is the sidecar mirror of the D1 generation sweep: it rewrites
376
- * each dropped feedpath's `by-feed/<hash>/index.parquet` with `removedAt` set
377
- * and deletes its outstanding deltas (write-new-base + delete-deltas,
378
- * ADR-0002). Bounded per feedpath, so memory stays flat regardless of site
379
- * size. Live feedpaths are never touched.
400
+ * each dropped feedpath's `by-feed/<hash>/index.parquet` with `removedAt` set,
401
+ * advances its projection watermark, and grace-retires outstanding deltas.
402
+ * Bounded per feedpath, so memory stays flat regardless of site size. Live
403
+ * feedpaths are never touched.
380
404
  */
381
- reconcile: (ctx: TenantCtx, opts: {
405
+ reconcile: (ctx: TenantCtx, generation: CompleteSitemapGeneration, opts: {
382
406
  liveFeedpaths: readonly string[];
383
- at?: number;
384
407
  }) => Promise<ReconcileResult>;
385
408
  }
386
- interface CreateSitemapStoreOptions {
409
+ interface CreateSitemapReadStoreOptions {
387
410
  dataSource: DataSource;
388
411
  /** Override the feedpath hash (test seam). */
389
412
  hash?: (path: string) => string;
413
+ }
414
+ type SitemapMutation = <T>(ctx: TenantCtx, fn: () => Promise<T>) => Promise<T>;
415
+ interface CreateSitemapStoreOptions extends CreateSitemapReadStoreOptions {
416
+ withMutation: SitemapMutation;
390
417
  now?: () => number;
391
418
  }
419
+ declare function createSitemapReadStore(opts: CreateSitemapReadStoreOptions): SitemapReadStore;
392
420
  declare function createSitemapStore(opts: CreateSitemapStoreOptions): SitemapStore;
393
421
  interface IndexingMetadataRecord {
394
422
  url: string;
@@ -432,4 +460,4 @@ interface CreateEmptyTypesStoreOptions {
432
460
  now?: () => number;
433
461
  }
434
462
  declare function createEmptyTypesStore(opts: CreateEmptyTypesStoreOptions): EmptyTypesStore;
435
- export { CompactUrlsOptions, CompactUrlsResult, CreateEmptyTypesStoreOptions, CreateIndexingMetadataStoreOptions, CreateInspectionStoreOptions, CreateSitemapStoreOptions, DateRange, DeltaEntry, EmptyTypesDoc, EmptyTypesStore, INSPECTION_HISTORY_MAX_BYTES, IndexingMetadataIndex, IndexingMetadataRecord, IndexingMetadataStore, InspectionEventRow, InspectionHistoryShard, InspectionIndex, InspectionParquetRow, InspectionRecord, InspectionStore, LoadUrlsOptions, ParsedUrl, QueryDimDeps, QueryDimMeta, QueryDimRecord, QueryDimStore, ReconcileResult, SitemapHistoryDoc, SitemapIndex, SitemapRecord, SitemapStore, SitemapUrlRecord, SnapshotUrlsResult, buildQueryDimRecords, createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createQueryDimStore, createSitemapStore, emptyTypesKey, hashSortedUrlList, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionIndexKey, inspectionParquetKey, queryDimMetaKey, queryDimParquetKey, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix, sitemapUrlsPrefix };
463
+ export { CompactUrlsOptions, CompactUrlsResult, CompleteSitemapGeneration, CreateEmptyTypesStoreOptions, CreateIndexingMetadataStoreOptions, CreateInspectionStoreOptions, CreateSitemapReadStoreOptions, CreateSitemapStoreOptions, DateRange, DeltaEntry, EmptyTypesDoc, EmptyTypesStore, INSPECTION_HISTORY_MAX_BYTES, IndexingMetadataIndex, IndexingMetadataRecord, IndexingMetadataStore, InspectionEventRow, InspectionHistoryShard, InspectionIndex, InspectionParquetRow, InspectionRecord, InspectionStore, LoadUrlsOptions, ParsedUrl, QueryDimDeps, QueryDimMeta, QueryDimRecord, QueryDimStore, ReconcileResult, SITEMAP_PROJECTION_GRACE_MS, SitemapGenerationKey, SitemapHistoryDoc, SitemapIndex, SitemapMembershipEvent, SitemapMutation, SitemapProjectionFeed, SitemapProjectionFiles, SitemapProjectionManifest, SitemapReadStore, SitemapRecord, SitemapStore, SitemapUrlRecord, SnapshotUrlsResult, buildQueryDimRecords, createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createQueryDimStore, createSitemapReadStore, createSitemapStore, decodeSitemapProjectionManifest, emptySitemapProjectionManifest, emptyTypesKey, hashSortedUrlList, hashUrl, hashUrlList, indexingMetadataIndexKey, inspectionBaseKey, inspectionEventKey, inspectionEventsPrefix, inspectionHistoryPrefix, inspectionHistoryShardKey, inspectionIndexKey, inspectionParquetKey, parseSitemapProjectionManifest, parseSitemapUrlsDeltaKey, queryDimMetaKey, queryDimParquetKey, selectSitemapProjectionFiles, sitemapHistoryKey, sitemapIndexKey, sitemapUrlsDeltaKey, sitemapUrlsEventKey, sitemapUrlsEventSeedKey, sitemapUrlsEventsPrefix, sitemapUrlsGenerationKey, sitemapUrlsIndexKey, sitemapUrlsIndexPrefix, sitemapUrlsPendingGenerationKey, sitemapUrlsPendingGenerationsPrefix, sitemapUrlsPrefix, sitemapUrlsProjectionManifestKey, sitemapUrlsReconcileGenerationKey, withSitemapProjectionFeed };