@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.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
- let lastError;
533
- for (let attempt = 0; attempt <= retryCount; attempt++) {
534
- try {
535
- const result = await resolvedTransport(
536
- url,
537
- fetchInit,
538
- requestOptions?.transportOptions
539
- );
540
- if (result.ok) {
541
- return {
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
- error: result.data,
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 (entry.previousData !== void 0) {
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 names = new Set(plugins.map((p) => p.name));
914
+ const pluginNames = new Set(plugins.map((p) => p.name));
869
915
  for (const plugin of plugins) {
870
- for (const dep of plugin.dependencies ?? []) {
871
- if (!names.has(dep)) {
872
- throw new Error(
873
- `Plugin "${plugin.name}" depends on "${dep}" which is not registered`
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 sortByDependencies(plugins) {
880
- const sorted = [];
881
- const visited = /* @__PURE__ */ new Set();
882
- const visiting = /* @__PURE__ */ new Set();
883
- const pluginMap = new Map(plugins.map((p) => [p.name, p]));
884
- function visit(plugin) {
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 = sortByDependencies(initialPlugins);
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 coreFetch();
984
+ response = await tracedCoreFetch();
950
985
  } else {
951
986
  const chain = middlewares.reduceRight(
952
- (next, middleware) => {
953
- return () => middleware(
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 client = new Spoosh<ApiSchema, Error>('/api');
1046
+ * const spoosh = new Spoosh<ApiSchema, Error>('/api');
1011
1047
  *
1012
1048
  * // With default headers
1013
- * const client = new Spoosh<ApiSchema, Error>('/api', {
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 client = new Spoosh<ApiSchema, Error>('/api', {
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 **new** Spoosh instance with updated plugin types (immutable pattern).
1033
- * Each call to `.use()` replaces the previous plugins rather than adding to them.
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 new Spoosh instance with the specified plugins
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 client = new Spoosh<Schema, Error>('/api').use([
1076
+ * const spoosh = new Spoosh<Schema, Error>('/api').use([
1057
1077
  * cachePlugin({ staleTime: 5000 }),
1058
- * retryPlugin({ retries: 3, retryDelay: 1000 }),
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 client = new Spoosh<ApiSchema, Error>('/api').use([...]);
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: { headers: {} },
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;