@gscdump/nuxt 0.21.3 → 0.22.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.
Files changed (43) hide show
  1. package/README.md +36 -27
  2. package/app/components/GscDashboardPage.vue +2 -2
  3. package/app/components/GscHero.vue +3 -4
  4. package/app/components/GscLazyTopResult.vue +7 -5
  5. package/app/components/GscPerformanceChart.vue +4 -4
  6. package/app/components/GscSourceDebugPanel.vue +2 -1
  7. package/app/composables/_useGscBackfill.ts +13 -5
  8. package/app/composables/_useGscQueryDispatcher.ts +5 -4
  9. package/app/composables/_useGscResource.ts +144 -56
  10. package/app/composables/useGscAnalytics.ts +13 -4
  11. package/app/composables/useGscAnalyticsSourceInfo.ts +11 -5
  12. package/app/composables/useGscAnalyzer.ts +33 -25
  13. package/app/composables/useGscAnalyzerBatch.ts +2 -2
  14. package/app/composables/useGscAnalyzerDefs.ts +2 -1
  15. package/app/composables/useGscCountries.ts +5 -6
  16. package/app/composables/useGscCurrentSite.ts +32 -11
  17. package/app/composables/useGscInspectionHistory.ts +5 -5
  18. package/app/composables/useGscInspections.ts +5 -5
  19. package/app/composables/useGscPanelContext.ts +9 -2
  20. package/app/composables/useGscQuery.ts +6 -8
  21. package/app/composables/useGscQueryDispatcher.ts +14 -122
  22. package/app/composables/useGscRequestIndexingInspect.ts +20 -11
  23. package/app/composables/useGscResource.ts +11 -114
  24. package/app/composables/useGscRollup.ts +66 -81
  25. package/app/composables/useGscSearchAppearance.ts +5 -6
  26. package/app/composables/useGscSiteAnalyzer.ts +456 -0
  27. package/app/composables/useGscSitemapHistory.ts +5 -5
  28. package/app/composables/useGscSitemaps.ts +5 -5
  29. package/app/queries/gsc.ts +163 -0
  30. package/app/utils/duckdb-wasm.ts +166 -0
  31. package/app/utils/gsc-rpc.ts +18 -0
  32. package/module.ts +1 -1
  33. package/nuxt.config.ts +25 -0
  34. package/package.json +140 -8
  35. package/types.ts +8 -7
  36. package/app/composables/useGscEngine.ts +0 -16
  37. package/app/composables/useGscParquetTable.ts +0 -199
  38. package/app/composables/useGscRollupTable.ts +0 -78
  39. package/app/composables/useGscRowQuery.ts +0 -56
  40. package/app/composables/useGscSnapshotAnalyzer.contract.ts +0 -150
  41. package/app/composables/useGscSnapshotAnalyzer.routing.ts +0 -87
  42. package/app/composables/useGscSnapshotAnalyzer.ts +0 -559
  43. package/app/utils/setup-gsc-fetch-auth.ts +0 -62
@@ -11,12 +11,14 @@
11
11
  // re-boot. Writes progress to the shared map so <GscBootProgress> lights up
12
12
  // on boot regardless of mode.
13
13
 
14
- import type { AnalysisParams, AnalysisResult } from '@gscdump/analysis'
15
14
  import type { AnalysisSourcesResponse, SourceInfoResponse } from '@gscdump/contracts'
16
15
  import type { AttachedTablesHandle, BrowserAnalysisRuntime, DuckDBWasmBootResult, QueryResult } from '@gscdump/engine-duckdb-wasm'
16
+ import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
17
+ import type { AnalyzerRegistry } from '@gscdump/engine/analyzer'
17
18
  import type { SiteLoadProgress } from './useGscAnalytics'
18
- import { defaultAnalyzerRegistry } from '@gscdump/analysis'
19
19
  import { coerceRow } from '@gscdump/engine'
20
+ import { gscQueries } from '../queries/gsc'
21
+ import { useGscRpc } from '../utils/gsc-rpc'
20
22
  import { useGscSharedSiteResource } from './_useGscSharedSiteResource'
21
23
  import { useGscAnalyticsContext } from './useGscAnalytics'
22
24
  import { useGscAnalyticsClient } from './useGscAnalyticsClient'
@@ -55,23 +57,13 @@ const DEFAULT_ATTACH_FETCH_CONCURRENCY = 2
55
57
  const DEFAULT_ATTACH_MAX_FILES = 32
56
58
  const DEFAULT_ATTACH_MAX_BYTES = 16 * 1024 * 1024
57
59
 
60
+ let defaultAnalyzerRegistryPromise: Promise<AnalyzerRegistry> | null = null
61
+
58
62
  interface AnalyzerRange {
59
63
  start: string
60
64
  end: string
61
65
  }
62
66
 
63
- interface AnalysisSourcesClient {
64
- getAnalysisSources: (
65
- siteId: string,
66
- tables?: string[] | string | { tables?: string[] | string, searchType?: NonNullable<AnalysisParams['searchType']>, start?: string, end?: string },
67
- options?: { searchType?: NonNullable<AnalysisParams['searchType']>, start?: string, end?: string },
68
- ) => Promise<AnalysisSourcesResponse>
69
- getSourceInfo: (
70
- siteId: string,
71
- options?: { searchType?: NonNullable<AnalysisParams['searchType']>, start?: string, end?: string },
72
- ) => Promise<SourceInfoResponse>
73
- }
74
-
75
67
  function normalizeSearchType(searchType: AnalysisParams['searchType']): NonNullable<AnalysisParams['searchType']> {
76
68
  return searchType ?? DEFAULT_SEARCH_TYPE
77
69
  }
@@ -89,18 +81,26 @@ function normalizeRange(range: AnalyzerRange | null | undefined): AnalyzerRange
89
81
  return range?.start && range?.end ? range : null
90
82
  }
91
83
 
92
- function loadSourceInfo(siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<SourceInfoResponse> {
93
- return (useGscAnalyticsClient() as unknown as AnalysisSourcesClient).getSourceInfo(siteId, {
84
+ type AnalyticsClient = ReturnType<typeof useGscAnalyticsClient>
85
+
86
+ function loadSourceInfo(client: AnalyticsClient, siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<SourceInfoResponse> {
87
+ return client.getSourceInfo(siteId, {
94
88
  searchType,
95
89
  ...(range ? { start: range.start, end: range.end } : {}),
96
- })
90
+ }) as Promise<SourceInfoResponse>
97
91
  }
98
92
 
99
- function loadAnalysisSources(siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<AnalysisSourcesResponse> {
100
- return (useGscAnalyticsClient() as unknown as AnalysisSourcesClient).getAnalysisSources(siteId, undefined, {
93
+ function loadAnalysisSources(client: AnalyticsClient, siteId: string, searchType: NonNullable<AnalysisParams['searchType']>, range: AnalyzerRange | null): Promise<AnalysisSourcesResponse> {
94
+ return client.getAnalysisSources(siteId, undefined, {
101
95
  searchType,
102
96
  ...(range ? { start: range.start, end: range.end } : {}),
103
- })
97
+ }) as Promise<AnalysisSourcesResponse>
98
+ }
99
+
100
+ function loadDefaultAnalyzerRegistry(): Promise<AnalyzerRegistry> {
101
+ defaultAnalyzerRegistryPromise ??= import('@gscdump/analysis/registry')
102
+ .then(m => m.defaultAnalyzerRegistry)
103
+ return defaultAnalyzerRegistryPromise
104
104
  }
105
105
 
106
106
  /**
@@ -191,6 +191,8 @@ function createInstance(
191
191
  const attachedTables = ref<string[]>([])
192
192
  const timings = ref<GscAnalyzerTimings | null>(null)
193
193
  const manifestVersion = ref<string | undefined>(undefined)
194
+ const client = useGscAnalyticsClient()
195
+ const rpc = useGscRpc()
194
196
 
195
197
  function patch(p: Partial<SiteLoadProgress>): void {
196
198
  patchProgress(siteId, { source: 'duckdb', ...p })
@@ -266,7 +268,7 @@ function createInstance(
266
268
  patch({ stage: 'manifest', startedAt: Date.now(), filesAttached: 0, filesTotal: 0, error: undefined, endedAt: undefined })
267
269
  // Probe the server-resolved source first. Its kind + attachedTables bit
268
270
  // decides whether we boot DuckDB-WASM (expensive) or proxy to the server.
269
- const info = await loadSourceInfo(siteId, searchType, range)
271
+ const info = await loadSourceInfo(client, siteId, searchType, range)
270
272
  mode = info.browserAttachEligible ? 'browser-attached' : 'server'
271
273
 
272
274
  if (mode === 'server') {
@@ -297,7 +299,7 @@ function createInstance(
297
299
 
298
300
  patch({ stage: 'manifest' })
299
301
  const t1 = performance.now()
300
- const sources = await loadAnalysisSources(siteId, searchType, range)
302
+ const sources = await loadAnalysisSources(client, siteId, searchType, range)
301
303
  const manifestMs = performance.now() - t1
302
304
 
303
305
  const t2 = performance.now()
@@ -332,7 +334,11 @@ function createInstance(
332
334
  }
333
335
 
334
336
  async function runServerAnalyze(params: AnalysisParams, _signal?: AbortSignal): Promise<AnalysisResult & { queryMs: number }> {
335
- const out = await useGscAnalyticsClient().analyze<AnalysisResult & { queryMs?: number }>(siteId, { ...params, searchType: params.searchType ?? searchType })
337
+ const out = await rpc.execute(
338
+ gscQueries.analyze(siteId),
339
+ { ...params, searchType: params.searchType ?? searchType } as never,
340
+ { silent: true },
341
+ ) as AnalysisResult & { queryMs?: number }
336
342
  return {
337
343
  results: coerceResults(out.results) as AnalysisResult['results'],
338
344
  meta: out.meta as AnalysisResult['meta'],
@@ -353,7 +359,9 @@ function createInstance(
353
359
  const p = (async () => {
354
360
  if (!rt)
355
361
  return runServerAnalyze(scopedParams, opts?.signal)
356
- const out = await rt.analyze(scopedParams as never, defaultAnalyzerRegistry, { signal: opts?.signal })
362
+ const registry = await loadDefaultAnalyzerRegistry()
363
+ opts?.signal?.throwIfAborted?.()
364
+ const out = await rt.analyze(scopedParams as never, registry, { signal: opts?.signal })
357
365
  opts?.signal?.throwIfAborted?.()
358
366
  return {
359
367
  results: coerceResults(out.results) as AnalysisResult['results'],
@@ -376,7 +384,7 @@ function createInstance(
376
384
  await boot
377
385
  if (mode !== 'browser-attached' || !runtime || !bootedDb)
378
386
  return false
379
- const sources = await loadAnalysisSources(siteId, searchType, range)
387
+ const sources = await loadAnalysisSources(client, siteId, searchType, range)
380
388
  if (!runtime.isStale(sources.manifestVersion))
381
389
  return false
382
390
  // Drop the stale views before swapping in the new partitions. The runtime
@@ -16,7 +16,7 @@ export interface GscAnalyzerBatchEntry<TResult> {
16
16
  }
17
17
 
18
18
  export interface GscAnalyzerBatchRunner {
19
- analyze: (params: { type: string, dateStart?: string, dateEnd?: string }) => Promise<unknown>
19
+ analyze: (params: { type: string, startDate?: string, endDate?: string }) => Promise<unknown>
20
20
  }
21
21
 
22
22
  export interface UseGscAnalyzerBatchOptions {
@@ -72,7 +72,7 @@ export function useGscAnalyzerBatch<TResult = unknown>(
72
72
  return
73
73
  states.value = { ...states.value, [id]: { status: 'running', result: null, error: null } }
74
74
  try {
75
- const result = await runner.analyze({ type: id, dateStart: range.start, dateEnd: range.end })
75
+ const result = await runner.analyze({ type: id, startDate: range.start, endDate: range.end })
76
76
  if (token !== myToken)
77
77
  return
78
78
  states.value = { ...states.value, [id]: { status: 'done', result: result as TResult, error: null } }
@@ -12,7 +12,8 @@
12
12
  // `ActionSource` union from `@gscdump/analysis`, so misspelt or unknown
13
13
  // sources fail at typecheck instead of at runtime in the priority runner.
14
14
 
15
- import type { ActionSource, AnalysisTool } from '@gscdump/analysis'
15
+ import type { ActionSource } from '@gscdump/analysis'
16
+ import type { AnalysisTool } from '@gscdump/engine/analysis-types'
16
17
  import type { Component } from '@vue/runtime-core'
17
18
 
18
19
  export type GscAnalyzerKind = 'analyzer' | 'semantic' | 'action'
@@ -5,8 +5,8 @@
5
5
  import type { CountriesResponse, CountryRow, GscApiRange } from '@gscdump/contracts'
6
6
  import type { ComputedRef, Ref } from '@vue/runtime-core'
7
7
  import type { GscResourceStatus } from './_useGscResource'
8
+ import { gscQueries } from '../queries/gsc'
8
9
  import { useGscResource } from './_useGscResource'
9
- import { useGscAnalyticsClient } from './useGscAnalyticsClient'
10
10
 
11
11
  export interface UseGscCountriesReturn {
12
12
  response: Readonly<Ref<CountriesResponse | null>>
@@ -14,8 +14,8 @@ export interface UseGscCountriesReturn {
14
14
  range: ComputedRef<GscApiRange | null>
15
15
  source: ComputedRef<CountriesResponse['source'] | null>
16
16
  loading: ComputedRef<boolean>
17
- status: Ref<GscResourceStatus>
18
- error: Ref<Error | null>
17
+ status: Readonly<Ref<GscResourceStatus>>
18
+ error: Readonly<Ref<Error | null>>
19
19
  refresh: () => Promise<void>
20
20
  }
21
21
 
@@ -23,14 +23,13 @@ export function useGscCountries(
23
23
  siteId: MaybeRefOrGetter<string | null | undefined>,
24
24
  range: MaybeRefOrGetter<{ start: string, end: string } | null | undefined>,
25
25
  ): UseGscCountriesReturn {
26
- const { data, status, loading, error, refresh } = useGscResource({
26
+ const { data, status, loading, error, refresh } = useGscResource<[string, string, string], CountriesResponse>({
27
27
  keys: [
28
28
  siteId,
29
29
  () => toValue(range)?.start,
30
30
  () => toValue(range)?.end,
31
31
  ] as const,
32
- fetcher: (id: string, start: string, end: string) =>
33
- useGscAnalyticsClient().getCountries(id, { start, end }),
32
+ operation: (id, start, end) => gscQueries.countries(id, { start, end }),
34
33
  isEmpty: r => r.rows.length === 0,
35
34
  })
36
35
 
@@ -1,15 +1,36 @@
1
- // Dashboard route convention `/sites/[id]/*` analytics layer's
2
- // `useGscSite(siteId)`. Pages collapse to:
3
- // const { siteId, site } = useGscCurrentSite()
4
- // The analytics layer is otherwise route-agnostic — this composable is the
5
- // one place the `id` param name is hard-coded. Hosts using a different param
6
- // name should provide their own equivalent.
7
-
8
- export function useGscCurrentSite(): {
1
+ import type { ComputedRef, InjectionKey } from 'vue'
2
+
3
+ export interface GscCurrentSiteContext {
9
4
  siteId: ComputedRef<string>
10
5
  site: ReturnType<typeof useGscSite>
11
- } {
6
+ }
7
+
8
+ export const gscCurrentSiteKey: InjectionKey<GscCurrentSiteContext> = Symbol('gsc-current-site')
9
+
10
+ function createGscCurrentSiteContext(siteId: MaybeRefOrGetter<string | null | undefined>): GscCurrentSiteContext {
11
+ const resolvedSiteId = computed(() => String(toValue(siteId) ?? ''))
12
+ return { siteId: resolvedSiteId, site: useGscSite(resolvedSiteId) }
13
+ }
14
+
15
+ /**
16
+ * Provide the current-site context for a dashboard subtree. Hosts with a route
17
+ * shape other than `/sites/[id]/*` should call this from their layout/page.
18
+ */
19
+ export function provideGscCurrentSite(siteId: MaybeRefOrGetter<string | null | undefined>): GscCurrentSiteContext {
20
+ const ctx = createGscCurrentSiteContext(siteId)
21
+ provide(gscCurrentSiteKey, ctx)
22
+ return ctx
23
+ }
24
+
25
+ /**
26
+ * Read the current-site context. Falls back to the historical `/sites/[id]/*`
27
+ * route convention for existing hosts that have not added a provider yet.
28
+ */
29
+ export function useGscCurrentSite(): GscCurrentSiteContext {
30
+ const provided = inject(gscCurrentSiteKey, null)
31
+ if (provided)
32
+ return provided
33
+
12
34
  const route = useRoute()
13
- const siteId = computed(() => String(route.params.id))
14
- return { siteId, site: useGscSite(siteId) }
35
+ return createGscCurrentSiteContext(() => route.params.id as string | undefined)
15
36
  }
@@ -5,16 +5,16 @@
5
5
  import type { InspectionHistoryRecord, InspectionHistoryResponse } from '@gscdump/contracts'
6
6
  import type { ComputedRef, Ref } from '@vue/runtime-core'
7
7
  import type { GscResourceStatus } from './_useGscResource'
8
+ import { gscQueries } from '../queries/gsc'
8
9
  import { useGscResource } from './_useGscResource'
9
- import { useGscAnalyticsClient } from './useGscAnalyticsClient'
10
10
 
11
11
  export interface UseGscInspectionHistoryReturn {
12
12
  response: Readonly<Ref<InspectionHistoryResponse | null>>
13
13
  records: ComputedRef<InspectionHistoryRecord[]>
14
14
  url: ComputedRef<string | null>
15
15
  loading: ComputedRef<boolean>
16
- status: Ref<GscResourceStatus>
17
- error: Ref<Error | null>
16
+ status: Readonly<Ref<GscResourceStatus>>
17
+ error: Readonly<Ref<Error | null>>
18
18
  refresh: () => Promise<void>
19
19
  }
20
20
 
@@ -22,9 +22,9 @@ export function useGscInspectionHistory(
22
22
  siteId: MaybeRefOrGetter<string | null | undefined>,
23
23
  urlHash: MaybeRefOrGetter<string | null | undefined>,
24
24
  ): UseGscInspectionHistoryReturn {
25
- const { data, status, loading, error, refresh } = useGscResource({
25
+ const { data, status, loading, error, refresh } = useGscResource<[string, string], InspectionHistoryResponse>({
26
26
  keys: [siteId, urlHash] as const,
27
- fetcher: (id: string, hash: string) => useGscAnalyticsClient().getInspectionHistory(id, hash),
27
+ operation: (id, hash) => gscQueries.inspectionHistory(id, hash),
28
28
  })
29
29
 
30
30
  const records = computed(() => data.value?.records ?? [])
@@ -5,23 +5,23 @@
5
5
  import type { InspectionHistoryRecord, InspectionIndex } from '@gscdump/contracts'
6
6
  import type { ComputedRef, Ref } from '@vue/runtime-core'
7
7
  import type { GscResourceStatus } from './_useGscResource'
8
+ import { gscQueries } from '../queries/gsc'
8
9
  import { useGscResource } from './_useGscResource'
9
- import { useGscAnalyticsClient } from './useGscAnalyticsClient'
10
10
 
11
11
  export interface UseGscInspectionsReturn {
12
12
  index: Readonly<Ref<InspectionIndex | null>>
13
13
  records: ComputedRef<InspectionHistoryRecord[]>
14
14
  statusCounts: ComputedRef<{ PASS: number, NEUTRAL: number, FAIL: number, unknown: number }>
15
15
  loading: ComputedRef<boolean>
16
- status: Ref<GscResourceStatus>
17
- error: Ref<Error | null>
16
+ status: Readonly<Ref<GscResourceStatus>>
17
+ error: Readonly<Ref<Error | null>>
18
18
  refresh: () => Promise<void>
19
19
  }
20
20
 
21
21
  export function useGscInspections(siteId: MaybeRefOrGetter<string | null | undefined>): UseGscInspectionsReturn {
22
- const { data, status, loading, error, refresh } = useGscResource({
22
+ const { data, status, loading, error, refresh } = useGscResource<[string], InspectionIndex>({
23
23
  keys: [siteId] as const,
24
- fetcher: (id: string) => useGscAnalyticsClient().getInspections(id),
24
+ operation: (id: string) => gscQueries.inspections(id),
25
25
  })
26
26
 
27
27
  const records = computed<InspectionHistoryRecord[]>(() =>
@@ -6,11 +6,18 @@
6
6
  // `/analyze` provides these once; panels inject. Type-only — runtime values
7
7
  // are the InjectionKey Symbols themselves.
8
8
 
9
+ import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
9
10
  import type { InjectionKey } from '@vue/runtime-core'
10
- import type { GscAnalyzerInstance } from './useGscAnalyzer'
11
+
12
+ export interface GscPanelRunner {
13
+ /** Positional-form SQL runner — `{ rows, queryMs }`. */
14
+ query: (sql: string, params?: readonly unknown[]) => Promise<{ rows: Record<string, unknown>[], queryMs: number }>
15
+ /** Registry-driven analyzer dispatch over the same per-site DuckDB-WASM runtime. */
16
+ analyze: (params: AnalysisParams, opts?: { signal?: AbortSignal }) => Promise<AnalysisResult & { queryMs: number }>
17
+ }
11
18
 
12
19
  export interface GscPanelRunnerContext {
13
- runner: GscAnalyzerInstance
20
+ runner: GscPanelRunner
14
21
  ready: Ref<boolean>
15
22
  }
16
23
 
@@ -6,12 +6,13 @@
6
6
  // AnalysisParams to `/api/__gsc/sites/[siteId]/analyze`. Override if your server
7
7
  // uses a different contract.
8
8
 
9
- import type { AnalysisParams, AnalysisResult } from '@gscdump/analysis'
9
+ import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
10
10
  import type { ComputedRef, Ref, WatchSource } from '@vue/runtime-core'
11
+ import { gscQueries, withDefaultGscSearchType } from '../queries/gsc'
11
12
  import { classifyGscError } from '../utils/gsc-error'
13
+ import { useGscRpc } from '../utils/gsc-rpc'
12
14
  import { useGscBackfill } from './_useGscBackfill'
13
15
  import { useGscQueryDispatcher } from './_useGscQueryDispatcher'
14
- import { useGscAnalyticsClient } from './useGscAnalyticsClient'
15
16
  import { useGscAnalyzer } from './useGscAnalyzer'
16
17
  import { _useGscAuthInternal } from './useGscAuth'
17
18
 
@@ -128,17 +129,14 @@ function isEmpty(v: unknown): boolean {
128
129
  return false
129
130
  }
130
131
 
131
- async function defaultServerFallback<T>(siteId: string, params: AnalysisParams): Promise<T> {
132
- return await useGscAnalyticsClient().analyze<T>(siteId, params)
133
- }
134
-
135
132
  const DEFAULT_SEARCH_TYPE: NonNullable<AnalysisParams['searchType']> = 'web'
136
133
 
137
134
  function withDefaultSearchType(params: AnalysisParams): AnalysisParams {
138
- return { ...params, searchType: params.searchType ?? DEFAULT_SEARCH_TYPE }
135
+ return withDefaultGscSearchType(params)
139
136
  }
140
137
 
141
138
  export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): UseGscQueryReturn<T> {
139
+ const rpc = useGscRpc()
142
140
  const querySearchType = computed(() => toValue(opts.params).searchType ?? DEFAULT_SEARCH_TYPE)
143
141
  const queryRange = computed(() => {
144
142
  const params = toValue(opts.params)
@@ -176,7 +174,7 @@ export function useGscQuery<T = AnalysisResult>(opts: UseGscQueryOptions<T>): Us
176
174
 
177
175
  async function runServer(siteId: string): Promise<void> {
178
176
  const t0 = performance.now()
179
- const fn = opts.serverFallback ?? defaultServerFallback
177
+ const fn = opts.serverFallback ?? (async (siteId: string, params: AnalysisParams) => rpc.execute(gscQueries.analyze(siteId), params as never, { silent: true }) as Promise<T>)
180
178
  const out = await fn(siteId, withDefaultSearchType(toValue(opts.params))) as T
181
179
  data.value = out
182
180
  engine.value = 'server'
@@ -1,122 +1,14 @@
1
- // Per-query dispatch policy + fallback telemetry behind one swappable seam.
2
- //
3
- // `useGscQuery` owns *intent* (the caller's `engine: 'auto' | 'browser' | 'server'`);
4
- // the dispatcher owns *policy* (how `auto` resolves against current auth, what
5
- // to do with auto-fallback events). Default impl is no-op telemetry — hosts
6
- // that want fallback beacons override the provide with a configured factory.
7
-
8
- import type { _useGscAuthInternal } from './useGscAuth'
9
- import type { GscQueryDecisionReason, GscQueryEngine } from './useGscQuery'
10
- import { useGscEngine } from './useGscEngine'
11
-
12
- type InternalAuthState = ReturnType<typeof _useGscAuthInternal>['value']
13
-
14
- export interface GscEngineDecision {
15
- mode: 'browser' | 'server'
16
- reason: GscQueryDecisionReason
17
- /** The resolved intent before the auth/optin mapping — useful for callers that branch on "did the user ask for auto?". */
18
- requested: GscQueryEngine
19
- detail?: string
20
- }
21
-
22
- export interface GscFallbackEvent {
23
- reason: string
24
- at: number
25
- url: string
26
- }
27
-
28
- export interface PickEngineOpts {
29
- /** Per-call override. Wins over `useGscEngine()` + `runtimeConfig.public.analytics.defaultEngine`. */
30
- perCall?: GscQueryEngine
31
- }
32
-
33
- export interface GscQueryDispatcher {
34
- /**
35
- * Resolve the active engine into a concrete mode + reason. Consults the
36
- * resolution chain: `opts.perCall` → `useGscEngine()` → runtimeConfig →
37
- * `'auto'`. Then maps `auto` against `auth.browserAnalyzerEnabled`.
38
- */
39
- pickEngine: (auth: InternalAuthState, opts?: PickEngineOpts) => GscEngineDecision
40
- /** Called once per auto-fallback. Default impl is a no-op. */
41
- reportFallback: (event: GscFallbackEvent) => void
42
- }
43
-
44
- export interface CreateDefaultDispatcherOpts {
45
- /** When set, fallbacks beacon to this URL with batched payloads. */
46
- telemetryEndpoint?: string
47
- }
48
-
49
- const FLUSH_DELAY_MS = 5000
50
-
51
- function createReporter(endpoint: string): (event: GscFallbackEvent) => void {
52
- const buffer: GscFallbackEvent[] = []
53
- let flushScheduled = false
54
-
55
- function flush(): void {
56
- flushScheduled = false
57
- if (buffer.length === 0)
58
- return
59
- const events = buffer.splice(0)
60
- const body = JSON.stringify({ events })
61
- if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
62
- navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }))
63
- return
64
- }
65
- fetch(endpoint, { method: 'POST', body, headers: { 'content-type': 'application/json' } }).catch(() => {})
66
- }
67
-
68
- function scheduleFlush(): void {
69
- if (flushScheduled || typeof window === 'undefined')
70
- return
71
- flushScheduled = true
72
- setTimeout(flush, FLUSH_DELAY_MS)
73
- if (typeof document !== 'undefined') {
74
- const handler = (): void => {
75
- if (document.visibilityState === 'hidden')
76
- flush()
77
- }
78
- document.addEventListener('visibilitychange', handler, { once: true })
79
- }
80
- }
81
-
82
- return (event: GscFallbackEvent): void => {
83
- buffer.push(event)
84
- scheduleFlush()
85
- }
86
- }
87
-
88
- export function createDefaultGscQueryDispatcher(
89
- opts: CreateDefaultDispatcherOpts = {},
90
- ): GscQueryDispatcher {
91
- const reportFallback = opts.telemetryEndpoint
92
- ? createReporter(opts.telemetryEndpoint)
93
- : (): void => {}
94
-
95
- return {
96
- pickEngine(auth, opts = {}): GscEngineDecision {
97
- const requested = opts.perCall ?? resolveDefaultEngine()
98
- if (requested === 'server')
99
- return { mode: 'server', reason: 'forced:server', requested }
100
- if (requested === 'browser')
101
- return { mode: 'browser', reason: 'forced:browser', requested }
102
- // 'auto': when the host has wired setGscAuth, derive from the per-user
103
- // browserAnalyzerEnabled flag. When unwired, preserve legacy 'auto' = browser.
104
- if (auth._initialized && !auth.browserAnalyzerEnabled)
105
- return { mode: 'server', reason: 'optin:off', requested }
106
- return { mode: 'browser', reason: 'auto:browser', requested }
107
- },
108
- reportFallback,
109
- }
110
- }
111
-
112
- function resolveDefaultEngine(): GscQueryEngine {
113
- const override = useGscEngine().value
114
- if (override)
115
- return override
116
- const cfg = useRuntimeConfig().public.analytics as { defaultEngine?: GscQueryEngine } | undefined
117
- return cfg?.defaultEngine ?? 'auto'
118
- }
119
-
120
- export function useGscQueryDispatcher(): GscQueryDispatcher {
121
- return useNuxtApp().$gscQueryDispatcher
122
- }
1
+ // Public re-export. The implementation lives in `_useGscQueryDispatcher.ts`;
2
+ // the module's `imports:extend` hook strips `_use*` from host auto-imports so
3
+ // consumers see the wrapper at `useGscQueryDispatcher` and the layer's own
4
+ // composables import the internal file directly. Named re-exports because
5
+ // Nuxt's auto-import scanner doesn't traverse `export *`.
6
+ export {
7
+ type CreateDefaultDispatcherOpts,
8
+ createDefaultGscQueryDispatcher,
9
+ type GscEngineDecision,
10
+ type GscFallbackEvent,
11
+ type GscQueryDispatcher,
12
+ type PickEngineOpts,
13
+ useGscQueryDispatcher,
14
+ } from './_useGscQueryDispatcher'
@@ -1,20 +1,29 @@
1
1
  import type { IndexingInspectRateLimited, IndexingInspectResponse } from '@gscdump/contracts'
2
- import { useGscAnalyticsClient } from './useGscAnalyticsClient'
2
+ import { invalidateNuxtQueries } from 'nuxt-use-query/query-cache'
3
+ import { gscQueries } from '../queries/gsc'
4
+ import { useGscRpc } from '../utils/gsc-rpc'
3
5
 
4
6
  export function useGscRequestIndexingInspect(): (
5
7
  siteId: string,
6
8
  urls: string[],
7
9
  ) => Promise<IndexingInspectResponse | IndexingInspectRateLimited> {
8
- const client = useGscAnalyticsClient()
10
+ const rpc = useGscRpc()
9
11
  return async (siteId, urls) => {
10
- return client.requestIndexingInspect(siteId, { urls }).catch((err: unknown) => {
11
- const status = (err as { status?: number, statusCode?: number, response?: { status?: number } })?.status
12
- ?? (err as { statusCode?: number })?.statusCode
13
- ?? (err as { response?: { status?: number } })?.response?.status
14
- const data = (err as { data?: unknown })?.data as IndexingInspectRateLimited | undefined
15
- if (status === 429 && data && data.error === 'rate_limited')
16
- return data
17
- throw err
18
- })
12
+ return rpc.execute(gscQueries.indexingInspect(siteId), { urls }, { silent: true })
13
+ .then((res) => {
14
+ invalidateNuxtQueries(`gsc:inspections:${siteId}`)
15
+ invalidateNuxtQueries(`gsc:inspection-history:${siteId}`)
16
+ return res as IndexingInspectResponse | IndexingInspectRateLimited
17
+ })
18
+ .catch((err: unknown) => {
19
+ const e = err as { type?: string, status?: number, statusCode?: number, response?: { status?: number }, data?: unknown }
20
+ const status = e.status
21
+ ?? e.statusCode
22
+ ?? e.response?.status
23
+ const data = e.data as IndexingInspectRateLimited | undefined
24
+ if (status === 429 && data && data.error === 'rate_limited')
25
+ return data
26
+ throw err
27
+ })
19
28
  }
20
29
  }