@symbo.ls/fetch 3.6.3 → 3.6.6
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 +724 -0
- package/adapters/local.js +148 -0
- package/adapters/rest.js +147 -0
- package/adapters/supabase.js +153 -0
- package/index.js +940 -68
- package/package.json +22 -18
- package/LICENSE +0 -21
- package/dist/cjs/index.js +0 -103
- package/dist/esm/index.js +0 -73
- package/dist/iife/index.js +0 -2992
package/index.js
CHANGED
|
@@ -1,96 +1,968 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
const { window, overwriteDeep, deepDestringifyFunctions } = utils
|
|
3
|
+
import { isArray, isFunction, isObject, isString, exec } from '@domql/utils'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
window && window.location
|
|
8
|
-
? window.location.host.includes('dev.')
|
|
9
|
-
: utils.isDevelopment()
|
|
5
|
+
// --- Adapter resolution ---
|
|
10
6
|
|
|
11
|
-
const
|
|
12
|
-
? 'http://localhost:8080/get'
|
|
13
|
-
: 'https://api.symbols.app/get'
|
|
7
|
+
const ADAPTER_METHODS = ['select', 'rpc', 'insert', 'update', 'delete', 'subscribe']
|
|
14
8
|
|
|
15
|
-
const
|
|
16
|
-
|
|
9
|
+
const BUILTIN_ADAPTERS = {
|
|
10
|
+
supabase: () => import('./adapters/supabase.js'),
|
|
11
|
+
rest: () => import('./adapters/rest.js'),
|
|
12
|
+
local: () => import('./adapters/local.js')
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
export const
|
|
15
|
+
export const registerAdapter = (name, loader) => {
|
|
16
|
+
BUILTIN_ADAPTERS[name] = loader
|
|
17
|
+
}
|
|
20
18
|
|
|
21
|
-
export const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
export const createAdapter = (config) => {
|
|
20
|
+
const adapter = {}
|
|
21
|
+
for (const method of ADAPTER_METHODS) {
|
|
22
|
+
if (config[method]) adapter[method] = config[method]
|
|
23
|
+
}
|
|
24
|
+
adapter.name = config.name || 'custom'
|
|
25
|
+
return adapter
|
|
26
|
+
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
export const resolveDb = async (config) => {
|
|
29
|
+
if (!config) return null
|
|
30
|
+
if (typeof config.select === 'function') return config
|
|
31
|
+
|
|
32
|
+
const { adapter, ...options } = config
|
|
33
|
+
const name = typeof adapter === 'string' ? adapter : typeof config === 'string' ? config : null
|
|
34
|
+
if (!name) return null
|
|
35
|
+
|
|
36
|
+
const loader = BUILTIN_ADAPTERS[name]
|
|
37
|
+
if (!loader) throw new Error(`Unknown db adapter: "${name}". Use registerAdapter() for adapters with optional deps.`)
|
|
38
|
+
|
|
39
|
+
const mod = await loader()
|
|
40
|
+
return mod.setup(options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Duration parsing ---
|
|
44
|
+
|
|
45
|
+
const parseDuration = (val) => {
|
|
46
|
+
if (!val) return 0
|
|
47
|
+
if (typeof val === 'number') return val
|
|
48
|
+
const match = val.match(/^(\d+)(ms|s|m|h)$/)
|
|
49
|
+
if (!match) return 0
|
|
50
|
+
const n = parseInt(match[1])
|
|
51
|
+
const unit = match[2]
|
|
52
|
+
if (unit === 'ms') return n
|
|
53
|
+
if (unit === 's') return n * 1000
|
|
54
|
+
if (unit === 'm') return n * 60000
|
|
55
|
+
if (unit === 'h') return n * 3600000
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Query cache ---
|
|
60
|
+
|
|
61
|
+
const cacheStore = new Map()
|
|
62
|
+
const querySubscribers = new Map() // key -> Set of elements
|
|
63
|
+
const activeQueries = new Map() // key -> Promise (deduplication)
|
|
64
|
+
|
|
65
|
+
const buildCacheKey = (config) => {
|
|
66
|
+
if (config.cache?.key) return config.cache.key
|
|
67
|
+
const params = resolveParamsSync(config.params)
|
|
68
|
+
return `${config.from}:${config.method}:${JSON.stringify(params || '')}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const getCacheEntry = (key) => cacheStore.get(key) || null
|
|
72
|
+
|
|
73
|
+
const setCacheEntry = (key, data, error) => {
|
|
74
|
+
const existing = cacheStore.get(key)
|
|
75
|
+
const entry = {
|
|
76
|
+
data,
|
|
77
|
+
error,
|
|
78
|
+
time: Date.now(),
|
|
79
|
+
stale: false
|
|
80
|
+
}
|
|
81
|
+
cacheStore.set(key, entry)
|
|
82
|
+
|
|
83
|
+
// Notify all subscribers of this query
|
|
84
|
+
const subs = querySubscribers.get(key)
|
|
85
|
+
if (subs) {
|
|
86
|
+
for (const sub of subs) {
|
|
87
|
+
if (isFunction(sub)) sub(entry)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return entry
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const invalidateCache = (key) => {
|
|
95
|
+
if (key) {
|
|
96
|
+
const entry = cacheStore.get(key)
|
|
97
|
+
if (entry) entry.stale = true
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
// Invalidate all
|
|
101
|
+
for (const [, entry] of cacheStore) {
|
|
102
|
+
entry.stale = true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const removeCache = (key) => {
|
|
107
|
+
if (key) {
|
|
108
|
+
cacheStore.delete(key)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
cacheStore.clear()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const subscribeToCacheKey = (key, callback) => {
|
|
115
|
+
if (!querySubscribers.has(key)) querySubscribers.set(key, new Set())
|
|
116
|
+
querySubscribers.get(key).add(callback)
|
|
117
|
+
return () => {
|
|
118
|
+
const subs = querySubscribers.get(key)
|
|
119
|
+
if (subs) {
|
|
120
|
+
subs.delete(callback)
|
|
121
|
+
if (subs.size === 0) querySubscribers.delete(key)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Cache config resolution ---
|
|
127
|
+
|
|
128
|
+
const parseCacheConfig = (cache) => {
|
|
129
|
+
if (!cache && cache !== false) return { staleTime: 60000, gcTime: 300000 }
|
|
130
|
+
if (cache === false) return null
|
|
131
|
+
if (cache === true) return { staleTime: 60000, gcTime: 300000 }
|
|
132
|
+
if (typeof cache === 'number') return { staleTime: cache, gcTime: Math.max(cache * 5, 300000) }
|
|
133
|
+
if (isString(cache)) return { staleTime: parseDuration(cache), gcTime: 300000 }
|
|
134
|
+
return {
|
|
135
|
+
staleTime: parseDuration(cache.stale || cache.staleTime) || 60000,
|
|
136
|
+
gcTime: parseDuration(cache.gc || cache.gcTime || cache.expire) || 300000,
|
|
137
|
+
key: cache.key
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- GC ---
|
|
142
|
+
|
|
143
|
+
let gcTimer = null
|
|
144
|
+
|
|
145
|
+
const startGC = () => {
|
|
146
|
+
if (gcTimer) return
|
|
147
|
+
gcTimer = setInterval(() => {
|
|
148
|
+
const now = Date.now()
|
|
149
|
+
for (const [key, entry] of cacheStore) {
|
|
150
|
+
const subs = querySubscribers.get(key)
|
|
151
|
+
const hasSubscribers = subs && subs.size > 0
|
|
152
|
+
if (!hasSubscribers && (now - entry.time) > (entry.gcTime || 300000)) {
|
|
153
|
+
cacheStore.delete(key)
|
|
37
154
|
}
|
|
155
|
+
}
|
|
156
|
+
}, 30000)
|
|
157
|
+
if (gcTimer.unref) gcTimer.unref()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
startGC()
|
|
161
|
+
|
|
162
|
+
// --- Retry logic ---
|
|
163
|
+
|
|
164
|
+
const DEFAULT_RETRY = 3
|
|
165
|
+
const DEFAULT_RETRY_DELAY = (attempt) => Math.min(1000 * 2 ** attempt, 30000)
|
|
166
|
+
|
|
167
|
+
const resolveRetryConfig = (config) => {
|
|
168
|
+
const retry = config.retry
|
|
169
|
+
if (retry === false) return { count: 0 }
|
|
170
|
+
if (retry === true || retry === undefined) return { count: DEFAULT_RETRY, delay: DEFAULT_RETRY_DELAY }
|
|
171
|
+
if (typeof retry === 'number') return { count: retry, delay: DEFAULT_RETRY_DELAY }
|
|
172
|
+
return {
|
|
173
|
+
count: retry.count ?? DEFAULT_RETRY,
|
|
174
|
+
delay: isFunction(retry.delay) ? retry.delay : (typeof retry.delay === 'number' ? () => retry.delay : DEFAULT_RETRY_DELAY)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const withRetry = async (fn, retryConfig) => {
|
|
179
|
+
const { count, delay } = retryConfig
|
|
180
|
+
let lastError
|
|
181
|
+
for (let attempt = 0; attempt <= count; attempt++) {
|
|
182
|
+
try {
|
|
183
|
+
const result = await fn()
|
|
184
|
+
if (result?.error) {
|
|
185
|
+
lastError = result.error
|
|
186
|
+
if (attempt < count) {
|
|
187
|
+
const ms = isFunction(delay) ? delay(attempt, lastError) : delay
|
|
188
|
+
await new Promise(r => setTimeout(r, ms))
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
return result
|
|
192
|
+
}
|
|
193
|
+
return result
|
|
194
|
+
} catch (e) {
|
|
195
|
+
lastError = e
|
|
196
|
+
if (attempt < count) {
|
|
197
|
+
const ms = isFunction(delay) ? delay(attempt, e) : delay
|
|
198
|
+
await new Promise(r => setTimeout(r, ms))
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
throw e
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { data: null, error: lastError }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Refetch on window focus / reconnect ---
|
|
208
|
+
|
|
209
|
+
const globalListeners = { focus: new Set(), online: new Set() }
|
|
210
|
+
let globalListenersAttached = false
|
|
211
|
+
|
|
212
|
+
const attachGlobalListeners = () => {
|
|
213
|
+
if (globalListenersAttached || typeof window === 'undefined') return
|
|
214
|
+
globalListenersAttached = true
|
|
215
|
+
|
|
216
|
+
const onFocus = () => {
|
|
217
|
+
for (const fn of globalListeners.focus) fn()
|
|
218
|
+
}
|
|
219
|
+
const onOnline = () => {
|
|
220
|
+
for (const fn of globalListeners.online) fn()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
window.addEventListener('visibilitychange', () => {
|
|
224
|
+
if (document.visibilityState === 'visible') onFocus()
|
|
225
|
+
})
|
|
226
|
+
window.addEventListener('focus', onFocus)
|
|
227
|
+
window.addEventListener('online', onOnline)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Fetch config resolution ---
|
|
231
|
+
|
|
232
|
+
const resolveFetchConfig = (fetchProp, element) => {
|
|
233
|
+
const ref = element.__ref
|
|
234
|
+
|
|
235
|
+
if (fetchProp === true) {
|
|
236
|
+
return { from: ref.__state || element.key, method: 'select', query: true, on: 'create' }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isString(fetchProp)) {
|
|
240
|
+
return { from: fetchProp, method: 'select', query: true, on: 'create' }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isObject(fetchProp)) {
|
|
244
|
+
const config = { ...fetchProp }
|
|
245
|
+
config.from = config.from || ref.__state || element.key
|
|
246
|
+
config.method = config.method || 'select'
|
|
247
|
+
config.on = config.on || 'create'
|
|
248
|
+
if (config.query === undefined) config.query = true
|
|
249
|
+
return config
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const resolveParamsSync = (params) => {
|
|
256
|
+
if (!params || isFunction(params)) return params
|
|
257
|
+
return params
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const resolveParams = (params, element) => {
|
|
261
|
+
if (!params) return undefined
|
|
262
|
+
if (isFunction(params)) return params(element, element.state)
|
|
263
|
+
const resolved = {}
|
|
264
|
+
for (const key in params) {
|
|
265
|
+
const val = params[key]
|
|
266
|
+
resolved[key] = isFunction(val) ? val(element, element.state) : val
|
|
267
|
+
}
|
|
268
|
+
return resolved
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const initAdapterAuth = async (adapter, context) => {
|
|
272
|
+
if (adapter.__authInitialized) return
|
|
273
|
+
adapter.__authInitialized = true
|
|
274
|
+
if (!adapter.getSession) return
|
|
275
|
+
const updateAuth = (user, session) => {
|
|
276
|
+
const root = context.state?.root
|
|
277
|
+
if (root?.update) {
|
|
278
|
+
root.update({ auth: { user, session, loading: false } })
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const session = await adapter.getSession()
|
|
283
|
+
updateAuth(session?.user || null, session)
|
|
284
|
+
} catch (e) {}
|
|
285
|
+
if (adapter.onAuthStateChange) {
|
|
286
|
+
adapter.onAuthStateChange((event, session) => {
|
|
287
|
+
updateAuth(session?.user || null, session)
|
|
38
288
|
})
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const resolveAdapter = async (db, context) => {
|
|
293
|
+
// When adapter is already resolved (e.g. by getDB()), still init auth
|
|
294
|
+
if (isFunction(db.select)) {
|
|
295
|
+
if (db.auth !== false) await initAdapterAuth(db, context)
|
|
296
|
+
return db
|
|
297
|
+
}
|
|
298
|
+
if (db.__resolved) {
|
|
299
|
+
if (db.auth !== false) await initAdapterAuth(db.__resolved, context)
|
|
300
|
+
return db.__resolved
|
|
301
|
+
}
|
|
302
|
+
if (db.__resolving) return db.__resolving
|
|
303
|
+
|
|
304
|
+
db.__resolving = resolveDb(db)
|
|
305
|
+
const resolved = await db.__resolving
|
|
306
|
+
db.__resolved = resolved
|
|
307
|
+
context.db = resolved
|
|
308
|
+
delete db.__resolving
|
|
309
|
+
|
|
310
|
+
// Auto-init auth when adapter supports it and db.auth is enabled
|
|
311
|
+
if (db.auth !== false) await initAdapterAuth(resolved, context)
|
|
312
|
+
|
|
313
|
+
return resolved
|
|
314
|
+
}
|
|
39
315
|
|
|
40
|
-
|
|
316
|
+
const triggerCallback = (element, name, ...args) => {
|
|
317
|
+
const fn = isFunction(element[name]) ? element[name]
|
|
318
|
+
: (element.props && isFunction(element.props[name])) ? element.props[name]
|
|
319
|
+
: null
|
|
320
|
+
if (fn) fn.call(element, ...args, element, element.state, element.context)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const collectFormData = (element) => {
|
|
324
|
+
const data = {}
|
|
325
|
+
const node = element.node
|
|
326
|
+
if (!node) return data
|
|
327
|
+
|
|
328
|
+
if (node.tagName === 'FORM') {
|
|
329
|
+
const formData = new FormData(node)
|
|
330
|
+
for (const [key, value] of formData.entries()) {
|
|
331
|
+
data[key] = value
|
|
332
|
+
}
|
|
333
|
+
return data
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const inputs = node.querySelectorAll('input, textarea, select')
|
|
337
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
338
|
+
const input = inputs[i]
|
|
339
|
+
const name = input.name || input.getAttribute('name')
|
|
340
|
+
if (!name) continue
|
|
341
|
+
if (input.type === 'checkbox') {
|
|
342
|
+
data[name] = input.checked
|
|
343
|
+
} else if (input.type === 'file') {
|
|
344
|
+
data[name] = input.files
|
|
345
|
+
} else {
|
|
346
|
+
data[name] = input.value
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return data
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --- State update helpers ---
|
|
353
|
+
|
|
354
|
+
const updateElementState = (element, data, stateKey, options = {}) => {
|
|
355
|
+
const stateData = stateKey ? { [stateKey]: data } : data
|
|
356
|
+
if (element.state?.update) {
|
|
357
|
+
element.state.update(stateData, { preventFetch: true, ...options })
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const setFetchStatus = (element, status) => {
|
|
362
|
+
const ref = element.__ref
|
|
363
|
+
ref.__fetchStatus = status
|
|
364
|
+
ref.__fetching = status.isFetching
|
|
365
|
+
ref.__fetchError = status.error
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// --- Core fetch runner ---
|
|
369
|
+
|
|
370
|
+
const runFetch = async (config, element, context, opts = {}) => {
|
|
371
|
+
const db = context?.db
|
|
372
|
+
if (!db) return
|
|
373
|
+
|
|
374
|
+
// enabled check
|
|
375
|
+
if (config.enabled === false) return
|
|
376
|
+
if (isFunction(config.enabled) && !config.enabled(element, element.state)) return
|
|
377
|
+
|
|
378
|
+
const adapter = await resolveAdapter(db, context)
|
|
379
|
+
if (!adapter) return
|
|
380
|
+
|
|
381
|
+
const ref = element.__ref
|
|
382
|
+
const {
|
|
383
|
+
from, method, query, params: rawParams, cache: cacheRaw, transform,
|
|
384
|
+
single, auth, fields, as: stateKey, limit, offset, order,
|
|
385
|
+
headers, baseUrl,
|
|
386
|
+
// Pagination
|
|
387
|
+
page, cursor, getNextPageParam, getPreviousPageParam, infinite,
|
|
388
|
+
// TanStack-style options
|
|
389
|
+
placeholderData, initialData, select: selectTransform,
|
|
390
|
+
keepPreviousData
|
|
391
|
+
} = config
|
|
392
|
+
|
|
393
|
+
let select
|
|
394
|
+
if (query && isFunction(element.getQuery)) {
|
|
395
|
+
const q = element.getQuery(adapter.name || 'paths')
|
|
396
|
+
if (q) select = q.select || (q.length && q.join(',')) || undefined
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const params = resolveParams(rawParams, element)
|
|
400
|
+
const cacheConfig = parseCacheConfig(cacheRaw)
|
|
401
|
+
const retryConfig = resolveRetryConfig(config)
|
|
402
|
+
|
|
403
|
+
// Build cache key
|
|
404
|
+
const cacheKey = cacheConfig
|
|
405
|
+
? (cacheRaw?.key || `${from}:${method}:${JSON.stringify(params || '')}${infinite ? ':infinite' : ''}${page ? ':p' + JSON.stringify(page) : ''}`)
|
|
406
|
+
: null
|
|
407
|
+
|
|
408
|
+
// Apply initial/placeholder data on first mount
|
|
409
|
+
if (!ref.__fetchInitialized && cacheKey) {
|
|
410
|
+
ref.__fetchInitialized = true
|
|
411
|
+
if (initialData !== undefined) {
|
|
412
|
+
const initData = isFunction(initialData) ? initialData() : initialData
|
|
413
|
+
setCacheEntry(cacheKey, initData, null)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check cache (stale-while-revalidate)
|
|
418
|
+
if ((method === 'select' || method === 'rpc') && cacheKey && cacheConfig) {
|
|
419
|
+
const cached = getCacheEntry(cacheKey)
|
|
420
|
+
if (cached && !cached.error) {
|
|
421
|
+
const age = Date.now() - cached.time
|
|
422
|
+
const isStale = cached.stale || age > (cacheConfig.staleTime || 0)
|
|
423
|
+
let data = cached.data
|
|
424
|
+
if (selectTransform) data = selectTransform(data, element, element.state)
|
|
425
|
+
if (transform) data = transform(data, element, element.state)
|
|
426
|
+
updateElementState(element, data, stateKey)
|
|
427
|
+
if (!isStale) {
|
|
428
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: true, error: null, status: 'success', fetchStatus: 'idle' })
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
// Stale — continue to background refetch
|
|
432
|
+
} else if (placeholderData !== undefined) {
|
|
433
|
+
const phData = isFunction(placeholderData) ? placeholderData(element, element.state) : placeholderData
|
|
434
|
+
if (phData !== undefined) {
|
|
435
|
+
updateElementState(element, phData, stateKey)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Deduplication — if same query is already in-flight, share the promise
|
|
441
|
+
if (cacheKey && activeQueries.has(cacheKey) && !opts.force) {
|
|
442
|
+
const existing = activeQueries.get(cacheKey)
|
|
443
|
+
try {
|
|
444
|
+
const result = await existing
|
|
445
|
+
if (result?.data !== undefined) {
|
|
446
|
+
let finalData = result.data
|
|
447
|
+
if (selectTransform) finalData = selectTransform(finalData, element, element.state)
|
|
448
|
+
if (transform) finalData = transform(finalData, element, element.state)
|
|
449
|
+
updateElementState(element, finalData, stateKey)
|
|
450
|
+
triggerCallback(element, 'onFetchComplete', finalData)
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
// Error handled by the original caller
|
|
454
|
+
}
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Set loading status
|
|
459
|
+
const isFirstLoad = !getCacheEntry(cacheKey)?.data
|
|
460
|
+
setFetchStatus(element, {
|
|
461
|
+
isFetching: true,
|
|
462
|
+
isLoading: isFirstLoad,
|
|
463
|
+
isStale: false,
|
|
464
|
+
isSuccess: false,
|
|
465
|
+
error: null,
|
|
466
|
+
status: 'pending',
|
|
467
|
+
fetchStatus: 'fetching'
|
|
468
|
+
})
|
|
469
|
+
triggerCallback(element, 'onFetchStart')
|
|
470
|
+
|
|
471
|
+
const doFetch = async () => {
|
|
472
|
+
if (auth !== false && adapter.getSession) {
|
|
473
|
+
const session = await adapter.getSession()
|
|
474
|
+
if (auth === true && !session) {
|
|
475
|
+
const err = { message: 'Not authenticated' }
|
|
476
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error: err, status: 'error', fetchStatus: 'idle' })
|
|
477
|
+
triggerCallback(element, 'onFetchError', err)
|
|
478
|
+
return { data: null, error: err }
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const fn = adapter[method]
|
|
483
|
+
if (!isFunction(fn)) return { data: null, error: { message: `Method "${method}" not found on adapter` } }
|
|
484
|
+
|
|
485
|
+
const request = { from, select, params, single, limit, offset, order, headers, baseUrl }
|
|
486
|
+
|
|
487
|
+
// Pagination support
|
|
488
|
+
if (page !== undefined) {
|
|
489
|
+
if (isObject(page)) {
|
|
490
|
+
if (page.offset !== undefined) request.offset = page.offset
|
|
491
|
+
if (page.limit !== undefined) request.limit = page.limit
|
|
492
|
+
if (page.cursor !== undefined) request.params = { ...request.params, cursor: page.cursor }
|
|
493
|
+
} else if (typeof page === 'number') {
|
|
494
|
+
const pageSize = config.pageSize || limit || 20
|
|
495
|
+
request.offset = (page - 1) * pageSize
|
|
496
|
+
request.limit = pageSize
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (cursor !== undefined) {
|
|
501
|
+
request.params = { ...request.params, cursor }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (method === 'insert' || method === 'update' || method === 'upsert') {
|
|
505
|
+
let data
|
|
506
|
+
if (fields === true || config.on === 'submit') {
|
|
507
|
+
data = collectFormData(element)
|
|
508
|
+
} else if (isArray(fields)) {
|
|
509
|
+
const formData = collectFormData(element)
|
|
510
|
+
data = {}
|
|
511
|
+
for (const f of fields) data[f] = formData[f]
|
|
512
|
+
} else if (element.state?.parse) {
|
|
513
|
+
data = element.state.parse()
|
|
514
|
+
} else if (isObject(element.state)) {
|
|
515
|
+
data = { ...element.state }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (transform) data = transform(data, element, element.state)
|
|
519
|
+
request.data = data
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (method === 'rpc') {
|
|
523
|
+
let rpcParams = params
|
|
524
|
+
if (config.on === 'submit') {
|
|
525
|
+
const formData = collectFormData(element)
|
|
526
|
+
rpcParams = config.transformParams
|
|
527
|
+
? config.transformParams(formData, element, element.state)
|
|
528
|
+
: formData
|
|
529
|
+
}
|
|
530
|
+
request.params = rpcParams
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (method === 'subscribe') {
|
|
534
|
+
const unsubscribe = adapter.subscribe(
|
|
535
|
+
{ from, params, on: config.subscribeOn },
|
|
536
|
+
(newData, oldData, payload) => {
|
|
537
|
+
updateElementState(element, newData, stateKey)
|
|
538
|
+
triggerCallback(element, 'onFetchComplete', newData)
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
ref.__unsubscribe = unsubscribe
|
|
542
|
+
return { data: null, error: null, subscribed: true }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return fn(request)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Execute with retry
|
|
549
|
+
const fetchPromise = withRetry(doFetch, retryConfig)
|
|
550
|
+
|
|
551
|
+
// Store for deduplication
|
|
552
|
+
if (cacheKey) activeQueries.set(cacheKey, fetchPromise)
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
const result = await fetchPromise
|
|
556
|
+
if (result?.subscribed) return
|
|
557
|
+
|
|
558
|
+
const { data, error } = result || {}
|
|
559
|
+
|
|
560
|
+
if (error) {
|
|
561
|
+
if (cacheKey) setCacheEntry(cacheKey, null, error)
|
|
562
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error, status: 'error', fetchStatus: 'idle' })
|
|
563
|
+
triggerCallback(element, 'onFetchError', error)
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (data !== undefined) {
|
|
568
|
+
// Update cache
|
|
569
|
+
if ((method === 'select' || method === 'rpc') && cacheKey) {
|
|
570
|
+
const entry = setCacheEntry(cacheKey, data, null)
|
|
571
|
+
entry.gcTime = cacheConfig?.gcTime || 300000
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let finalData = data
|
|
575
|
+
|
|
576
|
+
// Infinite query — append pages
|
|
577
|
+
if (infinite && isArray(data)) {
|
|
578
|
+
if (!ref.__pages) ref.__pages = []
|
|
579
|
+
if (opts.direction === 'previous') {
|
|
580
|
+
ref.__pages.unshift(data)
|
|
581
|
+
} else {
|
|
582
|
+
ref.__pages.push(data)
|
|
583
|
+
}
|
|
584
|
+
finalData = ref.__pages.flat()
|
|
585
|
+
|
|
586
|
+
// Resolve pagination cursors
|
|
587
|
+
if (getNextPageParam) {
|
|
588
|
+
ref.__nextPageParam = getNextPageParam(data, ref.__pages)
|
|
589
|
+
ref.__hasNextPage = ref.__nextPageParam != null
|
|
590
|
+
}
|
|
591
|
+
if (getPreviousPageParam) {
|
|
592
|
+
ref.__prevPageParam = getPreviousPageParam(data, ref.__pages)
|
|
593
|
+
ref.__hasPreviousPage = ref.__prevPageParam != null
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (selectTransform) finalData = selectTransform(finalData, element, element.state)
|
|
598
|
+
if (transform) finalData = transform(finalData, element, element.state)
|
|
599
|
+
|
|
600
|
+
// keepPreviousData — don't clear state while fetching new page
|
|
601
|
+
if (!keepPreviousData || finalData !== undefined) {
|
|
602
|
+
updateElementState(element, finalData, stateKey)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: true, error: null, status: 'success', fetchStatus: 'idle' })
|
|
606
|
+
triggerCallback(element, 'onFetchComplete', finalData)
|
|
607
|
+
}
|
|
41
608
|
} catch (e) {
|
|
42
|
-
|
|
43
|
-
|
|
609
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error: e, status: 'error', fetchStatus: 'idle' })
|
|
610
|
+
triggerCallback(element, 'onFetchError', e)
|
|
611
|
+
} finally {
|
|
612
|
+
if (cacheKey) activeQueries.delete(cacheKey)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// --- Event binding ---
|
|
617
|
+
|
|
618
|
+
const bindEvent = (config, element, context) => {
|
|
619
|
+
const on = config.on
|
|
620
|
+
const ref = element.__ref
|
|
621
|
+
if (!ref.__fetchListeners) ref.__fetchListeners = []
|
|
622
|
+
const runner = config.__runner || runFetch
|
|
623
|
+
|
|
624
|
+
if (on === 'submit') {
|
|
625
|
+
const handler = (e) => {
|
|
626
|
+
e.preventDefault()
|
|
627
|
+
runner(config, element, context)
|
|
628
|
+
}
|
|
629
|
+
const node = element.node
|
|
630
|
+
if (node) {
|
|
631
|
+
node.addEventListener('submit', handler)
|
|
632
|
+
ref.__fetchListeners.push(() => node.removeEventListener('submit', handler))
|
|
633
|
+
}
|
|
634
|
+
} else if (on === 'click') {
|
|
635
|
+
const handler = () => runner(config, element, context)
|
|
636
|
+
const node = element.node
|
|
637
|
+
if (node) {
|
|
638
|
+
node.addEventListener('click', handler)
|
|
639
|
+
ref.__fetchListeners.push(() => node.removeEventListener('click', handler))
|
|
640
|
+
}
|
|
641
|
+
} else if (on === 'stateChange') {
|
|
642
|
+
ref.__fetchOnStateChange = () => runner(config, element, context)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// --- Refetch interval / focus / reconnect binding ---
|
|
647
|
+
|
|
648
|
+
const bindAutoRefetch = (config, element, context) => {
|
|
649
|
+
const ref = element.__ref
|
|
650
|
+
if (!ref.__fetchCleanups) ref.__fetchCleanups = []
|
|
651
|
+
|
|
652
|
+
attachGlobalListeners()
|
|
653
|
+
|
|
654
|
+
// refetchInterval
|
|
655
|
+
const interval = config.refetchInterval
|
|
656
|
+
if (interval) {
|
|
657
|
+
const ms = typeof interval === 'number' ? interval : parseDuration(interval)
|
|
658
|
+
if (ms > 0) {
|
|
659
|
+
const timer = setInterval(() => {
|
|
660
|
+
// Only refetch if visible (unless refetchIntervalInBackground is set)
|
|
661
|
+
if (config.refetchIntervalInBackground || typeof document === 'undefined' || document.visibilityState === 'visible') {
|
|
662
|
+
runFetch(config, element, context, { force: true })
|
|
663
|
+
}
|
|
664
|
+
}, ms)
|
|
665
|
+
ref.__fetchCleanups.push(() => clearInterval(timer))
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// refetchOnWindowFocus
|
|
670
|
+
if (config.refetchOnWindowFocus !== false) {
|
|
671
|
+
const onFocus = () => {
|
|
672
|
+
const cacheKey = buildCacheKey(config)
|
|
673
|
+
const entry = getCacheEntry(cacheKey)
|
|
674
|
+
const cacheConfig = parseCacheConfig(config.cache)
|
|
675
|
+
if (!entry || entry.stale || (Date.now() - entry.time) > (cacheConfig?.staleTime || 0)) {
|
|
676
|
+
runFetch(config, element, context, { force: true })
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
globalListeners.focus.add(onFocus)
|
|
680
|
+
ref.__fetchCleanups.push(() => globalListeners.focus.delete(onFocus))
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// refetchOnReconnect
|
|
684
|
+
if (config.refetchOnReconnect !== false) {
|
|
685
|
+
const onOnline = () => runFetch(config, element, context, { force: true })
|
|
686
|
+
globalListeners.online.add(onOnline)
|
|
687
|
+
ref.__fetchCleanups.push(() => globalListeners.online.delete(onOnline))
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// --- Optimistic updates ---
|
|
692
|
+
|
|
693
|
+
const applyOptimisticUpdate = (element, config, mutationData) => {
|
|
694
|
+
if (!config.optimistic) return null
|
|
695
|
+
const ref = element.__ref
|
|
696
|
+
const stateKey = config.as
|
|
697
|
+
|
|
698
|
+
// Snapshot current state for rollback
|
|
699
|
+
const snapshot = element.state?.parse ? element.state.parse() : (isObject(element.state) ? { ...element.state } : element.state)
|
|
700
|
+
ref.__optimisticSnapshot = snapshot
|
|
701
|
+
|
|
702
|
+
const optimisticData = isFunction(config.optimistic)
|
|
703
|
+
? config.optimistic(mutationData, snapshot, element)
|
|
704
|
+
: config.optimistic
|
|
705
|
+
|
|
706
|
+
if (optimisticData !== undefined) {
|
|
707
|
+
updateElementState(element, optimisticData, stateKey)
|
|
44
708
|
}
|
|
709
|
+
|
|
710
|
+
return snapshot
|
|
45
711
|
}
|
|
46
712
|
|
|
47
|
-
|
|
48
|
-
const
|
|
713
|
+
const rollbackOptimistic = (element, config) => {
|
|
714
|
+
const ref = element.__ref
|
|
715
|
+
const snapshot = ref.__optimisticSnapshot
|
|
716
|
+
if (snapshot !== undefined) {
|
|
717
|
+
updateElementState(element, snapshot, config.as)
|
|
718
|
+
delete ref.__optimisticSnapshot
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// --- Mutation runner (insert/update/delete with optimistic + invalidation) ---
|
|
723
|
+
|
|
724
|
+
const runMutation = async (config, element, context) => {
|
|
725
|
+
const db = context?.db
|
|
726
|
+
if (!db) return
|
|
727
|
+
|
|
728
|
+
const adapter = await resolveAdapter(db, context)
|
|
729
|
+
if (!adapter) return
|
|
730
|
+
|
|
731
|
+
const ref = element.__ref
|
|
732
|
+
const { method, from, fields, transform, as: stateKey, on: trigger, auth, headers, baseUrl, invalidates, optimistic, onMutate, onSuccess, onError, onSettled } = config
|
|
733
|
+
|
|
734
|
+
// Collect mutation data
|
|
735
|
+
let mutationData
|
|
736
|
+
if (fields === true || trigger === 'submit') {
|
|
737
|
+
mutationData = collectFormData(element)
|
|
738
|
+
} else if (isArray(fields)) {
|
|
739
|
+
const formData = collectFormData(element)
|
|
740
|
+
mutationData = {}
|
|
741
|
+
for (const f of fields) mutationData[f] = formData[f]
|
|
742
|
+
} else if (element.state?.parse) {
|
|
743
|
+
mutationData = element.state.parse()
|
|
744
|
+
} else if (isObject(element.state)) {
|
|
745
|
+
mutationData = { ...element.state }
|
|
746
|
+
}
|
|
49
747
|
|
|
50
|
-
if (
|
|
51
|
-
const data = await fetchRemote(key, editor)
|
|
52
|
-
const evalData =
|
|
53
|
-
IS_DEVELOPMENT || options.isDevelopment
|
|
54
|
-
? deepDestringifyFunctions(data)
|
|
55
|
-
: deepDestringifyFunctions(data.releases[0])
|
|
748
|
+
if (transform) mutationData = transform(mutationData, element, element.state)
|
|
56
749
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
750
|
+
// onMutate callback (runs before fetch)
|
|
751
|
+
if (isFunction(onMutate)) onMutate(mutationData, element, element.state)
|
|
752
|
+
|
|
753
|
+
// Apply optimistic update
|
|
754
|
+
const snapshot = optimistic ? applyOptimisticUpdate(element, config, mutationData) : null
|
|
755
|
+
|
|
756
|
+
ref.__fetching = true
|
|
757
|
+
setFetchStatus(element, { isFetching: true, isLoading: false, isStale: false, isSuccess: false, error: null, status: 'pending', fetchStatus: 'fetching' })
|
|
758
|
+
triggerCallback(element, 'onFetchStart')
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
if (auth !== false && adapter.getSession) {
|
|
762
|
+
const session = await adapter.getSession()
|
|
763
|
+
if (auth === true && !session) {
|
|
764
|
+
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
765
|
+
const err = { message: 'Not authenticated' }
|
|
766
|
+
triggerCallback(element, 'onFetchError', err)
|
|
767
|
+
return
|
|
64
768
|
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const fn = adapter[method]
|
|
772
|
+
if (!isFunction(fn)) return
|
|
773
|
+
|
|
774
|
+
const request = { from, data: mutationData, headers, baseUrl }
|
|
775
|
+
if (config.params) request.params = resolveParams(config.params, element)
|
|
776
|
+
|
|
777
|
+
const retryConfig = resolveRetryConfig(config)
|
|
778
|
+
const result = await withRetry(() => fn(request), retryConfig)
|
|
779
|
+
const { data, error } = result || {}
|
|
780
|
+
|
|
781
|
+
if (error) {
|
|
782
|
+
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
783
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error, status: 'error', fetchStatus: 'idle' })
|
|
784
|
+
triggerCallback(element, 'onFetchError', error)
|
|
785
|
+
if (isFunction(onError)) onError(error, mutationData, element)
|
|
786
|
+
if (isFunction(onSettled)) onSettled(null, error, mutationData, element)
|
|
787
|
+
return
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
delete ref.__optimisticSnapshot
|
|
791
|
+
|
|
792
|
+
if (data !== undefined) {
|
|
793
|
+
const finalData = stateKey ? { [stateKey]: data } : data
|
|
794
|
+
if (element.state?.update) element.state.update(finalData, { preventFetch: true })
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: true, error: null, status: 'success', fetchStatus: 'idle' })
|
|
798
|
+
triggerCallback(element, 'onFetchComplete', data)
|
|
799
|
+
if (isFunction(onSuccess)) onSuccess(data, mutationData, element)
|
|
800
|
+
if (isFunction(onSettled)) onSettled(data, null, mutationData, element)
|
|
801
|
+
|
|
802
|
+
// Invalidate related queries
|
|
803
|
+
if (invalidates) {
|
|
804
|
+
const keys = isArray(invalidates) ? invalidates : [invalidates]
|
|
805
|
+
for (const k of keys) {
|
|
806
|
+
if (k === true || k === '*') {
|
|
807
|
+
// Invalidate all queries matching this `from`
|
|
808
|
+
for (const [ck] of cacheStore) {
|
|
809
|
+
if (ck.startsWith(from + ':')) invalidateCache(ck)
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
invalidateCache(k)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
} catch (e) {
|
|
817
|
+
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
818
|
+
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error: e, status: 'error', fetchStatus: 'idle' })
|
|
819
|
+
triggerCallback(element, 'onFetchError', e)
|
|
820
|
+
if (isFunction(onError)) onError(e, mutationData, element)
|
|
821
|
+
if (isFunction(onSettled)) onSettled(null, e, mutationData, element)
|
|
822
|
+
} finally {
|
|
823
|
+
ref.__fetching = false
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// --- Public API ---
|
|
828
|
+
|
|
829
|
+
export const executeFetch = (param, element, state, context) => {
|
|
830
|
+
if (!param) return
|
|
831
|
+
const db = context?.db
|
|
832
|
+
if (!db) return
|
|
833
|
+
|
|
834
|
+
const fetchProp = exec(param, element)
|
|
835
|
+
if (!fetchProp) return
|
|
836
|
+
|
|
837
|
+
const configs = isArray(fetchProp)
|
|
838
|
+
? fetchProp.map(c => resolveFetchConfig(exec(c, element), element)).filter(Boolean)
|
|
839
|
+
: [resolveFetchConfig(fetchProp, element)].filter(Boolean)
|
|
840
|
+
|
|
841
|
+
for (const config of configs) {
|
|
842
|
+
const isMutation = config.method === 'insert' || config.method === 'update' || config.method === 'upsert' || config.method === 'delete'
|
|
843
|
+
const runner = isMutation ? runMutation : runFetch
|
|
844
|
+
|
|
845
|
+
if (config.on === 'create') {
|
|
846
|
+
runner(config, element, context)
|
|
65
847
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'designSystem',
|
|
69
|
-
'components',
|
|
70
|
-
'snippets',
|
|
71
|
-
'pages',
|
|
72
|
-
'utils',
|
|
73
|
-
'files',
|
|
74
|
-
'packages',
|
|
75
|
-
'functions'
|
|
76
|
-
].forEach((key) => {
|
|
77
|
-
overwriteDeep(options[key], evalData[key.toLowerCase()])
|
|
848
|
+
Promise.resolve().then(() => {
|
|
849
|
+
bindEvent({ ...config, __runner: runner }, element, context)
|
|
78
850
|
})
|
|
79
851
|
}
|
|
852
|
+
|
|
853
|
+
// Bind auto-refetch for queries
|
|
854
|
+
if (!isMutation && config.on === 'create') {
|
|
855
|
+
if (config.refetchInterval || config.refetchOnWindowFocus !== false || config.refetchOnReconnect !== false) {
|
|
856
|
+
bindAutoRefetch(config, element, context)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
80
859
|
}
|
|
81
860
|
|
|
82
|
-
|
|
83
|
-
|
|
861
|
+
// Expose imperative methods on element
|
|
862
|
+
const ref = element.__ref
|
|
863
|
+
ref.refetch = (opts) => {
|
|
864
|
+
for (const config of configs) {
|
|
865
|
+
runFetch(config, element, context, { force: true, ...opts })
|
|
866
|
+
}
|
|
867
|
+
}
|
|
84
868
|
|
|
85
|
-
|
|
86
|
-
|
|
869
|
+
ref.fetchNextPage = () => {
|
|
870
|
+
const config = configs[0]
|
|
871
|
+
if (!config || !config.infinite) return
|
|
872
|
+
const nextParam = ref.__nextPageParam
|
|
873
|
+
if (nextParam == null) return
|
|
874
|
+
const nextConfig = { ...config, cursor: nextParam }
|
|
875
|
+
runFetch(nextConfig, element, context, { direction: 'next', force: true })
|
|
876
|
+
}
|
|
87
877
|
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
878
|
+
ref.fetchPreviousPage = () => {
|
|
879
|
+
const config = configs[0]
|
|
880
|
+
if (!config || !config.infinite) return
|
|
881
|
+
const prevParam = ref.__prevPageParam
|
|
882
|
+
if (prevParam == null) return
|
|
883
|
+
const prevConfig = { ...config, cursor: prevParam }
|
|
884
|
+
runFetch(prevConfig, element, context, { direction: 'previous', force: true })
|
|
95
885
|
}
|
|
96
886
|
}
|
|
887
|
+
|
|
888
|
+
// --- Query client (global cache management) ---
|
|
889
|
+
|
|
890
|
+
export const queryClient = {
|
|
891
|
+
invalidateQueries: (keyOrPattern) => {
|
|
892
|
+
if (!keyOrPattern) {
|
|
893
|
+
invalidateCache()
|
|
894
|
+
return
|
|
895
|
+
}
|
|
896
|
+
if (isString(keyOrPattern)) {
|
|
897
|
+
for (const [key] of cacheStore) {
|
|
898
|
+
if (key.startsWith(keyOrPattern) || key.includes(keyOrPattern)) {
|
|
899
|
+
invalidateCache(key)
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} else if (isArray(keyOrPattern)) {
|
|
903
|
+
const pattern = keyOrPattern.join(':')
|
|
904
|
+
for (const [key] of cacheStore) {
|
|
905
|
+
if (key.includes(pattern)) invalidateCache(key)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
removeQueries: (keyOrPattern) => {
|
|
911
|
+
if (!keyOrPattern) {
|
|
912
|
+
removeCache()
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
if (isString(keyOrPattern)) {
|
|
916
|
+
for (const [key] of cacheStore) {
|
|
917
|
+
if (key.startsWith(keyOrPattern) || key.includes(keyOrPattern)) {
|
|
918
|
+
removeCache(key)
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
getQueryData: (key) => {
|
|
925
|
+
const entry = getCacheEntry(key)
|
|
926
|
+
return entry?.data ?? undefined
|
|
927
|
+
},
|
|
928
|
+
|
|
929
|
+
setQueryData: (key, updater) => {
|
|
930
|
+
const existing = getCacheEntry(key)
|
|
931
|
+
const data = isFunction(updater) ? updater(existing?.data) : updater
|
|
932
|
+
setCacheEntry(key, data, null)
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
prefetchQuery: async (config, context) => {
|
|
936
|
+
const db = context?.db
|
|
937
|
+
if (!db) return
|
|
938
|
+
|
|
939
|
+
const adapter = await resolveAdapter(db, context)
|
|
940
|
+
if (!adapter) return
|
|
941
|
+
|
|
942
|
+
const cacheKey = config.cache?.key || `${config.from}:${config.method || 'select'}:${JSON.stringify(config.params || '')}`
|
|
943
|
+
const fn = adapter[config.method || 'select']
|
|
944
|
+
if (!isFunction(fn)) return
|
|
945
|
+
|
|
946
|
+
const result = await fn({
|
|
947
|
+
from: config.from,
|
|
948
|
+
params: config.params,
|
|
949
|
+
limit: config.limit,
|
|
950
|
+
offset: config.offset,
|
|
951
|
+
order: config.order,
|
|
952
|
+
single: config.single
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
if (result?.data !== undefined && !result?.error) {
|
|
956
|
+
const entry = setCacheEntry(cacheKey, result.data, null)
|
|
957
|
+
entry.gcTime = parseCacheConfig(config.cache)?.gcTime || 300000
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return result
|
|
961
|
+
},
|
|
962
|
+
|
|
963
|
+
getCache: () => cacheStore,
|
|
964
|
+
clear: () => removeCache()
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export { parseDuration }
|
|
968
|
+
export default executeFetch
|