@gscdump/nuxt 0.21.3 → 0.22.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 +15 -18
- package/app/composables/_useGscQueryDispatcher.ts +5 -4
- package/app/composables/useGscAnalytics.ts +11 -3
- package/app/composables/useGscAnalyzerBatch.ts +2 -2
- package/app/composables/useGscPanelContext.ts +9 -2
- package/app/composables/useGscParquetTable.ts +3 -4
- package/app/composables/useGscQueryDispatcher.ts +14 -122
- package/app/composables/useGscResource.ts +11 -114
- package/app/composables/useGscSiteAnalyzer.ts +500 -0
- package/app/utils/duckdb-wasm.ts +166 -0
- package/nuxt.config.ts +5 -0
- package/package.json +9 -8
- package/types.ts +8 -3
- package/app/composables/useGscEngine.ts +0 -16
- package/app/composables/useGscRollupTable.ts +0 -78
- package/app/composables/useGscRowQuery.ts +0 -56
- package/app/composables/useGscSnapshotAnalyzer.contract.ts +0 -150
- package/app/composables/useGscSnapshotAnalyzer.routing.ts +0 -87
- package/app/composables/useGscSnapshotAnalyzer.ts +0 -559
- package/app/utils/setup-gsc-fetch-auth.ts +0 -62
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/nuxt",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.22.1",
|
|
5
5
|
"description": "Nuxt layer: GSC analytics UI + DuckDB-WASM integration. Frontend-only; server primitives live in @gscdump/analysis, @gscdump/engine-sqlite, @gscdump/cloudflare, and host apps.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -47,20 +47,21 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@duckdb/duckdb-wasm": "^1.32.0",
|
|
50
|
-
"@nuxt/ui": "^4.
|
|
50
|
+
"@nuxt/ui": "^4.8.0",
|
|
51
51
|
"@unovis/ts": "^1.6.5",
|
|
52
52
|
"@unovis/vue": "^1.6.5",
|
|
53
53
|
"@vueuse/core": "^14.3.0",
|
|
54
54
|
"@vueuse/nuxt": "^14.3.0",
|
|
55
55
|
"defu": "^6.1.7",
|
|
56
|
+
"nuxt-use-query": "^0.1.0",
|
|
56
57
|
"ofetch": "^1.5.1",
|
|
57
58
|
"tailwindcss": "^4.3.0",
|
|
58
|
-
"@gscdump/analysis": "0.
|
|
59
|
-
"@gscdump/contracts": "0.
|
|
60
|
-
"@gscdump/engine": "0.
|
|
61
|
-
"@gscdump/engine-duckdb-wasm": "0.
|
|
62
|
-
"gscdump": "0.
|
|
63
|
-
"
|
|
59
|
+
"@gscdump/analysis": "0.22.1",
|
|
60
|
+
"@gscdump/contracts": "0.22.1",
|
|
61
|
+
"@gscdump/engine": "0.22.1",
|
|
62
|
+
"@gscdump/engine-duckdb-wasm": "0.22.1",
|
|
63
|
+
"@gscdump/sdk": "0.22.1",
|
|
64
|
+
"gscdump": "0.22.1"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@nuxt/kit": "^4.4.6",
|
package/types.ts
CHANGED
|
@@ -46,6 +46,7 @@ declare module '#app' {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type { GscAnalyzerInstance, GscAnalyzerTimings } from './app/composables/useGscAnalyzer'
|
|
49
50
|
export type {
|
|
50
51
|
GscAnalyzerBatchEntry,
|
|
51
52
|
GscAnalyzerBatchRunner,
|
|
@@ -65,15 +66,14 @@ export type {
|
|
|
65
66
|
GscAnalyzerPanelSpec,
|
|
66
67
|
GscAnalyzerStatTile,
|
|
67
68
|
} from './app/composables/useGscAnalyzerDefs'
|
|
68
|
-
|
|
69
69
|
export { gscPanelRunnerKey, useGscPanelRunner } from './app/composables/useGscPanelContext'
|
|
70
70
|
|
|
71
71
|
export type { GscPanelRunnerContext } from './app/composables/useGscPanelContext'
|
|
72
|
+
|
|
72
73
|
export type {
|
|
73
74
|
UseGscParquetTableOptions,
|
|
74
75
|
UseGscParquetTableReturn,
|
|
75
76
|
} from './app/composables/useGscParquetTable'
|
|
76
|
-
|
|
77
77
|
export type {
|
|
78
78
|
GscBackfillRunner,
|
|
79
79
|
GscQueryDecision,
|
|
@@ -85,7 +85,12 @@ export type {
|
|
|
85
85
|
UseGscQueryReturn,
|
|
86
86
|
} from './app/composables/useGscQuery'
|
|
87
87
|
|
|
88
|
-
export type {
|
|
88
|
+
export type {
|
|
89
|
+
GscFactTable,
|
|
90
|
+
GscTableStage,
|
|
91
|
+
GscTableStatus,
|
|
92
|
+
UseGscSiteAnalyzerReturn,
|
|
93
|
+
} from './app/composables/useGscSiteAnalyzer'
|
|
89
94
|
|
|
90
95
|
export type {
|
|
91
96
|
AnalysisSourcesResponse,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// App-wide engine override for `useGscQuery`. SSR-safe via `useState`.
|
|
2
|
-
//
|
|
3
|
-
// Resolution priority lives in `useGscQueryDispatcher.pickEngine`:
|
|
4
|
-
// per-call `opts.engine` → `useGscEngine().value` → `runtimeConfig.public.
|
|
5
|
-
// analytics.defaultEngine` → `'auto'`.
|
|
6
|
-
//
|
|
7
|
-
// Set the value once on the consumer side (e.g. from your auth plugin's
|
|
8
|
-
// `onReady` hook reading a per-user feature flag) and every query inherits.
|
|
9
|
-
|
|
10
|
-
import type { GscQueryEngine } from './useGscQuery'
|
|
11
|
-
|
|
12
|
-
const STATE_KEY = 'gscdump:engine'
|
|
13
|
-
|
|
14
|
-
export function useGscEngine(): Ref<GscQueryEngine | null> {
|
|
15
|
-
return useState<GscQueryEngine | null>(STATE_KEY, () => null)
|
|
16
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// Top-N rollup table state for list pages.
|
|
2
|
-
//
|
|
3
|
-
// Owns the canonical wiring previously duplicated across consumer apps:
|
|
4
|
-
// - shared `useGscPeriod` + windowed range
|
|
5
|
-
// - `useGscRollup` fetch
|
|
6
|
-
// - URL-synced search/sort via `useGscTableState`
|
|
7
|
-
// - debounced fuzzy filter on one row field
|
|
8
|
-
// - position-aware sort (sort key 'position' invokes `positionFor`)
|
|
9
|
-
// - top-N slice (default 100)
|
|
10
|
-
//
|
|
11
|
-
// Row shape contract: `impressions` + `sum_position` so `positionFor` works
|
|
12
|
-
// without a caller-supplied accessor.
|
|
13
|
-
|
|
14
|
-
import type { MaybeRefOrGetter, Ref } from '@vue/runtime-core'
|
|
15
|
-
|
|
16
|
-
interface SortState { column: string, direction: 'asc' | 'desc' }
|
|
17
|
-
|
|
18
|
-
export interface UseGscRollupTableOptions<TRow> {
|
|
19
|
-
siteId: MaybeRefOrGetter<string>
|
|
20
|
-
rollupKey: string
|
|
21
|
-
filterField: keyof TRow & string
|
|
22
|
-
defaultSort?: SortState
|
|
23
|
-
limit?: number
|
|
24
|
-
debounceMs?: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// eslint-disable-next-line ts/explicit-function-return-type -- complex generic inference; explicit type would diverge as composables evolve
|
|
28
|
-
export function useGscRollupTable<
|
|
29
|
-
TRow extends { impressions: number, sum_position: number },
|
|
30
|
-
>(opts: UseGscRollupTableOptions<TRow>) {
|
|
31
|
-
const { period, compareMode, stableData, range } = useGscPeriod()
|
|
32
|
-
const windowRange = computed(() => ({ start: range.value.start, end: range.value.end }))
|
|
33
|
-
|
|
34
|
-
const { data: payload, loading } = useGscRollup<TRow[]>(
|
|
35
|
-
opts.siteId,
|
|
36
|
-
opts.rollupKey,
|
|
37
|
-
{ range: windowRange },
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
const { q, sort, toggleSort } = useGscTableState({ defaultSort: opts.defaultSort })
|
|
41
|
-
|
|
42
|
-
const searchDebounced = refDebounced<string>(q, opts.debounceMs ?? 150)
|
|
43
|
-
|
|
44
|
-
const limit = opts.limit ?? 100
|
|
45
|
-
const rows = computed<TRow[]>(() => {
|
|
46
|
-
const needle = searchDebounced.value.trim().toLowerCase()
|
|
47
|
-
const source = (payload.value ?? []).slice()
|
|
48
|
-
const filtered = needle
|
|
49
|
-
? source.filter((r: TRow) => {
|
|
50
|
-
const v = (r as Record<string, unknown>)[opts.filterField]
|
|
51
|
-
return typeof v === 'string' && v.toLowerCase().includes(needle)
|
|
52
|
-
})
|
|
53
|
-
: source
|
|
54
|
-
const s = sort.value
|
|
55
|
-
if (s) {
|
|
56
|
-
const dir = s.direction === 'desc' ? -1 : 1
|
|
57
|
-
filtered.sort((a: TRow, b: TRow) => {
|
|
58
|
-
const av = s.column === 'position' ? positionFor(a) : ((a as Record<string, unknown>)[s.column] as number)
|
|
59
|
-
const bv = s.column === 'position' ? positionFor(b) : ((b as Record<string, unknown>)[s.column] as number)
|
|
60
|
-
return av < bv ? -1 * dir : av > bv ? 1 * dir : 0
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
return filtered.slice(0, limit)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
period,
|
|
68
|
-
compareMode,
|
|
69
|
-
stableData,
|
|
70
|
-
range,
|
|
71
|
-
q,
|
|
72
|
-
sort,
|
|
73
|
-
toggleSort,
|
|
74
|
-
payload: payload as Ref<TRow[] | null>,
|
|
75
|
-
loading,
|
|
76
|
-
rows,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Thin client wrapper over POST /api/__gsc/sites/[siteId]/rows. Takes a
|
|
2
|
-
// BuilderState (or a typed query builder) and returns reactive rows with
|
|
3
|
-
// auto-refetch when the state / site changes.
|
|
4
|
-
//
|
|
5
|
-
// Use for row-level page needs (country breakdowns, top-N with custom
|
|
6
|
-
// filters, per-page drilldowns, …). For analyzer-style transforms, use
|
|
7
|
-
// `useGscQuery` which dispatches via /analyze.
|
|
8
|
-
|
|
9
|
-
import type { QueryRow } from '@gscdump/analysis'
|
|
10
|
-
import type { GscRowQueryMeta, GscRowQueryResponse } from '@gscdump/contracts'
|
|
11
|
-
import type { ComputedRef, Ref } from '@vue/runtime-core'
|
|
12
|
-
import type { BuilderState } from 'gscdump/query'
|
|
13
|
-
import { useGscResource } from './_useGscResource'
|
|
14
|
-
import { useGscAnalyticsClient } from './useGscAnalyticsClient'
|
|
15
|
-
|
|
16
|
-
export type { GscRowQueryMeta, GscRowQueryResponse } from '@gscdump/contracts'
|
|
17
|
-
|
|
18
|
-
export interface UseGscRowQueryOptions {
|
|
19
|
-
site: MaybeRefOrGetter<string | null | undefined>
|
|
20
|
-
state: MaybeRefOrGetter<BuilderState | null | undefined>
|
|
21
|
-
/** Gate the query; when `false` stays idle (useful for lazy tabs). */
|
|
22
|
-
enabled?: MaybeRefOrGetter<boolean>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface UseGscRowQueryReturn<T> {
|
|
26
|
-
rows: ComputedRef<T[]>
|
|
27
|
-
meta: ComputedRef<GscRowQueryMeta | null>
|
|
28
|
-
loading: Ref<boolean>
|
|
29
|
-
error: Ref<Error | null>
|
|
30
|
-
refresh: () => Promise<void>
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function useGscRowQuery<T = QueryRow>(
|
|
34
|
-
opts: UseGscRowQueryOptions,
|
|
35
|
-
): UseGscRowQueryReturn<T> {
|
|
36
|
-
const resource = useGscResource<[string, BuilderState], GscRowQueryResponse<T>>({
|
|
37
|
-
keys: [
|
|
38
|
-
() => (opts.enabled === undefined || toValue(opts.enabled) ? toValue(opts.site) : null),
|
|
39
|
-
// BuilderState is structural; stringify so keys[]-watch picks up shape changes.
|
|
40
|
-
() => {
|
|
41
|
-
const s = toValue(opts.state)
|
|
42
|
-
return s ? JSON.stringify(s) as unknown as BuilderState : null
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
fetcher: site => useGscAnalyticsClient().queryRows<T>(site, toValue(opts.state) as BuilderState),
|
|
46
|
-
isEmpty: r => r.rows.length === 0,
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
rows: computed(() => resource.data.value?.rows ?? []),
|
|
51
|
-
meta: computed(() => resource.data.value?.meta ?? null),
|
|
52
|
-
loading: resource.loading as unknown as Ref<boolean>,
|
|
53
|
-
error: resource.error,
|
|
54
|
-
refresh: resource.refresh,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CONTRACT — consolidated analyzer composable public API (Wave-2, frozen).
|
|
3
|
-
*
|
|
4
|
-
* The single composable that replaces BOTH `useGscAnalyzer.ts` (pkg) and
|
|
5
|
-
* `useBrowserAnalyzer.ts` (app). One OPFS-backed, attach-once, browser-
|
|
6
|
-
* eligibility-aware analyzer.
|
|
7
|
-
*
|
|
8
|
-
* This file is the INTERFACE ONLY — the Wave-2 implementation agent writes
|
|
9
|
-
* `useGscSnapshotAnalyzer.ts` against these types, then deletes the two old
|
|
10
|
-
* composables. Naming follows the package convention (`useGsc*`).
|
|
11
|
-
*
|
|
12
|
-
* Behaviour the types lock in:
|
|
13
|
-
* - ATTACH-ONCE — DuckDB-WASM boots once; OPFS-cached parquet attaches once
|
|
14
|
-
* per `(site, searchType)`. Filter / date-range changes within the attached
|
|
15
|
-
* span re-query, they do NOT re-attach.
|
|
16
|
-
* - OPFS-backed — files resolved via the file-resolution endpoint are cached
|
|
17
|
-
* in OPFS (content-hash verified); steady-state queries are zero-network.
|
|
18
|
-
* - RESULT LRU — identical archetype queries replay from an in-memory LRU.
|
|
19
|
-
* - BROWSER-ELIGIBILITY AWARE — when the file-resolution endpoint returns a
|
|
20
|
-
* server-tail directive for a table, `query()` for that table transparently
|
|
21
|
-
* routes server-side; the consumer does not branch.
|
|
22
|
-
*
|
|
23
|
-
* TYPES ONLY — no composable body.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import type { GscSearchType } from '@gscdump/contracts'
|
|
27
|
-
import type {
|
|
28
|
-
ArchetypeQuery,
|
|
29
|
-
ArchetypeResult,
|
|
30
|
-
ArchetypeResultRow,
|
|
31
|
-
} from '@gscdump/sdk'
|
|
32
|
-
import type { MaybeRefOrGetter, Ref } from 'vue'
|
|
33
|
-
|
|
34
|
-
/** Date window the analyzer is attached over. */
|
|
35
|
-
export interface AnalyzerRange {
|
|
36
|
-
start: string
|
|
37
|
-
end: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Boot / attach lifecycle phase, surfaced for the progress UI. */
|
|
41
|
-
export type AnalyzerPhase
|
|
42
|
-
= | 'idle'
|
|
43
|
-
| 'resolving' // calling the file-resolution endpoint
|
|
44
|
-
| 'booting' // DuckDB-WASM boot
|
|
45
|
-
| 'downloading' // fetching parquet into OPFS
|
|
46
|
-
| 'attaching' // registering OPFS files as DuckDB views
|
|
47
|
-
| 'ready'
|
|
48
|
-
| 'error'
|
|
49
|
-
|
|
50
|
-
/** Progressive-load progress, drives `<GscBootProgress>`. */
|
|
51
|
-
export interface AnalyzerProgress {
|
|
52
|
-
phase: AnalyzerPhase
|
|
53
|
-
/** Files fetched into OPFS so far. */
|
|
54
|
-
filesReady: number
|
|
55
|
-
/** Total files to fetch for the current attach. */
|
|
56
|
-
filesTotal: number
|
|
57
|
-
/** Bytes fetched into OPFS so far. */
|
|
58
|
-
bytesReady: number
|
|
59
|
-
bytesTotal: number
|
|
60
|
-
startedAt?: number
|
|
61
|
-
endedAt?: number
|
|
62
|
-
error?: string
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** OPFS storage health, surfaced so the UI can warn before eviction. */
|
|
66
|
-
export interface AnalyzerStorageState {
|
|
67
|
-
/** `navigator.storage.persist()` result. `false` => eviction-eligible. */
|
|
68
|
-
persisted: boolean
|
|
69
|
-
/** Estimated bytes used by this origin's OPFS. */
|
|
70
|
-
usageBytes?: number
|
|
71
|
-
/** Estimated quota. */
|
|
72
|
-
quotaBytes?: number
|
|
73
|
-
/** True after a `QuotaExceededError` forced a degraded (server-tail) path. */
|
|
74
|
-
degraded: boolean
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Per-table routing the analyzer resolved. `'browser'` tables answer locally
|
|
79
|
-
* from OPFS; `'server'` tables route to the server tail. Consumers may read
|
|
80
|
-
* this to show "deep history served from the cloud" UX.
|
|
81
|
-
*/
|
|
82
|
-
export type AnalyzerTableRouting = Record<string, 'browser' | 'server'>
|
|
83
|
-
|
|
84
|
-
/** Options for `useGscSnapshotAnalyzer`. */
|
|
85
|
-
export interface GscSnapshotAnalyzerOptions {
|
|
86
|
-
/** Max entries in the result LRU. Default implementation-defined. */
|
|
87
|
-
resultCacheSize?: number
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The consolidated analyzer instance. Refs are reactive over the currently
|
|
92
|
-
* bound `(site, searchType, range)` — switching any of them rebinds and the
|
|
93
|
-
* refs track the new attach with no manual mirroring.
|
|
94
|
-
*/
|
|
95
|
-
export interface GscSnapshotAnalyzer {
|
|
96
|
-
/** True once the analyzer can serve `query()` (browser attached OR server-tail ready). */
|
|
97
|
-
ready: Ref<boolean>
|
|
98
|
-
/** True while resolving / booting / attaching. */
|
|
99
|
-
initializing: Ref<boolean>
|
|
100
|
-
error: Ref<Error | null>
|
|
101
|
-
progress: Ref<AnalyzerProgress>
|
|
102
|
-
storage: Ref<AnalyzerStorageState>
|
|
103
|
-
/** Per-table browser vs server routing for the current attach. */
|
|
104
|
-
routing: Ref<AnalyzerTableRouting>
|
|
105
|
-
/** Snapshot version of the currently-attached file set (re-attach trigger). */
|
|
106
|
-
snapshotVersion: Ref<string | undefined>
|
|
107
|
-
/** The site currently bound. */
|
|
108
|
-
currentSiteId: Ref<string | null>
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Run a typed archetype query. Routes automatically:
|
|
112
|
-
* - browser-eligible tables → DuckDB-WASM over OPFS files.
|
|
113
|
-
* - server-tail tables → R2 SQL or server DuckDB per the directive.
|
|
114
|
-
* Identical queries replay from the result LRU. Honours `signal`.
|
|
115
|
-
*/
|
|
116
|
-
query: <R extends ArchetypeResultRow = ArchetypeResultRow>(
|
|
117
|
-
q: ArchetypeQuery,
|
|
118
|
-
opts?: { signal?: AbortSignal },
|
|
119
|
-
) => Promise<ArchetypeResult<R>>
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Re-probe the file-resolution endpoint; if `snapshotVersion` changed,
|
|
123
|
-
* detach stale views and re-attach the fresher parquet in place (the
|
|
124
|
-
* DuckDB-WASM runtime stays alive). Resolves true if it re-attached.
|
|
125
|
-
* No-op for fully server-tail-routed sites.
|
|
126
|
-
*/
|
|
127
|
-
refresh: () => Promise<boolean>
|
|
128
|
-
|
|
129
|
-
/** Drop the result LRU without detaching. Cheap cache bust. */
|
|
130
|
-
clearCache: () => void
|
|
131
|
-
|
|
132
|
-
/** Detach views, close the runtime, release OPFS handles. Idempotent. */
|
|
133
|
-
dispose: () => Promise<void>
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Get (or create) the consolidated analyzer for a site. Per-`(site,
|
|
138
|
-
* searchType, range)` cached + refcounted across the app, so panels on the
|
|
139
|
-
* same site share one DuckDB-WASM boot and one OPFS attach.
|
|
140
|
-
*
|
|
141
|
-
* Replaces `useGscAnalyzer` (pkg) and `useBrowserAnalyzer` /
|
|
142
|
-
* `provideBrowserAnalyzer` / `useSharedBrowserAnalyzer` (app) — those are
|
|
143
|
-
* deleted once consumers migrate.
|
|
144
|
-
*/
|
|
145
|
-
export type UseGscSnapshotAnalyzer = (
|
|
146
|
-
siteId: MaybeRefOrGetter<string | null | undefined>,
|
|
147
|
-
searchType?: MaybeRefOrGetter<GscSearchType | undefined>,
|
|
148
|
-
range?: MaybeRefOrGetter<AnalyzerRange | null | undefined>,
|
|
149
|
-
options?: GscSnapshotAnalyzerOptions,
|
|
150
|
-
) => GscSnapshotAnalyzer
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure routing + caching helpers for `useGscSnapshotAnalyzer`.
|
|
3
|
-
*
|
|
4
|
-
* Extracted into a standalone, Nuxt-free module so the routing decision and
|
|
5
|
-
* the result LRU are unit-testable without a Nuxt runtime (the composable file
|
|
6
|
-
* itself depends on Nuxt auto-imports).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { ArchetypeQuery } from '@gscdump/sdk'
|
|
10
|
-
import type { AnalyzerTableRouting } from './useGscSnapshotAnalyzer.contract'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Decide where an archetype query runs given the analyzer's per-table routing.
|
|
14
|
-
*
|
|
15
|
-
* - `aux-cloud-only` is always `cloud` (not an Iceberg query).
|
|
16
|
-
* - Otherwise the fact-table the archetype reads decides: a table the resolver
|
|
17
|
-
* marked `browser` runs locally; `server` (or degraded by a quota error)
|
|
18
|
-
* routes to the server tail. An unknown table fails safe to `server`.
|
|
19
|
-
*/
|
|
20
|
-
export function routeArchetype(
|
|
21
|
-
query: ArchetypeQuery,
|
|
22
|
-
routing: AnalyzerTableRouting,
|
|
23
|
-
tableOf: (q: ArchetypeQuery) => string | null,
|
|
24
|
-
): 'browser' | 'server' | 'cloud' {
|
|
25
|
-
if (query.archetype === 'aux-cloud-only')
|
|
26
|
-
return 'cloud'
|
|
27
|
-
const table = tableOf(query)
|
|
28
|
-
if (!table)
|
|
29
|
-
return 'cloud'
|
|
30
|
-
return routing[table] === 'browser' ? 'browser' : 'server'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Stable hash of an archetype query for the result LRU. Archetype inputs are
|
|
35
|
-
* flat + declarative; keys are sorted so insertion order can't perturb the key.
|
|
36
|
-
*/
|
|
37
|
-
export function archetypeQueryHash(query: ArchetypeQuery): string {
|
|
38
|
-
return JSON.stringify(query, Object.keys(query as unknown as Record<string, unknown>).sort())
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Compose the LRU cache key. Keyed by `snapshotVersion` so a fresh compaction
|
|
43
|
-
* (new snapshot) invalidates every cached result without an explicit bust.
|
|
44
|
-
*/
|
|
45
|
-
export function resultCacheKey(snapshotVersion: string | undefined, query: ArchetypeQuery): string {
|
|
46
|
-
return `${snapshotVersion ?? 'none'}::${archetypeQueryHash(query)}`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ResultLru<V> {
|
|
50
|
-
get: (key: string) => V | undefined
|
|
51
|
-
set: (key: string, value: V) => void
|
|
52
|
-
clear: () => void
|
|
53
|
-
readonly size: number
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Minimal insertion-ordered LRU. Re-inserts on `get` to mark recency. */
|
|
57
|
-
export function createResultLru<V>(capacity: number): ResultLru<V> {
|
|
58
|
-
const map = new Map<string, V>()
|
|
59
|
-
const cap = Math.max(1, Math.floor(capacity))
|
|
60
|
-
return {
|
|
61
|
-
get(key) {
|
|
62
|
-
if (!map.has(key))
|
|
63
|
-
return undefined
|
|
64
|
-
const value = map.get(key)!
|
|
65
|
-
map.delete(key)
|
|
66
|
-
map.set(key, value)
|
|
67
|
-
return value
|
|
68
|
-
},
|
|
69
|
-
set(key, value) {
|
|
70
|
-
if (map.has(key))
|
|
71
|
-
map.delete(key)
|
|
72
|
-
map.set(key, value)
|
|
73
|
-
while (map.size > cap) {
|
|
74
|
-
const oldest = map.keys().next().value
|
|
75
|
-
if (oldest === undefined)
|
|
76
|
-
break
|
|
77
|
-
map.delete(oldest)
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
clear() {
|
|
81
|
-
map.clear()
|
|
82
|
-
},
|
|
83
|
-
get size() {
|
|
84
|
-
return map.size
|
|
85
|
-
},
|
|
86
|
-
}
|
|
87
|
-
}
|