sibujs 3.1.0 → 3.2.1

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.
Files changed (78) hide show
  1. package/README.md +6 -0
  2. package/dist/browser.cjs +16 -8
  3. package/dist/browser.js +6 -5
  4. package/dist/build.cjs +235 -147
  5. package/dist/build.js +35 -24
  6. package/dist/cdn.global.js +7 -7
  7. package/dist/{chunk-WYU7CYJ3.js → chunk-2C4E3HBM.js} +5 -5
  8. package/dist/{chunk-3DYB5B3S.js → chunk-4JCAUOLN.js} +45 -23
  9. package/dist/{chunk-2HAGQWDV.js → chunk-5N74TKLD.js} +1 -1
  10. package/dist/{chunk-SVVAUX7J.js → chunk-7XDYVJLE.js} +19 -9
  11. package/dist/{chunk-2N2UL7O4.js → chunk-BGNLPNGV.js} +20 -12
  12. package/dist/{chunk-RK4BQG25.js → chunk-C427DVQF.js} +1 -1
  13. package/dist/{chunk-ZIBE2SAT.js → chunk-FDY42FIU.js} +3 -2
  14. package/dist/{chunk-GQ7RRFPU.js → chunk-FOI23UJL.js} +11 -1
  15. package/dist/{chunk-2RA7SHDA.js → chunk-GOJMFRBL.js} +20 -4
  16. package/dist/{chunk-IVOUCSZL.js → chunk-GOUM4JCT.js} +6 -6
  17. package/dist/chunk-H3SRKIYX.js +17 -0
  18. package/dist/{chunk-3DJH25UO.js → chunk-H6PCHJZQ.js} +2 -2
  19. package/dist/{chunk-UCS6AMJ7.js → chunk-HMJFCBRR.js} +26 -3
  20. package/dist/{chunk-JYD2PWXH.js → chunk-HXMS4SNP.js} +22 -15
  21. package/dist/{chunk-SC437AMI.js → chunk-JYXOEYI4.js} +12 -18
  22. package/dist/{chunk-KB3BA2XK.js → chunk-NFYWLRUO.js} +11 -18
  23. package/dist/{chunk-QNQY5DUS.js → chunk-NPIEEKPT.js} +20 -11
  24. package/dist/{chunk-UYX2NDOH.js → chunk-OYLPZO4N.js} +33 -15
  25. package/dist/{chunk-LYTCUZ7H.js → chunk-RDRSWYNP.js} +1 -1
  26. package/dist/{chunk-2ZJ7TSW4.js → chunk-RLUJL2MV.js} +4 -8
  27. package/dist/{chunk-CR4MXPHB.js → chunk-V2MTG5FT.js} +99 -36
  28. package/dist/{chunk-CNZ35WI2.js → chunk-VJE6DDYM.js} +2 -2
  29. package/dist/{chunk-PMSDFTK3.js → chunk-VOCE4NNK.js} +157 -75
  30. package/dist/{chunk-WKUXSE7V.js → chunk-X67UYC74.js} +12 -11
  31. package/dist/{chunk-EFOAE5NC.js → chunk-YFDGQWDA.js} +1 -1
  32. package/dist/{chunk-3U4ZVXVD.js → chunk-Z2FWAE4B.js} +6 -2
  33. package/dist/data.cjs +190 -94
  34. package/dist/data.d.cts +7 -1
  35. package/dist/data.d.ts +7 -1
  36. package/dist/data.js +8 -8
  37. package/dist/devtools.cjs +38 -10
  38. package/dist/devtools.d.cts +1 -1
  39. package/dist/devtools.d.ts +1 -1
  40. package/dist/devtools.js +6 -6
  41. package/dist/ecosystem.cjs +123 -63
  42. package/dist/ecosystem.js +9 -9
  43. package/dist/extras.cjs +380 -196
  44. package/dist/extras.d.cts +2 -2
  45. package/dist/extras.d.ts +2 -2
  46. package/dist/extras.js +27 -24
  47. package/dist/index.cjs +214 -136
  48. package/dist/index.d.cts +15 -2
  49. package/dist/index.d.ts +15 -2
  50. package/dist/index.js +15 -13
  51. package/dist/{introspect-BZWKvQUZ.d.ts → introspect-DOZfmC-4.d.ts} +1 -1
  52. package/dist/{introspect-DsJlDD2T.d.cts → introspect-RjLfIFpL.d.cts} +1 -1
  53. package/dist/motion.cjs +10 -0
  54. package/dist/motion.js +3 -3
  55. package/dist/patterns.cjs +45 -40
  56. package/dist/patterns.js +8 -7
  57. package/dist/performance.cjs +101 -25
  58. package/dist/performance.d.cts +2 -2
  59. package/dist/performance.d.ts +2 -2
  60. package/dist/performance.js +8 -7
  61. package/dist/plugins.cjs +234 -160
  62. package/dist/plugins.d.cts +1 -1
  63. package/dist/plugins.d.ts +1 -1
  64. package/dist/plugins.js +127 -69
  65. package/dist/{ssr-FXD2PPMC.js → ssr-2QDQ27EV.js} +5 -3
  66. package/dist/{ssr-CrVNy6Pa.d.cts → ssr-D62yFwuw.d.cts} +8 -1
  67. package/dist/{ssr-CrVNy6Pa.d.ts → ssr-D62yFwuw.d.ts} +8 -1
  68. package/dist/ssr.cjs +145 -66
  69. package/dist/ssr.d.cts +1 -1
  70. package/dist/ssr.d.ts +1 -1
  71. package/dist/ssr.js +12 -10
  72. package/dist/testing.cjs +9 -4
  73. package/dist/testing.js +3 -3
  74. package/dist/ui.cjs +54 -38
  75. package/dist/ui.js +10 -9
  76. package/dist/widgets.cjs +40 -24
  77. package/dist/widgets.js +8 -8
  78. package/package.json +3 -1
package/dist/data.cjs CHANGED
@@ -210,7 +210,7 @@ function track(effectFn, subscriber) {
210
210
  function reactiveBinding(commit) {
211
211
  const run = () => {
212
212
  const s = subscriber;
213
- if (s._reentrant) return;
213
+ if (s._disposed || s._reentrant) return;
214
214
  s._reentrant = true;
215
215
  try {
216
216
  retrack(commit, subscriber);
@@ -226,8 +226,12 @@ function reactiveBinding(commit) {
226
226
  subscriber._runEpoch = 0;
227
227
  subscriber._runs = 0;
228
228
  subscriber._reentrant = false;
229
+ subscriber._disposed = false;
229
230
  run();
230
- return subscriber._dispose ?? (subscriber._dispose = () => cleanup(subscriber));
231
+ return subscriber._dispose ?? (subscriber._dispose = () => {
232
+ subscriber._disposed = true;
233
+ cleanup(subscriber);
234
+ });
231
235
  }
232
236
  function recordDependency(signal2) {
233
237
  if (!currentSubscriber) return;
@@ -417,6 +421,7 @@ function derived(getter, options) {
417
421
  const equals = options?.equals;
418
422
  const cs = {};
419
423
  cs._d = false;
424
+ cs._init = false;
420
425
  cs._g = getter;
421
426
  cs.__v = 0;
422
427
  const markDirty = () => {
@@ -425,11 +430,18 @@ function derived(getter, options) {
425
430
  };
426
431
  markDirty._c = 1;
427
432
  markDirty._sig = cs;
433
+ const recompute = () => {
434
+ const next = getter();
435
+ cs._v = equals && cs._init ? equals(cs._v, next) ? cs._v : next : next;
436
+ cs._d = false;
437
+ cs._init = true;
438
+ };
428
439
  track(() => {
429
440
  let threw = true;
430
441
  try {
431
442
  cs._v = getter();
432
443
  cs._d = false;
444
+ cs._init = true;
433
445
  threw = false;
434
446
  } finally {
435
447
  if (threw) cs._d = true;
@@ -445,20 +457,13 @@ function derived(getter, options) {
445
457
  }
446
458
  if (trackingSuspended) {
447
459
  if (cs._d) {
460
+ const prev = cs._v;
448
461
  evaluating = true;
449
- let threw = true;
450
462
  try {
451
- const prev = cs._v;
452
- retrack(() => {
453
- const next = getter();
454
- cs._v = equals && cs._v !== void 0 ? equals(cs._v, next) ? cs._v : next : next;
455
- cs._d = false;
456
- threw = false;
457
- }, markDirty);
463
+ retrack(recompute, markDirty);
458
464
  if (!Object.is(prev, cs._v)) cs.__v++;
459
465
  } finally {
460
466
  evaluating = false;
461
- if (threw) cs._d = true;
462
467
  }
463
468
  }
464
469
  return cs._v;
@@ -467,18 +472,11 @@ function derived(getter, options) {
467
472
  if (cs._d) {
468
473
  const oldValue = cs._v;
469
474
  evaluating = true;
470
- let threw = true;
471
475
  try {
472
- retrack(() => {
473
- const next = getter();
474
- cs._v = equals && cs._v !== void 0 ? equals(cs._v, next) ? cs._v : next : next;
475
- cs._d = false;
476
- threw = false;
477
- }, markDirty);
476
+ retrack(recompute, markDirty);
478
477
  if (!Object.is(oldValue, cs._v)) cs.__v++;
479
478
  } finally {
480
479
  evaluating = false;
481
- if (threw) cs._d = true;
482
480
  }
483
481
  if (hook && oldValue !== cs._v) {
484
482
  hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
@@ -499,11 +497,15 @@ function derived(getter, options) {
499
497
  var als = null;
500
498
  try {
501
499
  if (typeof process !== "undefined" && process.versions && process.versions.node) {
502
- const req = Function("return typeof require==='function'?require:null")();
503
- if (req) {
504
- const mod = req("node:async_hooks");
505
- als = new mod.AsyncLocalStorage();
500
+ let mod = null;
501
+ const getBuiltin = process.getBuiltinModule;
502
+ if (typeof getBuiltin === "function") {
503
+ mod = getBuiltin("node:async_hooks");
504
+ } else {
505
+ const req = Function("return typeof require==='function'?require:null")();
506
+ if (req) mod = req("node:async_hooks");
506
507
  }
508
+ if (mod) als = new mod.AsyncLocalStorage();
507
509
  }
508
510
  } catch {
509
511
  als = null;
@@ -519,6 +521,17 @@ function getSSRStore() {
519
521
  function isSSR() {
520
522
  return getSSRStore().ssr;
521
523
  }
524
+ function getRequestScopedCache(name) {
525
+ if (!isSSR()) return null;
526
+ const store = getSSRStore();
527
+ const caches = store.caches ?? (store.caches = /* @__PURE__ */ new Map());
528
+ let c = caches.get(name);
529
+ if (!c) {
530
+ c = /* @__PURE__ */ new Map();
531
+ caches.set(name, c);
532
+ }
533
+ return c;
534
+ }
522
535
 
523
536
  // src/core/signals/effect.ts
524
537
  var _g = globalThis;
@@ -605,6 +618,7 @@ function effect(effectFn, options) {
605
618
  ctx.fn(ctx.onCleanup);
606
619
  };
607
620
  const sub = (() => {
621
+ if (ctx.disposed) return;
608
622
  if (ctx.running) {
609
623
  ctx.rerunPending = true;
610
624
  return;
@@ -801,9 +815,12 @@ async function withRetry(fn, options, onRetry, signal2) {
801
815
  }
802
816
 
803
817
  // src/data/query.ts
804
- var queryCache = /* @__PURE__ */ new Map();
805
- function getOrCreateEntry(key, initialData) {
806
- let entry = queryCache.get(key);
818
+ var globalQueryCache = /* @__PURE__ */ new Map();
819
+ function getActiveQueryCache() {
820
+ return getRequestScopedCache("query") ?? globalQueryCache;
821
+ }
822
+ function getOrCreateEntry(cache, key, initialData) {
823
+ let entry = cache.get(key);
807
824
  if (!entry) {
808
825
  entry = {
809
826
  data: initialData,
@@ -815,7 +832,7 @@ function getOrCreateEntry(key, initialData) {
815
832
  listeners: /* @__PURE__ */ new Set(),
816
833
  refetchers: /* @__PURE__ */ new Set()
817
834
  };
818
- queryCache.set(key, entry);
835
+ cache.set(key, entry);
819
836
  }
820
837
  return entry;
821
838
  }
@@ -835,6 +852,7 @@ function query(key, fetcher, options = {}) {
835
852
  select
836
853
  } = options;
837
854
  const resolveKey = typeof key === "function" ? key : () => key;
855
+ const cache = getActiveQueryCache();
838
856
  const [data, setData] = signal(initialData);
839
857
  const [isFetching, setIsFetching] = signal(false);
840
858
  const [error, setError] = signal(void 0);
@@ -846,16 +864,16 @@ function query(key, fetcher, options = {}) {
846
864
  const isStale = derived(() => {
847
865
  data();
848
866
  if (!currentKey) return true;
849
- const entry = queryCache.get(currentKey);
867
+ const entry = cache.get(currentKey);
850
868
  if (!entry || entry.dataUpdatedAt === 0) return true;
851
869
  return Date.now() - entry.dataUpdatedAt >= staleTime;
852
870
  });
853
871
  async function doFetch() {
854
872
  if (disposed || !currentKey || !enabled) return;
855
873
  const key2 = currentKey;
856
- let entry = queryCache.get(key2);
874
+ let entry = cache.get(key2);
857
875
  if (!entry) {
858
- entry = getOrCreateEntry(key2);
876
+ entry = getOrCreateEntry(cache, key2);
859
877
  entry.listeners.add(onCacheUpdate);
860
878
  entry.refetchers.add(doFetch);
861
879
  }
@@ -930,7 +948,7 @@ function query(key, fetcher, options = {}) {
930
948
  }
931
949
  function onCacheUpdate() {
932
950
  if (disposed || !currentKey) return;
933
- const entry = queryCache.get(currentKey);
951
+ const entry = cache.get(currentKey);
934
952
  if (!entry) {
935
953
  batch(() => {
936
954
  setData(void 0);
@@ -950,7 +968,7 @@ function query(key, fetcher, options = {}) {
950
968
  const effectCleanup = effect(() => {
951
969
  const key2 = resolveKey();
952
970
  if (currentKey !== null && currentKey !== key2) {
953
- const oldEntry = queryCache.get(currentKey);
971
+ const oldEntry = cache.get(currentKey);
954
972
  if (oldEntry) {
955
973
  oldEntry.listeners.delete(onCacheUpdate);
956
974
  oldEntry.refetchers.delete(doFetch);
@@ -958,13 +976,13 @@ function query(key, fetcher, options = {}) {
958
976
  if (oldEntry.subscribers <= 0 && cacheTime >= 0) {
959
977
  const oldKey = currentKey;
960
978
  if (oldEntry.gcTimer !== null) clearTimeout(oldEntry.gcTimer);
961
- oldEntry.gcTimer = setTimeout(() => queryCache.delete(oldKey), cacheTime);
979
+ oldEntry.gcTimer = setTimeout(() => cache.delete(oldKey), cacheTime);
962
980
  }
963
981
  }
964
982
  }
965
983
  const keyChanged = currentKey !== key2;
966
984
  currentKey = key2;
967
- const entry = getOrCreateEntry(key2, initialData);
985
+ const entry = getOrCreateEntry(cache, key2, initialData);
968
986
  if (keyChanged) entry.subscribers++;
969
987
  if (entry.gcTimer !== null) {
970
988
  clearTimeout(entry.gcTimer);
@@ -1018,7 +1036,7 @@ function query(key, fetcher, options = {}) {
1018
1036
  effectCleanup();
1019
1037
  if (intervalTimer) clearInterval(intervalTimer);
1020
1038
  if (currentKey) {
1021
- const entry = queryCache.get(currentKey);
1039
+ const entry = cache.get(currentKey);
1022
1040
  if (entry) {
1023
1041
  entry.listeners.delete(onCacheUpdate);
1024
1042
  entry.refetchers.delete(doFetch);
@@ -1026,7 +1044,7 @@ function query(key, fetcher, options = {}) {
1026
1044
  if (entry.subscribers <= 0 && cacheTime >= 0) {
1027
1045
  const key2 = currentKey;
1028
1046
  if (entry.gcTimer !== null) clearTimeout(entry.gcTimer);
1029
- entry.gcTimer = setTimeout(() => queryCache.delete(key2), cacheTime);
1047
+ entry.gcTimer = setTimeout(() => cache.delete(key2), cacheTime);
1030
1048
  }
1031
1049
  }
1032
1050
  }
@@ -1049,7 +1067,7 @@ function query(key, fetcher, options = {}) {
1049
1067
  }
1050
1068
  function invalidateQueries(keyOrPredicate) {
1051
1069
  const predicate = typeof keyOrPredicate === "function" ? keyOrPredicate : (k) => k === keyOrPredicate;
1052
- for (const [key, entry] of queryCache.entries()) {
1070
+ for (const [key, entry] of getActiveQueryCache().entries()) {
1053
1071
  if (predicate(key)) {
1054
1072
  entry.dataUpdatedAt = 0;
1055
1073
  for (const refetcher of entry.refetchers) refetcher();
@@ -1057,10 +1075,10 @@ function invalidateQueries(keyOrPredicate) {
1057
1075
  }
1058
1076
  }
1059
1077
  function getQueryData(key) {
1060
- return queryCache.get(key)?.data;
1078
+ return getActiveQueryCache().get(key)?.data;
1061
1079
  }
1062
1080
  function setQueryData(key, data) {
1063
- const entry = queryCache.get(key);
1081
+ const entry = getActiveQueryCache().get(key);
1064
1082
  if (!entry) return;
1065
1083
  const newData = typeof data === "function" ? data(entry.data) : data;
1066
1084
  entry.data = newData;
@@ -1070,14 +1088,15 @@ function setQueryData(key, data) {
1070
1088
  function clearQueryCache() {
1071
1089
  const activeListeners = [];
1072
1090
  const activeRefetchers = [];
1073
- for (const entry of queryCache.values()) {
1091
+ const activeCache = getActiveQueryCache();
1092
+ for (const entry of activeCache.values()) {
1074
1093
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
1075
1094
  if (entry.subscribers > 0) {
1076
1095
  for (const listener of entry.listeners) activeListeners.push(listener);
1077
1096
  for (const refetcher of entry.refetchers) activeRefetchers.push(refetcher);
1078
1097
  }
1079
1098
  }
1080
- queryCache.clear();
1099
+ activeCache.clear();
1081
1100
  for (const listener of activeListeners) listener();
1082
1101
  for (const refetcher of activeRefetchers) {
1083
1102
  refetcher().catch((err) => {
@@ -1088,10 +1107,11 @@ function clearQueryCache() {
1088
1107
  }
1089
1108
  }
1090
1109
  function __resetQueryCache() {
1091
- for (const entry of queryCache.values()) {
1110
+ const activeCache = getActiveQueryCache();
1111
+ for (const entry of activeCache.values()) {
1092
1112
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
1093
1113
  }
1094
- queryCache.clear();
1114
+ activeCache.clear();
1095
1115
  }
1096
1116
 
1097
1117
  // src/data/mutation.ts
@@ -1103,7 +1123,11 @@ function mutation(mutationFn, options = {}) {
1103
1123
  const isSuccess = derived(() => status() === "success");
1104
1124
  const isIdle = derived(() => status() === "idle");
1105
1125
  let runId = 0;
1126
+ let abortController = null;
1106
1127
  async function execute(variables) {
1128
+ abortController?.abort();
1129
+ abortController = new AbortController();
1130
+ const signal2 = abortController.signal;
1107
1131
  const myRun = ++runId;
1108
1132
  let context2;
1109
1133
  batch(() => {
@@ -1115,7 +1139,7 @@ function mutation(mutationFn, options = {}) {
1115
1139
  if (options.onMutate) {
1116
1140
  context2 = await options.onMutate(variables);
1117
1141
  }
1118
- const result = await withRetry(() => mutationFn(variables), options.retry);
1142
+ const result = await withRetry(() => mutationFn(variables, signal2), options.retry, void 0, signal2);
1119
1143
  if (myRun !== runId) return result;
1120
1144
  batch(() => {
1121
1145
  setData(result);
@@ -1127,6 +1151,7 @@ function mutation(mutationFn, options = {}) {
1127
1151
  return result;
1128
1152
  } catch (err) {
1129
1153
  const errorObj = err instanceof Error ? err : new Error(String(err));
1154
+ if (errorObj instanceof DOMException && errorObj.name === "AbortError") throw errorObj;
1130
1155
  if (myRun !== runId) throw errorObj;
1131
1156
  batch(() => {
1132
1157
  setError(errorObj);
@@ -1140,6 +1165,8 @@ function mutation(mutationFn, options = {}) {
1140
1165
  }
1141
1166
  function reset() {
1142
1167
  runId++;
1168
+ abortController?.abort();
1169
+ abortController = null;
1143
1170
  batch(() => {
1144
1171
  setData(void 0);
1145
1172
  setError(void 0);
@@ -1155,6 +1182,7 @@ function mutation(mutationFn, options = {}) {
1155
1182
  isIdle,
1156
1183
  mutate: (variables) => {
1157
1184
  execute(variables).catch((err) => {
1185
+ if (err instanceof DOMException && err.name === "AbortError") return;
1158
1186
  if (typeof console !== "undefined") {
1159
1187
  console.warn("[SibuJS mutation] mutate() failed; check `.error()` signal or onError option.", err);
1160
1188
  }
@@ -1171,6 +1199,7 @@ function infiniteQuery(key, fetcher, options) {
1171
1199
  getNextPageParam,
1172
1200
  getPreviousPageParam,
1173
1201
  initialPageParam = 0,
1202
+ maxPages,
1174
1203
  enabled = true,
1175
1204
  retry: retryOptions,
1176
1205
  onSuccess,
@@ -1194,8 +1223,9 @@ function infiniteQuery(key, fetcher, options) {
1194
1223
  let abortController = null;
1195
1224
  let disposed = false;
1196
1225
  let runId = 0;
1197
- async function fetchPage(pageParam, direction) {
1198
- if (disposed) return;
1226
+ let inFlight = null;
1227
+ function fetchPage(pageParam, direction) {
1228
+ if (disposed) return Promise.resolve();
1199
1229
  abortController?.abort();
1200
1230
  abortController = new AbortController();
1201
1231
  const signal2 = abortController.signal;
@@ -1206,39 +1236,44 @@ function infiniteQuery(key, fetcher, options) {
1206
1236
  if (direction === "prev") setIsFetchingPrev(true);
1207
1237
  setError(void 0);
1208
1238
  });
1209
- try {
1210
- const page = await withRetry(() => fetcher({ signal: signal2, pageParam }), retryOptions, void 0, signal2);
1211
- if (disposed || myRun !== runId) return;
1212
- const currentPages = pages();
1213
- let newPages;
1214
- if (direction === "prev") {
1215
- newPages = [page, ...currentPages];
1216
- } else {
1217
- newPages = [...currentPages, page];
1239
+ const promise = (async () => {
1240
+ try {
1241
+ const page = await withRetry(() => fetcher({ signal: signal2, pageParam }), retryOptions, void 0, signal2);
1242
+ if (disposed || myRun !== runId) return;
1243
+ const currentPages = pages();
1244
+ let newPages = direction === "prev" ? [page, ...currentPages] : [...currentPages, page];
1245
+ if (maxPages != null && maxPages > 0 && newPages.length > maxPages) {
1246
+ newPages = direction === "prev" ? newPages.slice(0, maxPages) : newPages.slice(newPages.length - maxPages);
1247
+ }
1248
+ const nextParam = getNextPageParam(newPages[newPages.length - 1], newPages);
1249
+ const prevParam = getPreviousPageParam?.(newPages[0], newPages);
1250
+ batch(() => {
1251
+ setPages(newPages);
1252
+ setNextPageParam(nextParam);
1253
+ setPrevPageParam(prevParam);
1254
+ setIsFetching(false);
1255
+ setIsFetchingNext(false);
1256
+ setIsFetchingPrev(false);
1257
+ });
1258
+ onSuccess?.(newPages);
1259
+ } catch (err) {
1260
+ if (disposed || myRun !== runId) return;
1261
+ if (err instanceof DOMException && err.name === "AbortError") return;
1262
+ const errorObj = err instanceof Error ? err : new Error(String(err));
1263
+ batch(() => {
1264
+ setError(errorObj);
1265
+ setIsFetching(false);
1266
+ setIsFetchingNext(false);
1267
+ setIsFetchingPrev(false);
1268
+ });
1269
+ onError?.(errorObj);
1218
1270
  }
1219
- const nextParam = getNextPageParam(page, newPages);
1220
- const prevParam = getPreviousPageParam?.(newPages[0], newPages);
1221
- batch(() => {
1222
- setPages(newPages);
1223
- setNextPageParam(nextParam);
1224
- setPrevPageParam(prevParam);
1225
- setIsFetching(false);
1226
- setIsFetchingNext(false);
1227
- setIsFetchingPrev(false);
1228
- });
1229
- onSuccess?.(newPages);
1230
- } catch (err) {
1231
- if (disposed || myRun !== runId) return;
1232
- if (err instanceof DOMException && err.name === "AbortError") return;
1233
- const errorObj = err instanceof Error ? err : new Error(String(err));
1234
- batch(() => {
1235
- setError(errorObj);
1236
- setIsFetching(false);
1237
- setIsFetchingNext(false);
1238
- setIsFetchingPrev(false);
1239
- });
1240
- onError?.(errorObj);
1241
- }
1271
+ })();
1272
+ inFlight = promise;
1273
+ void promise.finally(() => {
1274
+ if (inFlight === promise) inFlight = null;
1275
+ });
1276
+ return promise;
1242
1277
  }
1243
1278
  const effectCleanup = effect(() => {
1244
1279
  resolveKey();
@@ -1253,11 +1288,13 @@ function infiniteQuery(key, fetcher, options) {
1253
1288
  }
1254
1289
  });
1255
1290
  function fetchNextPage() {
1291
+ if (inFlight) return inFlight;
1256
1292
  const param = nextPageParam();
1257
1293
  if (param === void 0) return Promise.resolve();
1258
1294
  return fetchPage(param, "next");
1259
1295
  }
1260
1296
  function fetchPreviousPage() {
1297
+ if (inFlight) return inFlight;
1261
1298
  const param = prevPageParam();
1262
1299
  if (param === void 0) return Promise.resolve();
1263
1300
  return fetchPage(param, "prev");
@@ -1296,13 +1333,17 @@ function infiniteQuery(key, fetcher, options) {
1296
1333
  function previous(getter) {
1297
1334
  const [previous2, setPrevious] = signal(void 0);
1298
1335
  let current = getter();
1299
- effect(() => {
1336
+ const stop = effect(() => {
1300
1337
  const next = getter();
1301
1338
  if (!Object.is(next, current)) {
1302
1339
  setPrevious(current);
1303
1340
  current = next;
1304
1341
  }
1305
1342
  });
1343
+ Object.defineProperty(previous2, "dispose", {
1344
+ value: stop,
1345
+ enumerable: false
1346
+ });
1306
1347
  return previous2;
1307
1348
  }
1308
1349
 
@@ -1310,7 +1351,7 @@ function previous(getter) {
1310
1351
  function debounce(getter, delay) {
1311
1352
  const [debounced, setDebounced] = signal(getter());
1312
1353
  let timer = null;
1313
- effect(() => {
1354
+ const stop = effect(() => {
1314
1355
  const value = getter();
1315
1356
  if (timer !== null) clearTimeout(timer);
1316
1357
  timer = setTimeout(() => {
@@ -1318,6 +1359,16 @@ function debounce(getter, delay) {
1318
1359
  timer = null;
1319
1360
  }, delay);
1320
1361
  });
1362
+ Object.defineProperty(debounced, "dispose", {
1363
+ value: () => {
1364
+ stop();
1365
+ if (timer !== null) {
1366
+ clearTimeout(timer);
1367
+ timer = null;
1368
+ }
1369
+ },
1370
+ enumerable: false
1371
+ });
1321
1372
  return debounced;
1322
1373
  }
1323
1374
 
@@ -1327,7 +1378,8 @@ function throttle(getter, interval) {
1327
1378
  let cooldown = false;
1328
1379
  let pending = null;
1329
1380
  let lastEmitted = getter();
1330
- effect(() => {
1381
+ let timer = null;
1382
+ const stop = effect(() => {
1331
1383
  const value = getter();
1332
1384
  if (!cooldown) {
1333
1385
  if (!Object.is(value, lastEmitted)) {
@@ -1335,7 +1387,8 @@ function throttle(getter, interval) {
1335
1387
  lastEmitted = value;
1336
1388
  cooldown = true;
1337
1389
  pending = null;
1338
- setTimeout(() => {
1390
+ timer = setTimeout(() => {
1391
+ timer = null;
1339
1392
  cooldown = false;
1340
1393
  if (pending !== null) {
1341
1394
  const trailingValue = pending.value;
@@ -1349,6 +1402,16 @@ function throttle(getter, interval) {
1349
1402
  pending = { value };
1350
1403
  }
1351
1404
  });
1405
+ Object.defineProperty(throttled, "dispose", {
1406
+ value: () => {
1407
+ stop();
1408
+ if (timer !== null) {
1409
+ clearTimeout(timer);
1410
+ timer = null;
1411
+ }
1412
+ },
1413
+ enumerable: false
1414
+ });
1352
1415
  return throttled;
1353
1416
  }
1354
1417
 
@@ -1477,28 +1540,45 @@ function idbGet(db, store, key) {
1477
1540
  req.onerror = () => reject(req.error);
1478
1541
  });
1479
1542
  }
1480
- function idbPut(db, store, item) {
1543
+ function idbPut(db, store, item, key) {
1481
1544
  return new Promise((resolve, reject) => {
1482
1545
  const tx = db.transaction(store, "readwrite");
1483
- tx.objectStore(store).put(item);
1546
+ if (key !== void 0) tx.objectStore(store).put(item, key);
1547
+ else tx.objectStore(store).put(item);
1484
1548
  tx.oncomplete = () => resolve();
1485
1549
  tx.onerror = () => reject(tx.error);
1486
1550
  });
1487
1551
  }
1488
- function idbPutWithChange(db, item, change) {
1552
+ function coalesceAndAddChange(tx, change, keyPath) {
1553
+ const store = tx.objectStore("_changes");
1554
+ const targetKey = change.item[keyPath];
1555
+ const cursorReq = store.openCursor();
1556
+ cursorReq.onsuccess = () => {
1557
+ const cursor = cursorReq.result;
1558
+ if (cursor) {
1559
+ const existing = cursor.value;
1560
+ const k = existing.item[keyPath];
1561
+ if (targetKey != null && k === targetKey) cursor.delete();
1562
+ cursor.continue();
1563
+ } else {
1564
+ store.put(change);
1565
+ }
1566
+ };
1567
+ }
1568
+ function idbPutWithChange(db, item, change, keyPath) {
1489
1569
  return new Promise((resolve, reject) => {
1490
1570
  const tx = db.transaction(["items", "_changes"], "readwrite");
1491
1571
  tx.objectStore("items").put(item);
1492
- tx.objectStore("_changes").put(change);
1572
+ coalesceAndAddChange(tx, change, keyPath);
1493
1573
  tx.oncomplete = () => resolve();
1494
1574
  tx.onerror = () => reject(tx.error);
1495
1575
  });
1496
1576
  }
1497
- function idbDeleteWithChange(db, key, change) {
1577
+ function idbDeleteWithChange(db, key, change, keyPath) {
1498
1578
  return new Promise((resolve, reject) => {
1499
1579
  const tx = db.transaction(["items", "_changes"], "readwrite");
1500
1580
  tx.objectStore("items").delete(key);
1501
- tx.objectStore("_changes").put(change);
1581
+ coalesceAndAddChange(tx, change, keyPath);
1502
1582
  tx.oncomplete = () => resolve();
1503
1583
  tx.onerror = () => reject(tx.error);
1504
1584
  });
@@ -1558,13 +1638,18 @@ async function offlineStore(options) {
1558
1638
  setPendingCount(changes.length);
1559
1639
  }
1560
1640
  async function put(item) {
1561
- await idbPutWithChange(db, item, { type: "put", item, timestamp: Date.now() });
1641
+ await idbPutWithChange(db, item, { type: "put", item, timestamp: Date.now() }, keyPath);
1562
1642
  await refreshData();
1563
1643
  }
1564
1644
  async function remove(key) {
1565
1645
  const existing = await idbGet(db, "items", key);
1566
1646
  if (existing) {
1567
- await idbDeleteWithChange(db, key, { type: "delete", item: existing, timestamp: Date.now() });
1647
+ await idbDeleteWithChange(
1648
+ db,
1649
+ key,
1650
+ { type: "delete", item: existing, timestamp: Date.now() },
1651
+ keyPath
1652
+ );
1568
1653
  await refreshData();
1569
1654
  }
1570
1655
  }
@@ -1590,6 +1675,8 @@ async function offlineStore(options) {
1590
1675
  snapshot.map((e) => e.key)
1591
1676
  );
1592
1677
  if (closed) return;
1678
+ } else if (typeof console !== "undefined") {
1679
+ console.warn(`[offlineStore] push rejected by adapter${result.error ? `: ${result.error}` : ""}`);
1593
1680
  }
1594
1681
  }
1595
1682
  const remoteItems = await adapter.pull(lastSynced());
@@ -1608,7 +1695,7 @@ async function offlineStore(options) {
1608
1695
  await idbPutMany(db, "items", safeRemote);
1609
1696
  if (closed) return;
1610
1697
  const now = Date.now();
1611
- await idbPut(db, "_meta", now);
1698
+ await idbPut(db, "_meta", now, "lastSynced");
1612
1699
  if (closed) return;
1613
1700
  setLastSynced(now);
1614
1701
  await refreshData();
@@ -1707,14 +1794,20 @@ function loaderData() {
1707
1794
  async function preloadRoute(route, context2, callerSignal) {
1708
1795
  if (!route.loader) return void 0;
1709
1796
  const controller = new AbortController();
1797
+ let onAbort = null;
1710
1798
  if (callerSignal) {
1711
1799
  if (callerSignal.aborted) {
1712
1800
  controller.abort();
1713
1801
  } else {
1714
- callerSignal.addEventListener("abort", () => controller.abort(), { once: true });
1802
+ onAbort = () => controller.abort();
1803
+ callerSignal.addEventListener("abort", onAbort, { once: true });
1715
1804
  }
1716
1805
  }
1717
- return route.loader(context2, { signal: controller.signal });
1806
+ try {
1807
+ return await route.loader(context2, { signal: controller.signal });
1808
+ } finally {
1809
+ if (onAbort) callerSignal?.removeEventListener("abort", onAbort);
1810
+ }
1718
1811
  }
1719
1812
 
1720
1813
  // src/ui/socket.ts
@@ -1820,9 +1913,12 @@ function socket(url, options) {
1820
1913
  }
1821
1914
 
1822
1915
  // src/utils/sanitize.ts
1916
+ function stripControlChars(value) {
1917
+ return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
1918
+ }
1823
1919
  var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
1824
1920
  function sanitizeUrl(url) {
1825
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
1921
+ const trimmed = stripControlChars(url).trim();
1826
1922
  if (!trimmed) return "";
1827
1923
  const lower = trimmed.toLowerCase();
1828
1924
  let schemeEnd = -1;
package/dist/data.d.cts CHANGED
@@ -133,7 +133,7 @@ interface MutationResult<TData, TVariables> {
133
133
  /** Reset state to idle */
134
134
  reset: () => void;
135
135
  }
136
- declare function mutation<TData, TVariables = void, TContext = unknown>(mutationFn: (variables: TVariables) => Promise<TData>, options?: MutationOptions<TData, TVariables, TContext>): MutationResult<TData, TVariables>;
136
+ declare function mutation<TData, TVariables = void, TContext = unknown>(mutationFn: (variables: TVariables, signal?: AbortSignal) => Promise<TData>, options?: MutationOptions<TData, TVariables, TContext>): MutationResult<TData, TVariables>;
137
137
 
138
138
  interface InfiniteQueryOptions<TData, TPageParam = number> {
139
139
  /** Get the param for the next page. Return undefined to signal end. */
@@ -142,6 +142,12 @@ interface InfiniteQueryOptions<TData, TPageParam = number> {
142
142
  getPreviousPageParam?: (firstPage: TData, allPages: TData[]) => TPageParam | undefined;
143
143
  /** Initial page param. Default: 0 (for number) */
144
144
  initialPageParam?: TPageParam;
145
+ /**
146
+ * Maximum number of pages to retain. When exceeded, the oldest page is
147
+ * dropped from the opposite end (sliding window) to bound memory. Unset =
148
+ * unbounded.
149
+ */
150
+ maxPages?: number;
145
151
  /** Whether to fetch on creation. Default: true */
146
152
  enabled?: boolean;
147
153
  /** Retry options */