@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.
- package/README.md +21 -9
- package/app/components/GscDashboardPage.vue +2 -2
- package/app/components/GscHero.vue +3 -4
- package/app/components/GscLazyTopResult.vue +7 -5
- package/app/components/GscPerformanceChart.vue +4 -4
- package/app/components/GscSourceDebugPanel.vue +2 -1
- package/app/composables/_useGscBackfill.ts +13 -5
- package/app/composables/_useGscResource.ts +144 -56
- package/app/composables/useGscAnalytics.ts +2 -1
- package/app/composables/useGscAnalyticsSourceInfo.ts +11 -5
- package/app/composables/useGscAnalyzer.ts +33 -25
- package/app/composables/useGscAnalyzerDefs.ts +2 -1
- package/app/composables/useGscCountries.ts +5 -6
- package/app/composables/useGscCurrentSite.ts +32 -11
- package/app/composables/useGscInspectionHistory.ts +5 -5
- package/app/composables/useGscInspections.ts +5 -5
- package/app/composables/useGscPanelContext.ts +1 -1
- package/app/composables/useGscQuery.ts +6 -8
- package/app/composables/useGscRequestIndexingInspect.ts +20 -11
- package/app/composables/useGscRollup.ts +66 -81
- package/app/composables/useGscSearchAppearance.ts +5 -6
- package/app/composables/useGscSiteAnalyzer.ts +93 -137
- package/app/composables/useGscSitemapHistory.ts +5 -5
- package/app/composables/useGscSitemaps.ts +5 -5
- package/app/queries/gsc.ts +163 -0
- package/app/utils/gsc-rpc.ts +18 -0
- package/module.ts +1 -1
- package/nuxt.config.ts +20 -0
- package/package.json +139 -8
- package/types.ts +1 -5
- package/app/composables/useGscParquetTable.ts +0 -198
package/README.md
CHANGED
|
@@ -31,13 +31,25 @@ pnpm add @gscdump/nuxt
|
|
|
31
31
|
export default defineNuxtConfig({
|
|
32
32
|
extends: ['@gscdump/nuxt'],
|
|
33
33
|
runtimeConfig: {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
public: {
|
|
35
|
+
analytics: {
|
|
36
|
+
apiBase: '', // same-origin by default; set to your analytics origin in consumer mode
|
|
37
|
+
},
|
|
36
38
|
},
|
|
37
39
|
},
|
|
38
40
|
})
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
Nuxt auto-imports the layer composables. If you prefer explicit imports, use
|
|
44
|
+
per-composable entrypoints:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { setGscAuth } from '@gscdump/nuxt/composables/useGscAuth'
|
|
48
|
+
import { useGscRollup } from '@gscdump/nuxt/composables/useGscRollup'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The primary query interface is `useGscQuery` / `useGscAnalyzer`.
|
|
52
|
+
|
|
41
53
|
## Register an auth provider
|
|
42
54
|
|
|
43
55
|
```ts
|
|
@@ -60,11 +72,11 @@ export default defineNitroPlugin((nitro) => {
|
|
|
60
72
|
|
|
61
73
|
| Need | Endpoint | Client composable |
|
|
62
74
|
| --- | --- | --- |
|
|
63
|
-
| Pre-aggregated widgets (top pages, daily totals) | `GET /api/sites/:siteId/rollup/:id` | `useGscRollup` / `useGscRollups` / `useGscRollupFanout` |
|
|
64
|
-
| Raw rows filtered by dimension/range (`where(eq(page, ...))`) | `POST /api/sites/:siteId/rows` | `
|
|
65
|
-
| Analyzer result (striking-distance, CTR anomaly, …) | `POST /api/sites/:siteId/analyze` | `useGscAnalyzer(siteId).analyze(...)` |
|
|
75
|
+
| Pre-aggregated widgets (top pages, daily totals) | `GET /api/__gsc/sites/:siteId/rollup/:id` | `useGscRollup` / `useGscRollups` / `useGscRollupFanout` |
|
|
76
|
+
| Raw rows filtered by dimension/range (`where(eq(page, ...))`) | `POST /api/__gsc/sites/:siteId/rows` | `useGscQuery({ siteId, params })` |
|
|
77
|
+
| Analyzer result (striking-distance, CTR anomaly, …) | `POST /api/__gsc/sites/:siteId/analyze` | `useGscAnalyzer(siteId).analyze(...)` |
|
|
66
78
|
|
|
67
|
-
Rule of thumb: reach for `/rows` + `
|
|
79
|
+
Rule of thumb: reach for `/rows` + `useGscQuery` for detail pages that
|
|
68
80
|
need a freeform slice of the data. Only fall back to `/analyze` when the
|
|
69
81
|
server runs an actual analyzer (anything in the registry); hitting it for
|
|
70
82
|
plain row lookups routes through analyzer-gating logic you don't need.
|
|
@@ -78,9 +90,9 @@ the data — e.g. nuxtseo.com pro consuming gscdump.com. Two pieces:
|
|
|
78
90
|
in `nuxt.config.ts` (or via `NUXT_PUBLIC_ANALYTICS_API_BASE`). Empty =
|
|
79
91
|
same-origin (the default origin-mode shape).
|
|
80
92
|
|
|
81
|
-
2. **Hand the layer
|
|
82
|
-
|
|
83
|
-
`
|
|
93
|
+
2. **Hand the layer per-viewer auth**: register a client plugin that trades
|
|
94
|
+
the viewer's host session for an api key, then call
|
|
95
|
+
`setGscAuth({ apiKey: key })`. The layer's `useGscFetch`
|
|
84
96
|
attaches the header to every `/api/__gsc/*` call automatically. Browser
|
|
85
97
|
parquet reads use exact-object URLs minted by the source endpoint; runtime
|
|
86
98
|
preflights can include this header, while DuckDB-WASM range reads are
|
|
@@ -14,10 +14,10 @@ const gapClass = computed(() => (
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<div class="flex flex-col min-h-screen">
|
|
17
|
+
<div class="flex flex-col min-h-screen overflow-x-clip">
|
|
18
18
|
<slot name="header" />
|
|
19
19
|
<div
|
|
20
|
-
class="max-w-[1128px] px-4 sm:px-6 lg:px-9 pt-4 pb-10 flex flex-col flex-1 w-full"
|
|
20
|
+
class="max-w-[1128px] min-w-0 px-4 sm:px-6 lg:px-9 pt-4 pb-10 flex flex-col flex-1 w-full"
|
|
21
21
|
:class="gapClass"
|
|
22
22
|
>
|
|
23
23
|
<slot />
|
|
@@ -204,10 +204,9 @@ const chartPrevData = computed(() => stats.value?.previous?.series ?? [])
|
|
|
204
204
|
v-if="stats && chartData.length"
|
|
205
205
|
class="rounded-lg border border-default bg-default p-4"
|
|
206
206
|
>
|
|
207
|
-
<
|
|
208
|
-
:
|
|
209
|
-
:
|
|
210
|
-
:show-comparison="compareMode !== 'none' && chartPrevData.length > 0"
|
|
207
|
+
<GscPerformanceChart
|
|
208
|
+
:value="chartData"
|
|
209
|
+
:prev-value="compareMode !== 'none' ? chartPrevData : null"
|
|
211
210
|
:height="220"
|
|
212
211
|
/>
|
|
213
212
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useIntersectionObserver } from '@vueuse/core'
|
|
3
|
-
import {
|
|
3
|
+
import { gscQueries } from '../queries/gsc'
|
|
4
|
+
import { useGscRpc } from '../utils/gsc-rpc'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
siteUrl: string
|
|
@@ -21,14 +22,15 @@ const { stop } = useIntersectionObserver(
|
|
|
21
22
|
return
|
|
22
23
|
loaded.value = true
|
|
23
24
|
stop()
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const request = useGscRpc().query(
|
|
26
|
+
gscQueries.topAssociation(props.siteUrl, {
|
|
26
27
|
type: props.type,
|
|
27
28
|
identifier: props.identifier,
|
|
28
29
|
startDate: props.startDate,
|
|
29
30
|
endDate: props.endDate,
|
|
30
|
-
},
|
|
31
|
-
}
|
|
31
|
+
}),
|
|
32
|
+
) as unknown as Promise<{ value: string | null }>
|
|
33
|
+
request
|
|
32
34
|
.then(r => value.value = r.value)
|
|
33
35
|
.catch(() => {})
|
|
34
36
|
},
|
|
@@ -297,11 +297,11 @@ const position = (d: DataRow) => d.position ?? 0
|
|
|
297
297
|
<div v-if="loading" class="loading-skeleton">
|
|
298
298
|
<div class="flex-1 flex items-end gap-1">
|
|
299
299
|
<div v-for="i in 30" :key="i" class="flex-1 h-full flex">
|
|
300
|
-
<
|
|
300
|
+
<USkeleton class="w-full self-end" :style="{ height: `${28 + ((i * 17) % 56)}%` }" />
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
<div class="flex justify-between mt-3">
|
|
304
|
-
<
|
|
304
|
+
<USkeleton v-for="i in 5" :key="i" class="h-3 w-12" />
|
|
305
305
|
</div>
|
|
306
306
|
</div>
|
|
307
307
|
|
|
@@ -444,11 +444,11 @@ const position = (d: DataRow) => d.position ?? 0
|
|
|
444
444
|
<div class="loading-skeleton">
|
|
445
445
|
<div class="flex-1 flex items-end gap-1">
|
|
446
446
|
<div v-for="i in 30" :key="i" class="flex-1">
|
|
447
|
-
<
|
|
447
|
+
<USkeleton class="w-full self-end" :style="{ height: `${28 + ((i * 17) % 56)}%` }" />
|
|
448
448
|
</div>
|
|
449
449
|
</div>
|
|
450
450
|
<div class="flex justify-between mt-3">
|
|
451
|
-
<
|
|
451
|
+
<USkeleton v-for="i in 5" :key="i" class="h-3 w-12" />
|
|
452
452
|
</div>
|
|
453
453
|
</div>
|
|
454
454
|
</template>
|
|
@@ -37,11 +37,12 @@ interface WhoamiResponse {
|
|
|
37
37
|
}
|
|
38
38
|
const whoami = ref<WhoamiResponse | null>(null)
|
|
39
39
|
const whoamiLoading = ref(false)
|
|
40
|
+
const analyticsClient = useGscAnalyticsClient()
|
|
40
41
|
watchEffect(() => {
|
|
41
42
|
if (!import.meta.client || resolvedSite.value || !open.value || whoami.value || whoamiLoading.value)
|
|
42
43
|
return
|
|
43
44
|
whoamiLoading.value = true
|
|
44
|
-
const request =
|
|
45
|
+
const request = analyticsClient.whoami() as Promise<WhoamiResponse>
|
|
45
46
|
request
|
|
46
47
|
.then((res) => { whoami.value = res })
|
|
47
48
|
.catch(() => { /* surface via empty state, not a toast */ })
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// on-demand backfill request, polls sync-progress, and invokes a refetch once
|
|
3
3
|
// the requested range is synced.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { invalidateNuxtQueries } from 'nuxt-use-query/query-cache'
|
|
6
|
+
import { gscQueries } from '../queries/gsc'
|
|
7
|
+
import { useGscRpc } from '../utils/gsc-rpc'
|
|
7
8
|
|
|
8
9
|
interface BackfillRange {
|
|
9
10
|
startDate: string
|
|
@@ -35,6 +36,7 @@ export function useGscBackfill(): GscBackfillState & {
|
|
|
35
36
|
maybeTrigger: (meta: unknown, siteId: string, refetch: () => Promise<unknown> | unknown) => boolean
|
|
36
37
|
reset: () => void
|
|
37
38
|
} {
|
|
39
|
+
const rpc = useGscRpc()
|
|
38
40
|
const pending = ref(false)
|
|
39
41
|
const range = ref<BackfillRange | null>(null)
|
|
40
42
|
const percent = ref(0)
|
|
@@ -48,7 +50,7 @@ export function useGscBackfill(): GscBackfillState & {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
async function isRangeCovered(siteId: string, req: BackfillRange): Promise<boolean> {
|
|
51
|
-
const progress = await
|
|
53
|
+
const progress = await rpc.query(gscQueries.syncProgress(), { silent: true }).catch(() => null) as SyncProgressResponse | null
|
|
52
54
|
const site = progress?.sites?.find(s => s.id === siteId)
|
|
53
55
|
if (!site?.oldestDateSynced || !site?.newestDateSynced)
|
|
54
56
|
return false
|
|
@@ -63,7 +65,7 @@ export function useGscBackfill(): GscBackfillState & {
|
|
|
63
65
|
error.value = null
|
|
64
66
|
attemptedKeys.add(rangeKey(siteId, req))
|
|
65
67
|
|
|
66
|
-
await
|
|
68
|
+
await rpc.execute(gscQueries.backfill(siteId), { ...req }, { silent: true }).catch((err: unknown) => {
|
|
67
69
|
error.value = (err as { data?: { message?: string }, message?: string })?.data?.message
|
|
68
70
|
|| (err as Error)?.message
|
|
69
71
|
|| 'Failed to queue backfill'
|
|
@@ -86,8 +88,14 @@ export function useGscBackfill(): GscBackfillState & {
|
|
|
86
88
|
if (!covered)
|
|
87
89
|
error.value = 'Backfill did not complete within the expected window. Try again later.'
|
|
88
90
|
|
|
89
|
-
if (covered)
|
|
91
|
+
if (covered) {
|
|
92
|
+
// Invalidate everything that depends on the newly synced range so
|
|
93
|
+
// consumers refetch instead of relying on the caller-passed refetch.
|
|
94
|
+
invalidateNuxtQueries(key => key.startsWith(`gsc:source-info:${siteId}`)
|
|
95
|
+
|| key.startsWith(`gsc:analysis-sources:${siteId}`)
|
|
96
|
+
|| key === 'gsc:sync-progress')
|
|
90
97
|
await refetch()
|
|
98
|
+
}
|
|
91
99
|
pending.value = false
|
|
92
100
|
range.value = null
|
|
93
101
|
}
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
// Site-keyed async resource composable. Owns key-watch,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
|
1
|
+
// Site-keyed async resource composable. Owns key-watch, status classification,
|
|
2
|
+
// refresh, and cache policy for every read-only `/api/__gsc/*` operation in
|
|
3
|
+
// the layer. Resource composables stay thin adapters that wire reactive keys
|
|
4
|
+
// to `gscQueries.*(...)` operations (preferred) — or to a `fetcher` for the
|
|
5
|
+
// rollup composables that mix multiple operations + progress side-effects.
|
|
11
6
|
|
|
12
7
|
import type { ComputedRef, Ref, WatchSource } from '@vue/runtime-core'
|
|
8
|
+
import type { NuxtRpcQueryOperation } from 'nuxt-use-query/rpc'
|
|
13
9
|
import type { GscErrorStatus } from '../utils/gsc-error'
|
|
10
|
+
import { useNuxtRpcQuery } from 'nuxt-use-query/rpc'
|
|
14
11
|
import { classifyGscError } from '../utils/gsc-error'
|
|
12
|
+
import { useGscFetch } from '../utils/gsc-fetch'
|
|
15
13
|
|
|
16
14
|
export type GscResourceStatus = 'idle' | 'pending' | 'success' | 'empty' | GscErrorStatus
|
|
17
15
|
|
|
18
16
|
export interface UseGscResourceOptions<TArgs extends readonly unknown[], TData> {
|
|
19
17
|
/** Reactive args passed to the fetcher. Resource stays idle while any required key is null/undefined. */
|
|
20
18
|
keys: { [I in keyof TArgs]: MaybeRefOrGetter<TArgs[I] | null | undefined> }
|
|
21
|
-
/**
|
|
22
|
-
|
|
19
|
+
/** RPC query operation invoked with the resolved keys. Preferred over `fetcher` — gets Zod validation + shared cache keying. */
|
|
20
|
+
operation?: (...args: TArgs) => NuxtRpcQueryOperation<any, any>
|
|
21
|
+
/** Composite async fetcher invoked with the resolved keys. Use only when an operation can't model the request (multi-call, progress side-effects). */
|
|
22
|
+
fetcher?: (...args: TArgs) => Promise<TData | null>
|
|
23
23
|
/** Predicate for the `empty` status — defaults to checking `null`/`[]`/typical container fields. */
|
|
24
24
|
isEmpty?: (data: TData) => boolean
|
|
25
25
|
/** Extra reactive sources that should retrigger a fetch. */
|
|
26
26
|
watchSources?: WatchSource[]
|
|
27
|
+
/** Optional namespace for the fetcher-mode cache key. Defaults to `gsc-resource`. Ignored when `operation` is provided (the operation owns its key). */
|
|
28
|
+
namespace?: string
|
|
29
|
+
/** Milliseconds before cached data is considered stale. Defaults to `nuxt-use-query`'s 0. */
|
|
30
|
+
staleTime?: number
|
|
31
|
+
/** Evict cached payload after the last consumer unmounts. Defaults to `nuxt-use-query`'s 5 min. */
|
|
32
|
+
gcTime?: number
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export interface UseGscResourceReturn<TData> {
|
|
30
|
-
data: Ref<TData | null
|
|
31
|
-
status: Ref<GscResourceStatus
|
|
36
|
+
data: Readonly<Ref<TData | null>>
|
|
37
|
+
status: Readonly<Ref<GscResourceStatus>>
|
|
32
38
|
loading: ComputedRef<boolean>
|
|
33
|
-
error: Ref<Error | null
|
|
39
|
+
error: Readonly<Ref<Error | null>>
|
|
34
40
|
refresh: () => Promise<void>
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -53,14 +59,8 @@ function defaultIsEmpty(v: unknown): boolean {
|
|
|
53
59
|
export function useGscResource<TArgs extends readonly unknown[], TData>(
|
|
54
60
|
opts: UseGscResourceOptions<TArgs, TData>,
|
|
55
61
|
): UseGscResourceReturn<TData> {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
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 {
|
|
62
|
+
const namespace = opts.namespace ?? 'gsc-resource'
|
|
63
|
+
const resolvedArgs = computed<TArgs | null>(() => {
|
|
64
64
|
const out: unknown[] = []
|
|
65
65
|
for (const k of opts.keys) {
|
|
66
66
|
const v = toValue(k)
|
|
@@ -69,46 +69,134 @@ export function useGscResource<TArgs extends readonly unknown[], TData>(
|
|
|
69
69
|
out.push(v)
|
|
70
70
|
}
|
|
71
71
|
return out as unknown as TArgs
|
|
72
|
-
}
|
|
72
|
+
})
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
74
|
+
const enabled = computed(() => resolvedArgs.value != null)
|
|
75
|
+
const gscFetch = useGscFetch()
|
|
76
|
+
|
|
77
|
+
// Operation branch: route through useNuxtRpcQuery so we inherit Zod
|
|
78
|
+
// response validation, RPC key serialization, and shared cache invalidation.
|
|
79
|
+
if (opts.operation) {
|
|
80
|
+
const operation = computed<NuxtRpcQueryOperation<any, any> | null>(() => {
|
|
81
|
+
const args = resolvedArgs.value
|
|
82
|
+
return args ? opts.operation!(...args) : null
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Idle placeholder when args aren't ready — useNuxtRpcQuery is always
|
|
86
|
+
// mounted, but the `enabled` gate prevents the request firing.
|
|
87
|
+
const fallbackOp = { key: `${namespace}:idle`, path: `${namespace}:idle`, response: { parse: (v: unknown) => v } as any }
|
|
88
|
+
|
|
89
|
+
const query = useNuxtRpcQuery<any>(
|
|
90
|
+
() => operation.value ?? fallbackOp,
|
|
91
|
+
{
|
|
92
|
+
enabled,
|
|
93
|
+
watch: opts.watchSources,
|
|
94
|
+
staleTime: opts.staleTime,
|
|
95
|
+
gcTime: opts.gcTime,
|
|
96
|
+
$fetch: gscFetch as any,
|
|
97
|
+
} as any,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const data = computed<TData | null>(() => {
|
|
101
|
+
if (!enabled.value)
|
|
102
|
+
return null
|
|
103
|
+
return (query.displayData.value ?? null) as TData | null
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const error = computed<Error | null>(() => {
|
|
107
|
+
if (!enabled.value)
|
|
108
|
+
return null
|
|
109
|
+
const e = query.error.value
|
|
110
|
+
return e == null ? null : e instanceof Error ? e : new Error(String(e))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const status = computed<GscResourceStatus>(() => {
|
|
114
|
+
if (!enabled.value)
|
|
115
|
+
return 'idle'
|
|
116
|
+
if (query.status.value === 'pending')
|
|
117
|
+
return 'pending'
|
|
118
|
+
if (query.status.value === 'error') {
|
|
119
|
+
const classified = classifyGscError(error.value)
|
|
120
|
+
return classified.status
|
|
121
|
+
}
|
|
122
|
+
const out = data.value
|
|
123
|
+
if (query.status.value === 'success')
|
|
124
|
+
return out == null || (opts.isEmpty ?? defaultIsEmpty)(out) ? 'empty' : 'success'
|
|
125
|
+
return 'idle'
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const loading = computed(() => status.value === 'pending')
|
|
129
|
+
|
|
130
|
+
async function refresh(): Promise<void> {
|
|
131
|
+
if (!enabled.value)
|
|
95
132
|
return
|
|
96
|
-
|
|
97
|
-
error.value = e instanceof Error ? e : new Error(String(e))
|
|
98
|
-
status.value = classified.status
|
|
99
|
-
data.value = null
|
|
133
|
+
await query.refresh()
|
|
100
134
|
}
|
|
135
|
+
|
|
136
|
+
return { data, status, loading, error, refresh }
|
|
101
137
|
}
|
|
102
138
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
139
|
+
// Fetcher branch: composables that can't be modelled as a single RPC
|
|
140
|
+
// operation (rollup fan-out, progress tracking) drop down to a raw
|
|
141
|
+
// useNuxtQuery + bound $fetch.
|
|
142
|
+
const queryKey = computed(() => {
|
|
143
|
+
const args = resolvedArgs.value
|
|
144
|
+
return args ? `${namespace}:${JSON.stringify(args)}` : `${namespace}:idle`
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const fetchResource = (async () => {
|
|
148
|
+
const args = resolvedArgs.value
|
|
149
|
+
if (!args)
|
|
150
|
+
return null
|
|
151
|
+
if (opts.fetcher)
|
|
152
|
+
return await opts.fetcher(...args)
|
|
153
|
+
throw new Error('useGscResource: no operation or fetcher configured')
|
|
154
|
+
}) as unknown as typeof $fetch
|
|
155
|
+
|
|
156
|
+
const query = useNuxtQuery<TData | null, Error>(() => queryKey.value, {
|
|
157
|
+
key: () => queryKey.value,
|
|
158
|
+
enabled,
|
|
159
|
+
watch: opts.watchSources,
|
|
160
|
+
staleTime: opts.staleTime,
|
|
161
|
+
gcTime: opts.gcTime,
|
|
162
|
+
$fetch: fetchResource,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const data = computed<TData | null>(() => {
|
|
166
|
+
if (!enabled.value)
|
|
167
|
+
return null
|
|
168
|
+
return (query.displayData.value ?? null) as TData | null
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const error = computed<Error | null>(() => {
|
|
172
|
+
if (!enabled.value)
|
|
173
|
+
return null
|
|
174
|
+
const e = query.error.value
|
|
175
|
+
return e == null ? null : e instanceof Error ? e : new Error(String(e))
|
|
176
|
+
})
|
|
108
177
|
|
|
109
|
-
|
|
110
|
-
|
|
178
|
+
const status = computed<GscResourceStatus>(() => {
|
|
179
|
+
if (!enabled.value)
|
|
180
|
+
return 'idle'
|
|
181
|
+
if (query.status.value === 'pending')
|
|
182
|
+
return 'pending'
|
|
183
|
+
if (query.status.value === 'error') {
|
|
184
|
+
const classified = classifyGscError(error.value)
|
|
185
|
+
return classified.status
|
|
186
|
+
}
|
|
187
|
+
const out = data.value
|
|
188
|
+
if (query.status.value === 'success')
|
|
189
|
+
return out == null || (opts.isEmpty ?? defaultIsEmpty)(out) ? 'empty' : 'success'
|
|
190
|
+
return 'idle'
|
|
111
191
|
})
|
|
112
192
|
|
|
193
|
+
const loading = computed(() => status.value === 'pending')
|
|
194
|
+
|
|
195
|
+
async function refresh(): Promise<void> {
|
|
196
|
+
if (!enabled.value)
|
|
197
|
+
return
|
|
198
|
+
await query.refresh()
|
|
199
|
+
}
|
|
200
|
+
|
|
113
201
|
return { data, status, loading, error, refresh }
|
|
114
202
|
}
|
|
@@ -127,6 +127,7 @@ export function useGscBootProgress(): {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function createContext(): GscAnalyticsContext {
|
|
130
|
+
const client = useGscAnalyticsClient()
|
|
130
131
|
const sites = ref<SiteListItem[] | null>(null)
|
|
131
132
|
const sitesLoading = ref(false)
|
|
132
133
|
const sitesError = ref<Error | null>(null)
|
|
@@ -135,7 +136,7 @@ function createContext(): GscAnalyticsContext {
|
|
|
135
136
|
async function refreshSites(): Promise<void> {
|
|
136
137
|
sitesLoading.value = true
|
|
137
138
|
sitesError.value = null
|
|
138
|
-
sites.value = await
|
|
139
|
+
sites.value = await client.listSites().catch((err: unknown) => {
|
|
139
140
|
sitesError.value = err instanceof Error ? err : new Error(String(err))
|
|
140
141
|
return null
|
|
141
142
|
})
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// entry per site/searchType/range via the shared site-resource seam, so they
|
|
10
10
|
// collapse to one network read per session slice.
|
|
11
11
|
|
|
12
|
-
import type { SourceCapabilities } from '@gscdump/analysis'
|
|
13
12
|
import type { SourceInfoOptions } from '@gscdump/contracts'
|
|
13
|
+
import type { SourceCapabilities } from '@gscdump/engine/source'
|
|
14
14
|
import { acquireSharedEntry, useGscSharedSiteResource } from './_useGscSharedSiteResource'
|
|
15
15
|
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
16
16
|
|
|
@@ -69,12 +69,17 @@ function sourceInfoKey(siteId: string, options?: SourceInfoOptions): string {
|
|
|
69
69
|
])
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function fetchInto(
|
|
72
|
+
function fetchInto(
|
|
73
|
+
entry: SourceInfoEntry,
|
|
74
|
+
siteId: string,
|
|
75
|
+
options?: SourceInfoOptions,
|
|
76
|
+
client = useGscAnalyticsClient(),
|
|
77
|
+
): Promise<void> {
|
|
73
78
|
if (entry.pending.value)
|
|
74
79
|
return entry.pending.value
|
|
75
80
|
entry.loading.value = true
|
|
76
81
|
entry.error.value = null
|
|
77
|
-
const p = (
|
|
82
|
+
const p = (client.getSourceInfo(siteId, options) as Promise<GscAnalyticsSourceInfo>)
|
|
78
83
|
.then((data) => {
|
|
79
84
|
entry.info.value = data
|
|
80
85
|
})
|
|
@@ -112,6 +117,7 @@ export function useGscAnalyticsSourceInfo(
|
|
|
112
117
|
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
113
118
|
options: MaybeRefOrGetter<SourceInfoOptions | null | undefined> = null,
|
|
114
119
|
): GscAnalyticsSourceInfoState {
|
|
120
|
+
const client = useGscAnalyticsClient()
|
|
115
121
|
const cacheKey = computed(() => {
|
|
116
122
|
const id = toValue(siteId)
|
|
117
123
|
return id ? sourceInfoKey(id, toValue(options) ?? undefined) : null
|
|
@@ -129,7 +135,7 @@ export function useGscAnalyticsSourceInfo(
|
|
|
129
135
|
if (!id)
|
|
130
136
|
return
|
|
131
137
|
if (entry.info.value == null && entry.pending.value == null && entry.error.value == null)
|
|
132
|
-
void fetchInto(entry, id, toValue(options) ?? undefined)
|
|
138
|
+
void fetchInto(entry, id, toValue(options) ?? undefined, client)
|
|
133
139
|
}, { immediate: true })
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -143,7 +149,7 @@ export function useGscAnalyticsSourceInfo(
|
|
|
143
149
|
if (!entry || !id)
|
|
144
150
|
return
|
|
145
151
|
entry.info.value = null
|
|
146
|
-
await fetchInto(entry, id, toValue(options) ?? undefined)
|
|
152
|
+
await fetchInto(entry, id, toValue(options) ?? undefined, client)
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
function supports(analyzerId: MaybeRefOrGetter<string>): ComputedRef<boolean> {
|