@gscdump/engine-gsc-api 4.0.1 → 4.1.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/README.md CHANGED
@@ -50,6 +50,37 @@ Create a Source per request if your host manages token refresh between requests.
50
50
  | `fetchGscDaily` | Read daily metrics |
51
51
  | `runGscSyncSlice` | Read a bounded Search Analytics sync slice |
52
52
  | `runGscSearchAppearanceContextSlice` | Read a Search Appearance context slice |
53
+ | `planGscSyncWork` | Find ledger gaps and size Sync windows |
54
+ | `gscSyncFanout` | Build the standard Search Analytics table fanout |
55
+ | `tablesCoveredByGscSync` | List the tables a Sync slice must write |
56
+ | `ledgerTablesForGscSync` | List the ledger tables needed for one fanout |
57
+ | `planGscBackfillDates` | Select interior repair or backward Backfill dates |
58
+
59
+ ## Plan stored Sync work
60
+
61
+ Pass the dates you want to cover, the planned `(table, searchType)` fanout, and
62
+ the dates already committed to your ledger. The plan returns only missing
63
+ windows. Record zero-row slices in the ledger too, so they do not repeat.
64
+
65
+ ```ts
66
+ import { gscSyncFanout, planGscSyncWork } from '@gscdump/engine-gsc-api'
67
+
68
+ const plan = planGscSyncWork({
69
+ dates: ['2026-09-01', '2026-09-02'],
70
+ fanout: gscSyncFanout(['web'], { omitWebDates: true }),
71
+ ledger: [{ table: 'pages', searchType: 'web', date: '2026-09-01' }],
72
+ pagesPerDayEstimate: 100,
73
+ })
74
+
75
+ for (const window of plan.windows) {
76
+ // Run runGscSyncSlice for this table and date range, then commit its ledger rows.
77
+ console.log(window)
78
+ }
79
+ ```
80
+
81
+ The defaults match the hosted Worker's tested window budget. Set `rowBudget`,
82
+ `maxSpanDays`, and `maxWindowsPerRun` for your own runtime. The window count
83
+ limit can widen a span beyond `maxSpanDays` during a long catch-up.
53
84
 
54
85
  ## Limits and fallback
55
86
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CreateLiveGscSourceOptions, canProxyToGsc, createLiveGscSource } from "./live.mjs";
2
2
  import { FetchTopNOptions, GscDailyRow, GscRange, GscTopNRow, fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
3
3
  import { GSC_API_CAPABILITIES, GscApiQuerySourceOptions, createGscApiQuerySource } from "./source.mjs";
4
+ import { GscSyncEntryWork, GscSyncFanoutEntry, GscSyncLedgerRow, GscSyncWindow, GscSyncWorkPlan, PlanGscBackfillDatesOptions, PlanGscBackwardDatesOptions, PlanGscSyncWorkOptions, gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, tablesCoveredByGscSync } from "./sync-plan.mjs";
4
5
  import { GscApiRow, RunGscSearchAppearanceContextSliceOptions, RunGscSearchAppearanceContextSliceResult, RunGscSyncSliceOptions, RunGscSyncSliceResult, SearchAppearanceContextGrain, SearchAppearanceContextTable, SyncSliceDimensionFilter, SyncSliceDomainFilter, runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
5
- export { type CreateLiveGscSourceOptions, type FetchTopNOptions, GSC_API_CAPABILITIES, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscTopNRow, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
6
+ export { type CreateLiveGscSourceOptions, type FetchTopNOptions, GSC_API_CAPABILITIES, type GscApiQuerySourceOptions, type GscApiRow, type GscDailyRow, type GscRange, type GscSyncEntryWork, type GscSyncFanoutEntry, type GscSyncLedgerRow, type GscSyncWindow, type GscSyncWorkPlan, type GscTopNRow, type PlanGscBackfillDatesOptions, type PlanGscBackwardDatesOptions, type PlanGscSyncWorkOptions, type RunGscSearchAppearanceContextSliceOptions, type RunGscSearchAppearanceContextSliceResult, type RunGscSyncSliceOptions, type RunGscSyncSliceResult, type SearchAppearanceContextGrain, type SearchAppearanceContextTable, type SyncSliceDimensionFilter, type SyncSliceDomainFilter, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, runGscSearchAppearanceContextSlice, runGscSyncSlice, tablesCoveredByGscSync };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
2
2
  import { GSC_API_CAPABILITIES, createGscApiQuerySource } from "./source.mjs";
3
3
  import { canProxyToGsc, createLiveGscSource } from "./live.mjs";
4
+ import { gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, tablesCoveredByGscSync } from "./sync-plan.mjs";
4
5
  import { runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
5
- export { GSC_API_CAPABILITIES, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
6
+ export { GSC_API_CAPABILITIES, canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, runGscSearchAppearanceContextSlice, runGscSyncSlice, tablesCoveredByGscSync };
@@ -0,0 +1,63 @@
1
+ import { SearchType, TableName } from "@gscdump/engine/contracts";
2
+ export interface GscSyncFanoutEntry {
3
+ table: string;
4
+ searchType?: SearchType;
5
+ }
6
+ export interface GscSyncLedgerRow {
7
+ table: string;
8
+ searchType: string;
9
+ date: string;
10
+ }
11
+ export interface GscSyncWindow extends GscSyncFanoutEntry {
12
+ startDate: string;
13
+ endDate: string;
14
+ }
15
+ export interface PlanGscSyncWorkOptions {
16
+ dates: readonly string[];
17
+ fanout: readonly GscSyncFanoutEntry[];
18
+ ledger: readonly GscSyncLedgerRow[];
19
+ pagesPerDayEstimate?: number | null;
20
+ /** Target rows per window. The default is the hosted Worker's tested budget. */
21
+ rowBudget?: number;
22
+ /** Target window cap. A maxWindowsPerRun limit can widen it. */
23
+ maxSpanDays?: number;
24
+ /** Largest number of windows per contiguous run. */
25
+ maxWindowsPerRun?: number;
26
+ }
27
+ export interface GscSyncEntryWork {
28
+ entry: GscSyncFanoutEntry;
29
+ missingDates: string[];
30
+ windows: GscSyncWindow[];
31
+ }
32
+ export interface GscSyncWorkPlan {
33
+ entries: GscSyncEntryWork[];
34
+ missingDates: string[];
35
+ windows: GscSyncWindow[];
36
+ }
37
+ /** Build the standard Search Analytics fanout. Web keeps its legacy absent searchType field. */
38
+ export declare function gscSyncFanout(searchTypes: readonly SearchType[], options?: {
39
+ omitWebDates?: boolean;
40
+ }): GscSyncFanoutEntry[];
41
+ /** The web queries slice also writes the dates table. Other search types use a dates slice. */
42
+ export declare function tablesCoveredByGscSync(entry: GscSyncFanoutEntry): TableName[];
43
+ /** The minimal set of ledger tables needed to inspect this fanout. */
44
+ export declare function ledgerTablesForGscSync(fanout: readonly GscSyncFanoutEntry[]): TableName[];
45
+ /** Plan only slices missing at least one table that their job must write. */
46
+ export declare function planGscSyncWork(options: PlanGscSyncWorkOptions): GscSyncWorkPlan;
47
+ export interface PlanGscBackfillDatesOptions {
48
+ _tag: 'repair';
49
+ /** Interior repair never probes before this date. */
50
+ oldestCoveredDate: string;
51
+ newestAvailableDate: string;
52
+ coveredDates: Iterable<string>;
53
+ targetDays: number;
54
+ }
55
+ export interface PlanGscBackwardDatesOptions {
56
+ _tag: 'backfill';
57
+ startFromDate: string;
58
+ oldestAvailableDate: string;
59
+ targetDays: number;
60
+ maxDays: number;
61
+ }
62
+ /** Pick interior gaps or extend history backwards from the oldest covered date. */
63
+ export declare function planGscBackfillDates(options: PlanGscBackfillDatesOptions | PlanGscBackwardDatesOptions): string[];
@@ -0,0 +1,112 @@
1
+ import { TABLES_BY_SEARCH_TYPE } from "@gscdump/engine/sync-config";
2
+ import { addDays } from "gscdump/dates";
3
+ const DEFAULT_ROW_BUDGET = 25e3;
4
+ const DEFAULT_MAX_SPAN_DAYS = 31;
5
+ const DEFAULT_MAX_WINDOWS_PER_RUN = 14;
6
+ const TABLE_DENSITY = {
7
+ pages: 1,
8
+ countries: .3,
9
+ dates: 1,
10
+ queries: 3,
11
+ page_queries: 8
12
+ };
13
+ function gscSyncFanout(searchTypes, options = {}) {
14
+ return searchTypes.flatMap((searchType) => TABLES_BY_SEARCH_TYPE[searchType].filter((table) => !(searchType === "web" && options.omitWebDates && table === "dates")).map((table) => ({
15
+ table,
16
+ ...searchType === "web" ? {} : { searchType }
17
+ })));
18
+ }
19
+ function tablesCoveredByGscSync(entry) {
20
+ switch (entry.table) {
21
+ case "pages": return ["pages"];
22
+ case "countries": return ["countries"];
23
+ case "page_queries": return ["page_queries"];
24
+ case "search_appearance": return ["search_appearance"];
25
+ case "search_appearance_pages": return ["search_appearance_pages"];
26
+ case "search_appearance_queries": return ["search_appearance_queries"];
27
+ case "search_appearance_page_queries": return ["search_appearance_page_queries"];
28
+ case "hourly_pages": return ["hourly_pages"];
29
+ case "queries": return entry.searchType === void 0 || entry.searchType === "web" ? ["queries", "dates"] : ["queries"];
30
+ case "dates": return entry.searchType === void 0 || entry.searchType === "web" ? [] : ["dates"];
31
+ default: throw new RangeError(`Unknown GSC sync fanout table: '${entry.table}'`);
32
+ }
33
+ }
34
+ function ledgerTablesForGscSync(fanout) {
35
+ return [...new Set(fanout.flatMap(tablesCoveredByGscSync))];
36
+ }
37
+ function positiveInteger(value, fallback) {
38
+ if (value === void 0) return fallback;
39
+ if (!Number.isFinite(value)) throw new RangeError("Sync window limits must be finite numbers");
40
+ return Math.max(1, Math.floor(value));
41
+ }
42
+ function windowDays(estimate, table, totalDays, options) {
43
+ const maxSpan = positiveInteger(options.maxSpanDays, DEFAULT_MAX_SPAN_DAYS);
44
+ const maxWindows = positiveInteger(options.maxWindowsPerRun, DEFAULT_MAX_WINDOWS_PER_RUN);
45
+ const rowBudget = positiveInteger(options.rowBudget, DEFAULT_ROW_BUDGET);
46
+ const density = estimate && estimate > 0 ? Math.max(1, Math.floor(rowBudget / Math.max(1, estimate * (TABLE_DENSITY[table] ?? 1)))) : 1;
47
+ return Math.max(Math.min(maxSpan, density), Math.ceil(totalDays / maxWindows));
48
+ }
49
+ function contiguousRuns(dates) {
50
+ const runs = [];
51
+ for (const date of dates) {
52
+ const last = runs.at(-1);
53
+ if (last && addDays(last.end, 1) === date) {
54
+ last.end = date;
55
+ last.days++;
56
+ } else runs.push({
57
+ start: date,
58
+ end: date,
59
+ days: 1
60
+ });
61
+ }
62
+ return runs;
63
+ }
64
+ function windowsForRun(entry, run, spanDays) {
65
+ const windows = [];
66
+ for (let startDate = run.start; startDate <= run.end; startDate = addDays(windows.at(-1).endDate, 1)) {
67
+ const endDate = addDays(startDate, spanDays - 1);
68
+ windows.push({
69
+ ...entry,
70
+ startDate,
71
+ endDate: endDate < run.end ? endDate : run.end
72
+ });
73
+ }
74
+ return windows;
75
+ }
76
+ function planGscSyncWork(options) {
77
+ const dates = [...new Set(options.dates)].sort();
78
+ const covered = new Set(options.ledger.map((row) => `${row.searchType}|${row.table}|${row.date}`));
79
+ const entries = [];
80
+ for (const entry of options.fanout) {
81
+ const tables = tablesCoveredByGscSync(entry);
82
+ if (tables.length === 0) continue;
83
+ const searchType = entry.searchType ?? "web";
84
+ const missingDates = dates.filter((date) => tables.some((table) => !covered.has(`${searchType}|${table}|${date}`)));
85
+ if (missingDates.length === 0) continue;
86
+ const windows = contiguousRuns(missingDates).flatMap((run) => windowsForRun(entry, run, windowDays(options.pagesPerDayEstimate, entry.table, run.days, options)));
87
+ entries.push({
88
+ entry,
89
+ missingDates,
90
+ windows
91
+ });
92
+ }
93
+ return {
94
+ entries,
95
+ missingDates: [...new Set(entries.flatMap((entry) => entry.missingDates))].sort(),
96
+ windows: entries.flatMap((entry) => entry.windows)
97
+ };
98
+ }
99
+ function planGscBackfillDates(options) {
100
+ if (!Number.isFinite(options.targetDays) || options._tag === "backfill" && !Number.isFinite(options.maxDays)) throw new RangeError("Backfill limits must be finite numbers");
101
+ const limit = Math.max(0, Math.floor(Math.min(options.targetDays, options._tag === "backfill" ? options.maxDays : options.targetDays)));
102
+ if (options._tag === "repair") {
103
+ const covered = new Set(options.coveredDates);
104
+ const dates = [];
105
+ for (let date = options.oldestCoveredDate; date <= options.newestAvailableDate && dates.length < limit; date = addDays(date, 1)) if (!covered.has(date)) dates.push(date);
106
+ return dates;
107
+ }
108
+ const dates = [];
109
+ for (let date = options.startFromDate; date >= options.oldestAvailableDate && dates.length < limit; date = addDays(date, -1)) dates.push(date);
110
+ return dates;
111
+ }
112
+ export { gscSyncFanout, ledgerTablesForGscSync, planGscBackfillDates, planGscSyncWork, tablesCoveredByGscSync };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-gsc-api",
3
3
  "type": "module",
4
- "version": "4.0.1",
4
+ "version": "4.1.0",
5
5
  "description": "GSC live-API engine adapter — wraps the Search Analytics REST API as an AnalysisQuerySource for typed analyzer dispatch.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -36,8 +36,8 @@
36
36
  "node": ">=22.13.0"
37
37
  },
38
38
  "dependencies": {
39
- "@gscdump/engine": "^4.0.1",
40
- "gscdump": "^4.0.1"
39
+ "@gscdump/engine": "^4.1.0",
40
+ "gscdump": "^4.1.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^5.0.1"