@spoosh/core 0.12.0 → 0.13.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/README.md +1 -1
- package/dist/index.d.mts +369 -58
- package/dist/index.d.ts +369 -58
- package/dist/index.js +145 -120
- package/dist/index.mjs +145 -120
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -291,6 +291,60 @@ function resolvePath(path, params) {
|
|
|
291
291
|
});
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// src/utils/errors.ts
|
|
295
|
+
var isNetworkError = (err) => err instanceof TypeError;
|
|
296
|
+
var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
|
|
297
|
+
|
|
298
|
+
// src/utils/clone.ts
|
|
299
|
+
function clone(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
300
|
+
if (value === void 0 || value === null || typeof value !== "object") {
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
if (seen.has(value)) {
|
|
304
|
+
return seen.get(value);
|
|
305
|
+
}
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
const arr = [];
|
|
308
|
+
seen.set(value, arr);
|
|
309
|
+
return value.map((v) => clone(v, seen));
|
|
310
|
+
}
|
|
311
|
+
if (value instanceof Date) {
|
|
312
|
+
return new Date(value.getTime());
|
|
313
|
+
}
|
|
314
|
+
if (value instanceof RegExp) {
|
|
315
|
+
return new RegExp(value.source, value.flags);
|
|
316
|
+
}
|
|
317
|
+
if (value.constructor !== Object) {
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
const obj = {};
|
|
321
|
+
seen.set(value, obj);
|
|
322
|
+
for (const key in value) {
|
|
323
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
324
|
+
obj[key] = clone(value[key], seen);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return obj;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/utils/tracer.ts
|
|
331
|
+
function createTracer(plugin, trace) {
|
|
332
|
+
const step = (stage, reason, options) => {
|
|
333
|
+
trace?.step(() => ({
|
|
334
|
+
plugin,
|
|
335
|
+
stage,
|
|
336
|
+
reason,
|
|
337
|
+
color: options?.color,
|
|
338
|
+
diff: options?.diff
|
|
339
|
+
}));
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
return: (msg, options) => step("return", msg, options),
|
|
343
|
+
log: (msg, options) => step("log", msg, options),
|
|
344
|
+
skip: (msg, options) => step("skip", msg, options)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
294
348
|
// src/transport/fetch.ts
|
|
295
349
|
var fetchTransport = async (url, init) => {
|
|
296
350
|
const res = await fetch(url, init);
|
|
@@ -373,9 +427,6 @@ var xhrTransport = (url, init, options) => {
|
|
|
373
427
|
};
|
|
374
428
|
|
|
375
429
|
// src/fetch.ts
|
|
376
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
377
|
-
var isNetworkError = (err) => err instanceof TypeError;
|
|
378
|
-
var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
|
|
379
430
|
async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags) {
|
|
380
431
|
return executeCoreFetch({
|
|
381
432
|
baseUrl,
|
|
@@ -425,9 +476,6 @@ async function executeCoreFetch(config) {
|
|
|
425
476
|
...fetchDefaults
|
|
426
477
|
} = defaultOptions;
|
|
427
478
|
const inputFields = buildInputFields(requestOptions);
|
|
428
|
-
const maxRetries = requestOptions?.retries ?? 3;
|
|
429
|
-
const baseDelay = requestOptions?.retryDelay ?? 1e3;
|
|
430
|
-
const retryCount = maxRetries === false ? 0 : maxRetries;
|
|
431
479
|
const finalPath = path;
|
|
432
480
|
const url = buildUrl(baseUrl, finalPath, requestOptions?.query);
|
|
433
481
|
let headers = await mergeHeaders(defaultHeaders, requestOptions?.headers);
|
|
@@ -468,50 +516,44 @@ async function executeCoreFetch(config) {
|
|
|
468
516
|
const resolvedTransport = resolveTransport(
|
|
469
517
|
requestOptions?.transport ?? defaultTransport
|
|
470
518
|
);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
status: result.status,
|
|
482
|
-
data: result.data,
|
|
483
|
-
headers: result.headers,
|
|
484
|
-
error: void 0,
|
|
485
|
-
...inputFields
|
|
486
|
-
};
|
|
487
|
-
}
|
|
519
|
+
if (requestOptions && headers) {
|
|
520
|
+
requestOptions.headers = headers;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const result = await resolvedTransport(
|
|
524
|
+
url,
|
|
525
|
+
fetchInit,
|
|
526
|
+
requestOptions?.transportOptions
|
|
527
|
+
);
|
|
528
|
+
if (result.ok) {
|
|
488
529
|
return {
|
|
489
530
|
status: result.status,
|
|
490
|
-
|
|
531
|
+
data: result.data,
|
|
491
532
|
headers: result.headers,
|
|
533
|
+
error: void 0,
|
|
534
|
+
...inputFields
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const error = result.data !== void 0 && result.data !== "" ? result.data : {};
|
|
538
|
+
return {
|
|
539
|
+
status: result.status,
|
|
540
|
+
error,
|
|
541
|
+
headers: result.headers,
|
|
542
|
+
data: void 0,
|
|
543
|
+
...inputFields
|
|
544
|
+
};
|
|
545
|
+
} catch (err) {
|
|
546
|
+
if (isAbortError(err)) {
|
|
547
|
+
return {
|
|
548
|
+
status: 0,
|
|
549
|
+
error: err,
|
|
492
550
|
data: void 0,
|
|
551
|
+
aborted: true,
|
|
493
552
|
...inputFields
|
|
494
553
|
};
|
|
495
|
-
} catch (err) {
|
|
496
|
-
if (isAbortError(err)) {
|
|
497
|
-
return {
|
|
498
|
-
status: 0,
|
|
499
|
-
error: err,
|
|
500
|
-
data: void 0,
|
|
501
|
-
aborted: true,
|
|
502
|
-
...inputFields
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
lastError = err;
|
|
506
|
-
if (isNetworkError(err) && attempt < retryCount) {
|
|
507
|
-
const delayMs = baseDelay * Math.pow(2, attempt);
|
|
508
|
-
await delay(delayMs);
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
return { status: 0, error: lastError, data: void 0, ...inputFields };
|
|
512
554
|
}
|
|
555
|
+
return { status: 0, error: err, data: void 0, ...inputFields };
|
|
513
556
|
}
|
|
514
|
-
return { status: 0, error: lastError, data: void 0, ...inputFields };
|
|
515
557
|
}
|
|
516
558
|
|
|
517
559
|
// src/proxy/handler.ts
|
|
@@ -647,7 +689,7 @@ function createStateManager() {
|
|
|
647
689
|
if (entry.tags) {
|
|
648
690
|
existing.tags = entry.tags;
|
|
649
691
|
}
|
|
650
|
-
if (
|
|
692
|
+
if ("previousData" in entry) {
|
|
651
693
|
existing.previousData = entry.previousData;
|
|
652
694
|
}
|
|
653
695
|
if (entry.stale !== void 0) {
|
|
@@ -804,47 +846,31 @@ function createEventEmitter() {
|
|
|
804
846
|
|
|
805
847
|
// src/plugins/executor.ts
|
|
806
848
|
function validateDependencies(plugins) {
|
|
807
|
-
const
|
|
849
|
+
const pluginNames = new Set(plugins.map((p) => p.name));
|
|
808
850
|
for (const plugin of plugins) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
851
|
+
if (plugin.dependencies) {
|
|
852
|
+
for (const dep of plugin.dependencies) {
|
|
853
|
+
if (!pluginNames.has(dep)) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`Plugin "${plugin.name}" depends on "${dep}", but "${dep}" is not registered.`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
814
858
|
}
|
|
815
859
|
}
|
|
816
860
|
}
|
|
817
861
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (visited.has(plugin.name)) return;
|
|
825
|
-
if (visiting.has(plugin.name)) {
|
|
826
|
-
throw new Error(
|
|
827
|
-
`Circular dependency detected involving "${plugin.name}"`
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
visiting.add(plugin.name);
|
|
831
|
-
for (const dep of plugin.dependencies ?? []) {
|
|
832
|
-
const depPlugin = pluginMap.get(dep);
|
|
833
|
-
if (depPlugin) visit(depPlugin);
|
|
834
|
-
}
|
|
835
|
-
visiting.delete(plugin.name);
|
|
836
|
-
visited.add(plugin.name);
|
|
837
|
-
sorted.push(plugin);
|
|
838
|
-
}
|
|
839
|
-
for (const plugin of plugins) {
|
|
840
|
-
visit(plugin);
|
|
841
|
-
}
|
|
842
|
-
return sorted;
|
|
862
|
+
function sortByPriority(plugins) {
|
|
863
|
+
return [...plugins].sort((a, b) => {
|
|
864
|
+
const priorityA = a.priority ?? 0;
|
|
865
|
+
const priorityB = b.priority ?? 0;
|
|
866
|
+
return priorityA - priorityB;
|
|
867
|
+
});
|
|
843
868
|
}
|
|
844
869
|
function createPluginExecutor(initialPlugins = []) {
|
|
845
870
|
validateDependencies(initialPlugins);
|
|
846
|
-
const plugins =
|
|
871
|
+
const plugins = sortByPriority(initialPlugins);
|
|
847
872
|
const frozenPlugins = Object.freeze([...plugins]);
|
|
873
|
+
const contextEnhancers = [];
|
|
848
874
|
const createPluginAccessor = (context) => ({
|
|
849
875
|
get(name) {
|
|
850
876
|
const plugin = plugins.find((p) => p.name === name);
|
|
@@ -883,41 +909,47 @@ function createPluginExecutor(initialPlugins = []) {
|
|
|
883
909
|
(p) => p.operations.includes(operationType)
|
|
884
910
|
);
|
|
885
911
|
const middlewares = applicablePlugins.filter((p) => p.middleware).map((p) => p.middleware);
|
|
912
|
+
const tracedCoreFetch = async () => {
|
|
913
|
+
const fetchTracer = context.tracer?.("spoosh:fetch");
|
|
914
|
+
fetchTracer?.log("Network request");
|
|
915
|
+
return coreFetch();
|
|
916
|
+
};
|
|
886
917
|
let response;
|
|
887
918
|
if (middlewares.length === 0) {
|
|
888
|
-
response = await
|
|
919
|
+
response = await tracedCoreFetch();
|
|
889
920
|
} else {
|
|
890
921
|
const chain = middlewares.reduceRight(
|
|
891
|
-
(next, middleware) =>
|
|
892
|
-
|
|
893
|
-
context,
|
|
894
|
-
next
|
|
895
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
896
|
-
);
|
|
897
|
-
},
|
|
898
|
-
coreFetch
|
|
922
|
+
(next, middleware) => () => middleware(context, next),
|
|
923
|
+
tracedCoreFetch
|
|
899
924
|
);
|
|
900
925
|
response = await chain();
|
|
901
926
|
}
|
|
902
927
|
for (const plugin of applicablePlugins) {
|
|
903
928
|
if (plugin.afterResponse) {
|
|
904
|
-
const newResponse = await plugin.afterResponse(
|
|
905
|
-
context,
|
|
906
|
-
response
|
|
907
|
-
);
|
|
929
|
+
const newResponse = await plugin.afterResponse(context, response);
|
|
908
930
|
if (newResponse) {
|
|
909
931
|
response = newResponse;
|
|
910
932
|
}
|
|
911
933
|
}
|
|
912
934
|
}
|
|
935
|
+
context.eventEmitter.emit(
|
|
936
|
+
"spoosh:request-complete",
|
|
937
|
+
{ context, queryKey: context.queryKey }
|
|
938
|
+
);
|
|
913
939
|
return response;
|
|
914
940
|
},
|
|
915
941
|
getPlugins() {
|
|
916
942
|
return frozenPlugins;
|
|
917
943
|
},
|
|
944
|
+
registerContextEnhancer(enhancer) {
|
|
945
|
+
contextEnhancers.push(enhancer);
|
|
946
|
+
},
|
|
918
947
|
createContext(input) {
|
|
919
948
|
const ctx = input;
|
|
920
949
|
ctx.plugins = createPluginAccessor(ctx);
|
|
950
|
+
for (const enhancer of contextEnhancers) {
|
|
951
|
+
enhancer(ctx);
|
|
952
|
+
}
|
|
921
953
|
return ctx;
|
|
922
954
|
}
|
|
923
955
|
};
|
|
@@ -946,15 +978,15 @@ var Spoosh = class _Spoosh {
|
|
|
946
978
|
* @example
|
|
947
979
|
* ```ts
|
|
948
980
|
* // Simple usage
|
|
949
|
-
* const
|
|
981
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api');
|
|
950
982
|
*
|
|
951
983
|
* // With default headers
|
|
952
|
-
* const
|
|
984
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api', {
|
|
953
985
|
* headers: { 'X-API-Key': 'secret' }
|
|
954
986
|
* });
|
|
955
987
|
*
|
|
956
988
|
* // With XHR transport (narrows available options to XHR-compatible fields)
|
|
957
|
-
* const
|
|
989
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api', {
|
|
958
990
|
* transport: 'xhr',
|
|
959
991
|
* credentials: 'include',
|
|
960
992
|
* });
|
|
@@ -968,33 +1000,17 @@ var Spoosh = class _Spoosh {
|
|
|
968
1000
|
/**
|
|
969
1001
|
* Adds plugins to the Spoosh instance.
|
|
970
1002
|
*
|
|
971
|
-
* Returns a
|
|
972
|
-
*
|
|
1003
|
+
* Returns a configured Spoosh instance with the specified plugins.
|
|
1004
|
+
* Can only be called once - the returned instance does not have `.use()`.
|
|
973
1005
|
*
|
|
974
1006
|
* @template TNewPlugins - The const tuple type of the new plugins array
|
|
975
1007
|
* @param plugins - Array of plugin instances to use
|
|
976
|
-
* @returns A
|
|
977
|
-
*
|
|
978
|
-
* @example Single use() call
|
|
979
|
-
* ```ts
|
|
980
|
-
* const client = new Spoosh<Schema, Error>('/api')
|
|
981
|
-
* .use([cachePlugin(), retryPlugin(), debouncePlugin()]);
|
|
982
|
-
* ```
|
|
983
|
-
*
|
|
984
|
-
* @example Chaining use() calls (replaces plugins)
|
|
985
|
-
* ```ts
|
|
986
|
-
* const client1 = new Spoosh<Schema, Error>('/api')
|
|
987
|
-
* .use([cachePlugin()]);
|
|
1008
|
+
* @returns A configured Spoosh instance (without `.use()` method)
|
|
988
1009
|
*
|
|
989
|
-
* // This replaces cachePlugin with retryPlugin
|
|
990
|
-
* const client2 = client1.use([retryPlugin()]);
|
|
991
|
-
* ```
|
|
992
|
-
*
|
|
993
|
-
* @example With plugin configuration
|
|
994
1010
|
* ```ts
|
|
995
|
-
* const
|
|
1011
|
+
* const spoosh = new Spoosh<Schema, Error>('/api').use([
|
|
996
1012
|
* cachePlugin({ staleTime: 5000 }),
|
|
997
|
-
*
|
|
1013
|
+
* invalidationPlugin(),
|
|
998
1014
|
* prefetchPlugin(),
|
|
999
1015
|
* ]);
|
|
1000
1016
|
* ```
|
|
@@ -1051,7 +1067,7 @@ var Spoosh = class _Spoosh {
|
|
|
1051
1067
|
*
|
|
1052
1068
|
* @example
|
|
1053
1069
|
* ```ts
|
|
1054
|
-
* const
|
|
1070
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api').use([...]);
|
|
1055
1071
|
* const { api } = client;
|
|
1056
1072
|
*
|
|
1057
1073
|
* // GET request
|
|
@@ -1481,7 +1497,7 @@ function createInfiniteReadController(options) {
|
|
|
1481
1497
|
(key) => stateManager.subscribeCache(key, notify)
|
|
1482
1498
|
);
|
|
1483
1499
|
};
|
|
1484
|
-
const createContext = (pageKey) => {
|
|
1500
|
+
const createContext = (pageKey, requestOptions) => {
|
|
1485
1501
|
return pluginExecutor.createContext({
|
|
1486
1502
|
operationType: "infiniteRead",
|
|
1487
1503
|
path,
|
|
@@ -1490,7 +1506,12 @@ function createInfiniteReadController(options) {
|
|
|
1490
1506
|
tags,
|
|
1491
1507
|
requestTimestamp: Date.now(),
|
|
1492
1508
|
instanceId,
|
|
1493
|
-
request: {
|
|
1509
|
+
request: {
|
|
1510
|
+
headers: {},
|
|
1511
|
+
query: requestOptions?.query,
|
|
1512
|
+
params: requestOptions?.params,
|
|
1513
|
+
body: requestOptions?.body
|
|
1514
|
+
},
|
|
1494
1515
|
temp: /* @__PURE__ */ new Map(),
|
|
1495
1516
|
pluginOptions,
|
|
1496
1517
|
stateManager,
|
|
@@ -1514,7 +1535,7 @@ function createInfiniteReadController(options) {
|
|
|
1514
1535
|
notify();
|
|
1515
1536
|
abortController = new AbortController();
|
|
1516
1537
|
const signal = abortController.signal;
|
|
1517
|
-
const context = createContext(pageKey);
|
|
1538
|
+
const context = createContext(pageKey, mergedRequest);
|
|
1518
1539
|
const coreFetch = async () => {
|
|
1519
1540
|
const fetchPromise = (async () => {
|
|
1520
1541
|
try {
|
|
@@ -1669,7 +1690,7 @@ function createInfiniteReadController(options) {
|
|
|
1669
1690
|
loadFromTracker();
|
|
1670
1691
|
cachedState = computeState();
|
|
1671
1692
|
subscribeToPages();
|
|
1672
|
-
const context = createContext(trackerKey);
|
|
1693
|
+
const context = createContext(trackerKey, initialRequest);
|
|
1673
1694
|
pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
|
|
1674
1695
|
refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
|
|
1675
1696
|
const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
|
|
@@ -1686,7 +1707,7 @@ function createInfiniteReadController(options) {
|
|
|
1686
1707
|
}
|
|
1687
1708
|
},
|
|
1688
1709
|
unmount() {
|
|
1689
|
-
const context = createContext(trackerKey);
|
|
1710
|
+
const context = createContext(trackerKey, initialRequest);
|
|
1690
1711
|
pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
|
|
1691
1712
|
pageSubscriptions.forEach((unsub) => unsub());
|
|
1692
1713
|
pageSubscriptions = [];
|
|
@@ -1694,7 +1715,7 @@ function createInfiniteReadController(options) {
|
|
|
1694
1715
|
refetchUnsubscribe = null;
|
|
1695
1716
|
},
|
|
1696
1717
|
update(previousContext) {
|
|
1697
|
-
const context = createContext(trackerKey);
|
|
1718
|
+
const context = createContext(trackerKey, initialRequest);
|
|
1698
1719
|
pluginExecutor.executeUpdateLifecycle(
|
|
1699
1720
|
"infiniteRead",
|
|
1700
1721
|
context,
|
|
@@ -1702,7 +1723,7 @@ function createInfiniteReadController(options) {
|
|
|
1702
1723
|
);
|
|
1703
1724
|
},
|
|
1704
1725
|
getContext() {
|
|
1705
|
-
return createContext(trackerKey);
|
|
1726
|
+
return createContext(trackerKey, initialRequest);
|
|
1706
1727
|
},
|
|
1707
1728
|
setPluginOptions(opts) {
|
|
1708
1729
|
pluginOptions = opts;
|
|
@@ -1715,6 +1736,7 @@ export {
|
|
|
1715
1736
|
Spoosh,
|
|
1716
1737
|
__DEV__,
|
|
1717
1738
|
buildUrl,
|
|
1739
|
+
clone,
|
|
1718
1740
|
containsFile,
|
|
1719
1741
|
createClient,
|
|
1720
1742
|
createEventEmitter,
|
|
@@ -1726,6 +1748,7 @@ export {
|
|
|
1726
1748
|
createProxyHandler,
|
|
1727
1749
|
createSelectorProxy,
|
|
1728
1750
|
createStateManager,
|
|
1751
|
+
createTracer,
|
|
1729
1752
|
executeFetch,
|
|
1730
1753
|
extractMethodFromSelector,
|
|
1731
1754
|
extractPathFromSelector,
|
|
@@ -1733,7 +1756,9 @@ export {
|
|
|
1733
1756
|
form,
|
|
1734
1757
|
generateTags,
|
|
1735
1758
|
getContentType,
|
|
1759
|
+
isAbortError,
|
|
1736
1760
|
isJsonBody,
|
|
1761
|
+
isNetworkError,
|
|
1737
1762
|
isSpooshBody,
|
|
1738
1763
|
json,
|
|
1739
1764
|
mergeHeaders,
|