@uniweb/kit 0.9.42 → 0.9.43
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.43",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"fuse.js": "^7.0.0",
|
|
41
41
|
"shiki": "^3.0.0",
|
|
42
42
|
"tailwind-merge": "^3.6.0",
|
|
43
|
-
"@uniweb/core": "0.7.
|
|
43
|
+
"@uniweb/core": "0.7.33",
|
|
44
44
|
"@uniweb/scene": "0.1.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -22,8 +22,12 @@ import { emptyResult } from './result.js'
|
|
|
22
22
|
const indexCache = new Map()
|
|
23
23
|
const fuseCache = new Map()
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Bumped to v2 when stored entries gained a validator. The version is part of
|
|
26
|
+
// the key, so every v1 entry — which had no way to be revalidated and could
|
|
27
|
+
// outlive any number of rebuilds — is orphaned rather than trusted.
|
|
28
|
+
const STORAGE_VERSION = 'v2'
|
|
29
|
+
const STORAGE_PREFIX = 'uniweb:search:'
|
|
30
|
+
const STORAGE_KEY = (cacheKey) => `${STORAGE_PREFIX}${STORAGE_VERSION}:${cacheKey}`
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Default Fuse.js options optimized for site search
|
|
@@ -63,14 +67,12 @@ function loadFromStorage(cacheKey) {
|
|
|
63
67
|
const storage = getStorage()
|
|
64
68
|
if (!storage) return null
|
|
65
69
|
|
|
66
|
-
const raw = storage.getItem(
|
|
70
|
+
const raw = storage.getItem(STORAGE_KEY(cacheKey))
|
|
67
71
|
if (!raw) return null
|
|
68
72
|
|
|
69
73
|
try {
|
|
70
74
|
const parsed = JSON.parse(raw)
|
|
71
|
-
if (Array.isArray(parsed.entries))
|
|
72
|
-
return parsed
|
|
73
|
-
}
|
|
75
|
+
if (Array.isArray(parsed.payload?.entries)) return parsed
|
|
74
76
|
return null
|
|
75
77
|
} catch {
|
|
76
78
|
return null
|
|
@@ -78,21 +80,52 @@ function loadFromStorage(cacheKey) {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
* Save index to localStorage
|
|
83
|
+
* Save index to localStorage, together with the validator that lets a later
|
|
84
|
+
* load ask the server whether it is still current.
|
|
85
|
+
*
|
|
82
86
|
* @param {string} cacheKey - Cache key
|
|
83
87
|
* @param {Object} payload - Index data
|
|
88
|
+
* @param {{etag?: string|null, lastModified?: string|null}} [validator]
|
|
84
89
|
*/
|
|
85
|
-
function saveToStorage(cacheKey, payload) {
|
|
90
|
+
function saveToStorage(cacheKey, payload, validator = {}) {
|
|
86
91
|
const storage = getStorage()
|
|
87
92
|
if (!storage) return
|
|
88
93
|
|
|
89
94
|
try {
|
|
90
|
-
storage.setItem(
|
|
95
|
+
storage.setItem(
|
|
96
|
+
STORAGE_KEY(cacheKey),
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
payload,
|
|
99
|
+
etag: validator.etag || null,
|
|
100
|
+
lastModified: validator.lastModified || null,
|
|
101
|
+
})
|
|
102
|
+
)
|
|
91
103
|
} catch {
|
|
92
104
|
// Ignore quota errors
|
|
93
105
|
}
|
|
94
106
|
}
|
|
95
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Drop entries written by an older storage schema.
|
|
110
|
+
*
|
|
111
|
+
* A search index runs to hundreds of kilobytes, so an orphaned one is a real
|
|
112
|
+
* bite out of a origin's storage quota rather than a tidiness issue.
|
|
113
|
+
*/
|
|
114
|
+
function pruneOldVersions() {
|
|
115
|
+
const storage = getStorage()
|
|
116
|
+
if (!storage) return
|
|
117
|
+
|
|
118
|
+
const current = `${STORAGE_PREFIX}${STORAGE_VERSION}:`
|
|
119
|
+
const stale = []
|
|
120
|
+
for (let i = 0; i < storage.length; i++) {
|
|
121
|
+
const key = storage.key(i)
|
|
122
|
+
if (key?.startsWith(STORAGE_PREFIX) && !key.startsWith(current)) stale.push(key)
|
|
123
|
+
}
|
|
124
|
+
stale.forEach((key) => {
|
|
125
|
+
try { storage.removeItem(key) } catch { /* ignore */ }
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
/**
|
|
97
130
|
* Load search index for a locale
|
|
98
131
|
* @param {string} indexUrl - URL to fetch the index from
|
|
@@ -104,32 +137,72 @@ function saveToStorage(cacheKey, payload) {
|
|
|
104
137
|
export async function loadSearchIndex(indexUrl, options = {}) {
|
|
105
138
|
const { cacheKey = indexUrl, useStorage = true } = options
|
|
106
139
|
|
|
107
|
-
//
|
|
140
|
+
// Memory cache: scoped to one page load, where the index cannot change
|
|
141
|
+
// underneath us. This is the only cache read that needs no validation.
|
|
108
142
|
if (indexCache.has(cacheKey)) {
|
|
109
143
|
return indexCache.get(cacheKey)
|
|
110
144
|
}
|
|
111
145
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
if (useStorage) pruneOldVersions()
|
|
147
|
+
const cached = useStorage ? loadFromStorage(cacheKey) : null
|
|
148
|
+
|
|
149
|
+
// ALWAYS ask the server, even holding a stored copy.
|
|
150
|
+
//
|
|
151
|
+
// Returning the stored index unconditionally — what this did before — has no
|
|
152
|
+
// expiry and no way to notice a rebuild, so a visitor who searched once kept
|
|
153
|
+
// answering from that index for as long as the entry survived. A redeploy
|
|
154
|
+
// did not dislodge it.
|
|
155
|
+
//
|
|
156
|
+
// It also collided across sites. The key is the index URL, which is
|
|
157
|
+
// `/search-index.json` for every Uniweb project, and localStorage is scoped
|
|
158
|
+
// to an origin — so two projects sharing a dev port shared one entry, and a
|
|
159
|
+
// search on one could return the other's pages. Revalidating settles that
|
|
160
|
+
// too: a different server answers 200 with its own index and replaces it.
|
|
161
|
+
const headers = {}
|
|
162
|
+
if (cached?.etag) headers['If-None-Match'] = cached.etag
|
|
163
|
+
else if (cached?.lastModified) headers['If-Modified-Since'] = cached.lastModified
|
|
164
|
+
|
|
165
|
+
let response
|
|
166
|
+
try {
|
|
167
|
+
// `no-cache` = revalidate, not "don't cache". `force-cache` (the previous
|
|
168
|
+
// value) told the browser to prefer any stored response regardless of age,
|
|
169
|
+
// which defeated revalidation a second time over.
|
|
170
|
+
response = await fetch(indexUrl, { headers, cache: 'no-cache' })
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// Offline or unreachable. A stored index is better than no search at all,
|
|
173
|
+
// and this is the one path where serving it unvalidated is the right call.
|
|
115
174
|
if (cached) {
|
|
116
|
-
indexCache.set(cacheKey, cached)
|
|
117
|
-
return cached
|
|
175
|
+
indexCache.set(cacheKey, cached.payload)
|
|
176
|
+
return cached.payload
|
|
118
177
|
}
|
|
178
|
+
throw error
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Unchanged since we stored it — the whole point of keeping the validator.
|
|
182
|
+
if (response.status === 304 && cached) {
|
|
183
|
+
indexCache.set(cacheKey, cached.payload)
|
|
184
|
+
return cached.payload
|
|
119
185
|
}
|
|
120
186
|
|
|
121
|
-
// Fetch from server
|
|
122
|
-
const response = await fetch(indexUrl, { cache: 'force-cache' })
|
|
123
187
|
if (!response.ok) {
|
|
188
|
+
if (cached) {
|
|
189
|
+
indexCache.set(cacheKey, cached.payload)
|
|
190
|
+
return cached.payload
|
|
191
|
+
}
|
|
124
192
|
throw new Error(`Failed to load search index: ${response.status}`)
|
|
125
193
|
}
|
|
126
194
|
|
|
127
195
|
const payload = await response.json()
|
|
128
196
|
|
|
129
|
-
// Cache the result
|
|
130
197
|
indexCache.set(cacheKey, payload)
|
|
131
198
|
if (useStorage) {
|
|
132
|
-
|
|
199
|
+
// A host that sends no validator still gets stored — it just costs a full
|
|
200
|
+
// fetch next time instead of a 304. What it never does is get served as
|
|
201
|
+
// though it were known to be current.
|
|
202
|
+
saveToStorage(cacheKey, payload, {
|
|
203
|
+
etag: response.headers?.get?.('etag'),
|
|
204
|
+
lastModified: response.headers?.get?.('last-modified'),
|
|
205
|
+
})
|
|
133
206
|
}
|
|
134
207
|
|
|
135
208
|
return payload
|
|
@@ -145,7 +218,7 @@ export function clearSearchCache(cacheKey) {
|
|
|
145
218
|
fuseCache.delete(cacheKey)
|
|
146
219
|
const storage = getStorage()
|
|
147
220
|
if (storage) {
|
|
148
|
-
storage.removeItem(
|
|
221
|
+
storage.removeItem(STORAGE_KEY(cacheKey))
|
|
149
222
|
}
|
|
150
223
|
} else {
|
|
151
224
|
indexCache.clear()
|