@gscdump/nuxt 0.22.1 → 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.
@@ -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
@@ -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,7 +6,7 @@
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/analysis'
9
+ import type { AnalysisParams, AnalysisResult } from '@gscdump/engine/analysis-types'
10
10
  import type { InjectionKey } from '@vue/runtime-core'
11
11
 
12
12
  export interface GscPanelRunner {
@@ -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,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
  }
@@ -11,9 +11,10 @@
11
11
 
12
12
  import type { RollupEnvelope } from '@gscdump/contracts'
13
13
  import type { SiteListItem } from './useGscAnalytics'
14
+ import { gscQueries } from '../queries/gsc'
15
+ import { useGscRpc } from '../utils/gsc-rpc'
14
16
  import { useGscResource } from './_useGscResource'
15
17
  import { useGscAnalyticsContext } from './useGscAnalytics'
16
- import { useGscAnalyticsClient } from './useGscAnalyticsClient'
17
18
 
18
19
  type RollupsInput = MaybeRefOrGetter<string | readonly string[]>
19
20
  type SiteLike = string | { id: string } | SiteListItem
@@ -62,9 +63,11 @@ export function useGscRollup<T = unknown>(
62
63
  opts: UseGscRollupOptions = {},
63
64
  ): UseGscRollupReturn<T> {
64
65
  const ctx = useGscAnalyticsContext()
66
+ const rpc = useGscRpc()
65
67
  const resource = useGscResource<[string, string], RollupEnvelope<T> | null>({
68
+ namespace: 'gsc-rollup',
66
69
  keys: [siteId, rollupId],
67
- fetcher: (id, rid) => fetchOne<T>(id, rid, ctx, toValue(opts.range) ?? null),
70
+ fetcher: (id, rid) => fetchOne<T>(rpc, id, rid, ctx, toValue(opts.range) ?? null),
68
71
  watchSources: [() => toValue(opts.range)?.start, () => toValue(opts.range)?.end],
69
72
  isEmpty: env => env == null,
70
73
  })
@@ -84,43 +87,31 @@ export function useGscRollups<T = unknown>(
84
87
  opts: UseGscRollupOptions = {},
85
88
  ): UseGscRollupsReturn<T> {
86
89
  const ctx = useGscAnalyticsContext()
87
-
88
- const envelopes = ref<Record<string, RollupEnvelope<T> | null>>({})
89
- const loading = ref(false)
90
-
91
- async function refresh(): Promise<void> {
92
- const sid = toValue(siteId)
93
- const rids = normaliseRollups(toValue(rollupIds))
94
- if (!sid || rids.length === 0) {
95
- envelopes.value = {}
96
- return
97
- }
98
- loading.value = true
99
- const range = toValue(opts.range) ?? null
100
- const next: Record<string, RollupEnvelope<T> | null> = {}
101
- try {
90
+ const rpc = useGscRpc()
91
+ const rollupKey = computed(() => encodeStringList(normaliseRollups(toValue(rollupIds))))
92
+ const resource = useGscResource<[string, string], Record<string, RollupEnvelope<T> | null>>({
93
+ namespace: 'gsc-rollups',
94
+ keys: [siteId, rollupKey],
95
+ fetcher: async (sid, encoded) => {
96
+ const rids = decodeStringList(encoded)
97
+ const next: Record<string, RollupEnvelope<T> | null> = {}
98
+ const range = toValue(opts.range) ?? null
102
99
  await Promise.all(rids.map(async (rid) => {
103
- next[rid] = await fetchOne<T>(sid, rid, ctx, range)
100
+ next[rid] = await fetchOne<T>(rpc, sid, rid, ctx, range)
104
101
  }))
105
- envelopes.value = next
106
- }
107
- finally {
108
- loading.value = false
109
- }
110
- }
111
-
112
- watch(
113
- () => [toValue(siteId), normaliseRollups(toValue(rollupIds)).join(','), toValue(opts.range)?.start, toValue(opts.range)?.end],
114
- refresh,
115
- { immediate: true },
116
- )
102
+ return next
103
+ },
104
+ watchSources: [() => toValue(opts.range)?.start, () => toValue(opts.range)?.end],
105
+ isEmpty: value => Object.keys(value).length === 0,
106
+ })
107
+ const envelopes = computed<Record<string, RollupEnvelope<T> | null>>(() => resource.data.value ?? {})
117
108
 
118
109
  return {
119
110
  get: (rollupId: string) => envelopes.value[rollupId] ?? null,
120
111
  payload: (rollupId: string) => envelopes.value[rollupId]?.payload ?? null,
121
- envelopes,
122
- loading: loading as Readonly<Ref<boolean>>,
123
- refresh,
112
+ envelopes: envelopes as unknown as Ref<Record<string, RollupEnvelope<T> | null>>,
113
+ loading: resource.loading as unknown as Readonly<Ref<boolean>>,
114
+ refresh: resource.refresh,
124
115
  }
125
116
  }
126
117
 
@@ -134,56 +125,39 @@ export function useGscRollupFanout<T = unknown>(
134
125
  opts: UseGscRollupOptions = {},
135
126
  ): UseGscRollupFanoutReturn<T> {
136
127
  const ctx = useGscAnalyticsContext()
137
- const envelopes = ref<Record<string, RollupEnvelope<T> | null>>({})
138
- const loading = ref(false)
128
+ const rpc = useGscRpc()
139
129
  const progress = ref<{ completed: number, total: number }>({ completed: 0, total: 0 })
140
- // Ensures late-arriving promises from a superseded run don't mutate state
141
- // for the current run (e.g. when the range changes rapidly).
142
- let runToken = 0
143
-
144
- async function refresh(): Promise<void> {
145
- const token = ++runToken
146
- const siteIds = normaliseSites(toValue(sites))
147
- const rid = toValue(rollupId)
148
- if (siteIds.length === 0 || !rid) {
149
- envelopes.value = {}
150
- progress.value = { completed: 0, total: 0 }
151
- return
152
- }
153
- loading.value = true
154
- // Seed envelopes with nulls so the UI can render skeleton rows before any
155
- // fetch resolves. Writes land incrementally as each site returns.
156
- const seeded: Record<string, RollupEnvelope<T> | null> = {}
157
- for (const sid of siteIds) seeded[sid] = null
158
- envelopes.value = seeded
159
- progress.value = { completed: 0, total: siteIds.length }
160
- const range = toValue(opts.range) ?? null
161
- try {
130
+ const siteKey = computed(() => encodeStringList(normaliseSites(toValue(sites))))
131
+ const resource = useGscResource<[string, string], Record<string, RollupEnvelope<T> | null>>({
132
+ namespace: 'gsc-rollup-fanout',
133
+ keys: [siteKey, rollupId],
134
+ fetcher: async (encoded, rid) => {
135
+ const siteIds = decodeStringList(encoded)
136
+ progress.value = { completed: 0, total: siteIds.length }
137
+ const range = toValue(opts.range) ?? null
138
+ const next: Record<string, RollupEnvelope<T> | null> = {}
162
139
  await Promise.all(siteIds.map(async (sid) => {
163
- const env = await fetchOne<T>(sid, rid, ctx, range)
164
- if (token !== runToken)
165
- return
166
- envelopes.value = { ...envelopes.value, [sid]: env }
140
+ next[sid] = await fetchOne<T>(rpc, sid, rid, ctx, range)
167
141
  progress.value = { completed: progress.value.completed + 1, total: siteIds.length }
168
142
  }))
169
- }
170
- finally {
171
- if (token === runToken)
172
- loading.value = false
173
- }
174
- }
175
-
176
- watch(
177
- () => [normaliseSites(toValue(sites)).join(','), toValue(rollupId), toValue(opts.range)?.start, toValue(opts.range)?.end],
178
- refresh,
179
- { immediate: true },
180
- )
143
+ return next
144
+ },
145
+ watchSources: [() => toValue(opts.range)?.start, () => toValue(opts.range)?.end],
146
+ isEmpty: value => Object.keys(value).length === 0,
147
+ })
148
+ const envelopes = computed<Record<string, RollupEnvelope<T> | null>>(() => resource.data.value ?? {})
149
+ watch(resource.loading, (loading) => {
150
+ if (loading)
151
+ return
152
+ if (!resource.data.value)
153
+ progress.value = { completed: 0, total: 0 }
154
+ }, { immediate: true })
181
155
 
182
156
  return {
183
- envelopes,
184
- loading: loading as Readonly<Ref<boolean>>,
157
+ envelopes: envelopes as unknown as Ref<Record<string, RollupEnvelope<T> | null>>,
158
+ loading: resource.loading as unknown as Readonly<Ref<boolean>>,
185
159
  progress: progress as Readonly<Ref<{ completed: number, total: number }>>,
186
- refresh,
160
+ refresh: resource.refresh,
187
161
  }
188
162
  }
189
163
 
@@ -204,7 +178,19 @@ function normaliseSites(input: unknown): string[] {
204
178
  return out
205
179
  }
206
180
 
181
+ function encodeStringList(values: string[]): string {
182
+ return values.length > 0 ? JSON.stringify(values) : ''
183
+ }
184
+
185
+ function decodeStringList(encoded: string): string[] {
186
+ const parsed = JSON.parse(encoded) as unknown
187
+ return Array.isArray(parsed)
188
+ ? parsed.filter((v): v is string => typeof v === 'string' && v.length > 0)
189
+ : []
190
+ }
191
+
207
192
  async function fetchOne<T>(
193
+ rpc: ReturnType<typeof useGscRpc>,
208
194
  siteId: string,
209
195
  rollupId: string,
210
196
  ctx: ReturnType<typeof useGscAnalyticsContext>,
@@ -220,11 +206,10 @@ async function fetchOne<T>(
220
206
  endedAt: undefined,
221
207
  })
222
208
  try {
223
- const env = await useGscAnalyticsClient().getRollup<T>(
224
- siteId,
225
- rollupId,
226
- range ? { start: range.start, end: range.end } : undefined,
227
- )
209
+ const env = await rpc.query(
210
+ gscQueries.rollup(siteId, rollupId, range ?? undefined),
211
+ { silent: true },
212
+ ) as RollupEnvelope<T>
228
213
  ctx.patchProgress(siteId, { stage: 'ready', filesAttached: 1, endedAt: Date.now() })
229
214
  return env
230
215
  }
@@ -5,8 +5,8 @@
5
5
  import type { GscApiRange, SearchAppearanceResponse, SearchAppearanceRow } 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 UseGscSearchAppearanceReturn {
12
12
  response: Readonly<Ref<SearchAppearanceResponse | null>>
@@ -14,8 +14,8 @@ export interface UseGscSearchAppearanceReturn {
14
14
  range: ComputedRef<GscApiRange | null>
15
15
  source: ComputedRef<SearchAppearanceResponse['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 useGscSearchAppearance(
23
23
  siteId: MaybeRefOrGetter<string | null | undefined>,
24
24
  range: MaybeRefOrGetter<{ start: string, end: string } | null | undefined>,
25
25
  ): UseGscSearchAppearanceReturn {
26
- const { data, status, loading, error, refresh } = useGscResource({
26
+ const { data, status, loading, error, refresh } = useGscResource<[string, string, string], SearchAppearanceResponse>({
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().getSearchAppearance(id, { start, end }),
32
+ operation: (id, start, end) => gscQueries.searchAppearance(id, { start, end }),
34
33
  isEmpty: r => r.rows.length === 0,
35
34
  })
36
35