@symbo.ls/fetch 3.14.663 → 3.14.665
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/CHANGELOG.md +36 -0
- package/README.md +18 -1
- package/adapters/rest.js +79 -11
- package/dist/cjs/adapters/rest.js +1 -1
- package/dist/cjs/early.js +1 -1
- package/dist/cjs/earlyScript.js +15 -16
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/inflight.js +1 -0
- package/dist/esm/adapters/rest.js +1 -1
- package/dist/esm/early.js +1 -1
- package/dist/esm/earlyScript.js +14 -15
- package/dist/esm/index.js +1 -1
- package/dist/esm/inflight.js +1 -0
- package/early.js +104 -37
- package/earlyScript.js +16 -19
- package/index.js +95 -56
- package/inflight.js +39 -0
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { isArray, isFunction, isObject, isString, exec } from '@symbo.ls/utils'
|
|
4
4
|
import { createEffect } from '@symbo.ls/signal'
|
|
5
5
|
import { dropEarly } from './early.js'
|
|
6
|
+
import * as restAdapter from './adapters/rest.js'
|
|
6
7
|
|
|
7
8
|
// --- Adapter resolution ---
|
|
8
9
|
|
|
@@ -32,8 +33,24 @@ const ADAPTER_METHODS = ['select', 'rpc', 'insert', 'update', 'upsert', 'delete'
|
|
|
32
33
|
// declaration silently dies (SMBLS-FETCH-RESOLVEDB-EMPTY-RESULT-1).
|
|
33
34
|
// Keeping these local is safe because every copy of this file ships
|
|
34
35
|
// its own adapters/ chunks; the imports always resolve in-bundle.
|
|
36
|
+
//
|
|
37
|
+
// The REST adapter ships WITH the plugin, not as a lazy file: an app with
|
|
38
|
+
// `fetch` reads its first data through it, and as a file of its own it was
|
|
39
|
+
// the third load in a row before that first read — in smbls/core's split
|
|
40
|
+
// build: the plugin (7.8 KB gzip), then the early/in-flight code the plugin
|
|
41
|
+
// shared with the adapter (1.5 KB), then the adapter (2.7 KB) on the first
|
|
42
|
+
// resolveDb. Static here, the three are one file.
|
|
43
|
+
//
|
|
44
|
+
// `local` stays a lazy file: an app that never configures it never loads it.
|
|
45
|
+
// That is also why every microtask in this module is queued with
|
|
46
|
+
// queueMicrotask, never `Promise.resolve().then(fn)`: parcel 2 (the bundler
|
|
47
|
+
// `smbls build` runs) rewrites the first such call after an `import()` in a
|
|
48
|
+
// file into `Promise.resolve().then(() => <that import>).then(fn)`, and this
|
|
49
|
+
// `import()` comes first in the file. The `on: 'stateChange'` run loaded the
|
|
50
|
+
// local adapter that way on XMA's first "Show more" click (xma.info,
|
|
51
|
+
// 2026-10-16). __tests__/localAdapterStaysLazy.test.js holds the rule.
|
|
35
52
|
const BUILTIN_ADAPTERS = {
|
|
36
|
-
rest: () =>
|
|
53
|
+
rest: () => Promise.resolve(restAdapter),
|
|
37
54
|
local: () => import('./adapters/local.js')
|
|
38
55
|
}
|
|
39
56
|
|
|
@@ -277,17 +294,14 @@ const newerEntry = (key, seq) => {
|
|
|
277
294
|
// A request that started before its key's last invalidation (or removal).
|
|
278
295
|
const isOutdated = (key, seq) => (invalidatedAt.get(key) || 0) > seq
|
|
279
296
|
|
|
297
|
+
// Every request on `key` that started before now is outdated from here on.
|
|
298
|
+
const markInvalidated = (key) => { invalidatedAt.set(key, ++fetchSeq) }
|
|
299
|
+
|
|
280
300
|
// Removing an entry also marks its key, so a reply still in flight writes
|
|
281
301
|
// its rows back as stale, never as fresh.
|
|
282
302
|
const removeCache = (key) => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
invalidatedAt.set(key, ++fetchSeq)
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
for (const [cacheKey] of cacheStore) invalidatedAt.set(cacheKey, ++fetchSeq)
|
|
289
|
-
for (const [inFlightKey] of activeQueries) invalidatedAt.set(inFlightKey, ++fetchSeq)
|
|
290
|
-
cacheStore.clear()
|
|
303
|
+
cacheStore.delete(key)
|
|
304
|
+
markInvalidated(key)
|
|
291
305
|
}
|
|
292
306
|
|
|
293
307
|
const subscribeToCacheKey = (key, callback) => {
|
|
@@ -671,6 +685,10 @@ const updateElementState = (element, data, stateKey, options = {}) => {
|
|
|
671
685
|
}
|
|
672
686
|
}
|
|
673
687
|
|
|
688
|
+
// The settled statuses (setFetchStatus copies, so one object serves every write).
|
|
689
|
+
const SUCCESS = Object.freeze({ isFetching: false, isLoading: false, isStale: false, isSuccess: true, error: null, status: 'success', fetchStatus: 'idle' })
|
|
690
|
+
const errorStatus = (error) => ({ isFetching: false, isLoading: false, isStale: false, isSuccess: false, error, status: 'error', fetchStatus: 'idle' })
|
|
691
|
+
|
|
674
692
|
const setFetchStatus = (element, status) => {
|
|
675
693
|
// Element may have been torn down (route change, modal close, parent
|
|
676
694
|
// unmount) between the runFetch entry guard and this call site —
|
|
@@ -754,6 +772,20 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
754
772
|
keepPreviousData
|
|
755
773
|
} = config
|
|
756
774
|
|
|
775
|
+
// The rows as the element gets them: `select` (a function) then `transform`.
|
|
776
|
+
const shape = (data) => {
|
|
777
|
+
if (isFunction(selectTransform)) data = selectTransform(data, element, element.state)
|
|
778
|
+
if (transform) data = transform(data, element, element.state)
|
|
779
|
+
return data
|
|
780
|
+
}
|
|
781
|
+
// This query failed: its status, then the element's and the config's
|
|
782
|
+
// onFetchError.
|
|
783
|
+
const fail = (error) => {
|
|
784
|
+
setFetchStatus(element, errorStatus(error))
|
|
785
|
+
triggerCallback(element, 'onFetchError', error)
|
|
786
|
+
triggerConfigCallback(config, element, 'onFetchError', error)
|
|
787
|
+
}
|
|
788
|
+
|
|
757
789
|
let select = isString(selectTransform) ? selectTransform : undefined
|
|
758
790
|
if (query && isFunction(element.getQuery)) {
|
|
759
791
|
const q = element.getQuery(adapter.name || 'paths')
|
|
@@ -805,14 +837,12 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
805
837
|
ref.__pages = [cached.data]
|
|
806
838
|
setPageCursors(ref, config, cached.data)
|
|
807
839
|
}
|
|
808
|
-
|
|
809
|
-
if (isFunction(selectTransform)) data = selectTransform(data, element, element.state)
|
|
810
|
-
if (transform) data = transform(data, element, element.state)
|
|
840
|
+
const data = shape(cached.data)
|
|
811
841
|
if (!isCurrent()) return
|
|
812
842
|
// The status first: the state update re-renders, and a render that
|
|
813
843
|
// reads `__fetchStatus` must see the answer it renders (report #25).
|
|
814
844
|
if (!isStale) {
|
|
815
|
-
setFetchStatus(element,
|
|
845
|
+
setFetchStatus(element, SUCCESS)
|
|
816
846
|
updateElementState(element, data, stateKey)
|
|
817
847
|
return
|
|
818
848
|
}
|
|
@@ -834,11 +864,6 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
834
864
|
// The shared request failed: so did this query, with the same error. It
|
|
835
865
|
// used to read an error answer as null rows (onFetchComplete(null)) and a
|
|
836
866
|
// thrown one as nothing at all.
|
|
837
|
-
const fail = (error) => {
|
|
838
|
-
setFetchStatus(element, { isFetching: false, isLoading: false, isStale: false, isSuccess: false, error, status: 'error', fetchStatus: 'idle' })
|
|
839
|
-
triggerCallback(element, 'onFetchError', error)
|
|
840
|
-
triggerConfigCallback(config, element, 'onFetchError', error)
|
|
841
|
-
}
|
|
842
867
|
try {
|
|
843
868
|
const result = await Promise.race([existing, new Promise((_, r) => setTimeout(() => r('DEDUP_TIMEOUT'), 15000))])
|
|
844
869
|
if (!isCurrent()) return
|
|
@@ -848,9 +873,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
848
873
|
// A request that started later (an invalidation's refetch) may have
|
|
849
874
|
// landed first: its rows are the newer answer.
|
|
850
875
|
const newer = !infinite && newerEntry(cacheKey, requestSeqs.get(existing) || 0)
|
|
851
|
-
|
|
852
|
-
if (isFunction(selectTransform)) finalData = selectTransform(finalData, element, element.state)
|
|
853
|
-
if (transform) finalData = transform(finalData, element, element.state)
|
|
876
|
+
const finalData = shape(newer ? newer.data : result.data)
|
|
854
877
|
if (!isCurrent()) return
|
|
855
878
|
updateElementState(element, finalData, stateKey)
|
|
856
879
|
triggerCallback(element, 'onFetchComplete', finalData)
|
|
@@ -891,9 +914,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
891
914
|
if (!isCurrent()) return
|
|
892
915
|
if (auth === true && !session) {
|
|
893
916
|
const err = { message: 'Not authenticated' }
|
|
894
|
-
|
|
895
|
-
triggerCallback(element, 'onFetchError', err)
|
|
896
|
-
triggerConfigCallback(config, element, 'onFetchError', err)
|
|
917
|
+
fail(err)
|
|
897
918
|
return { data: null, error: err }
|
|
898
919
|
}
|
|
899
920
|
}
|
|
@@ -1011,9 +1032,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
1011
1032
|
if (error) {
|
|
1012
1033
|
// An older request's failure never replaces a newer answer.
|
|
1013
1034
|
if (cacheKey && !newerEntry(cacheKey, requestSeq)) setCacheEntry(cacheKey, null, error, requestSeq)
|
|
1014
|
-
|
|
1015
|
-
triggerCallback(element, 'onFetchError', error)
|
|
1016
|
-
triggerConfigCallback(config, element, 'onFetchError', error)
|
|
1035
|
+
fail(error)
|
|
1017
1036
|
if (_emitNet) _emitNet({
|
|
1018
1037
|
phase: 'error', mode: 'query', from, method, cacheKey,
|
|
1019
1038
|
error: error.message || error,
|
|
@@ -1057,14 +1076,13 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
1057
1076
|
setPageCursors(ref, config, data)
|
|
1058
1077
|
}
|
|
1059
1078
|
|
|
1060
|
-
|
|
1061
|
-
if (transform) finalData = transform(finalData, element, element.state)
|
|
1079
|
+
finalData = shape(finalData)
|
|
1062
1080
|
if (!isCurrent()) return
|
|
1063
1081
|
|
|
1064
1082
|
// The status first: the state update re-renders, and a render that
|
|
1065
1083
|
// reads `__fetchStatus` saw the request still pending beside its own
|
|
1066
1084
|
// rows (report #25).
|
|
1067
|
-
setFetchStatus(element,
|
|
1085
|
+
setFetchStatus(element, SUCCESS)
|
|
1068
1086
|
|
|
1069
1087
|
// keepPreviousData — don't clear state while fetching new page
|
|
1070
1088
|
if (!keepPreviousData || finalData !== undefined) {
|
|
@@ -1080,9 +1098,7 @@ const runFetch = async (config, element, context, opts = {}) => {
|
|
|
1080
1098
|
}
|
|
1081
1099
|
} catch (e) {
|
|
1082
1100
|
if (!isCurrent()) return
|
|
1083
|
-
|
|
1084
|
-
triggerCallback(element, 'onFetchError', e)
|
|
1085
|
-
triggerConfigCallback(config, element, 'onFetchError', e)
|
|
1101
|
+
fail(e)
|
|
1086
1102
|
if (_emitNet) _emitNet({
|
|
1087
1103
|
phase: 'error', mode: 'query', from, method, cacheKey,
|
|
1088
1104
|
error: e.message || String(e),
|
|
@@ -1158,8 +1174,8 @@ const bindStateChange = (config, element, context, runner) => {
|
|
|
1158
1174
|
if (inputs === last) return
|
|
1159
1175
|
last = inputs
|
|
1160
1176
|
// Outside the effect: the run reads state as well, and none of that may
|
|
1161
|
-
// become the effect's dependency.
|
|
1162
|
-
|
|
1177
|
+
// become the effect's dependency. (queueMicrotask: see BUILTIN_ADAPTERS.)
|
|
1178
|
+
queueMicrotask(() => runner(config, element, context))
|
|
1163
1179
|
})
|
|
1164
1180
|
if (Array.isArray(ref.__effects)) ref.__effects.push(effect)
|
|
1165
1181
|
else ref.__fetchListeners.push(() => effect.dispose())
|
|
@@ -1309,10 +1325,10 @@ const invalidateMatching = (matches) => {
|
|
|
1309
1325
|
for (const [key, entry] of cacheStore) {
|
|
1310
1326
|
if (!matches(key)) continue
|
|
1311
1327
|
entry.stale = true
|
|
1312
|
-
|
|
1328
|
+
markInvalidated(key)
|
|
1313
1329
|
}
|
|
1314
1330
|
for (const [key] of activeQueries) {
|
|
1315
|
-
if (!cacheStore.has(key) && matches(key))
|
|
1331
|
+
if (!cacheStore.has(key) && matches(key)) markInvalidated(key)
|
|
1316
1332
|
}
|
|
1317
1333
|
return refreshMounted(matches)
|
|
1318
1334
|
}
|
|
@@ -1334,10 +1350,34 @@ const removeMatching = (matches) => {
|
|
|
1334
1350
|
if (matches(key)) removeCache(key)
|
|
1335
1351
|
}
|
|
1336
1352
|
for (const [key] of activeQueries) {
|
|
1337
|
-
if (matches(key))
|
|
1353
|
+
if (matches(key)) markInvalidated(key)
|
|
1338
1354
|
}
|
|
1339
1355
|
}
|
|
1340
1356
|
|
|
1357
|
+
const ALL_KEYS = () => true
|
|
1358
|
+
|
|
1359
|
+
// --- One query cache per server render ---
|
|
1360
|
+
//
|
|
1361
|
+
// The cache lives as long as this module: in a browser, the page. A server
|
|
1362
|
+
// renderer (@symbo.ls/brender) renders page after page in ONE module
|
|
1363
|
+
// instance — every route of `smbls build`, every request of a worker isolate
|
|
1364
|
+
// — and a page answered from the rows an earlier page left made no request:
|
|
1365
|
+
// nothing for that page's fetch seed (the browser asked again right after
|
|
1366
|
+
// boot; xma.info, 2026-10-16), and in an isolate serving several projects,
|
|
1367
|
+
// another project's rows under the same key. A context that sets
|
|
1368
|
+
// `isolateFetchCache` (brender sets it on every render) starts from an empty
|
|
1369
|
+
// cache: the first query it runs drops every entry an earlier context left,
|
|
1370
|
+
// marks the requests still out as outdated (never shared with it, never
|
|
1371
|
+
// written back as fresh) and forgets the queries mounted before it (an
|
|
1372
|
+
// invalidation it makes refetches its own). A browser context never sets it.
|
|
1373
|
+
const isolatedContexts = new WeakSet()
|
|
1374
|
+
const enterContext = (context) => {
|
|
1375
|
+
if (!context || !context.isolateFetchCache || isolatedContexts.has(context)) return
|
|
1376
|
+
isolatedContexts.add(context)
|
|
1377
|
+
removeMatching(ALL_KEYS)
|
|
1378
|
+
mountedQueries.clear()
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1341
1381
|
// --- Mutation runner (insert/update/delete with optimistic + invalidation) ---
|
|
1342
1382
|
|
|
1343
1383
|
const runMutation = async (config, element, context) => {
|
|
@@ -1372,7 +1412,6 @@ const runMutation = async (config, element, context) => {
|
|
|
1372
1412
|
// Apply optimistic update
|
|
1373
1413
|
const snapshot = optimistic ? applyOptimisticUpdate(element, config, mutationData) : null
|
|
1374
1414
|
|
|
1375
|
-
ref.__fetching = true
|
|
1376
1415
|
setFetchStatus(element, { isFetching: true, isLoading: false, isStale: false, isSuccess: false, error: null, status: 'pending', fetchStatus: 'fetching' })
|
|
1377
1416
|
triggerCallback(element, 'onFetchStart')
|
|
1378
1417
|
triggerConfigCallback(config, element, 'onFetchStart')
|
|
@@ -1412,7 +1451,7 @@ const runMutation = async (config, element, context) => {
|
|
|
1412
1451
|
|
|
1413
1452
|
if (error) {
|
|
1414
1453
|
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
1415
|
-
setFetchStatus(element,
|
|
1454
|
+
setFetchStatus(element, errorStatus(error))
|
|
1416
1455
|
triggerCallback(element, 'onFetchError', error)
|
|
1417
1456
|
if (isFunction(onError)) onError(error, mutationData, element)
|
|
1418
1457
|
if (isFunction(onSettled)) onSettled(null, error, mutationData, element)
|
|
@@ -1427,7 +1466,7 @@ const runMutation = async (config, element, context) => {
|
|
|
1427
1466
|
delete ref.__optimisticSnapshot
|
|
1428
1467
|
|
|
1429
1468
|
// The status first, as for a query: the state update re-renders.
|
|
1430
|
-
setFetchStatus(element,
|
|
1469
|
+
setFetchStatus(element, SUCCESS)
|
|
1431
1470
|
|
|
1432
1471
|
// A body value (Blob, stream, …) lands in state only under `as`.
|
|
1433
1472
|
if (data !== undefined && (stateKey || !isBodyValue(data))) {
|
|
@@ -1448,7 +1487,7 @@ const runMutation = async (config, element, context) => {
|
|
|
1448
1487
|
if (invalidates) runInvalidates(invalidates, from)
|
|
1449
1488
|
} catch (e) {
|
|
1450
1489
|
if (snapshot !== undefined) rollbackOptimistic(element, config)
|
|
1451
|
-
setFetchStatus(element,
|
|
1490
|
+
setFetchStatus(element, errorStatus(e))
|
|
1452
1491
|
triggerCallback(element, 'onFetchError', e)
|
|
1453
1492
|
triggerConfigCallback(config, element, 'onFetchError', e)
|
|
1454
1493
|
if (isFunction(onError)) onError(e, mutationData, element)
|
|
@@ -1487,6 +1526,7 @@ export const executeFetch = (param, element, state, context) => {
|
|
|
1487
1526
|
if (!param) return
|
|
1488
1527
|
const db = context?.fetch
|
|
1489
1528
|
if (!db) return
|
|
1529
|
+
enterContext(context)
|
|
1490
1530
|
|
|
1491
1531
|
const fetchProp = exec(param, element)
|
|
1492
1532
|
if (!fetchProp) return
|
|
@@ -1525,9 +1565,8 @@ export const executeFetch = (param, element, state, context) => {
|
|
|
1525
1565
|
if (config.on === 'create') {
|
|
1526
1566
|
runner(config, element, context)
|
|
1527
1567
|
} else {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
})
|
|
1568
|
+
// (queueMicrotask: see BUILTIN_ADAPTERS.)
|
|
1569
|
+
queueMicrotask(() => bindEvent({ ...config, __runner: runner }, element, context))
|
|
1531
1570
|
}
|
|
1532
1571
|
|
|
1533
1572
|
// Bind auto-refetch for queries
|
|
@@ -1582,11 +1621,7 @@ export const queryClient = {
|
|
|
1582
1621
|
// Drops every matching entry (same matches). A mounted query keeps the rows
|
|
1583
1622
|
// it shows; the next mount loads them afresh.
|
|
1584
1623
|
removeQueries: (keyOrPattern) => {
|
|
1585
|
-
|
|
1586
|
-
removeCache()
|
|
1587
|
-
return
|
|
1588
|
-
}
|
|
1589
|
-
removeMatching(keyMatcher(keyOrPattern))
|
|
1624
|
+
removeMatching(keyOrPattern ? keyMatcher(keyOrPattern) : ALL_KEYS)
|
|
1590
1625
|
},
|
|
1591
1626
|
|
|
1592
1627
|
getQueryData: (key) => {
|
|
@@ -1603,6 +1638,7 @@ export const queryClient = {
|
|
|
1603
1638
|
prefetchQuery: async (config, context) => {
|
|
1604
1639
|
const db = context?.fetch
|
|
1605
1640
|
if (!db) return
|
|
1641
|
+
enterContext(context)
|
|
1606
1642
|
|
|
1607
1643
|
const adapter = await resolveAdapter(db, context)
|
|
1608
1644
|
if (!adapter) return
|
|
@@ -1646,7 +1682,7 @@ export const queryClient = {
|
|
|
1646
1682
|
},
|
|
1647
1683
|
|
|
1648
1684
|
getCache: () => cacheStore,
|
|
1649
|
-
clear: () =>
|
|
1685
|
+
clear: () => removeMatching(ALL_KEYS),
|
|
1650
1686
|
|
|
1651
1687
|
// Drop every cache entry whose lang suffix isn't `keepLang`.
|
|
1652
1688
|
// Cache keys are of the form `${from}:${method}:${params}[:lang]` — entries
|
|
@@ -1688,11 +1724,13 @@ const keyFor = (keyOrConfig, element) => {
|
|
|
1688
1724
|
return elementQueryKey({ method: 'select', ...keyOrConfig }, element, element?.context)
|
|
1689
1725
|
}
|
|
1690
1726
|
|
|
1727
|
+
// Each enters the caller's context first, as a query does (enterContext): rows
|
|
1728
|
+
// a server render's project code writes before its first query stay its own.
|
|
1691
1729
|
export const fetchFunctions = {
|
|
1692
|
-
invalidateQueries (keyOrPattern) { return queryClient.invalidateQueries(keyOrPattern) },
|
|
1693
|
-
removeQueries (keyOrPattern) { return queryClient.removeQueries(keyOrPattern) },
|
|
1694
|
-
getQueryData (keyOrConfig) { return queryClient.getQueryData(keyFor(keyOrConfig, this)) },
|
|
1695
|
-
setQueryData (keyOrConfig, updater) { return queryClient.setQueryData(keyFor(keyOrConfig, this), updater) }
|
|
1730
|
+
invalidateQueries (keyOrPattern) { enterContext(this?.context); return queryClient.invalidateQueries(keyOrPattern) },
|
|
1731
|
+
removeQueries (keyOrPattern) { enterContext(this?.context); return queryClient.removeQueries(keyOrPattern) },
|
|
1732
|
+
getQueryData (keyOrConfig) { enterContext(this?.context); return queryClient.getQueryData(keyFor(keyOrConfig, this)) },
|
|
1733
|
+
setQueryData (keyOrConfig, updater) { enterContext(this?.context); return queryClient.setQueryData(keyFor(keyOrConfig, this), updater) }
|
|
1696
1734
|
}
|
|
1697
1735
|
|
|
1698
1736
|
// --- Plugin interface ---
|
|
@@ -1724,5 +1762,6 @@ export const fetchPlugin = {
|
|
|
1724
1762
|
}
|
|
1725
1763
|
|
|
1726
1764
|
export { parseDuration }
|
|
1727
|
-
export { earlyKey, EARLY_KEY, createEarlySeedScript } from './early.js'
|
|
1765
|
+
export { earlyKey, EARLY_KEY, createEarlySeedScript, createEarlySeedBlock, SEED_BLOCK } from './early.js'
|
|
1766
|
+
export { requestsInFlight, whenRequestsIdle } from './inflight.js'
|
|
1728
1767
|
export default executeFetch
|
package/inflight.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// --- Requests in flight ---
|
|
4
|
+
//
|
|
5
|
+
// Every request a REST adapter sends counts from its call until its body is
|
|
6
|
+
// read. smbls holds the hydration window over a pre-rendered page open until
|
|
7
|
+
// the first view's requests are done (smbls prerender.js), so a list that
|
|
8
|
+
// renders with its first data still adopts the server's rows.
|
|
9
|
+
//
|
|
10
|
+
// The count lives on globalThis under a Symbol.for slot, like the adapter
|
|
11
|
+
// registry (index.js CUSTOM_ADAPTERS): the copy of this package that smbls
|
|
12
|
+
// asks and the copy an app bundle sends through may be two module instances.
|
|
13
|
+
|
|
14
|
+
const KEY = Symbol.for('@symbo.ls/fetch:IN_FLIGHT')
|
|
15
|
+
|
|
16
|
+
const state = () => globalThis[KEY] || (globalThis[KEY] = { count: 0, waiters: [] })
|
|
17
|
+
|
|
18
|
+
export const requestStarted = () => {
|
|
19
|
+
state().count++
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const requestFinished = () => {
|
|
23
|
+
const s = state()
|
|
24
|
+
s.count = s.count > 0 ? s.count - 1 : 0
|
|
25
|
+
if (s.count === 0 && s.waiters.length) {
|
|
26
|
+
const waiters = s.waiters
|
|
27
|
+
s.waiters = []
|
|
28
|
+
for (const resolve of waiters) resolve()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** How many REST adapter requests are in flight on this page. */
|
|
33
|
+
export const requestsInFlight = () => state().count
|
|
34
|
+
|
|
35
|
+
/** Resolves once no REST adapter request is in flight (at once when none is). */
|
|
36
|
+
export const whenRequestsIdle = () => {
|
|
37
|
+
const s = state()
|
|
38
|
+
return s.count === 0 ? Promise.resolve() : new Promise((resolve) => { s.waiters.push(resolve) })
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/fetch",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.665",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"adapters"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@symbo.ls/signal": "^3.14.
|
|
45
|
-
"@symbo.ls/utils": "^3.14.
|
|
44
|
+
"@symbo.ls/signal": "^3.14.789",
|
|
45
|
+
"@symbo.ls/utils": "^3.14.787"
|
|
46
46
|
},
|
|
47
47
|
"sideEffects": false,
|
|
48
48
|
"publishConfig": {
|