@gscdump/nuxt 0.22.4 → 1.5.1
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 +10 -137
- package/dist/module.d.mts +22 -0
- package/dist/module.mjs +30 -0
- package/dist/runtime-config.d.mts +12 -0
- package/dist/runtime-config.mjs +27 -0
- package/package.json +26 -176
- package/app/assets/css/main.css +0 -2
- package/app/components/GscAnalyzerPanel.vue +0 -94
- package/app/components/GscAnonymizationBanner.vue +0 -46
- package/app/components/GscBootProgress.vue +0 -297
- package/app/components/GscCommandPalette.vue +0 -77
- package/app/components/GscDashboardPage.vue +0 -26
- package/app/components/GscEngineBadge.vue +0 -28
- package/app/components/GscHero.vue +0 -214
- package/app/components/GscLazyTopResult.vue +0 -47
- package/app/components/GscPerformanceChart.vue +0 -532
- package/app/components/GscPositionDistributionChart.vue +0 -63
- package/app/components/GscQueryLabel.vue +0 -63
- package/app/components/GscSitePageHeader.vue +0 -40
- package/app/components/GscSourceDebugPanel.vue +0 -196
- package/app/components/GscSyncStatusCell.vue +0 -54
- package/app/components/GscTopRollupCard.vue +0 -90
- package/app/composables/_useGscBackfill.ts +0 -119
- package/app/composables/_useGscQueryDispatcher.ts +0 -123
- package/app/composables/_useGscResource.ts +0 -202
- package/app/composables/_useGscSharedSiteResource.ts +0 -136
- package/app/composables/useGscAnalytics.ts +0 -206
- package/app/composables/useGscAnalyticsClient.ts +0 -9
- package/app/composables/useGscAnalyticsConfig.ts +0 -8
- package/app/composables/useGscAnalyticsSourceInfo.ts +0 -165
- package/app/composables/useGscAnalyzer.ts +0 -460
- package/app/composables/useGscAnalyzerBatch.ts +0 -106
- package/app/composables/useGscAnalyzerDefs.ts +0 -119
- package/app/composables/useGscAuth.ts +0 -115
- package/app/composables/useGscConsoleUrl.ts +0 -45
- package/app/composables/useGscCountries.ts +0 -46
- package/app/composables/useGscCurrentSite.ts +0 -36
- package/app/composables/useGscInspectionHistory.ts +0 -42
- package/app/composables/useGscInspections.ts +0 -52
- package/app/composables/useGscPanelContext.ts +0 -31
- package/app/composables/useGscPeriod.ts +0 -243
- package/app/composables/useGscQuery.ts +0 -301
- package/app/composables/useGscQueryDispatcher.ts +0 -14
- package/app/composables/useGscRequestIndexingInspect.ts +0 -29
- package/app/composables/useGscResource.ts +0 -11
- package/app/composables/useGscRollup.ts +0 -237
- package/app/composables/useGscSearchAppearance.ts +0 -46
- package/app/composables/useGscSiteAnalyzer.ts +0 -456
- package/app/composables/useGscSitemapHistory.ts +0 -41
- package/app/composables/useGscSitemaps.ts +0 -45
- package/app/composables/useGscTableState.ts +0 -119
- package/app/plugins/analytics.ts +0 -57
- package/app/queries/gsc.ts +0 -163
- package/app/utils/anonymization.ts +0 -24
- package/app/utils/country-names.ts +0 -56
- package/app/utils/duckdb-wasm.ts +0 -166
- package/app/utils/gsc-constants.ts +0 -10
- package/app/utils/gsc-error.ts +0 -58
- package/app/utils/gsc-fetch.ts +0 -94
- package/app/utils/gsc-filters.ts +0 -32
- package/app/utils/gsc-rows.ts +0 -72
- package/app/utils/gsc-rpc.ts +0 -18
- package/app/utils/position.ts +0 -7
- package/module.ts +0 -81
- package/nuxt.config.ts +0 -61
- package/types.ts +0 -115
|
@@ -1,202 +0,0 @@
|
|
|
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.
|
|
6
|
-
|
|
7
|
-
import type { ComputedRef, Ref, WatchSource } from '@vue/runtime-core'
|
|
8
|
-
import type { NuxtRpcQueryOperation } from 'nuxt-use-query/rpc'
|
|
9
|
-
import type { GscErrorStatus } from '../utils/gsc-error'
|
|
10
|
-
import { useNuxtRpcQuery } from 'nuxt-use-query/rpc'
|
|
11
|
-
import { classifyGscError } from '../utils/gsc-error'
|
|
12
|
-
import { useGscFetch } from '../utils/gsc-fetch'
|
|
13
|
-
|
|
14
|
-
export type GscResourceStatus = 'idle' | 'pending' | 'success' | 'empty' | GscErrorStatus
|
|
15
|
-
|
|
16
|
-
export interface UseGscResourceOptions<TArgs extends readonly unknown[], TData> {
|
|
17
|
-
/** Reactive args passed to the fetcher. Resource stays idle while any required key is null/undefined. */
|
|
18
|
-
keys: { [I in keyof TArgs]: MaybeRefOrGetter<TArgs[I] | null | undefined> }
|
|
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
|
-
/** 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
|
-
/** 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
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface UseGscResourceReturn<TData> {
|
|
36
|
-
data: Readonly<Ref<TData | null>>
|
|
37
|
-
status: Readonly<Ref<GscResourceStatus>>
|
|
38
|
-
loading: ComputedRef<boolean>
|
|
39
|
-
error: Readonly<Ref<Error | null>>
|
|
40
|
-
refresh: () => Promise<void>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function defaultIsEmpty(v: unknown): boolean {
|
|
44
|
-
if (v == null)
|
|
45
|
-
return true
|
|
46
|
-
if (Array.isArray(v))
|
|
47
|
-
return v.length === 0
|
|
48
|
-
if (typeof v === 'object') {
|
|
49
|
-
const rec = v as Record<string, unknown>
|
|
50
|
-
for (const key of ['rows', 'records', 'snapshots', 'items', 'results']) {
|
|
51
|
-
const arr = rec[key]
|
|
52
|
-
if (Array.isArray(arr))
|
|
53
|
-
return arr.length === 0
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function useGscResource<TArgs extends readonly unknown[], TData>(
|
|
60
|
-
opts: UseGscResourceOptions<TArgs, TData>,
|
|
61
|
-
): UseGscResourceReturn<TData> {
|
|
62
|
-
const namespace = opts.namespace ?? 'gsc-resource'
|
|
63
|
-
const resolvedArgs = computed<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
|
-
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)
|
|
132
|
-
return
|
|
133
|
-
await query.refresh()
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return { data, status, loading, error, refresh }
|
|
137
|
-
}
|
|
138
|
-
|
|
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
|
-
})
|
|
177
|
-
|
|
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'
|
|
191
|
-
})
|
|
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
|
-
|
|
201
|
-
return { data, status, loading, error, refresh }
|
|
202
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// Per-site shared resource bag. Consolidates the bind/unbind/refcount pattern
|
|
2
|
-
// that useGscAnalyzer and useGscAnalyticsSourceInfo each reinvented.
|
|
3
|
-
//
|
|
4
|
-
// Two entry points share one underlying bag (a module-scope WeakMap keyed by
|
|
5
|
-
// the NuxtApp instance, so each app gets isolated state and SSR is safe):
|
|
6
|
-
// - useGscSharedSiteResource → composable, refcounted, scope-disposed.
|
|
7
|
-
// - acquireSharedEntry → non-composable acquire for boot-time / non-
|
|
8
|
-
// setup callers (e.g. analyzer createInstance).
|
|
9
|
-
//
|
|
10
|
-
// Refcounting via `onDispose` is opt-in:
|
|
11
|
-
// - omitted → entry persists forever (cheap, share-only; e.g. /source-info)
|
|
12
|
-
// - provided → refcounted; runs onDispose + drops the entry at refs=0
|
|
13
|
-
//
|
|
14
|
-
// Returns a `bound` computed that tracks the entry for the current siteId. The
|
|
15
|
-
// caller wraps it in `computed(() => bound.value?.field ?? default)` to expose
|
|
16
|
-
// reactive fields.
|
|
17
|
-
|
|
18
|
-
import type { ComputedRef } from '@vue/runtime-core'
|
|
19
|
-
import type { NuxtApp } from 'nuxt/app'
|
|
20
|
-
|
|
21
|
-
interface CacheEntry<T> {
|
|
22
|
-
entry: T
|
|
23
|
-
refs: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type NamespaceBag = Map<string, Map<string, CacheEntry<unknown>>>
|
|
27
|
-
|
|
28
|
-
const bags = new WeakMap<NuxtApp, NamespaceBag>()
|
|
29
|
-
|
|
30
|
-
function namespaceMap(namespace: string): Map<string, CacheEntry<unknown>> {
|
|
31
|
-
const app = useNuxtApp()
|
|
32
|
-
let bag = bags.get(app)
|
|
33
|
-
if (!bag) {
|
|
34
|
-
bag = new Map()
|
|
35
|
-
bags.set(app, bag)
|
|
36
|
-
}
|
|
37
|
-
let m = bag.get(namespace)
|
|
38
|
-
if (!m) {
|
|
39
|
-
m = new Map()
|
|
40
|
-
bag.set(namespace, m)
|
|
41
|
-
}
|
|
42
|
-
return m
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Non-composable acquire — returns (and lazily creates) the shared entry for
|
|
47
|
-
* `(namespace, siteId)`. No refcount, no scope dispose. Intended for callers
|
|
48
|
-
* outside a Vue setup scope (analyzer boot IIFE, one-shot loaders) that still
|
|
49
|
-
* need to share state with the reactive `useGscSharedSiteResource` consumers.
|
|
50
|
-
*/
|
|
51
|
-
export function acquireSharedEntry<T>(
|
|
52
|
-
namespace: string,
|
|
53
|
-
siteId: string,
|
|
54
|
-
factory: (siteId: string) => T,
|
|
55
|
-
): T {
|
|
56
|
-
const cache = namespaceMap(namespace) as Map<string, CacheEntry<T>>
|
|
57
|
-
let inst = cache.get(siteId)
|
|
58
|
-
if (!inst) {
|
|
59
|
-
inst = { entry: factory(siteId), refs: 0 }
|
|
60
|
-
cache.set(siteId, inst)
|
|
61
|
-
}
|
|
62
|
-
return inst.entry
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface UseSharedSiteResourceOptions<T> {
|
|
66
|
-
/** Build the entry on first acquire for a site. */
|
|
67
|
-
factory: (siteId: string) => T
|
|
68
|
-
/** When set, refcount the entry and run on the last release (deletes from cache). */
|
|
69
|
-
onDispose?: (entry: T) => Promise<void> | void
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface UseSharedSiteResourceReturn<T> {
|
|
73
|
-
bound: ComputedRef<T | null>
|
|
74
|
-
currentSiteId: Ref<string | null>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function useGscSharedSiteResource<T>(
|
|
78
|
-
namespace: string,
|
|
79
|
-
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
80
|
-
opts: UseSharedSiteResourceOptions<T>,
|
|
81
|
-
): UseSharedSiteResourceReturn<T> {
|
|
82
|
-
const cache = namespaceMap(namespace) as Map<string, CacheEntry<T>>
|
|
83
|
-
|
|
84
|
-
const boundRef = shallowRef<T | null>(null)
|
|
85
|
-
const currentSiteId = ref<string | null>(null)
|
|
86
|
-
|
|
87
|
-
function bind(id: string): void {
|
|
88
|
-
unbind()
|
|
89
|
-
let inst = cache.get(id)
|
|
90
|
-
if (!inst) {
|
|
91
|
-
inst = { entry: opts.factory(id), refs: 0 }
|
|
92
|
-
cache.set(id, inst)
|
|
93
|
-
}
|
|
94
|
-
inst.refs++
|
|
95
|
-
boundRef.value = inst.entry
|
|
96
|
-
currentSiteId.value = id
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function unbind(): void {
|
|
100
|
-
const id = currentSiteId.value
|
|
101
|
-
if (!id) {
|
|
102
|
-
boundRef.value = null
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
const inst = cache.get(id)
|
|
106
|
-
boundRef.value = null
|
|
107
|
-
currentSiteId.value = null
|
|
108
|
-
if (!inst)
|
|
109
|
-
return
|
|
110
|
-
inst.refs--
|
|
111
|
-
if (inst.refs <= 0 && opts.onDispose) {
|
|
112
|
-
cache.delete(id)
|
|
113
|
-
void Promise.resolve(opts.onDispose(inst.entry)).catch(() => {})
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
watch(
|
|
118
|
-
() => toValue(siteId),
|
|
119
|
-
(id: string | null | undefined) => {
|
|
120
|
-
if (!id) {
|
|
121
|
-
unbind()
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
if (import.meta.client)
|
|
125
|
-
bind(id)
|
|
126
|
-
},
|
|
127
|
-
{ immediate: true },
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
onScopeDispose(unbind)
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
bound: computed(() => boundRef.value) as ComputedRef<T | null>,
|
|
134
|
-
currentSiteId,
|
|
135
|
-
}
|
|
136
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// Analytics context: sites list + shared boot-progress map + per-site analyzer
|
|
2
|
-
// cache. Provided app-wide by the layer's plugin as `nuxtApp.$gscAnalytics`.
|
|
3
|
-
// Pages reach in via narrower hooks (`useGscSites`, `useGscSite`,
|
|
4
|
-
// `useGscAnalyzer`, `useGscBootProgress`) — no global mutable `currentSite`,
|
|
5
|
-
// no leaked progress writers.
|
|
6
|
-
|
|
7
|
-
import type { SiteListItem } from '@gscdump/contracts'
|
|
8
|
-
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
9
|
-
|
|
10
|
-
export type { SiteListItem }
|
|
11
|
-
|
|
12
|
-
export type ReadBackend = 'd1' | 'r2'
|
|
13
|
-
|
|
14
|
-
export interface SiteRecord extends SiteListItem {
|
|
15
|
-
/** Canonical GSC site URL. Alias of `label` for back-compat. */
|
|
16
|
-
siteUrl: string
|
|
17
|
-
/**
|
|
18
|
-
* Which backend serves analysis for this site. Carried through from the
|
|
19
|
-
* sites endpoint; when omitted the value defaults to `'r2'` so legacy
|
|
20
|
-
* consumers keep working. Browser-attach eligibility is ultimately
|
|
21
|
-
* decided by the server's source provider via `browserAttachEligible`
|
|
22
|
-
* on `/api/__gsc/sites/:siteId/source-info`, not by this field.
|
|
23
|
-
*/
|
|
24
|
-
readBackend: ReadBackend
|
|
25
|
-
syncStatus?: string | null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type SiteLoadStage = 'idle' | 'wasm' | 'manifest' | 'attach' | 'ready' | 'error'
|
|
29
|
-
|
|
30
|
-
export interface SiteLoadProgress {
|
|
31
|
-
siteId: string
|
|
32
|
-
stage: SiteLoadStage
|
|
33
|
-
/** Sub-units (parquet files, rollup JSONs) completed. */
|
|
34
|
-
filesAttached: number
|
|
35
|
-
/** Sub-units expected; 0 while unknown. */
|
|
36
|
-
filesTotal: number
|
|
37
|
-
startedAt: number
|
|
38
|
-
endedAt?: number
|
|
39
|
-
error?: string
|
|
40
|
-
source?: 'duckdb' | 'rollup' | string
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface GscAnalyticsContext {
|
|
44
|
-
sites: Ref<SiteListItem[] | null>
|
|
45
|
-
sitesLoading: Readonly<Ref<boolean>>
|
|
46
|
-
sitesError: Readonly<Ref<Error | null>>
|
|
47
|
-
refreshSites: () => Promise<void>
|
|
48
|
-
progress: Readonly<Ref<Record<string, SiteLoadProgress>>>
|
|
49
|
-
/** Escape hatch for demos/tests — write raw progress entries. */
|
|
50
|
-
patchProgress: (siteId: string, patch: Partial<SiteLoadProgress>) => void
|
|
51
|
-
/** Escape hatch for demos/tests — clear progress map (all or one site). */
|
|
52
|
-
clearProgress: (siteId?: string) => void
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Factory exposed so the layer's Nuxt plugin can provide a single app-wide
|
|
56
|
-
// context via `provide: { gscAnalytics: createGscAnalyticsContext() }`.
|
|
57
|
-
export function createGscAnalyticsContext(): GscAnalyticsContext {
|
|
58
|
-
return createContext()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Resolve the shared context. The layer's plugin guarantees this is provided. */
|
|
62
|
-
export function useGscAnalyticsContext(): GscAnalyticsContext {
|
|
63
|
-
return useNuxtApp().$gscAnalytics
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Read-only sites list + refresh. */
|
|
67
|
-
export function useGscSites(): {
|
|
68
|
-
sites: Ref<SiteListItem[] | null>
|
|
69
|
-
loading: Readonly<Ref<boolean>>
|
|
70
|
-
error: Readonly<Ref<Error | null>>
|
|
71
|
-
refresh: () => Promise<void>
|
|
72
|
-
} {
|
|
73
|
-
const { sites, sitesLoading, sitesError, refreshSites } = useGscAnalyticsContext()
|
|
74
|
-
return { sites, loading: sitesLoading, error: sitesError, refresh: refreshSites }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Derived site record for a given id. Non-mutating; returns `null` until the list resolves. */
|
|
78
|
-
export function useGscSite(siteId: MaybeRefOrGetter<string | null | undefined>): Ref<SiteRecord | null> {
|
|
79
|
-
const { sites } = useGscAnalyticsContext()
|
|
80
|
-
return computed(() => {
|
|
81
|
-
const id = toValue(siteId)
|
|
82
|
-
if (!id)
|
|
83
|
-
return null
|
|
84
|
-
const found = sites.value?.find((s: SiteListItem) => s.id === id)
|
|
85
|
-
if (found)
|
|
86
|
-
return toSiteRecord(found)
|
|
87
|
-
// List not yet loaded — stamp a minimal record so downstream queries can
|
|
88
|
-
// dispatch against the id. Gets upgraded when sites resolves.
|
|
89
|
-
return {
|
|
90
|
-
id,
|
|
91
|
-
label: id,
|
|
92
|
-
hostname: id,
|
|
93
|
-
propertyType: 'url-prefix',
|
|
94
|
-
siteUrl: id,
|
|
95
|
-
readBackend: 'r2',
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function toSiteRecord(s: SiteListItem): SiteRecord {
|
|
101
|
-
return {
|
|
102
|
-
...s,
|
|
103
|
-
siteUrl: s.label,
|
|
104
|
-
readBackend: s.readBackend ?? 'r2',
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Read-only boot-progress map for UI indicators. */
|
|
109
|
-
export function useGscBootProgress(): {
|
|
110
|
-
progress: Readonly<Ref<Record<string, SiteLoadProgress>>>
|
|
111
|
-
entries: ComputedRef<SiteLoadProgress[]>
|
|
112
|
-
allReady: ComputedRef<boolean>
|
|
113
|
-
anyActive: ComputedRef<boolean>
|
|
114
|
-
} {
|
|
115
|
-
const { progress } = useGscAnalyticsContext()
|
|
116
|
-
const entries = computed<SiteLoadProgress[]>(() => {
|
|
117
|
-
const all = Object.values(progress.value) as SiteLoadProgress[]
|
|
118
|
-
return all.sort((a, b) => a.startedAt - b.startedAt)
|
|
119
|
-
})
|
|
120
|
-
const allReady = computed(() =>
|
|
121
|
-
entries.value.length > 0 && entries.value.every((s: SiteLoadProgress) => s.stage === 'ready'),
|
|
122
|
-
)
|
|
123
|
-
const anyActive = computed(() =>
|
|
124
|
-
entries.value.some((s: SiteLoadProgress) => s.stage !== 'ready' && s.stage !== 'idle' && s.stage !== 'error'),
|
|
125
|
-
)
|
|
126
|
-
return { progress, entries, allReady, anyActive }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function createContext(): GscAnalyticsContext {
|
|
130
|
-
const client = useGscAnalyticsClient()
|
|
131
|
-
const sites = ref<SiteListItem[] | null>(null)
|
|
132
|
-
const sitesLoading = ref(false)
|
|
133
|
-
const sitesError = ref<Error | null>(null)
|
|
134
|
-
const progress = ref<Record<string, SiteLoadProgress>>({})
|
|
135
|
-
|
|
136
|
-
async function refreshSites(): Promise<void> {
|
|
137
|
-
sitesLoading.value = true
|
|
138
|
-
sitesError.value = null
|
|
139
|
-
sites.value = await client.listSites().catch((err: unknown) => {
|
|
140
|
-
sitesError.value = err instanceof Error ? err : new Error(String(err))
|
|
141
|
-
return null
|
|
142
|
-
})
|
|
143
|
-
sitesLoading.value = false
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function patchProgress(siteId: string, patch: Partial<SiteLoadProgress>): void {
|
|
147
|
-
const prev = progress.value[siteId]
|
|
148
|
-
progress.value = {
|
|
149
|
-
...progress.value,
|
|
150
|
-
[siteId]: {
|
|
151
|
-
siteId,
|
|
152
|
-
stage: 'idle',
|
|
153
|
-
filesAttached: 0,
|
|
154
|
-
filesTotal: 0,
|
|
155
|
-
startedAt: prev?.startedAt ?? Date.now(),
|
|
156
|
-
...prev,
|
|
157
|
-
...patch,
|
|
158
|
-
},
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function clearProgress(siteId?: string): void {
|
|
163
|
-
if (!siteId) {
|
|
164
|
-
progress.value = {}
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
const next = { ...progress.value }
|
|
168
|
-
delete next[siteId]
|
|
169
|
-
progress.value = next
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Lazy-load: defer refreshSites until something actually reads `sites`.
|
|
173
|
-
// Eager kick-off used to race host plugins that set auth via `setGscAuth`;
|
|
174
|
-
// first call would 401 and the rest of the session would silently degrade.
|
|
175
|
-
// Triggering on first read still needs to wait until hydration is done:
|
|
176
|
-
// `refreshSites()` flips `sitesLoading` synchronously, and doing that during
|
|
177
|
-
// the client's first render makes the sidebar disagree with SSR markup.
|
|
178
|
-
let kickedOff = false
|
|
179
|
-
function maybeKickOff(): void {
|
|
180
|
-
if (kickedOff || !import.meta.client)
|
|
181
|
-
return
|
|
182
|
-
kickedOff = true
|
|
183
|
-
const start = (): void => {
|
|
184
|
-
void refreshSites()
|
|
185
|
-
}
|
|
186
|
-
const nuxtApp = useNuxtApp()
|
|
187
|
-
if (nuxtApp.isHydrating)
|
|
188
|
-
onNuxtReady(start)
|
|
189
|
-
else
|
|
190
|
-
queueMicrotask(start)
|
|
191
|
-
}
|
|
192
|
-
const lazySites = computed<SiteListItem[] | null>(() => {
|
|
193
|
-
maybeKickOff()
|
|
194
|
-
return sites.value
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
sites: lazySites as Readonly<Ref<SiteListItem[] | null>> as unknown as Ref<SiteListItem[] | null>,
|
|
199
|
-
sitesLoading: sitesLoading as Readonly<Ref<boolean>>,
|
|
200
|
-
sitesError: sitesError as Readonly<Ref<Error | null>>,
|
|
201
|
-
refreshSites,
|
|
202
|
-
progress: progress as Readonly<Ref<Record<string, SiteLoadProgress>>>,
|
|
203
|
-
patchProgress,
|
|
204
|
-
clearProgress,
|
|
205
|
-
}
|
|
206
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Reader for the layer's `AnalyticsClient`. Built once per NuxtApp by the
|
|
2
|
-
// layer plugin and provided as `$gscAnalyticsClient`. Hosts override by
|
|
3
|
-
// providing their own from a later plugin (e.g. to swap the SDK transport).
|
|
4
|
-
|
|
5
|
-
import type { AnalyticsClient } from '@gscdump/sdk'
|
|
6
|
-
|
|
7
|
-
export function useGscAnalyticsClient(): AnalyticsClient {
|
|
8
|
-
return useNuxtApp().$gscAnalyticsClient as AnalyticsClient
|
|
9
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Typed reader for `runtimeConfig.public.analytics`. Single accessor so leaf
|
|
2
|
-
// composables stop re-typing the cast and silently drift when keys are added.
|
|
3
|
-
|
|
4
|
-
import type { GscAnalyticsRuntimeConfig } from '../../types'
|
|
5
|
-
|
|
6
|
-
export function useGscAnalyticsConfig(): GscAnalyticsRuntimeConfig {
|
|
7
|
-
return useRuntimeConfig().public.analytics as unknown as GscAnalyticsRuntimeConfig
|
|
8
|
-
}
|