@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
@@ -1,114 +1,11 @@
1
- // Site-keyed async resource composable. Owns key-watch, stale-token discard,
2
- // status classification, refresh, and dispose for every read-only `/api/__gsc/*`
3
- // fetcher in the layer. Resource composables (useGscSitemaps,
4
- // useGscIndexingDiagnostics, …) are thin adapters that wire reactive keys to
5
- // `useGscAnalyticsClient().getX(...)` plus optional derived computeds.
6
- //
7
- // Stale-token (not AbortController) because `@gscdump/sdk`'s AnalyticsClient
8
- // doesn't accept a signal. Late-arriving promises from a superseded run are
9
- // dropped on `data`/`status` writes; the in-flight fetch still runs to
10
- // completion, which is acceptable for read-only GETs.
11
-
12
- import type { ComputedRef, Ref, WatchSource } from 'vue'
13
- import type { GscErrorStatus } from '../utils/gsc-error'
14
- import { classifyGscError } from '../utils/gsc-error'
15
-
16
- export type GscResourceStatus = 'idle' | 'pending' | 'success' | 'empty' | GscErrorStatus
17
-
18
- export interface UseGscResourceOptions<TArgs extends readonly unknown[], TData> {
19
- /** Reactive args passed to the fetcher. Resource stays idle while any required key is null/undefined. */
20
- keys: { [I in keyof TArgs]: MaybeRefOrGetter<TArgs[I] | null | undefined> }
21
- /** Async fetcher invoked with the resolved keys. */
22
- fetcher: (...args: TArgs) => Promise<TData | null>
23
- /** Predicate for the `empty` status — defaults to checking `null`/`[]`/typical container fields. */
24
- isEmpty?: (data: TData) => boolean
25
- /** Extra reactive sources that should retrigger a fetch. */
26
- watchSources?: WatchSource[]
27
- }
28
-
29
- export interface UseGscResourceReturn<TData> {
30
- data: Ref<TData | null>
31
- status: Ref<GscResourceStatus>
32
- loading: ComputedRef<boolean>
33
- error: Ref<Error | null>
34
- refresh: () => Promise<void>
35
- }
36
-
37
- function defaultIsEmpty(v: unknown): boolean {
38
- if (v == null)
39
- return true
40
- if (Array.isArray(v))
41
- return v.length === 0
42
- if (typeof v === 'object') {
43
- const rec = v as Record<string, unknown>
44
- for (const key of ['rows', 'records', 'snapshots', 'items', 'results']) {
45
- const arr = rec[key]
46
- if (Array.isArray(arr))
47
- return arr.length === 0
48
- }
49
- }
50
- return false
51
- }
52
-
53
- export function useGscResource<TArgs extends readonly unknown[], TData>(
54
- opts: UseGscResourceOptions<TArgs, TData>,
55
- ): UseGscResourceReturn<TData> {
56
- const data = shallowRef<TData | null>(null)
57
- const status = ref<GscResourceStatus>('idle')
58
- const error = ref<Error | null>(null)
59
- const loading = computed(() => status.value === 'pending')
60
-
61
- let runToken = 0
62
-
63
- function resolveKeys(): TArgs | null {
64
- const out: unknown[] = []
65
- for (const k of opts.keys) {
66
- const v = toValue(k)
67
- if (v == null || v === '')
68
- return null
69
- out.push(v)
70
- }
71
- return out as unknown as TArgs
72
- }
73
-
74
- async function refresh(): Promise<void> {
75
- const args = resolveKeys()
76
- const token = ++runToken
77
- if (!args) {
78
- data.value = null
79
- status.value = 'idle'
80
- error.value = null
81
- return
82
- }
83
- status.value = 'pending'
84
- error.value = null
85
- try {
86
- const out = await opts.fetcher(...args)
87
- if (token !== runToken)
88
- return
89
- data.value = out
90
- const empty = out == null || (opts.isEmpty ?? defaultIsEmpty)(out)
91
- status.value = empty ? 'empty' : 'success'
92
- }
93
- catch (e) {
94
- if (token !== runToken)
95
- return
96
- const classified = classifyGscError(e)
97
- error.value = e instanceof Error ? e : new Error(String(e))
98
- status.value = classified.status
99
- data.value = null
100
- }
101
- }
102
-
103
- watch(
104
- [...opts.keys.map(k => () => toValue(k)), ...(opts.watchSources ?? [])],
105
- refresh,
106
- { immediate: true },
107
- )
108
-
109
- onScopeDispose(() => {
110
- runToken++
111
- })
112
-
113
- return { data, status, loading, error, refresh }
114
- }
1
+ // Public re-export. The implementation lives in `_useGscResource.ts`; the
2
+ // module's `imports:extend` hook strips `_use*` from host auto-imports so
3
+ // consumers see the wrapper at `useGscResource` 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 GscResourceStatus,
8
+ useGscResource,
9
+ type UseGscResourceOptions,
10
+ type UseGscResourceReturn,
11
+ } from './_useGscResource'
@@ -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