@spoosh/core 0.12.1 → 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,
@@ -358,6 +360,56 @@ function resolvePath(path, params) {
358
360
  var isNetworkError = (err) => err instanceof TypeError;
359
361
  var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
360
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
+
361
413
  // src/transport/fetch.ts
362
414
  var fetchTransport = async (url, init) => {
363
415
  const res = await fetch(url, init);
@@ -529,6 +581,9 @@ async function executeCoreFetch(config) {
529
581
  const resolvedTransport = resolveTransport(
530
582
  requestOptions?.transport ?? defaultTransport
531
583
  );
584
+ if (requestOptions && headers) {
585
+ requestOptions.headers = headers;
586
+ }
532
587
  try {
533
588
  const result = await resolvedTransport(
534
589
  url,
@@ -544,9 +599,10 @@ async function executeCoreFetch(config) {
544
599
  ...inputFields
545
600
  };
546
601
  }
602
+ const error = result.data !== void 0 && result.data !== "" ? result.data : {};
547
603
  return {
548
604
  status: result.status,
549
- error: result.data,
605
+ error,
550
606
  headers: result.headers,
551
607
  data: void 0,
552
608
  ...inputFields
@@ -698,7 +754,7 @@ function createStateManager() {
698
754
  if (entry.tags) {
699
755
  existing.tags = entry.tags;
700
756
  }
701
- if (entry.previousData !== void 0) {
757
+ if ("previousData" in entry) {
702
758
  existing.previousData = entry.previousData;
703
759
  }
704
760
  if (entry.stale !== void 0) {
@@ -879,6 +935,7 @@ function createPluginExecutor(initialPlugins = []) {
879
935
  validateDependencies(initialPlugins);
880
936
  const plugins = sortByPriority(initialPlugins);
881
937
  const frozenPlugins = Object.freeze([...plugins]);
938
+ const contextEnhancers = [];
882
939
  const createPluginAccessor = (context) => ({
883
940
  get(name) {
884
941
  const plugin = plugins.find((p) => p.name === name);
@@ -917,41 +974,47 @@ function createPluginExecutor(initialPlugins = []) {
917
974
  (p) => p.operations.includes(operationType)
918
975
  );
919
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
+ };
920
982
  let response;
921
983
  if (middlewares.length === 0) {
922
- response = await coreFetch();
984
+ response = await tracedCoreFetch();
923
985
  } else {
924
986
  const chain = middlewares.reduceRight(
925
- (next, middleware) => {
926
- return () => middleware(
927
- context,
928
- next
929
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
930
- );
931
- },
932
- coreFetch
987
+ (next, middleware) => () => middleware(context, next),
988
+ tracedCoreFetch
933
989
  );
934
990
  response = await chain();
935
991
  }
936
992
  for (const plugin of applicablePlugins) {
937
993
  if (plugin.afterResponse) {
938
- const newResponse = await plugin.afterResponse(
939
- context,
940
- response
941
- );
994
+ const newResponse = await plugin.afterResponse(context, response);
942
995
  if (newResponse) {
943
996
  response = newResponse;
944
997
  }
945
998
  }
946
999
  }
1000
+ context.eventEmitter.emit(
1001
+ "spoosh:request-complete",
1002
+ { context, queryKey: context.queryKey }
1003
+ );
947
1004
  return response;
948
1005
  },
949
1006
  getPlugins() {
950
1007
  return frozenPlugins;
951
1008
  },
1009
+ registerContextEnhancer(enhancer) {
1010
+ contextEnhancers.push(enhancer);
1011
+ },
952
1012
  createContext(input) {
953
1013
  const ctx = input;
954
1014
  ctx.plugins = createPluginAccessor(ctx);
1015
+ for (const enhancer of contextEnhancers) {
1016
+ enhancer(ctx);
1017
+ }
955
1018
  return ctx;
956
1019
  }
957
1020
  };
@@ -1002,12 +1065,12 @@ var Spoosh = class _Spoosh {
1002
1065
  /**
1003
1066
  * Adds plugins to the Spoosh instance.
1004
1067
  *
1005
- * Returns a **new** Spoosh instance with updated plugin types (immutable pattern).
1006
- * 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()`.
1007
1070
  *
1008
1071
  * @template TNewPlugins - The const tuple type of the new plugins array
1009
1072
  * @param plugins - Array of plugin instances to use
1010
- * @returns A new Spoosh instance with the specified plugins
1073
+ * @returns A configured Spoosh instance (without `.use()` method)
1011
1074
  *
1012
1075
  * ```ts
1013
1076
  * const spoosh = new Spoosh<Schema, Error>('/api').use([
@@ -1499,7 +1562,7 @@ function createInfiniteReadController(options) {
1499
1562
  (key) => stateManager.subscribeCache(key, notify)
1500
1563
  );
1501
1564
  };
1502
- const createContext = (pageKey) => {
1565
+ const createContext = (pageKey, requestOptions) => {
1503
1566
  return pluginExecutor.createContext({
1504
1567
  operationType: "infiniteRead",
1505
1568
  path,
@@ -1508,7 +1571,12 @@ function createInfiniteReadController(options) {
1508
1571
  tags,
1509
1572
  requestTimestamp: Date.now(),
1510
1573
  instanceId,
1511
- request: { headers: {} },
1574
+ request: {
1575
+ headers: {},
1576
+ query: requestOptions?.query,
1577
+ params: requestOptions?.params,
1578
+ body: requestOptions?.body
1579
+ },
1512
1580
  temp: /* @__PURE__ */ new Map(),
1513
1581
  pluginOptions,
1514
1582
  stateManager,
@@ -1532,7 +1600,7 @@ function createInfiniteReadController(options) {
1532
1600
  notify();
1533
1601
  abortController = new AbortController();
1534
1602
  const signal = abortController.signal;
1535
- const context = createContext(pageKey);
1603
+ const context = createContext(pageKey, mergedRequest);
1536
1604
  const coreFetch = async () => {
1537
1605
  const fetchPromise = (async () => {
1538
1606
  try {
@@ -1687,7 +1755,7 @@ function createInfiniteReadController(options) {
1687
1755
  loadFromTracker();
1688
1756
  cachedState = computeState();
1689
1757
  subscribeToPages();
1690
- const context = createContext(trackerKey);
1758
+ const context = createContext(trackerKey, initialRequest);
1691
1759
  pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
1692
1760
  refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
1693
1761
  const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
@@ -1704,7 +1772,7 @@ function createInfiniteReadController(options) {
1704
1772
  }
1705
1773
  },
1706
1774
  unmount() {
1707
- const context = createContext(trackerKey);
1775
+ const context = createContext(trackerKey, initialRequest);
1708
1776
  pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
1709
1777
  pageSubscriptions.forEach((unsub) => unsub());
1710
1778
  pageSubscriptions = [];
@@ -1712,7 +1780,7 @@ function createInfiniteReadController(options) {
1712
1780
  refetchUnsubscribe = null;
1713
1781
  },
1714
1782
  update(previousContext) {
1715
- const context = createContext(trackerKey);
1783
+ const context = createContext(trackerKey, initialRequest);
1716
1784
  pluginExecutor.executeUpdateLifecycle(
1717
1785
  "infiniteRead",
1718
1786
  context,
@@ -1720,7 +1788,7 @@ function createInfiniteReadController(options) {
1720
1788
  );
1721
1789
  },
1722
1790
  getContext() {
1723
- return createContext(trackerKey);
1791
+ return createContext(trackerKey, initialRequest);
1724
1792
  },
1725
1793
  setPluginOptions(opts) {
1726
1794
  pluginOptions = opts;
package/dist/index.mjs CHANGED
@@ -295,6 +295,56 @@ function resolvePath(path, params) {
295
295
  var isNetworkError = (err) => err instanceof TypeError;
296
296
  var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
297
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
+
298
348
  // src/transport/fetch.ts
299
349
  var fetchTransport = async (url, init) => {
300
350
  const res = await fetch(url, init);
@@ -466,6 +516,9 @@ async function executeCoreFetch(config) {
466
516
  const resolvedTransport = resolveTransport(
467
517
  requestOptions?.transport ?? defaultTransport
468
518
  );
519
+ if (requestOptions && headers) {
520
+ requestOptions.headers = headers;
521
+ }
469
522
  try {
470
523
  const result = await resolvedTransport(
471
524
  url,
@@ -481,9 +534,10 @@ async function executeCoreFetch(config) {
481
534
  ...inputFields
482
535
  };
483
536
  }
537
+ const error = result.data !== void 0 && result.data !== "" ? result.data : {};
484
538
  return {
485
539
  status: result.status,
486
- error: result.data,
540
+ error,
487
541
  headers: result.headers,
488
542
  data: void 0,
489
543
  ...inputFields
@@ -635,7 +689,7 @@ function createStateManager() {
635
689
  if (entry.tags) {
636
690
  existing.tags = entry.tags;
637
691
  }
638
- if (entry.previousData !== void 0) {
692
+ if ("previousData" in entry) {
639
693
  existing.previousData = entry.previousData;
640
694
  }
641
695
  if (entry.stale !== void 0) {
@@ -816,6 +870,7 @@ function createPluginExecutor(initialPlugins = []) {
816
870
  validateDependencies(initialPlugins);
817
871
  const plugins = sortByPriority(initialPlugins);
818
872
  const frozenPlugins = Object.freeze([...plugins]);
873
+ const contextEnhancers = [];
819
874
  const createPluginAccessor = (context) => ({
820
875
  get(name) {
821
876
  const plugin = plugins.find((p) => p.name === name);
@@ -854,41 +909,47 @@ function createPluginExecutor(initialPlugins = []) {
854
909
  (p) => p.operations.includes(operationType)
855
910
  );
856
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
+ };
857
917
  let response;
858
918
  if (middlewares.length === 0) {
859
- response = await coreFetch();
919
+ response = await tracedCoreFetch();
860
920
  } else {
861
921
  const chain = middlewares.reduceRight(
862
- (next, middleware) => {
863
- return () => middleware(
864
- context,
865
- next
866
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
867
- );
868
- },
869
- coreFetch
922
+ (next, middleware) => () => middleware(context, next),
923
+ tracedCoreFetch
870
924
  );
871
925
  response = await chain();
872
926
  }
873
927
  for (const plugin of applicablePlugins) {
874
928
  if (plugin.afterResponse) {
875
- const newResponse = await plugin.afterResponse(
876
- context,
877
- response
878
- );
929
+ const newResponse = await plugin.afterResponse(context, response);
879
930
  if (newResponse) {
880
931
  response = newResponse;
881
932
  }
882
933
  }
883
934
  }
935
+ context.eventEmitter.emit(
936
+ "spoosh:request-complete",
937
+ { context, queryKey: context.queryKey }
938
+ );
884
939
  return response;
885
940
  },
886
941
  getPlugins() {
887
942
  return frozenPlugins;
888
943
  },
944
+ registerContextEnhancer(enhancer) {
945
+ contextEnhancers.push(enhancer);
946
+ },
889
947
  createContext(input) {
890
948
  const ctx = input;
891
949
  ctx.plugins = createPluginAccessor(ctx);
950
+ for (const enhancer of contextEnhancers) {
951
+ enhancer(ctx);
952
+ }
892
953
  return ctx;
893
954
  }
894
955
  };
@@ -939,12 +1000,12 @@ var Spoosh = class _Spoosh {
939
1000
  /**
940
1001
  * Adds plugins to the Spoosh instance.
941
1002
  *
942
- * Returns a **new** Spoosh instance with updated plugin types (immutable pattern).
943
- * 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()`.
944
1005
  *
945
1006
  * @template TNewPlugins - The const tuple type of the new plugins array
946
1007
  * @param plugins - Array of plugin instances to use
947
- * @returns A new Spoosh instance with the specified plugins
1008
+ * @returns A configured Spoosh instance (without `.use()` method)
948
1009
  *
949
1010
  * ```ts
950
1011
  * const spoosh = new Spoosh<Schema, Error>('/api').use([
@@ -1436,7 +1497,7 @@ function createInfiniteReadController(options) {
1436
1497
  (key) => stateManager.subscribeCache(key, notify)
1437
1498
  );
1438
1499
  };
1439
- const createContext = (pageKey) => {
1500
+ const createContext = (pageKey, requestOptions) => {
1440
1501
  return pluginExecutor.createContext({
1441
1502
  operationType: "infiniteRead",
1442
1503
  path,
@@ -1445,7 +1506,12 @@ function createInfiniteReadController(options) {
1445
1506
  tags,
1446
1507
  requestTimestamp: Date.now(),
1447
1508
  instanceId,
1448
- request: { headers: {} },
1509
+ request: {
1510
+ headers: {},
1511
+ query: requestOptions?.query,
1512
+ params: requestOptions?.params,
1513
+ body: requestOptions?.body
1514
+ },
1449
1515
  temp: /* @__PURE__ */ new Map(),
1450
1516
  pluginOptions,
1451
1517
  stateManager,
@@ -1469,7 +1535,7 @@ function createInfiniteReadController(options) {
1469
1535
  notify();
1470
1536
  abortController = new AbortController();
1471
1537
  const signal = abortController.signal;
1472
- const context = createContext(pageKey);
1538
+ const context = createContext(pageKey, mergedRequest);
1473
1539
  const coreFetch = async () => {
1474
1540
  const fetchPromise = (async () => {
1475
1541
  try {
@@ -1624,7 +1690,7 @@ function createInfiniteReadController(options) {
1624
1690
  loadFromTracker();
1625
1691
  cachedState = computeState();
1626
1692
  subscribeToPages();
1627
- const context = createContext(trackerKey);
1693
+ const context = createContext(trackerKey, initialRequest);
1628
1694
  pluginExecutor.executeLifecycle("onMount", "infiniteRead", context);
1629
1695
  refetchUnsubscribe = eventEmitter.on("refetch", (event) => {
1630
1696
  const isRelevant = event.queryKey === trackerKey || pageKeys.includes(event.queryKey);
@@ -1641,7 +1707,7 @@ function createInfiniteReadController(options) {
1641
1707
  }
1642
1708
  },
1643
1709
  unmount() {
1644
- const context = createContext(trackerKey);
1710
+ const context = createContext(trackerKey, initialRequest);
1645
1711
  pluginExecutor.executeLifecycle("onUnmount", "infiniteRead", context);
1646
1712
  pageSubscriptions.forEach((unsub) => unsub());
1647
1713
  pageSubscriptions = [];
@@ -1649,7 +1715,7 @@ function createInfiniteReadController(options) {
1649
1715
  refetchUnsubscribe = null;
1650
1716
  },
1651
1717
  update(previousContext) {
1652
- const context = createContext(trackerKey);
1718
+ const context = createContext(trackerKey, initialRequest);
1653
1719
  pluginExecutor.executeUpdateLifecycle(
1654
1720
  "infiniteRead",
1655
1721
  context,
@@ -1657,7 +1723,7 @@ function createInfiniteReadController(options) {
1657
1723
  );
1658
1724
  },
1659
1725
  getContext() {
1660
- return createContext(trackerKey);
1726
+ return createContext(trackerKey, initialRequest);
1661
1727
  },
1662
1728
  setPluginOptions(opts) {
1663
1729
  pluginOptions = opts;
@@ -1670,6 +1736,7 @@ export {
1670
1736
  Spoosh,
1671
1737
  __DEV__,
1672
1738
  buildUrl,
1739
+ clone,
1673
1740
  containsFile,
1674
1741
  createClient,
1675
1742
  createEventEmitter,
@@ -1681,6 +1748,7 @@ export {
1681
1748
  createProxyHandler,
1682
1749
  createSelectorProxy,
1683
1750
  createStateManager,
1751
+ createTracer,
1684
1752
  executeFetch,
1685
1753
  extractMethodFromSelector,
1686
1754
  extractPathFromSelector,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API toolkit with plugin middleware system",
6
6
  "keywords": [