@gscdump/sdk 0.36.0 → 0.36.2

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
@@ -36,6 +36,8 @@ interface HostedFetchOptions {
36
36
  headers?: HeadersInit;
37
37
  query?: Record<string, unknown>;
38
38
  body?: unknown;
39
+ /** Opt into in-flight de-dupe for safe read-style POST requests. */
40
+ dedupe?: boolean;
39
41
  [key: string]: unknown;
40
42
  }
41
43
  interface HostedClientOptions {
@@ -43,6 +45,10 @@ interface HostedClientOptions {
43
45
  fetch?: HostedFetch;
44
46
  headers?: HostedHeaders;
45
47
  validate?: boolean | 'request' | 'response';
48
+ /** Coalesce concurrent identical GET/HEAD requests within this client instance. */
49
+ dedupe?: boolean;
50
+ /** Optional shared in-flight map for de-duping across related client instances. */
51
+ dedupeScope?: Map<string, Promise<Result<unknown, PartnerApiError>>>;
46
52
  }
47
53
  type AnalyticsFetch = HostedFetch;
48
54
  type AnalyticsHeaders = HostedHeaders;
package/dist/index.mjs CHANGED
@@ -22,6 +22,11 @@ function withDefaultSearchType(value, searchType) {
22
22
  function searchTypeQuery(searchType) {
23
23
  return { searchType: searchType ?? "web" };
24
24
  }
25
+ function stableJson$1(value) {
26
+ if (value == null || typeof value !== "object") return JSON.stringify(value) ?? "null";
27
+ if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : stableJson$1(item)).join(",")}]`;
28
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson$1(item)}`).join(",")}}`;
29
+ }
25
30
  function dateRangeOptionsQuery(options) {
26
31
  const query = {};
27
32
  const start = options?.start ?? options?.startDate;
@@ -43,17 +48,17 @@ function tablesQuery(tablesOrOptions, options) {
43
48
  ...searchTypeQuery(source?.searchType),
44
49
  ...dateRangeOptionsQuery(source)
45
50
  };
46
- if (tables) query.tables = Array.isArray(tables) ? tables.join(",") : tables;
51
+ if (tables) query.tables = Array.isArray(tables) ? [...new Set(tables.filter(Boolean))].sort().join(",") : [...new Set(tables.split(",").map((t) => t.trim()).filter(Boolean))].sort().join(",");
47
52
  return query;
48
53
  }
49
54
  function dataQuery(state, options) {
50
55
  const opts = options;
51
56
  const scoped = withDefaultSearchType(state, opts?.searchType);
52
57
  const query = {
53
- q: JSON.stringify(scoped),
58
+ q: stableJson$1(scoped),
54
59
  searchType: scoped.searchType ?? "web"
55
60
  };
56
- if (opts?.comparison) query.qc = JSON.stringify(withDefaultSearchType(opts.comparison, scoped.searchType));
61
+ if (opts?.comparison) query.qc = stableJson$1(withDefaultSearchType(opts.comparison, scoped.searchType));
57
62
  if (opts?.filter) query.filter = opts.filter;
58
63
  return query;
59
64
  }
@@ -61,10 +66,10 @@ function dataDetailQuery(state, options) {
61
66
  const opts = options;
62
67
  const scoped = withDefaultSearchType(state, opts?.searchType);
63
68
  const query = {
64
- q: JSON.stringify(scoped),
69
+ q: stableJson$1(scoped),
65
70
  searchType: scoped.searchType ?? "web"
66
71
  };
67
- if (opts?.comparison) query.qc = JSON.stringify(withDefaultSearchType(opts.comparison, scoped.searchType));
72
+ if (opts?.comparison) query.qc = stableJson$1(withDefaultSearchType(opts.comparison, scoped.searchType));
68
73
  return query;
69
74
  }
70
75
  function analysisQuery(params) {
@@ -98,7 +103,7 @@ function indexingUrlsQuery(params = {}) {
98
103
  }
99
104
  function indexingDiagnosticsQuery(params = {}) {
100
105
  const query = {};
101
- if (params.sampleIssues) query.sampleIssues = Array.isArray(params.sampleIssues) ? params.sampleIssues.join(",") : params.sampleIssues;
106
+ if (params.sampleIssues) query.sampleIssues = Array.isArray(params.sampleIssues) ? [...new Set(params.sampleIssues.map((item) => String(item).trim()).filter(Boolean))].sort().join(",") : params.sampleIssues;
102
107
  if (params.sampleLimit != null) query.sampleLimit = params.sampleLimit;
103
108
  return query;
104
109
  }
@@ -206,20 +211,51 @@ function shouldValidate(options, phase) {
206
211
  function parseWith(schema, value) {
207
212
  return schema ? schema.parse(value) : value;
208
213
  }
214
+ function stableJson(value) {
215
+ if (value == null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
216
+ if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "undefined" : stableJson(item)).join(",")}]`;
217
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
218
+ }
219
+ function headersKey(headers) {
220
+ return [...headers.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("\n");
221
+ }
222
+ function isDedupeable(init) {
223
+ if ("signal" in init && init.signal) return false;
224
+ const method = (init.method ?? "GET").toUpperCase();
225
+ return method === "GET" || method === "HEAD" || init.dedupe === true;
226
+ }
209
227
  function createHostedRequester(options, defaults) {
210
228
  const fetchImpl = options.fetch ?? ofetch;
211
229
  const apiBase = trimApiBase(options.apiBase, defaults.apiBase);
230
+ const dedupe = options.dedupe !== false;
231
+ const inflight = options.dedupeScope ?? /* @__PURE__ */ new Map();
212
232
  async function requestResult(path, init = {}, responseSchema) {
233
+ const { dedupe: _dedupe, ...fetchInit } = init;
213
234
  const headers = mergeHeaders(await resolveHeaders(options), init.headers);
214
- try {
215
- const out = await fetchImpl(buildPath(apiBase, path), {
216
- ...init,
217
- headers
235
+ const fullPath = buildPath(apiBase, path);
236
+ const dedupeKey = dedupe && isDedupeable(init) ? `${(init.method ?? "GET").toUpperCase()} ${fullPath}\nq=${stableJson(init.query)}\nb=${stableJson(init.body)}\nh=${headersKey(headers)}` : null;
237
+ if (dedupeKey) {
238
+ const existing = inflight.get(dedupeKey);
239
+ if (existing) return existing;
240
+ }
241
+ const run = (async () => {
242
+ try {
243
+ const out = await fetchImpl(fullPath, {
244
+ ...fetchInit,
245
+ headers
246
+ });
247
+ return ok(shouldValidate(options, "response") ? parseWith(responseSchema, out) : out);
248
+ } catch (error) {
249
+ return err(toPartnerError(error));
250
+ }
251
+ })();
252
+ if (dedupeKey) {
253
+ inflight.set(dedupeKey, run);
254
+ run.finally(() => {
255
+ if (inflight.get(dedupeKey) === run) inflight.delete(dedupeKey);
218
256
  });
219
- return ok(shouldValidate(options, "response") ? parseWith(responseSchema, out) : out);
220
- } catch (error) {
221
- return err(toPartnerError(error));
222
257
  }
258
+ return run;
223
259
  }
224
260
  async function request(path, init = {}, responseSchema) {
225
261
  return unwrapResult(await requestResult(path, init, responseSchema), partnerErrorToException);
@@ -248,13 +284,15 @@ function createAnalyticsClient(options = {}) {
248
284
  analyze(siteId, params) {
249
285
  return request(analyticsRoutes.site.analyze(siteId), {
250
286
  method: "POST",
251
- body: withDefaultSearchType(params)
287
+ body: withDefaultSearchType(params),
288
+ dedupe: true
252
289
  });
253
290
  },
254
291
  queryRows(siteId, state) {
255
292
  return request(analyticsRoutes.site.rows(siteId), {
256
293
  method: "POST",
257
- body: withDefaultSearchType(state)
294
+ body: withDefaultSearchType(state),
295
+ dedupe: true
258
296
  }, analyticsEndpointSchemas.analyticsRows.response);
259
297
  },
260
298
  getRollup(siteId, rollupId, params) {
@@ -632,7 +670,8 @@ function createPartnerClient(options = {}) {
632
670
  const body = shouldValidate("request") ? partnerEndpointSchemas.getKeywordSparklines.body.parse(withSearchType) : withSearchType;
633
671
  return request(partnerRoutes.sites.keywordSparklines(siteId), {
634
672
  method: "POST",
635
- body
673
+ body,
674
+ dedupe: true
636
675
  }, partnerEndpointSchemas.getKeywordSparklines.response);
637
676
  },
638
677
  getQueryTrend(siteId, params) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/sdk",
3
3
  "type": "module",
4
- "version": "0.36.0",
4
+ "version": "0.36.2",
5
5
  "description": "Consumer SDK for hosted gscdump.com integrations.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -44,10 +44,10 @@
44
44
  "date-fns": "^4.4.0",
45
45
  "ofetch": "^1.5.1",
46
46
  "zod": "^4.4.3",
47
- "@gscdump/contracts": "0.36.0",
48
- "gscdump": "0.36.0",
49
- "@gscdump/analysis": "0.36.0",
50
- "@gscdump/engine": "0.36.0"
47
+ "@gscdump/engine": "0.36.2",
48
+ "@gscdump/analysis": "0.36.2",
49
+ "gscdump": "0.36.2",
50
+ "@gscdump/contracts": "0.36.2"
51
51
  },
52
52
  "devDependencies": {
53
53
  "typescript": "^6.0.3",