@symbo.ls/fetch 3.8.8 → 3.14.0
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/adapters/supabase.js +43 -11
- package/index.js +145 -24
- package/package.json +2 -2
package/adapters/supabase.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Cache Supabase clients by (url, key, authMode) so every `setup` call
|
|
4
|
+
// against the same project reuses a single GoTrueClient. Without this
|
|
5
|
+
// cache, each fresh adapter resolution (per db/fetch config, per query)
|
|
6
|
+
// spins up a new GoTrueClient on the same storage key — Supabase logs
|
|
7
|
+
// "Multiple GoTrueClient instances" warnings and auth state can drift.
|
|
8
|
+
const CLIENT_CACHE = new Map()
|
|
9
|
+
const clientCacheKey = (url, key, authMode) => `${url}|${key}|${authMode ? 'off' : 'on'}`
|
|
10
|
+
|
|
11
|
+
export const setup = async ({ url, key, projectId, createClient, auth, retry, adapter, ...supabaseOptions }) => {
|
|
4
12
|
const supabaseUrl = url || (projectId && `https://${projectId}.supabase.co`)
|
|
5
13
|
if (!supabaseUrl || !key) {
|
|
6
14
|
throw new Error('@symbo.ls/fetch supabase: url (or projectId) and key are required')
|
|
@@ -10,8 +18,16 @@ export const setup = async ({ url, key, projectId, createClient, ...options }) =
|
|
|
10
18
|
const mod = await import(/* webpackIgnore: true */ pkg)
|
|
11
19
|
createClient = mod.createClient
|
|
12
20
|
}
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
// Disable Supabase auto-auth when auth: false (prevents navigator.locks hang)
|
|
22
|
+
if (auth === false) {
|
|
23
|
+
supabaseOptions.auth = { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false }
|
|
24
|
+
}
|
|
25
|
+
const cacheKey = clientCacheKey(supabaseUrl, key, auth === false)
|
|
26
|
+
let client = CLIENT_CACHE.get(cacheKey)
|
|
27
|
+
if (!client) {
|
|
28
|
+
client = createClient(supabaseUrl, key, supabaseOptions)
|
|
29
|
+
if (client?.auth) CLIENT_CACHE.set(cacheKey, client)
|
|
30
|
+
}
|
|
15
31
|
if (!client || !client.auth) return null
|
|
16
32
|
return supabaseAdapter(client)
|
|
17
33
|
}
|
|
@@ -101,26 +117,42 @@ export const supabaseAdapter = (client) => ({
|
|
|
101
117
|
},
|
|
102
118
|
|
|
103
119
|
// CRUD
|
|
104
|
-
select: async ({ from, select, params, ...modifiers }) => {
|
|
105
|
-
|
|
120
|
+
select: async ({ from, schema, select, params, headers, ...modifiers }) => {
|
|
121
|
+
const base = schema ? client.schema(schema) : client
|
|
122
|
+
let q = base.from(from).select(select || '*')
|
|
106
123
|
q = applyFilters(q, params)
|
|
107
124
|
q = applyModifiers(q, modifiers)
|
|
125
|
+
if (headers) {
|
|
126
|
+
try {
|
|
127
|
+
for (const k in headers) {
|
|
128
|
+
if (q.setHeader) q = q.setHeader(k, headers[k])
|
|
129
|
+
else if (client.rest) client.rest.headers[k] = headers[k]
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn('[supabase] setHeader failed:', e.message)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
108
135
|
return q
|
|
109
136
|
},
|
|
110
137
|
|
|
111
138
|
rpc: ({ from, params }) => client.rpc(from, params),
|
|
112
139
|
|
|
113
|
-
insert: ({ from, data, select }) =>
|
|
114
|
-
client.
|
|
140
|
+
insert: ({ from, schema, data, select }) => {
|
|
141
|
+
const base = schema ? client.schema(schema) : client
|
|
142
|
+
return base.from(from).insert(data).select(select || '*')
|
|
143
|
+
},
|
|
115
144
|
|
|
116
|
-
update: ({ from, data, params, select }) => {
|
|
117
|
-
|
|
145
|
+
update: ({ from, schema, data, params, select }) => {
|
|
146
|
+
const base = schema ? client.schema(schema) : client
|
|
147
|
+
let q = base.from(from).update(data)
|
|
118
148
|
q = applyFilters(q, params)
|
|
119
149
|
return q.select(select || '*')
|
|
120
150
|
},
|
|
121
151
|
|
|
122
|
-
upsert: ({ from, data, select }) =>
|
|
123
|
-
client.
|
|
152
|
+
upsert: ({ from, schema, data, select }) => {
|
|
153
|
+
const base = schema ? client.schema(schema) : client
|
|
154
|
+
return base.from(from).upsert(data).select(select || '*')
|
|
155
|
+
},
|
|
124
156
|
|
|
125
157
|
delete: ({ from, params }) => {
|
|
126
158
|
let q = client.from(from).delete()
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import { isArray, isFunction, isObject, isString, exec } from '@
|
|
3
|
+
import { isArray, isFunction, isObject, isString, exec } from '@symbo.ls/utils'
|
|
4
4
|
|
|
5
5
|
// --- Adapter resolution ---
|
|
6
6
|
|
|
@@ -62,10 +62,16 @@ const cacheStore = new Map()
|
|
|
62
62
|
const querySubscribers = new Map() // key -> Set of elements
|
|
63
63
|
const activeQueries = new Map() // key -> Promise (deduplication)
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// Cache key includes lang when provided so different-locale responses don't
|
|
66
|
+
// collide. The fetch plugin injects Accept-Language on requests, so the same
|
|
67
|
+
// (from, method, params) tuple yields different bodies per lang. queryClient.
|
|
68
|
+
// dropNonLang(keepLang) leans on this suffix to evict stale-language entries
|
|
69
|
+
// on a setLang() event without touching the kept lang's cache.
|
|
70
|
+
const buildCacheKey = (config, lang) => {
|
|
66
71
|
if (config.cache?.key) return config.cache.key
|
|
67
72
|
const params = resolveParamsSync(config.params)
|
|
68
|
-
|
|
73
|
+
const base = `${config.from}:${config.method}:${JSON.stringify(params || '')}`
|
|
74
|
+
return lang ? `${base}:${lang}` : base
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
const getCacheEntry = (key) => cacheStore.get(key) || null
|
|
@@ -318,6 +324,9 @@ const resolveAdapter = async (db, context) => {
|
|
|
318
324
|
const resolved = await db.__resolving
|
|
319
325
|
delete db.__resolving
|
|
320
326
|
if (!resolved) return null
|
|
327
|
+
resolved.auth = db.auth
|
|
328
|
+
resolved.url = db.url
|
|
329
|
+
resolved.key = db.key
|
|
321
330
|
db.__resolved = resolved
|
|
322
331
|
context.fetch = resolved
|
|
323
332
|
|
|
@@ -328,9 +337,12 @@ const resolveAdapter = async (db, context) => {
|
|
|
328
337
|
}
|
|
329
338
|
|
|
330
339
|
const triggerCallback = (element, name, ...args) => {
|
|
331
|
-
const fn = isFunction(element[name]) ? element[name]
|
|
332
|
-
|
|
333
|
-
|
|
340
|
+
const fn = isFunction(element[name]) ? element[name] : null
|
|
341
|
+
if (fn) fn.call(element, ...args, element, element.state, element.context)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const triggerConfigCallback = (config, element, name, ...args) => {
|
|
345
|
+
const fn = config && isFunction(config[name]) ? config[name] : null
|
|
334
346
|
if (fn) fn.call(element, ...args, element, element.state, element.context)
|
|
335
347
|
}
|
|
336
348
|
|
|
@@ -366,14 +378,33 @@ const collectFormData = (element) => {
|
|
|
366
378
|
// --- State update helpers ---
|
|
367
379
|
|
|
368
380
|
const updateElementState = (element, data, stateKey, options = {}) => {
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
381
|
+
const isRoot = stateKey && stateKey.startsWith('~/')
|
|
382
|
+
const target = isRoot ? (element.state?.root || element.state) : element.state
|
|
383
|
+
if (!target?.update) return
|
|
384
|
+
|
|
385
|
+
if (isRoot) {
|
|
386
|
+
const key = stateKey.slice(2)
|
|
387
|
+
// If transform returned an object containing the key itself, spread on root
|
|
388
|
+
// e.g. transform returns { transactions: [...], totalSaved: 755 } for as: '~/transactions'
|
|
389
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && key in data) {
|
|
390
|
+
target.update(data, { preventFetch: true, ...options })
|
|
391
|
+
} else {
|
|
392
|
+
target.update({ [key]: data }, { preventFetch: true, ...options })
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
const stateData = stateKey ? { [stateKey]: data } : data
|
|
396
|
+
target.update(stateData, { preventFetch: true, ...options })
|
|
372
397
|
}
|
|
373
398
|
}
|
|
374
399
|
|
|
375
400
|
const setFetchStatus = (element, status) => {
|
|
376
|
-
|
|
401
|
+
// Element may have been torn down (route change, modal close, parent
|
|
402
|
+
// unmount) between the runFetch entry guard and this call site —
|
|
403
|
+
// `runFetch` checks `__ref` once at start, but a long-lived async fetch
|
|
404
|
+
// can outlive the element. Without this null-guard the framework throws
|
|
405
|
+
// `TypeError: Cannot set properties of null` from every status write.
|
|
406
|
+
const ref = element?.__ref
|
|
407
|
+
if (!ref) return
|
|
377
408
|
ref.__fetchStatus = status
|
|
378
409
|
ref.__fetching = status.isFetching
|
|
379
410
|
ref.__fetchError = status.error
|
|
@@ -385,13 +416,26 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
385
416
|
const db = context?.fetch
|
|
386
417
|
if (!db) return
|
|
387
418
|
|
|
419
|
+
// Element may have been torn down (unmounted, navigated away, modal
|
|
420
|
+
// closed) while a reactive dependency on its fetch config was still
|
|
421
|
+
// scheduling this run. `__ref` is the framework's marker for "alive" —
|
|
422
|
+
// if it's gone, bail before anything else touches `element`.
|
|
423
|
+
if (!element || !element.__ref) return
|
|
424
|
+
|
|
388
425
|
// enabled check
|
|
389
426
|
if (config.enabled === false) return
|
|
390
427
|
if (isFunction(config.enabled) && !config.enabled(element, element.state)) return
|
|
428
|
+
// Also honor `skip` as an alias — some call sites already use it.
|
|
429
|
+
if (config.skip === true) return
|
|
430
|
+
if (isFunction(config.skip) && config.skip(element, element.state)) return
|
|
391
431
|
|
|
392
432
|
const adapter = await resolveAdapter(db, context)
|
|
393
433
|
if (!adapter) return
|
|
394
434
|
|
|
435
|
+
// Re-check after the await — a slow adapter-resolve can outlive the
|
|
436
|
+
// element that triggered the fetch.
|
|
437
|
+
if (!element.__ref) return
|
|
438
|
+
|
|
395
439
|
const ref = element.__ref
|
|
396
440
|
const {
|
|
397
441
|
from, method, query, params: rawParams, cache: cacheRaw, transform,
|
|
@@ -404,19 +448,22 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
404
448
|
keepPreviousData
|
|
405
449
|
} = config
|
|
406
450
|
|
|
407
|
-
let select
|
|
451
|
+
let select = isString(selectTransform) ? selectTransform : undefined
|
|
408
452
|
if (query && isFunction(element.getQuery)) {
|
|
409
453
|
const q = element.getQuery(adapter.name || 'paths')
|
|
410
|
-
if (q) select = q.select || (q.length && q.join(',')) ||
|
|
454
|
+
if (q) select = q.select || (q.length && q.join(',')) || select
|
|
411
455
|
}
|
|
412
456
|
|
|
413
457
|
const params = resolveParams(rawParams, element, context)
|
|
414
458
|
const cacheConfig = parseCacheConfig(cacheRaw)
|
|
415
459
|
const retryConfig = resolveRetryConfig(config)
|
|
460
|
+
const lang = resolveLanguage(element, context)
|
|
416
461
|
|
|
417
|
-
// Build cache key
|
|
462
|
+
// Build cache key — lang suffix lets queryClient.dropNonLang(keepLang)
|
|
463
|
+
// evict stale-language entries on setLang() without flushing the rest.
|
|
464
|
+
const langSuffix = lang ? `:${lang}` : ''
|
|
418
465
|
const cacheKey = cacheConfig
|
|
419
|
-
? (cacheRaw?.key || `${from}:${method}:${JSON.stringify(params || '')}${infinite ? ':infinite' : ''}${page ? ':p' + JSON.stringify(page) : ''}`)
|
|
466
|
+
? (cacheRaw?.key || `${from}:${method}:${JSON.stringify(params || '')}${infinite ? ':infinite' : ''}${page ? ':p' + JSON.stringify(page) : ''}${langSuffix}`)
|
|
420
467
|
: null
|
|
421
468
|
|
|
422
469
|
// Apply initial/placeholder data on first mount
|
|
@@ -435,7 +482,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
435
482
|
const age = Date.now() - cached.time
|
|
436
483
|
const isStale = cached.stale || age > (cacheConfig.staleTime || 0)
|
|
437
484
|
let data = cached.data
|
|
438
|
-
if (selectTransform) data = selectTransform(data, element, element.state)
|
|
485
|
+
if (isFunction(selectTransform)) data = selectTransform(data, element, element.state)
|
|
439
486
|
if (transform) data = transform(data, element, element.state)
|
|
440
487
|
updateElementState(element, data, stateKey)
|
|
441
488
|
if (!isStale) {
|
|
@@ -451,17 +498,18 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
451
498
|
}
|
|
452
499
|
}
|
|
453
500
|
|
|
454
|
-
// Deduplication — if same query is already in-flight, share the promise
|
|
501
|
+
// Deduplication — if same query is already in-flight, share the promise (with timeout)
|
|
455
502
|
if (cacheKey && activeQueries.has(cacheKey) && !opts.force) {
|
|
456
503
|
const existing = activeQueries.get(cacheKey)
|
|
457
504
|
try {
|
|
458
|
-
const result = await existing
|
|
505
|
+
const result = await Promise.race([existing, new Promise((_, r) => setTimeout(() => r('DEDUP_TIMEOUT'), 15000))])
|
|
459
506
|
if (result?.data !== undefined) {
|
|
460
507
|
let finalData = result.data
|
|
461
|
-
if (selectTransform) finalData = selectTransform(finalData, element, element.state)
|
|
508
|
+
if (isFunction(selectTransform)) finalData = selectTransform(finalData, element, element.state)
|
|
462
509
|
if (transform) finalData = transform(finalData, element, element.state)
|
|
463
510
|
updateElementState(element, finalData, stateKey)
|
|
464
511
|
triggerCallback(element, 'onFetchComplete', finalData)
|
|
512
|
+
triggerConfigCallback(config, element, 'onFetchComplete', finalData)
|
|
465
513
|
}
|
|
466
514
|
} catch (e) {
|
|
467
515
|
// Error handled by the original caller
|
|
@@ -481,14 +529,20 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
481
529
|
fetchStatus: 'fetching'
|
|
482
530
|
})
|
|
483
531
|
triggerCallback(element, 'onFetchStart')
|
|
532
|
+
triggerConfigCallback(config, element, 'onFetchStart')
|
|
533
|
+
|
|
534
|
+
const _emitNet = context?.analyze?.emitNetwork
|
|
535
|
+
const __analyzeStart = _emitNet ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : 0
|
|
536
|
+
if (_emitNet) _emitNet({ phase: 'start', mode: 'query', from, method, cacheKey, lang })
|
|
484
537
|
|
|
485
538
|
const doFetch = async () => {
|
|
486
|
-
if (auth !== false && adapter.getSession) {
|
|
539
|
+
if (auth !== false && adapter.auth !== false && adapter.getSession) {
|
|
487
540
|
const session = await adapter.getSession()
|
|
488
541
|
if (auth === true && !session) {
|
|
489
542
|
const err = { message: 'Not authenticated' }
|
|
490
543
|
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error: err, status: 'error', fetchStatus: 'idle' })
|
|
491
544
|
triggerCallback(element, 'onFetchError', err)
|
|
545
|
+
triggerConfigCallback(config, element, 'onFetchError', err)
|
|
492
546
|
return { data: null, error: err }
|
|
493
547
|
}
|
|
494
548
|
}
|
|
@@ -556,6 +610,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
556
610
|
(newData, oldData, payload) => {
|
|
557
611
|
updateElementState(element, newData, stateKey)
|
|
558
612
|
triggerCallback(element, 'onFetchComplete', newData)
|
|
613
|
+
triggerConfigCallback(config, element, 'onFetchComplete', newData)
|
|
559
614
|
}
|
|
560
615
|
)
|
|
561
616
|
ref.__unsubscribe = unsubscribe
|
|
@@ -581,6 +636,12 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
581
636
|
if (cacheKey) setCacheEntry(cacheKey, null, error)
|
|
582
637
|
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error, status: 'error', fetchStatus: 'idle' })
|
|
583
638
|
triggerCallback(element, 'onFetchError', error)
|
|
639
|
+
triggerConfigCallback(config, element, 'onFetchError', error)
|
|
640
|
+
if (_emitNet) _emitNet({
|
|
641
|
+
phase: 'error', mode: 'query', from, method, cacheKey,
|
|
642
|
+
error: error.message || error,
|
|
643
|
+
durationMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - __analyzeStart
|
|
644
|
+
})
|
|
584
645
|
return
|
|
585
646
|
}
|
|
586
647
|
|
|
@@ -614,7 +675,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
614
675
|
}
|
|
615
676
|
}
|
|
616
677
|
|
|
617
|
-
if (selectTransform) finalData = selectTransform(finalData, element, element.state)
|
|
678
|
+
if (isFunction(selectTransform)) finalData = selectTransform(finalData, element, element.state)
|
|
618
679
|
if (transform) finalData = transform(finalData, element, element.state)
|
|
619
680
|
|
|
620
681
|
// keepPreviousData — don't clear state while fetching new page
|
|
@@ -624,10 +685,21 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
624
685
|
|
|
625
686
|
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: true, error: null, status: 'success', fetchStatus: 'idle' })
|
|
626
687
|
triggerCallback(element, 'onFetchComplete', finalData)
|
|
688
|
+
triggerConfigCallback(config, element, 'onFetchComplete', finalData)
|
|
689
|
+
if (_emitNet) _emitNet({
|
|
690
|
+
phase: 'success', mode: 'query', ok: true, from, method, cacheKey,
|
|
691
|
+
durationMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - __analyzeStart
|
|
692
|
+
})
|
|
627
693
|
}
|
|
628
694
|
} catch (e) {
|
|
629
695
|
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error: e, status: 'error', fetchStatus: 'idle' })
|
|
630
696
|
triggerCallback(element, 'onFetchError', e)
|
|
697
|
+
triggerConfigCallback(config, element, 'onFetchError', e)
|
|
698
|
+
if (_emitNet) _emitNet({
|
|
699
|
+
phase: 'error', mode: 'query', from, method, cacheKey,
|
|
700
|
+
error: e.message || String(e),
|
|
701
|
+
durationMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - __analyzeStart
|
|
702
|
+
})
|
|
631
703
|
} finally {
|
|
632
704
|
if (cacheKey) activeQueries.delete(cacheKey)
|
|
633
705
|
}
|
|
@@ -689,7 +761,8 @@ const bindAutoRefetch = (config, element, context) => {
|
|
|
689
761
|
// refetchOnWindowFocus
|
|
690
762
|
if (config.refetchOnWindowFocus !== false) {
|
|
691
763
|
const onFocus = () => {
|
|
692
|
-
const
|
|
764
|
+
const lang = resolveLanguage(element, context)
|
|
765
|
+
const cacheKey = buildCacheKey(config, lang)
|
|
693
766
|
const entry = getCacheEntry(cacheKey)
|
|
694
767
|
const cacheConfig = parseCacheConfig(config.cache)
|
|
695
768
|
if (!entry || entry.stale || (Date.now() - entry.time) > (cacheConfig?.staleTime || 0)) {
|
|
@@ -776,14 +849,20 @@ const runMutation = async (config, element, context) => {
|
|
|
776
849
|
ref.__fetching = true
|
|
777
850
|
setFetchStatus(element, { isFetching: true, isLoading: false, isStale: false, isSuccess: false, error: null, status: 'pending', fetchStatus: 'fetching' })
|
|
778
851
|
triggerCallback(element, 'onFetchStart')
|
|
852
|
+
triggerConfigCallback(config, element, 'onFetchStart')
|
|
853
|
+
|
|
854
|
+
const _emitNet = context?.analyze?.emitNetwork
|
|
855
|
+
const __analyzeStart = _emitNet ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : 0
|
|
856
|
+
if (_emitNet) _emitNet({ phase: 'start', mode: 'mutation', from, method })
|
|
779
857
|
|
|
780
858
|
try {
|
|
781
|
-
if (auth !== false && adapter.getSession) {
|
|
859
|
+
if (auth !== false && adapter.auth !== false && adapter.getSession) {
|
|
782
860
|
const session = await adapter.getSession()
|
|
783
861
|
if (auth === true && !session) {
|
|
784
862
|
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
785
863
|
const err = { message: 'Not authenticated' }
|
|
786
864
|
triggerCallback(element, 'onFetchError', err)
|
|
865
|
+
triggerConfigCallback(config, element, 'onFetchError', err)
|
|
787
866
|
return
|
|
788
867
|
}
|
|
789
868
|
}
|
|
@@ -810,6 +889,11 @@ const runMutation = async (config, element, context) => {
|
|
|
810
889
|
triggerCallback(element, 'onFetchError', error)
|
|
811
890
|
if (isFunction(onError)) onError(error, mutationData, element)
|
|
812
891
|
if (isFunction(onSettled)) onSettled(null, error, mutationData, element)
|
|
892
|
+
if (_emitNet) _emitNet({
|
|
893
|
+
phase: 'error', mode: 'mutation', from, method,
|
|
894
|
+
error: error.message || String(error),
|
|
895
|
+
durationMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - __analyzeStart
|
|
896
|
+
})
|
|
813
897
|
return
|
|
814
898
|
}
|
|
815
899
|
|
|
@@ -822,8 +906,13 @@ const runMutation = async (config, element, context) => {
|
|
|
822
906
|
|
|
823
907
|
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: true, error: null, status: 'success', fetchStatus: 'idle' })
|
|
824
908
|
triggerCallback(element, 'onFetchComplete', data)
|
|
909
|
+
triggerConfigCallback(config, element, 'onFetchComplete', data)
|
|
825
910
|
if (isFunction(onSuccess)) onSuccess(data, mutationData, element)
|
|
826
911
|
if (isFunction(onSettled)) onSettled(data, null, mutationData, element)
|
|
912
|
+
if (_emitNet) _emitNet({
|
|
913
|
+
phase: 'success', mode: 'mutation', ok: true, from, method,
|
|
914
|
+
durationMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - __analyzeStart
|
|
915
|
+
})
|
|
827
916
|
|
|
828
917
|
// Invalidate related queries
|
|
829
918
|
if (invalidates) {
|
|
@@ -843,8 +932,14 @@ const runMutation = async (config, element, context) => {
|
|
|
843
932
|
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
844
933
|
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error: e, status: 'error', fetchStatus: 'idle' })
|
|
845
934
|
triggerCallback(element, 'onFetchError', e)
|
|
935
|
+
triggerConfigCallback(config, element, 'onFetchError', e)
|
|
846
936
|
if (isFunction(onError)) onError(e, mutationData, element)
|
|
847
937
|
if (isFunction(onSettled)) onSettled(null, e, mutationData, element)
|
|
938
|
+
if (_emitNet) _emitNet({
|
|
939
|
+
phase: 'error', mode: 'mutation', from, method,
|
|
940
|
+
error: e.message || String(e),
|
|
941
|
+
durationMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - __analyzeStart
|
|
942
|
+
})
|
|
848
943
|
} finally {
|
|
849
944
|
ref.__fetching = false
|
|
850
945
|
}
|
|
@@ -987,7 +1082,31 @@ export const queryClient = {
|
|
|
987
1082
|
},
|
|
988
1083
|
|
|
989
1084
|
getCache: () => cacheStore,
|
|
990
|
-
clear: () => removeCache()
|
|
1085
|
+
clear: () => removeCache(),
|
|
1086
|
+
|
|
1087
|
+
// Drop every cache entry whose lang suffix isn't `keepLang`.
|
|
1088
|
+
// Cache keys are of the form `${from}:${method}:${params}[:lang]` — entries
|
|
1089
|
+
// without a lang suffix are kept (they were inserted before lang gating
|
|
1090
|
+
// was active). Used by setLang() to evict stale-language entries while
|
|
1091
|
+
// keeping the new active lang's entries warm.
|
|
1092
|
+
//
|
|
1093
|
+
// The English-fallback policy: callers typically pass `'en'` as keepLang
|
|
1094
|
+
// so English bundle stays cached even after switching to e.g. French,
|
|
1095
|
+
// since the page's English fallback may still render where French data
|
|
1096
|
+
// hasn't loaded yet.
|
|
1097
|
+
dropNonLang: (keepLang) => {
|
|
1098
|
+
if (!keepLang) return
|
|
1099
|
+
const keepSuffix = `:${keepLang}`
|
|
1100
|
+
for (const [key] of cacheStore) {
|
|
1101
|
+
// Entry without any lang suffix: presence of `:` in middle but not
|
|
1102
|
+
// matching any 2-3 letter ISO suffix at the end. Conservative: keep
|
|
1103
|
+
// entries whose tail doesn't look like `:xx` or `:xxx`.
|
|
1104
|
+
const tailMatch = key.match(/:([a-z]{2,3})$/i)
|
|
1105
|
+
if (!tailMatch) continue // no lang suffix → keep
|
|
1106
|
+
if (key.endsWith(keepSuffix)) continue // matches keepLang → keep
|
|
1107
|
+
removeCache(key)
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
991
1110
|
}
|
|
992
1111
|
|
|
993
1112
|
// --- Plugin interface ---
|
|
@@ -1010,8 +1129,10 @@ export const fetchPlugin = {
|
|
|
1010
1129
|
|
|
1011
1130
|
// Hook into element creation to auto-execute fetch configs
|
|
1012
1131
|
create (element) {
|
|
1013
|
-
const fetchProp = element.fetch
|
|
1132
|
+
const fetchProp = element.fetch
|
|
1014
1133
|
if (!fetchProp) return
|
|
1134
|
+
// Clear stale in-flight queries on root element creation
|
|
1135
|
+
if (!element.parent) activeQueries.clear()
|
|
1015
1136
|
executeFetch(fetchProp, element, element.state, element.context)
|
|
1016
1137
|
}
|
|
1017
1138
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/fetch",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"adapters"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@
|
|
28
|
+
"@symbo.ls/utils": "^3.14.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@supabase/supabase-js": "^2.0.0"
|