@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.js
CHANGED
|
@@ -24,6 +24,7 @@ __export(src_exports, {
|
|
|
24
24
|
Spoosh: () => Spoosh,
|
|
25
25
|
__DEV__: () => __DEV__,
|
|
26
26
|
buildUrl: () => buildUrl,
|
|
27
|
+
clone: () => clone,
|
|
27
28
|
containsFile: () => containsFile,
|
|
28
29
|
createClient: () => createClient,
|
|
29
30
|
createEventEmitter: () => createEventEmitter,
|
|
@@ -35,6 +36,7 @@ __export(src_exports, {
|
|
|
35
36
|
createProxyHandler: () => createProxyHandler,
|
|
36
37
|
createSelectorProxy: () => createSelectorProxy,
|
|
37
38
|
createStateManager: () => createStateManager,
|
|
39
|
+
createTracer: () => createTracer,
|
|
38
40
|
executeFetch: () => executeFetch,
|
|
39
41
|
extractMethodFromSelector: () => extractMethodFromSelector,
|
|
40
42
|
extractPathFromSelector: () => extractPathFromSelector,
|
|
@@ -42,7 +44,9 @@ __export(src_exports, {
|
|
|
42
44
|
form: () => form,
|
|
43
45
|
generateTags: () => generateTags,
|
|
44
46
|
getContentType: () => getContentType,
|
|
47
|
+
isAbortError: () => isAbortError,
|
|
45
48
|
isJsonBody: () => isJsonBody,
|
|
49
|
+
isNetworkError: () => isNetworkError,
|
|
46
50
|
isSpooshBody: () => isSpooshBody,
|
|
47
51
|
json: () => json,
|
|
48
52
|
mergeHeaders: () => mergeHeaders,
|
|
@@ -352,6 +356,60 @@ function resolvePath(path, params) {
|
|
|
352
356
|
});
|
|
353
357
|
}
|
|
354
358
|
|
|
359
|
+
// src/utils/errors.ts
|
|
360
|
+
var isNetworkError = (err) => err instanceof TypeError;
|
|
361
|
+
var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
|
|
362
|
+
|
|
363
|
+
// src/utils/clone.ts
|
|
364
|
+
function clone(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
365
|
+
if (value === void 0 || value === null || typeof value !== "object") {
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
if (seen.has(value)) {
|
|
369
|
+
return seen.get(value);
|
|
370
|
+
}
|
|
371
|
+
if (Array.isArray(value)) {
|
|
372
|
+
const arr = [];
|
|
373
|
+
seen.set(value, arr);
|
|
374
|
+
return value.map((v) => clone(v, seen));
|
|
375
|
+
}
|
|
376
|
+
if (value instanceof Date) {
|
|
377
|
+
return new Date(value.getTime());
|
|
378
|
+
}
|
|
379
|
+
if (value instanceof RegExp) {
|
|
380
|
+
return new RegExp(value.source, value.flags);
|
|
381
|
+
}
|
|
382
|
+
if (value.constructor !== Object) {
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
const obj = {};
|
|
386
|
+
seen.set(value, obj);
|
|
387
|
+
for (const key in value) {
|
|
388
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
389
|
+
obj[key] = clone(value[key], seen);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return obj;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/utils/tracer.ts
|
|
396
|
+
function createTracer(plugin, trace) {
|
|
397
|
+
const step = (stage, reason, options) => {
|
|
398
|
+
trace?.step(() => ({
|
|
399
|
+
plugin,
|
|
400
|
+
stage,
|
|
401
|
+
reason,
|
|
402
|
+
color: options?.color,
|
|
403
|
+
diff: options?.diff
|
|
404
|
+
}));
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
return: (msg, options) => step("return", msg, options),
|
|
408
|
+
log: (msg, options) => step("log", msg, options),
|
|
409
|
+
skip: (msg, options) => step("skip", msg, options)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
355
413
|
// src/transport/fetch.ts
|
|
356
414
|
var fetchTransport = async (url, init) => {
|
|
357
415
|
const res = await fetch(url, init);
|
|
@@ -434,9 +492,6 @@ var xhrTransport = (url, init, options) => {
|
|
|
434
492
|
};
|
|
435
493
|
|
|
436
494
|
// src/fetch.ts
|
|
437
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
438
|
-
var isNetworkError = (err) => err instanceof TypeError;
|
|
439
|
-
var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
|
|
440
495
|
async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags) {
|
|
441
496
|
return executeCoreFetch({
|
|
442
497
|
baseUrl,
|
|
@@ -486,9 +541,6 @@ async function executeCoreFetch(config) {
|
|
|
486
541
|
...fetchDefaults
|
|
487
542
|
} = defaultOptions;
|
|
488
543
|
const inputFields = buildInputFields(requestOptions);
|
|
489
|
-
const maxRetries = requestOptions?.retries ?? 3;
|
|
490
|
-
const baseDelay = requestOptions?.retryDelay ?? 1e3;
|
|
491
|
-
const retryCount = maxRetries === false ? 0 : maxRetries;
|
|
492
544
|
const finalPath = path;
|
|
493
545
|
const url = buildUrl(baseUrl, finalPath, requestOptions?.query);
|
|
494
546
|
let headers = await mergeHeaders(defaultHeaders, requestOptions?.headers);
|
|
@@ -529,50 +581,44 @@ async function executeCoreFetch(config) {
|
|
|
529
581
|
const resolvedTransport = resolveTransport(
|
|
530
582
|
requestOptions?.transport ?? defaultTransport
|
|
531
583
|
);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
status: result.status,
|
|
543
|
-
data: result.data,
|
|
544
|
-
headers: result.headers,
|
|
545
|
-
error: void 0,
|
|
546
|
-
...inputFields
|
|
547
|
-
};
|
|
548
|
-
}
|
|
584
|
+
if (requestOptions && headers) {
|
|
585
|
+
requestOptions.headers = headers;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const result = await resolvedTransport(
|
|
589
|
+
url,
|
|
590
|
+
fetchInit,
|
|
591
|
+
requestOptions?.transportOptions
|
|
592
|
+
);
|
|
593
|
+
if (result.ok) {
|
|
549
594
|
return {
|
|
550
595
|
status: result.status,
|
|
551
|
-
|
|
596
|
+
data: result.data,
|
|
552
597
|
headers: result.headers,
|
|
598
|
+
error: void 0,
|
|
599
|
+
...inputFields
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const error = result.data !== void 0 && result.data !== "" ? result.data : {};
|
|
603
|
+
return {
|
|
604
|
+
status: result.status,
|
|
605
|
+
error,
|
|
606
|
+
headers: result.headers,
|
|
607
|
+
data: void 0,
|
|
608
|
+
...inputFields
|
|
609
|
+
};
|
|
610
|
+
} catch (err) {
|
|
611
|
+
if (isAbortError(err)) {
|
|
612
|
+
return {
|
|
613
|
+
status: 0,
|
|
614
|
+
error: err,
|
|
553
615
|
data: void 0,
|
|
616
|
+
aborted: true,
|
|
554
617
|
...inputFields
|
|
555
618
|
};
|
|
556
|
-
} catch (err) {
|
|
557
|
-
if (isAbortError(err)) {
|
|
558
|
-
return {
|
|
559
|
-
status: 0,
|
|
560
|
-
error: err,
|
|
561
|
-
data: void 0,
|
|
562
|
-
aborted: true,
|
|
563
|
-
...inputFields
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
lastError = err;
|
|
567
|
-
if (isNetworkError(err) && attempt < retryCount) {
|
|
568
|
-
const delayMs = baseDelay * Math.pow(2, attempt);
|
|
569
|
-
await delay(delayMs);
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
return { status: 0, error: lastError, data: void 0, ...inputFields };
|
|
573
619
|
}
|
|
620
|
+
return { status: 0, error: err, data: void 0, ...inputFields };
|
|
574
621
|
}
|
|
575
|
-
return { status: 0, error: lastError, data: void 0, ...inputFields };
|
|
576
622
|
}
|
|
577
623
|
|
|
578
624
|
// src/proxy/handler.ts
|
|
@@ -708,7 +754,7 @@ function createStateManager() {
|
|
|
708
754
|
if (entry.tags) {
|
|
709
755
|
existing.tags = entry.tags;
|
|
710
756
|
}
|
|
711
|
-
if (
|
|
757
|
+
if ("previousData" in entry) {
|
|
712
758
|
existing.previousData = entry.previousData;
|
|
713
759
|
}
|
|
714
760
|
if (entry.stale !== void 0) {
|
|
@@ -865,47 +911,31 @@ function createEventEmitter() {
|
|
|
865
911
|
|
|
866
912
|
// src/plugins/executor.ts
|
|
867
913
|
function validateDependencies(plugins) {
|
|
868
|
-
const
|
|
914
|
+
const pluginNames = new Set(plugins.map((p) => p.name));
|
|
869
915
|
for (const plugin of plugins) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
916
|
+
if (plugin.dependencies) {
|
|
917
|
+
for (const dep of plugin.dependencies) {
|
|
918
|
+
if (!pluginNames.has(dep)) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`Plugin "${plugin.name}" depends on "${dep}", but "${dep}" is not registered.`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
875
923
|
}
|
|
876
924
|
}
|
|
877
925
|
}
|
|
878
926
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (visited.has(plugin.name)) return;
|
|
886
|
-
if (visiting.has(plugin.name)) {
|
|
887
|
-
throw new Error(
|
|
888
|
-
`Circular dependency detected involving "${plugin.name}"`
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
visiting.add(plugin.name);
|
|
892
|
-
for (const dep of plugin.dependencies ?? []) {
|
|
893
|
-
const depPlugin = pluginMap.get(dep);
|
|
894
|
-
if (depPlugin) visit(depPlugin);
|
|
895
|
-
}
|
|
896
|
-
visiting.delete(plugin.name);
|
|
897
|
-
visited.add(plugin.name);
|
|
898
|
-
sorted.push(plugin);
|
|
899
|
-
}
|
|
900
|
-
for (const plugin of plugins) {
|
|
901
|
-
visit(plugin);
|
|
902
|
-
}
|
|
903
|
-
return sorted;
|
|
927
|
+
function sortByPriority(plugins) {
|
|
928
|
+
return [...plugins].sort((a, b) => {
|
|
929
|
+
const priorityA = a.priority ?? 0;
|
|
930
|
+
const priorityB = b.priority ?? 0;
|
|
931
|
+
return priorityA - priorityB;
|
|
932
|
+
});
|
|
904
933
|
}
|
|
905
934
|
function createPluginExecutor(initialPlugins = []) {
|
|
906
935
|
validateDependencies(initialPlugins);
|
|
907
|
-
const plugins =
|
|
936
|
+
const plugins = sortByPriority(initialPlugins);
|
|
908
937
|
const frozenPlugins = Object.freeze([...plugins]);
|
|
938
|
+
const contextEnhancers = [];
|
|
909
939
|
const createPluginAccessor = (context) => ({
|
|
910
940
|
get(name) {
|
|
911
941
|
const plugin = plugins.find((p) => p.name === name);
|
|
@@ -944,41 +974,47 @@ function createPluginExecutor(initialPlugins = []) {
|
|
|
944
974
|
(p) => p.operations.includes(operationType)
|
|
945
975
|
);
|
|
946
976
|
const middlewares = applicablePlugins.filter((p) => p.middleware).map((p) => p.middleware);
|
|
977
|
+
const tracedCoreFetch = async () => {
|
|
978
|
+
const fetchTracer = context.tracer?.("spoosh:fetch");
|
|
979
|
+
fetchTracer?.log("Network request");
|
|
980
|
+
return coreFetch();
|
|
981
|
+
};
|
|
947
982
|
let response;
|
|
948
983
|
if (middlewares.length === 0) {
|
|
949
|
-
response = await
|
|
984
|
+
response = await tracedCoreFetch();
|
|
950
985
|
} else {
|
|
951
986
|
const chain = middlewares.reduceRight(
|
|
952
|
-
(next, middleware) =>
|
|
953
|
-
|
|
954
|
-
context,
|
|
955
|
-
next
|
|
956
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
957
|
-
);
|
|
958
|
-
},
|
|
959
|
-
coreFetch
|
|
987
|
+
(next, middleware) => () => middleware(context, next),
|
|
988
|
+
tracedCoreFetch
|
|
960
989
|
);
|
|
961
990
|
response = await chain();
|
|
962
991
|
}
|
|
963
992
|
for (const plugin of applicablePlugins) {
|
|
964
993
|
if (plugin.afterResponse) {
|
|
965
|
-
const newResponse = await plugin.afterResponse(
|
|
966
|
-
context,
|
|
967
|
-
response
|
|
968
|
-
);
|
|
994
|
+
const newResponse = await plugin.afterResponse(context, response);
|
|
969
995
|
if (newResponse) {
|
|
970
996
|
response = newResponse;
|
|
971
997
|
}
|
|
972
998
|
}
|
|
973
999
|
}
|
|
1000
|
+
context.eventEmitter.emit(
|
|
1001
|
+
"spoosh:request-complete",
|
|
1002
|
+
{ context, queryKey: context.queryKey }
|
|
1003
|
+
);
|
|
974
1004
|
return response;
|
|
975
1005
|
},
|
|
976
1006
|
getPlugins() {
|
|
977
1007
|
return frozenPlugins;
|
|
978
1008
|
},
|
|
1009
|
+
registerContextEnhancer(enhancer) {
|
|
1010
|
+
contextEnhancers.push(enhancer);
|
|
1011
|
+
},
|
|
979
1012
|
createContext(input) {
|
|
980
1013
|
const ctx = input;
|
|
981
1014
|
ctx.plugins = createPluginAccessor(ctx);
|
|
1015
|
+
for (const enhancer of contextEnhancers) {
|
|
1016
|
+
enhancer(ctx);
|
|
1017
|
+
}
|
|
982
1018
|
return ctx;
|
|
983
1019
|
}
|
|
984
1020
|
};
|
|
@@ -1007,15 +1043,15 @@ var Spoosh = class _Spoosh {
|
|
|
1007
1043
|
* @example
|
|
1008
1044
|
* ```ts
|
|
1009
1045
|
* // Simple usage
|
|
1010
|
-
* const
|
|
1046
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api');
|
|
1011
1047
|
*
|
|
1012
1048
|
* // With default headers
|
|
1013
|
-
* const
|
|
1049
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api', {
|
|
1014
1050
|
* headers: { 'X-API-Key': 'secret' }
|
|
1015
1051
|
* });
|
|
1016
1052
|
*
|
|
1017
1053
|
* // With XHR transport (narrows available options to XHR-compatible fields)
|
|
1018
|
-
* const
|
|
1054
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api', {
|
|
1019
1055
|
* transport: 'xhr',
|
|
1020
1056
|
* credentials: 'include',
|
|
1021
1057
|
* });
|
|
@@ -1029,33 +1065,17 @@ var Spoosh = class _Spoosh {
|
|
|
1029
1065
|
/**
|
|
1030
1066
|
* Adds plugins to the Spoosh instance.
|
|
1031
1067
|
*
|
|
1032
|
-
* Returns a
|
|
1033
|
-
*
|
|
1068
|
+
* Returns a configured Spoosh instance with the specified plugins.
|
|
1069
|
+
* Can only be called once - the returned instance does not have `.use()`.
|
|
1034
1070
|
*
|
|
1035
1071
|
* @template TNewPlugins - The const tuple type of the new plugins array
|
|
1036
1072
|
* @param plugins - Array of plugin instances to use
|
|
1037
|
-
* @returns A
|
|
1038
|
-
*
|
|
1039
|
-
* @example Single use() call
|
|
1040
|
-
* ```ts
|
|
1041
|
-
* const client = new Spoosh<Schema, Error>('/api')
|
|
1042
|
-
* .use([cachePlugin(), retryPlugin(), debouncePlugin()]);
|
|
1043
|
-
* ```
|
|
1044
|
-
*
|
|
1045
|
-
* @example Chaining use() calls (replaces plugins)
|
|
1046
|
-
* ```ts
|
|
1047
|
-
* const client1 = new Spoosh<Schema, Error>('/api')
|
|
1048
|
-
* .use([cachePlugin()]);
|
|
1073
|
+
* @returns A configured Spoosh instance (without `.use()` method)
|
|
1049
1074
|
*
|
|
1050
|
-
* // This replaces cachePlugin with retryPlugin
|
|
1051
|
-
* const client2 = client1.use([retryPlugin()]);
|
|
1052
|
-
* ```
|
|
1053
|
-
*
|
|
1054
|
-
* @example With plugin configuration
|
|
1055
1075
|
* ```ts
|
|
1056
|
-
* const
|
|
1076
|
+
* const spoosh = new Spoosh<Schema, Error>('/api').use([
|
|
1057
1077
|
* cachePlugin({ staleTime: 5000 }),
|
|
1058
|
-
*
|
|
1078
|
+
* invalidationPlugin(),
|
|
1059
1079
|
* prefetchPlugin(),
|
|
1060
1080
|
* ]);
|
|
1061
1081
|
* ```
|
|
@@ -1112,7 +1132,7 @@ var Spoosh = class _Spoosh {
|
|
|
1112
1132
|
*
|
|
1113
1133
|
* @example
|
|
1114
1134
|
* ```ts
|
|
1115
|
-
* const
|
|
1135
|
+
* const spoosh = new Spoosh<ApiSchema, Error>('/api').use([...]);
|
|
1116
1136
|
* const { api } = client;
|
|
1117
1137
|
*
|
|
1118
1138
|
* // GET request
|
|
@@ -1542,7 +1562,7 @@ function createInfiniteReadController(options) {
|
|
|
1542
1562
|
(key) => stateManager.subscribeCache(key, notify)
|
|
1543
1563
|
);
|
|
1544
1564
|
};
|
|
1545
|
-
const createContext = (pageKey) => {
|
|
1565
|
+
const createContext = (pageKey, requestOptions) => {
|
|
1546
1566
|
return pluginExecutor.createContext({
|
|
1547
1567
|
operationType: "infiniteRead",
|
|
1548
1568
|
path,
|
|
@@ -1551,7 +1571,12 @@ function createInfiniteReadController(options) {
|
|
|
1551
1571
|
tags,
|
|
1552
1572
|
requestTimestamp: Date.now(),
|
|
1553
1573
|
instanceId,
|
|
1554
|
-
request: {
|
|
1574
|
+
request: {
|
|
1575
|
+
headers: {},
|
|
1576
|
+
query: requestOptions?.query,
|
|
1577
|
+
params: requestOptions?.params,
|
|
1578
|
+
body: requestOptions?.body
|
|
1579
|
+
},
|
|
1555
1580
|
temp: /* @__PURE__ */ new Map(),
|
|
1556
1581
|
pluginOptions,
|
|
1557
1582
|
stateManager,
|
|
@@ -1575,7 +1600,7 @@ function createInfiniteReadController(options) {
|
|
|
1575
1600
|
notify();
|
|
1576
1601
|
abortController = new AbortController();
|
|
1577
1602
|
const signal = abortController.signal;
|
|
1578
|
-
const context = createContext(pageKey);
|
|
1603
|
+
const context = createContext(pageKey, mergedRequest);
|
|
1579
1604
|
const coreFetch = async () => {
|
|
1580
1605
|
const fetchPromise = (async () => {
|
|
1581
1606
|
try {
|
|
@@ -1730,7 +1755,7 @@ function createInfiniteReadController(options) {
|
|
|
1730
1755
|
loadFromTracker();
|
|
1731
1756
|
cachedState = computeState();
|
|
1732
1757
|
subscribeToPages();
|
|
1733
|
-
const context = createContext(trackerKey);
|
|
1758
|
+
const context = createContext(trackerKey, initialRequest);
|
|
1734
1759
|
pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
|
|
1735
1760
|
refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
|
|
1736
1761
|
const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
|
|
@@ -1747,7 +1772,7 @@ function createInfiniteReadController(options) {
|
|
|
1747
1772
|
}
|
|
1748
1773
|
},
|
|
1749
1774
|
unmount() {
|
|
1750
|
-
const context = createContext(trackerKey);
|
|
1775
|
+
const context = createContext(trackerKey, initialRequest);
|
|
1751
1776
|
pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
|
|
1752
1777
|
pageSubscriptions.forEach((unsub) => unsub());
|
|
1753
1778
|
pageSubscriptions = [];
|
|
@@ -1755,7 +1780,7 @@ function createInfiniteReadController(options) {
|
|
|
1755
1780
|
refetchUnsubscribe = null;
|
|
1756
1781
|
},
|
|
1757
1782
|
update(previousContext) {
|
|
1758
|
-
const context = createContext(trackerKey);
|
|
1783
|
+
const context = createContext(trackerKey, initialRequest);
|
|
1759
1784
|
pluginExecutor.executeUpdateLifecycle(
|
|
1760
1785
|
"infiniteRead",
|
|
1761
1786
|
context,
|
|
@@ -1763,7 +1788,7 @@ function createInfiniteReadController(options) {
|
|
|
1763
1788
|
);
|
|
1764
1789
|
},
|
|
1765
1790
|
getContext() {
|
|
1766
|
-
return createContext(trackerKey);
|
|
1791
|
+
return createContext(trackerKey, initialRequest);
|
|
1767
1792
|
},
|
|
1768
1793
|
setPluginOptions(opts) {
|
|
1769
1794
|
pluginOptions = opts;
|