@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/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
- let lastError;
472
- for (let attempt = 0; attempt <= retryCount; attempt++) {
473
- try {
474
- const result = await resolvedTransport(
475
- url,
476
- fetchInit,
477
- requestOptions?.transportOptions
478
- );
479
- if (result.ok) {
480
- return {
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
- error: result.data,
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 (entry.previousData !== void 0) {
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 names = new Set(plugins.map((p) => p.name));
849
+ const pluginNames = new Set(plugins.map((p) => p.name));
808
850
  for (const plugin of plugins) {
809
- for (const dep of plugin.dependencies ?? []) {
810
- if (!names.has(dep)) {
811
- throw new Error(
812
- `Plugin "${plugin.name}" depends on "${dep}" which is not registered`
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 sortByDependencies(plugins) {
819
- const sorted = [];
820
- const visited = /* @__PURE__ */ new Set();
821
- const visiting = /* @__PURE__ */ new Set();
822
- const pluginMap = new Map(plugins.map((p) => [p.name, p]));
823
- function visit(plugin) {
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 = sortByDependencies(initialPlugins);
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 coreFetch();
919
+ response = await tracedCoreFetch();
889
920
  } else {
890
921
  const chain = middlewares.reduceRight(
891
- (next, middleware) => {
892
- return () => middleware(
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 client = new Spoosh<ApiSchema, Error>('/api');
981
+ * const spoosh = new Spoosh<ApiSchema, Error>('/api');
950
982
  *
951
983
  * // With default headers
952
- * const client = new Spoosh<ApiSchema, Error>('/api', {
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 client = new Spoosh<ApiSchema, Error>('/api', {
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 **new** Spoosh instance with updated plugin types (immutable pattern).
972
- * Each call to `.use()` replaces the previous plugins rather than adding to them.
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 new Spoosh instance with the specified plugins
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 client = new Spoosh<Schema, Error>('/api').use([
1011
+ * const spoosh = new Spoosh<Schema, Error>('/api').use([
996
1012
  * cachePlugin({ staleTime: 5000 }),
997
- * retryPlugin({ retries: 3, retryDelay: 1000 }),
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 client = new Spoosh<ApiSchema, Error>('/api').use([...]);
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: { headers: {} },
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API toolkit with plugin middleware system",
6
6
  "keywords": [