@gscdump/nuxt 0.22.1 → 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.
Files changed (65) hide show
  1. package/README.md +10 -125
  2. package/dist/module.d.mts +22 -0
  3. package/dist/module.mjs +30 -0
  4. package/dist/runtime-config.d.mts +12 -0
  5. package/dist/runtime-config.mjs +27 -0
  6. package/package.json +25 -44
  7. package/app/assets/css/main.css +0 -2
  8. package/app/components/GscAnalyzerPanel.vue +0 -94
  9. package/app/components/GscAnonymizationBanner.vue +0 -46
  10. package/app/components/GscBootProgress.vue +0 -297
  11. package/app/components/GscCommandPalette.vue +0 -77
  12. package/app/components/GscDashboardPage.vue +0 -26
  13. package/app/components/GscEngineBadge.vue +0 -28
  14. package/app/components/GscHero.vue +0 -215
  15. package/app/components/GscLazyTopResult.vue +0 -45
  16. package/app/components/GscPerformanceChart.vue +0 -532
  17. package/app/components/GscPositionDistributionChart.vue +0 -63
  18. package/app/components/GscQueryLabel.vue +0 -63
  19. package/app/components/GscSitePageHeader.vue +0 -40
  20. package/app/components/GscSourceDebugPanel.vue +0 -195
  21. package/app/components/GscSyncStatusCell.vue +0 -54
  22. package/app/components/GscTopRollupCard.vue +0 -90
  23. package/app/composables/_useGscBackfill.ts +0 -111
  24. package/app/composables/_useGscQueryDispatcher.ts +0 -123
  25. package/app/composables/_useGscResource.ts +0 -114
  26. package/app/composables/_useGscSharedSiteResource.ts +0 -136
  27. package/app/composables/useGscAnalytics.ts +0 -205
  28. package/app/composables/useGscAnalyticsClient.ts +0 -9
  29. package/app/composables/useGscAnalyticsConfig.ts +0 -8
  30. package/app/composables/useGscAnalyticsSourceInfo.ts +0 -159
  31. package/app/composables/useGscAnalyzer.ts +0 -452
  32. package/app/composables/useGscAnalyzerBatch.ts +0 -106
  33. package/app/composables/useGscAnalyzerDefs.ts +0 -118
  34. package/app/composables/useGscAuth.ts +0 -115
  35. package/app/composables/useGscConsoleUrl.ts +0 -45
  36. package/app/composables/useGscCountries.ts +0 -47
  37. package/app/composables/useGscCurrentSite.ts +0 -15
  38. package/app/composables/useGscInspectionHistory.ts +0 -42
  39. package/app/composables/useGscInspections.ts +0 -52
  40. package/app/composables/useGscPanelContext.ts +0 -31
  41. package/app/composables/useGscParquetTable.ts +0 -198
  42. package/app/composables/useGscPeriod.ts +0 -243
  43. package/app/composables/useGscQuery.ts +0 -303
  44. package/app/composables/useGscQueryDispatcher.ts +0 -14
  45. package/app/composables/useGscRequestIndexingInspect.ts +0 -20
  46. package/app/composables/useGscResource.ts +0 -11
  47. package/app/composables/useGscRollup.ts +0 -252
  48. package/app/composables/useGscSearchAppearance.ts +0 -47
  49. package/app/composables/useGscSiteAnalyzer.ts +0 -500
  50. package/app/composables/useGscSitemapHistory.ts +0 -41
  51. package/app/composables/useGscSitemaps.ts +0 -45
  52. package/app/composables/useGscTableState.ts +0 -119
  53. package/app/plugins/analytics.ts +0 -57
  54. package/app/utils/anonymization.ts +0 -24
  55. package/app/utils/country-names.ts +0 -56
  56. package/app/utils/duckdb-wasm.ts +0 -166
  57. package/app/utils/gsc-constants.ts +0 -10
  58. package/app/utils/gsc-error.ts +0 -58
  59. package/app/utils/gsc-fetch.ts +0 -94
  60. package/app/utils/gsc-filters.ts +0 -32
  61. package/app/utils/gsc-rows.ts +0 -72
  62. package/app/utils/position.ts +0 -7
  63. package/module.ts +0 -81
  64. package/nuxt.config.ts +0 -41
  65. package/types.ts +0 -119
@@ -1,119 +0,0 @@
1
- // Reactive table state — search, pagination, sort, filter — with optional
2
- // URL sync. One source of truth across a page so deep-links and
3
- // back/forward navigation restore exactly what the user was looking at.
4
-
5
- export interface GscSortState {
6
- column: string
7
- direction: 'asc' | 'desc'
8
- }
9
-
10
- export interface UseGscTableStateOptions<TFilter = Record<string, string>> {
11
- /** Sync state into the URL query string. Default `true`. */
12
- syncUrl?: boolean
13
- /** Prefix for query keys (avoids collisions when multiple tables share a page). */
14
- prefix?: string
15
- defaultPage?: number
16
- defaultPageSize?: number
17
- defaultQ?: string
18
- defaultSort?: GscSortState | null
19
- defaultFilter?: TFilter
20
- }
21
-
22
- export interface UseGscTableStateReturn<TFilter = Record<string, string>> {
23
- q: Ref<string>
24
- page: Ref<number>
25
- pageSize: Ref<number>
26
- sort: Ref<GscSortState | null>
27
- filter: Ref<TFilter>
28
- reset: () => void
29
- /** Toggle a column's sort: desc → asc → off. */
30
- toggleSort: (column: string) => void
31
- }
32
-
33
- export function useGscTableState<TFilter extends Record<string, any> = Record<string, string>>(
34
- opts: UseGscTableStateOptions<TFilter> = {},
35
- ): UseGscTableStateReturn<TFilter> {
36
- const syncUrl = opts.syncUrl ?? true
37
- const prefix = opts.prefix ?? ''
38
- const k = (name: string): string => (prefix ? `${prefix}_${name}` : name)
39
-
40
- const defaultPage = opts.defaultPage ?? 1
41
- const defaultPageSize = opts.defaultPageSize ?? 25
42
- const defaultQ = opts.defaultQ ?? ''
43
- const defaultSort = opts.defaultSort ?? null
44
- const defaultFilter = (opts.defaultFilter ?? {}) as TFilter
45
-
46
- const route = syncUrl ? useRoute() : null
47
- const router = syncUrl ? useRouter() : null
48
-
49
- function readQuery<T>(key: string, fallback: T, parse: (raw: string) => T): T {
50
- if (!route)
51
- return fallback
52
- const raw = route.query[k(key)]
53
- if (raw == null)
54
- return fallback
55
- return parse(Array.isArray(raw) ? (raw[0] ?? '') : String(raw))
56
- }
57
-
58
- function writeQuery(key: string, value: string | null): void {
59
- if (!route || !router)
60
- return
61
- const query = { ...route.query }
62
- const fullKey = k(key)
63
- if (value == null || value === '')
64
- delete query[fullKey]
65
- else
66
- query[fullKey] = value
67
- void router.replace({ query })
68
- }
69
-
70
- const q = ref(readQuery('q', defaultQ, s => s))
71
- const page = ref(readQuery('page', defaultPage, s => Number(s) || defaultPage))
72
- const pageSize = ref(readQuery('pageSize', defaultPageSize, s => Number(s) || defaultPageSize))
73
- const sort = ref<GscSortState | null>(readQuery('sort', defaultSort, parseSort))
74
- const filter = ref(defaultFilter) as Ref<TFilter>
75
-
76
- if (syncUrl) {
77
- watch(q, v => writeQuery('q', v || null))
78
- watch(page, v => writeQuery('page', v === defaultPage ? null : String(v)))
79
- watch(pageSize, v => writeQuery('pageSize', v === defaultPageSize ? null : String(v)))
80
- watch(sort, v => writeQuery('sort', serializeSort(v)))
81
- }
82
-
83
- // Reset page when q/filter changes (standard table UX).
84
- watch([q, filter], () => {
85
- page.value = defaultPage
86
- }, { deep: true })
87
-
88
- function reset(): void {
89
- q.value = defaultQ
90
- page.value = defaultPage
91
- pageSize.value = defaultPageSize
92
- sort.value = defaultSort
93
- filter.value = defaultFilter
94
- }
95
-
96
- function toggleSort(column: string): void {
97
- const cur = sort.value
98
- if (!cur || cur.column !== column) {
99
- sort.value = { column, direction: 'desc' }
100
- return
101
- }
102
- sort.value = cur.direction === 'desc' ? { column, direction: 'asc' } : null
103
- }
104
-
105
- return { q, page, pageSize, sort, filter, reset, toggleSort }
106
- }
107
-
108
- function serializeSort(s: GscSortState | null): string | null {
109
- return s ? `${s.column}:${s.direction}` : null
110
- }
111
-
112
- function parseSort(raw: string): GscSortState | null {
113
- if (!raw)
114
- return null
115
- const [column, direction] = raw.split(':')
116
- if (!column || (direction !== 'asc' && direction !== 'desc'))
117
- return null
118
- return { column, direction }
119
- }
@@ -1,57 +0,0 @@
1
- // Layer-wide DI: analytics context, query dispatcher, fetch instance, and
2
- // analytics client. Each value is defined as a lazy getter on the NuxtApp so
3
- // non-GSC routes pay zero cost and host plugins (e.g. route-gated setGscAuth)
4
- // can run before any of these are materialized. Hosts override by reassigning
5
- // the same `$xxx` key from a later plugin (Nuxt's provide uses configurable
6
- // defineProperty, so the last writer wins).
7
-
8
- import type { AnalyticsClient, AnalyticsFetch } from '@gscdump/sdk'
9
- import type { $Fetch } from 'ofetch'
10
- import type { GscQueryDispatcher } from '../composables/_useGscQueryDispatcher'
11
- import type { GscAnalyticsContext } from '../composables/useGscAnalytics'
12
- import { createAnalyticsClient } from '@gscdump/sdk'
13
- import { createDefaultGscQueryDispatcher } from '../composables/_useGscQueryDispatcher'
14
- import { createGscAnalyticsContext } from '../composables/useGscAnalytics'
15
- import { useGscAnalyticsConfig } from '../composables/useGscAnalyticsConfig'
16
- import { resolveGscAuthHeaders } from '../composables/useGscAuth'
17
- import { createGscFetch } from '../utils/gsc-fetch'
18
-
19
- export default defineNuxtPlugin((nuxtApp) => {
20
- let _ctx: GscAnalyticsContext | undefined
21
- let _dispatcher: GscQueryDispatcher | undefined
22
- let _fetch: $Fetch | undefined
23
- let _client: AnalyticsClient | undefined
24
-
25
- function getFetch(): $Fetch {
26
- if (!_fetch) {
27
- const cfg = useGscAnalyticsConfig()
28
- _fetch = createGscFetch(cfg.apiBase, cfg.toastErrors)
29
- }
30
- return _fetch
31
- }
32
-
33
- const lazy: Record<string, () => unknown> = {
34
- $gscAnalytics: () => _ctx ??= createGscAnalyticsContext(),
35
- $gscQueryDispatcher: () => _dispatcher ??= createDefaultGscQueryDispatcher(),
36
- $gscFetch: () => getFetch(),
37
- $gscAnalyticsClient: () => {
38
- if (!_client) {
39
- const cfg = useGscAnalyticsConfig()
40
- _client = createAnalyticsClient({
41
- apiBase: cfg.apiBase || '',
42
- fetch: getFetch() as unknown as AnalyticsFetch,
43
- headers: () => new Headers(resolveGscAuthHeaders()),
44
- })
45
- }
46
- return _client
47
- },
48
- }
49
-
50
- for (const [key, factory] of Object.entries(lazy)) {
51
- Object.defineProperty(nuxtApp, key, {
52
- get: factory,
53
- configurable: true,
54
- enumerable: true,
55
- })
56
- }
57
- })
@@ -1,24 +0,0 @@
1
- // GSC anonymises queries on a per-day basis; summing query-grained rows
2
- // undercounts page impressions by this amount. Callers render a banner when
3
- // the trailing-28d weighted average crosses a threshold.
4
- //
5
- // Impression-weighted average matches what the user experiences when they
6
- // open the dashboard — daily % alone is noisy on low-traffic days.
7
-
8
- export interface DailyAnonInput {
9
- impressions: number
10
- anonymizedImpressionsPct: number
11
- }
12
-
13
- export function weightedAnonPct(days: readonly DailyAnonInput[] | null | undefined, window = 28): number | null {
14
- if (!days?.length)
15
- return null
16
- const trailing = days.slice(-window)
17
- let totalImpressions = 0
18
- let weighted = 0
19
- for (const d of trailing) {
20
- totalImpressions += d.impressions
21
- weighted += d.impressions * d.anonymizedImpressionsPct
22
- }
23
- return totalImpressions > 0 ? weighted / totalImpressions : null
24
- }
@@ -1,56 +0,0 @@
1
- export const COUNTRY_NAMES: Record<string, string> = {
2
- US: 'United States',
3
- GB: 'United Kingdom',
4
- DE: 'Germany',
5
- FR: 'France',
6
- CA: 'Canada',
7
- AU: 'Australia',
8
- IN: 'India',
9
- BR: 'Brazil',
10
- JP: 'Japan',
11
- IT: 'Italy',
12
- ES: 'Spain',
13
- NL: 'Netherlands',
14
- SE: 'Sweden',
15
- CH: 'Switzerland',
16
- MX: 'Mexico',
17
- KR: 'South Korea',
18
- RU: 'Russia',
19
- PL: 'Poland',
20
- BE: 'Belgium',
21
- AT: 'Austria',
22
- NO: 'Norway',
23
- DK: 'Denmark',
24
- FI: 'Finland',
25
- PT: 'Portugal',
26
- IE: 'Ireland',
27
- NZ: 'New Zealand',
28
- SG: 'Singapore',
29
- HK: 'Hong Kong',
30
- TW: 'Taiwan',
31
- IL: 'Israel',
32
- ZA: 'South Africa',
33
- AR: 'Argentina',
34
- CL: 'Chile',
35
- CO: 'Colombia',
36
- TH: 'Thailand',
37
- PH: 'Philippines',
38
- MY: 'Malaysia',
39
- ID: 'Indonesia',
40
- VN: 'Vietnam',
41
- TR: 'Turkey',
42
- CZ: 'Czech Republic',
43
- RO: 'Romania',
44
- HU: 'Hungary',
45
- GR: 'Greece',
46
- UA: 'Ukraine',
47
- EG: 'Egypt',
48
- NG: 'Nigeria',
49
- KE: 'Kenya',
50
- PK: 'Pakistan',
51
- BD: 'Bangladesh',
52
- AE: 'United Arab Emirates',
53
- SA: 'Saudi Arabia',
54
- }
55
-
56
- export const countryName = (code: string): string => COUNTRY_NAMES[code] || code
@@ -1,166 +0,0 @@
1
- // Shared browser-DuckDB-WASM helpers used by per-site analyzers and the
2
- // home-page multi-site fanout. Both consumers boot the same WASM bundle and
3
- // run the same in-memory parquet attach path; OPFS attach lives in the caller
4
- // because the two lifecycle models (refcounted long-lived vs ephemeral
5
- // per-run) need different handle ownership.
6
-
7
- import type { AsyncDuckDB, AsyncDuckDBConnection } from '@duckdb/duckdb-wasm'
8
- import type { DuckDBWasmBootResult, OpfsAttachedHandle } from '@gscdump/engine-duckdb-wasm'
9
-
10
- /**
11
- * Boot DuckDB-WASM using the layer's configured bundle base. The dynamic
12
- * `import('@gscdump/engine-duckdb-wasm')` keeps the WASM bundle out of the
13
- * SSR build; callers should also gate the call behind `import.meta.client`.
14
- *
15
- * The `filesystem` config disables HEAD-request preflight and forces full
16
- * HTTP reads — Cloudflare Workers strips `Content-Length` on streamed
17
- * responses, which makes DuckDB's default range-request path 404.
18
- */
19
- export async function bootGscDuckDBWasm(bundleBase?: string): Promise<DuckDBWasmBootResult> {
20
- const { bootDuckDBWasm } = await import('@gscdump/engine-duckdb-wasm')
21
- return bootDuckDBWasm({
22
- ...(bundleBase
23
- ? {
24
- bundles: {
25
- mvp: { mainModule: `${bundleBase}/duckdb-mvp.wasm`, mainWorker: `${bundleBase}/duckdb-browser-mvp.worker.js` },
26
- eh: { mainModule: `${bundleBase}/duckdb-eh.wasm`, mainWorker: `${bundleBase}/duckdb-browser-eh.worker.js` },
27
- },
28
- }
29
- : {}),
30
- config: { filesystem: { reliableHeadRequests: false, allowFullHTTPReads: true } },
31
- })
32
- }
33
-
34
- /**
35
- * Process-wide shared boot. The ~800ms WASM boot is the dominant cost on the
36
- * warm path (OPFS cache-hit means downloads are skipped); paying it once per
37
- * session instead of per refresh turns date-range changes / search-type
38
- * swaps into near-instant updates.
39
- *
40
- * Callers that want their own throwaway instance can keep using
41
- * {@link bootGscDuckDBWasm}; those that want session-wide reuse — the home
42
- * page fanout and the per-site analyzer — go through this.
43
- *
44
- * Browser-only. The `bundleBase` is captured from the first call; subsequent
45
- * calls reuse the promise and ignore the argument (it's a deployment-wide
46
- * constant in practice).
47
- */
48
- let sharedBootPromise: Promise<DuckDBWasmBootResult> | null = null
49
- let sharedBootColdMs: number | undefined
50
- export function sharedGscDuckDBWasm(bundleBase?: string): Promise<DuckDBWasmBootResult> {
51
- if (!sharedBootPromise) {
52
- if (typeof performance !== 'undefined')
53
- performance.mark('gsc:duckdb:boot:start')
54
- const start = typeof performance !== 'undefined' ? performance.now() : 0
55
- sharedBootPromise = bootGscDuckDBWasm(bundleBase).then((b) => {
56
- if (typeof performance !== 'undefined') {
57
- sharedBootColdMs = performance.now() - start
58
- performance.mark('gsc:duckdb:boot:end')
59
- }
60
- return b
61
- })
62
- }
63
- else if (typeof performance !== 'undefined') {
64
- performance.mark('gsc:duckdb:boot:reuse')
65
- }
66
- return sharedBootPromise
67
- }
68
-
69
- /** Diagnostics: ms paid for the one-time cold boot, or undefined if not yet booted. */
70
- export function sharedGscDuckDBWasmColdMs(): number | undefined {
71
- return sharedBootColdMs
72
- }
73
-
74
- /**
75
- * Fetch every URL into a `Uint8Array` in parallel. `onFileFetched` ticks
76
- * once per successful download. Separate from `registerParquetView` so
77
- * callers can run fetches outside any connection mutex.
78
- */
79
- export async function fetchParquetBuffers(
80
- urls: readonly string[],
81
- onFileFetched?: () => void,
82
- ): Promise<Uint8Array[]> {
83
- return Promise.all(urls.map(async (url) => {
84
- const r = await fetch(url, { credentials: 'omit' })
85
- if (!r.ok)
86
- throw new Error(`GET ${url} failed: ${r.status}`)
87
- const buf = new Uint8Array(await r.arrayBuffer())
88
- onFileFetched?.()
89
- return buf
90
- }))
91
- }
92
-
93
- /**
94
- * Register pre-fetched parquet buffers as virtual files, then build a single
95
- * view that unions them. `CAST(date AS DATE)` canonicalises the legacy-VARCHAR
96
- * / new-DATE encodings. File-name uniqueness is the caller's job; pass a
97
- * `viewName` that already encodes any site/table salt so concurrent attaches
98
- * across sites don't collide on the DuckDB virtual filesystem. Call under the
99
- * DuckDB-WASM single-connection mutex.
100
- */
101
- export async function registerParquetView(opts: {
102
- db: AsyncDuckDB
103
- conn: AsyncDuckDBConnection
104
- viewName: string
105
- buffers: readonly Uint8Array[]
106
- }): Promise<void> {
107
- const { db, conn, viewName, buffers } = opts
108
- const fileNames = buffers.map((_, i) => `${viewName}_${i}.parquet`)
109
- for (let i = 0; i < buffers.length; i++)
110
- await db.registerFileBuffer(fileNames[i]!, buffers[i]!)
111
- const fileList = fileNames.map(n => `'${n.replace(/'/g, '\'\'')}'`).join(',')
112
- await conn.query(`CREATE OR REPLACE VIEW ${viewName} AS SELECT * REPLACE (CAST(date AS DATE) AS date) FROM read_parquet([${fileList}], union_by_name = true)`)
113
- }
114
-
115
- /**
116
- * Attach a parquet file set into DuckDB-WASM, preferring the OPFS-backed
117
- * path (cache-hit skips download) and falling back to in-memory buffers when
118
- * OPFS is opted out, unavailable, or quota-exhausted.
119
- *
120
- * Returns the OPFS handle when the OPFS path succeeded — caller must keep
121
- * the handle alive and `detach()` on cleanup. Returns `null` when the
122
- * fallback path was used; nothing to detach. `onFileProgress` ticks once per
123
- * downloaded file in either path so the caller can drive a progress UI.
124
- *
125
- * `withDb` wraps the DDL/file-register calls when the caller serialises
126
- * against other DuckDB activity (e.g. a shared connection); pass `undefined`
127
- * when the caller already owns a private connection.
128
- */
129
- export async function attachParquetWithFallback(args: {
130
- db: AsyncDuckDB
131
- conn: AsyncDuckDBConnection
132
- viewName: string
133
- files: readonly { url: string, bytes: number, contentHash: string }[]
134
- version: string
135
- useOpfsCache: boolean
136
- fetchConcurrency?: number
137
- onFileProgress?: () => void
138
- withDb?: <T>(fn: () => Promise<T>) => Promise<T>
139
- }): Promise<OpfsAttachedHandle | null> {
140
- const { db, conn, viewName, files, version, useOpfsCache, onFileProgress } = args
141
- const lock = args.withDb ?? (<T>(fn: () => Promise<T>) => fn())
142
- const fetchConcurrency = args.fetchConcurrency ?? Math.min(files.length, 4)
143
-
144
- if (useOpfsCache) {
145
- const { attachOpfsParquetTables } = await import('@gscdump/engine-duckdb-wasm')
146
- const handle = await lock(() => attachOpfsParquetTables({
147
- db,
148
- conn,
149
- tables: [{
150
- table: viewName,
151
- files: files.map(f => ({ url: f.url, bytes: f.bytes, contentHash: f.contentHash })),
152
- }],
153
- version,
154
- fetchConcurrency,
155
- onFileProgress,
156
- }))
157
- if (!handle.degradedTables.includes(viewName))
158
- return handle
159
- // OPFS quota / failure — fall through to buffer path. Caller's progress
160
- // counter restarts; both paths report one tick per fetched file.
161
- }
162
-
163
- const buffers = await fetchParquetBuffers(files.map(f => f.url), onFileProgress)
164
- await lock(() => registerParquetView({ db, conn, viewName, buffers }))
165
- return null
166
- }
@@ -1,10 +0,0 @@
1
- // Constants shared across GSC-shaped components. Kept as layer exports so
2
- // consumers import from `#imports` like everything else.
3
-
4
- /**
5
- * Google Search Console data is considered "unstable" for this many days from
6
- * today (PST). Rows within the window may still shift as GSC finalizes clicks/
7
- * impressions; charts render those points under a dimmed / striped overlay so
8
- * users don't misread last-day dips as trends.
9
- */
10
- export const GSC_STABLE_LATENCY_DAYS = 3
@@ -1,58 +0,0 @@
1
- // Error classification shared by the layer's fetch wrappers and query hooks.
2
- // Returns a structured status the caller maps to UI (toast, banner, retry, …).
3
-
4
- export type GscErrorStatus
5
- = | 'auth-missing'
6
- | 'rate-limited'
7
- | 'network'
8
- | 'error'
9
-
10
- export interface GscClassifiedError {
11
- status: GscErrorStatus
12
- /** HTTP status code if the error came from a response, otherwise undefined. */
13
- code?: number
14
- /** Best-effort human message: server-supplied `message`, then `data.message`, then the error's own message. */
15
- message?: string
16
- /** Seconds the server suggested waiting (429 with `retryAfter` payload). */
17
- retryAfter?: number
18
- }
19
-
20
- export function classifyGscError(e: unknown): GscClassifiedError {
21
- const code = (e as { statusCode?: number, status?: number })?.statusCode
22
- ?? (e as { status?: number })?.status
23
- const message = extractMessage(e)
24
-
25
- if (code === 401 || code === 403)
26
- return { status: 'auth-missing', code, message }
27
- if (code === 429) {
28
- const retry = (e as { data?: { retryAfter?: number } })?.data?.retryAfter
29
- return {
30
- status: 'rate-limited',
31
- code,
32
- message,
33
- retryAfter: typeof retry === 'number' ? retry : undefined,
34
- }
35
- }
36
- // No code => didn't make it to the server (DNS, CORS, offline, abort).
37
- if (code == null && !isAbort(e))
38
- return { status: 'network', message }
39
-
40
- return { status: 'error', code, message }
41
- }
42
-
43
- function extractMessage(e: unknown): string | undefined {
44
- if (!e || typeof e !== 'object')
45
- return undefined
46
- const data = (e as { data?: unknown }).data
47
- if (data && typeof data === 'object') {
48
- const m = (data as { message?: unknown, error?: unknown }).message ?? (data as { error?: unknown }).error
49
- if (typeof m === 'string')
50
- return m
51
- }
52
- const m = (e as { message?: unknown }).message
53
- return typeof m === 'string' ? m : undefined
54
- }
55
-
56
- function isAbort(e: unknown): boolean {
57
- return (e as { name?: string })?.name === 'AbortError'
58
- }
@@ -1,94 +0,0 @@
1
- // Configured $fetch instance for the layer's `/api/__gsc/*` calls.
2
- //
3
- // When `runtimeConfig.public.analytics.apiBase` is set, requests are routed
4
- // to that origin (e.g. `https://gscdump.com`). Empty = same-origin, the
5
- // default for self-hosted deployments (gscdump.com itself).
6
- //
7
- // Auth: resolved via `useGscAuth` (host calls `setGscAuth` from a `'pre'`
8
- // plugin). `apiKey` populates `x-api-key`; `headers` carries any additional
9
- // auth headers. When no auth is wired, the layer falls back to cookies for
10
- // same-site / cookie-credentials deployments.
11
- //
12
- // Built once per NuxtApp by the layer plugin and provided as `$gscFetch`.
13
- // `useGscFetch()` is a thin reader; hosts override by providing their own
14
- // `$gscFetch` from a later plugin.
15
-
16
- import type { $Fetch } from 'ofetch'
17
- import { readGscAuth, resolveGscAuthHeaders } from '../composables/useGscAuth'
18
- import { classifyGscError } from './gsc-error'
19
-
20
- const TOAST_DEDUP_MS = 5000
21
-
22
- interface ToastDedup {
23
- recent: Map<string, number>
24
- }
25
-
26
- function shouldEmitToast(dedup: ToastDedup, key: string): boolean {
27
- const now = Date.now()
28
- const last = dedup.recent.get(key) ?? 0
29
- if (now - last < TOAST_DEDUP_MS)
30
- return false
31
- dedup.recent.set(key, now)
32
- if (dedup.recent.size > 32) {
33
- const oldest = [...dedup.recent.entries()].sort((a, b) => a[1] - b[1])[0]
34
- if (oldest)
35
- dedup.recent.delete(oldest[0])
36
- }
37
- return true
38
- }
39
-
40
- function defaultToastTitle(status: string): string {
41
- switch (status) {
42
- case 'auth-missing': return 'Sign in required'
43
- case 'rate-limited': return 'Rate limit exceeded'
44
- case 'network': return 'Network error'
45
- default: return 'Request failed'
46
- }
47
- }
48
-
49
- export function createGscFetch(cfgApiBase: string, toastErrors: boolean): $Fetch {
50
- const dedup: ToastDedup = { recent: new Map() }
51
- return $fetch.create({
52
- onRequest: ({ options }) => {
53
- const auth = readGscAuth()
54
- const apiBase = cfgApiBase ?? ''
55
- const authHeaders = resolveGscAuthHeaders(auth)
56
-
57
- const merged = new Headers(options.headers as HeadersInit | undefined)
58
- for (const [k, v] of Object.entries(authHeaders))
59
- merged.set(k, v)
60
- options.headers = merged
61
-
62
- if (apiBase && typeof options.baseURL !== 'string')
63
- options.baseURL = apiBase
64
-
65
- const hasAuth = Object.keys(authHeaders).length > 0
66
- if (hasAuth) {
67
- if (!options.credentials)
68
- options.credentials = 'omit'
69
- }
70
- else if (apiBase && !options.credentials) {
71
- // No explicit auth — fall back to cookies for cross-origin sessions.
72
- options.credentials = 'include'
73
- }
74
- },
75
- onResponseError: (ctx) => {
76
- if (!toastErrors || !import.meta.client)
77
- return
78
- const c = classifyGscError(ctx.error ?? ctx.response)
79
- const key = `${c.status}:${c.code ?? '-'}:${c.message ?? ''}`
80
- if (!shouldEmitToast(dedup, key))
81
- return
82
- const toast = useToast()
83
- toast.add({
84
- title: defaultToastTitle(c.status),
85
- description: c.message,
86
- color: c.status === 'rate-limited' ? 'warning' : 'error',
87
- })
88
- },
89
- }) as unknown as $Fetch
90
- }
91
-
92
- export function useGscFetch(): $Fetch {
93
- return useNuxtApp().$gscFetch as $Fetch
94
- }
@@ -1,32 +0,0 @@
1
- // Re-exports the typed query operators from `gscdump/query` so layer
2
- // consumers can compose filters without an extra import line. Auto-imported
3
- // like the layer's other utils.
4
- //
5
- // Usage:
6
- // const filter = and(
7
- // between(date, range.start, range.end),
8
- // eq(country, 'usa'),
9
- // )
10
- //
11
- // Replaces nuxtseo's wire-format `dateFilter` / `andFilter` helpers — both
12
- // formats coerce to the same shape server-side, so the typed primitives
13
- // are strictly better.
14
-
15
- export {
16
- and,
17
- between,
18
- contains,
19
- eq,
20
- gt,
21
- gte,
22
- inArray,
23
- like,
24
- lt,
25
- lte,
26
- ne,
27
- not,
28
- notRegex,
29
- or,
30
- regex,
31
- topLevel,
32
- } from 'gscdump/query'