sibujs 3.0.0 → 3.2.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.
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 +276 -150
  5. package/dist/build.js +35 -24
  6. package/dist/cdn.global.js +7 -7
  7. package/dist/{chunk-RJIRT46U.js → chunk-2C4E3HBM.js} +5 -5
  8. package/dist/{chunk-XDKP4T7G.js → chunk-4JCAUOLN.js} +45 -23
  9. package/dist/{chunk-VSNLICTS.js → chunk-5N74TKLD.js} +1 -1
  10. package/dist/{chunk-XVYB3J6C.js → chunk-7XDYVJLE.js} +19 -9
  11. package/dist/{chunk-L52H775O.js → chunk-BGNLPNGV.js} +20 -12
  12. package/dist/{chunk-6QZO7MMG.js → chunk-C427DVQF.js} +1 -1
  13. package/dist/{chunk-5WD7BYTZ.js → chunk-FDY42FIU.js} +3 -2
  14. package/dist/{chunk-4YTVESDX.js → chunk-FOI23UJL.js} +11 -1
  15. package/dist/{chunk-2RA7SHDA.js → chunk-GOJMFRBL.js} +20 -4
  16. package/dist/{chunk-2KM2724A.js → chunk-GOUM4JCT.js} +6 -6
  17. package/dist/chunk-H3SRKIYX.js +17 -0
  18. package/dist/{chunk-NEWH4O5U.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-DF3GTP4Q.js → chunk-JYXOEYI4.js} +12 -18
  22. package/dist/{chunk-KZA7ANXP.js → chunk-NFYWLRUO.js} +11 -18
  23. package/dist/{chunk-KH4OE6WY.js → chunk-NPIEEKPT.js} +20 -11
  24. package/dist/{chunk-V65KTDZW.js → chunk-OYLPZO4N.js} +33 -15
  25. package/dist/{chunk-LYTCUZ7H.js → chunk-RDRSWYNP.js} +1 -1
  26. package/dist/{chunk-UKMXT5T6.js → chunk-RLUJL2MV.js} +7 -12
  27. package/dist/{chunk-INBOWHQ3.js → chunk-V2MTG5FT.js} +99 -36
  28. package/dist/{chunk-CNZ35WI2.js → chunk-VJE6DDYM.js} +2 -2
  29. package/dist/{chunk-2JQUV4Y3.js → chunk-VOCE4NNK.js} +157 -75
  30. package/dist/{chunk-STFTTMO2.js → chunk-X67UYC74.js} +31 -12
  31. package/dist/{chunk-YMOIAHWA.js → chunk-YFDGQWDA.js} +1 -1
  32. package/dist/{chunk-L4DAT4WU.js → chunk-Z2FWAE4B.js} +28 -1
  33. package/dist/data.cjs +211 -93
  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 +163 -65
  42. package/dist/ecosystem.js +9 -9
  43. package/dist/extras.cjs +420 -198
  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 +255 -139
  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 +66 -39
  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 +243 -138
  62. package/dist/plugins.d.cts +1 -1
  63. package/dist/plugins.d.ts +1 -1
  64. package/dist/plugins.js +96 -45
  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 +185 -68
  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 +76 -39
  75. package/dist/ui.js +10 -9
  76. package/dist/widgets.cjs +61 -23
  77. package/dist/widgets.js +8 -8
  78. package/package.json +3 -1
package/dist/data.cjs CHANGED
@@ -189,7 +189,7 @@ function retrack(effectFn, subscriber) {
189
189
  }
190
190
  }
191
191
  function track(effectFn, subscriber) {
192
- if (!subscriber) subscriber = effectFn;
192
+ if (!subscriber) return reactiveBinding(effectFn);
193
193
  cleanup(subscriber);
194
194
  const prev = currentSubscriber;
195
195
  currentSubscriber = subscriber;
@@ -207,6 +207,32 @@ function track(effectFn, subscriber) {
207
207
  const sub = subscriber;
208
208
  return sub._dispose ?? (sub._dispose = () => cleanup(subscriber));
209
209
  }
210
+ function reactiveBinding(commit) {
211
+ const run = () => {
212
+ const s = subscriber;
213
+ if (s._disposed || s._reentrant) return;
214
+ s._reentrant = true;
215
+ try {
216
+ retrack(commit, subscriber);
217
+ } finally {
218
+ s._reentrant = false;
219
+ }
220
+ };
221
+ const subscriber = run;
222
+ subscriber.depsHead = null;
223
+ subscriber.depsTail = null;
224
+ subscriber._epoch = 0;
225
+ subscriber._structDirty = false;
226
+ subscriber._runEpoch = 0;
227
+ subscriber._runs = 0;
228
+ subscriber._reentrant = false;
229
+ subscriber._disposed = false;
230
+ run();
231
+ return subscriber._dispose ?? (subscriber._dispose = () => {
232
+ subscriber._disposed = true;
233
+ cleanup(subscriber);
234
+ });
235
+ }
210
236
  function recordDependency(signal2) {
211
237
  if (!currentSubscriber) return;
212
238
  const sub = currentSubscriber;
@@ -395,6 +421,7 @@ function derived(getter, options) {
395
421
  const equals = options?.equals;
396
422
  const cs = {};
397
423
  cs._d = false;
424
+ cs._init = false;
398
425
  cs._g = getter;
399
426
  cs.__v = 0;
400
427
  const markDirty = () => {
@@ -403,11 +430,18 @@ function derived(getter, options) {
403
430
  };
404
431
  markDirty._c = 1;
405
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
+ };
406
439
  track(() => {
407
440
  let threw = true;
408
441
  try {
409
442
  cs._v = getter();
410
443
  cs._d = false;
444
+ cs._init = true;
411
445
  threw = false;
412
446
  } finally {
413
447
  if (threw) cs._d = true;
@@ -423,20 +457,13 @@ function derived(getter, options) {
423
457
  }
424
458
  if (trackingSuspended) {
425
459
  if (cs._d) {
460
+ const prev = cs._v;
426
461
  evaluating = true;
427
- let threw = true;
428
462
  try {
429
- const prev = cs._v;
430
- retrack(() => {
431
- const next = getter();
432
- cs._v = equals && cs._v !== void 0 ? equals(cs._v, next) ? cs._v : next : next;
433
- cs._d = false;
434
- threw = false;
435
- }, markDirty);
463
+ retrack(recompute, markDirty);
436
464
  if (!Object.is(prev, cs._v)) cs.__v++;
437
465
  } finally {
438
466
  evaluating = false;
439
- if (threw) cs._d = true;
440
467
  }
441
468
  }
442
469
  return cs._v;
@@ -445,18 +472,11 @@ function derived(getter, options) {
445
472
  if (cs._d) {
446
473
  const oldValue = cs._v;
447
474
  evaluating = true;
448
- let threw = true;
449
475
  try {
450
- retrack(() => {
451
- const next = getter();
452
- cs._v = equals && cs._v !== void 0 ? equals(cs._v, next) ? cs._v : next : next;
453
- cs._d = false;
454
- threw = false;
455
- }, markDirty);
476
+ retrack(recompute, markDirty);
456
477
  if (!Object.is(oldValue, cs._v)) cs.__v++;
457
478
  } finally {
458
479
  evaluating = false;
459
- if (threw) cs._d = true;
460
480
  }
461
481
  if (hook && oldValue !== cs._v) {
462
482
  hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
@@ -477,11 +497,15 @@ function derived(getter, options) {
477
497
  var als = null;
478
498
  try {
479
499
  if (typeof process !== "undefined" && process.versions && process.versions.node) {
480
- const req = Function("return typeof require==='function'?require:null")();
481
- if (req) {
482
- const mod = req("node:async_hooks");
483
- 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");
484
507
  }
508
+ if (mod) als = new mod.AsyncLocalStorage();
485
509
  }
486
510
  } catch {
487
511
  als = null;
@@ -497,6 +521,17 @@ function getSSRStore() {
497
521
  function isSSR() {
498
522
  return getSSRStore().ssr;
499
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
+ }
500
535
 
501
536
  // src/core/signals/effect.ts
502
537
  var _g = globalThis;
@@ -583,6 +618,7 @@ function effect(effectFn, options) {
583
618
  ctx.fn(ctx.onCleanup);
584
619
  };
585
620
  const sub = (() => {
621
+ if (ctx.disposed) return;
586
622
  if (ctx.running) {
587
623
  ctx.rerunPending = true;
588
624
  return;
@@ -779,9 +815,12 @@ async function withRetry(fn, options, onRetry, signal2) {
779
815
  }
780
816
 
781
817
  // src/data/query.ts
782
- var queryCache = /* @__PURE__ */ new Map();
783
- function getOrCreateEntry(key, initialData) {
784
- 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);
785
824
  if (!entry) {
786
825
  entry = {
787
826
  data: initialData,
@@ -793,7 +832,7 @@ function getOrCreateEntry(key, initialData) {
793
832
  listeners: /* @__PURE__ */ new Set(),
794
833
  refetchers: /* @__PURE__ */ new Set()
795
834
  };
796
- queryCache.set(key, entry);
835
+ cache.set(key, entry);
797
836
  }
798
837
  return entry;
799
838
  }
@@ -813,6 +852,7 @@ function query(key, fetcher, options = {}) {
813
852
  select
814
853
  } = options;
815
854
  const resolveKey = typeof key === "function" ? key : () => key;
855
+ const cache = getActiveQueryCache();
816
856
  const [data, setData] = signal(initialData);
817
857
  const [isFetching, setIsFetching] = signal(false);
818
858
  const [error, setError] = signal(void 0);
@@ -824,16 +864,16 @@ function query(key, fetcher, options = {}) {
824
864
  const isStale = derived(() => {
825
865
  data();
826
866
  if (!currentKey) return true;
827
- const entry = queryCache.get(currentKey);
867
+ const entry = cache.get(currentKey);
828
868
  if (!entry || entry.dataUpdatedAt === 0) return true;
829
869
  return Date.now() - entry.dataUpdatedAt >= staleTime;
830
870
  });
831
871
  async function doFetch() {
832
872
  if (disposed || !currentKey || !enabled) return;
833
873
  const key2 = currentKey;
834
- let entry = queryCache.get(key2);
874
+ let entry = cache.get(key2);
835
875
  if (!entry) {
836
- entry = getOrCreateEntry(key2);
876
+ entry = getOrCreateEntry(cache, key2);
837
877
  entry.listeners.add(onCacheUpdate);
838
878
  entry.refetchers.add(doFetch);
839
879
  }
@@ -908,7 +948,7 @@ function query(key, fetcher, options = {}) {
908
948
  }
909
949
  function onCacheUpdate() {
910
950
  if (disposed || !currentKey) return;
911
- const entry = queryCache.get(currentKey);
951
+ const entry = cache.get(currentKey);
912
952
  if (!entry) {
913
953
  batch(() => {
914
954
  setData(void 0);
@@ -928,7 +968,7 @@ function query(key, fetcher, options = {}) {
928
968
  const effectCleanup = effect(() => {
929
969
  const key2 = resolveKey();
930
970
  if (currentKey !== null && currentKey !== key2) {
931
- const oldEntry = queryCache.get(currentKey);
971
+ const oldEntry = cache.get(currentKey);
932
972
  if (oldEntry) {
933
973
  oldEntry.listeners.delete(onCacheUpdate);
934
974
  oldEntry.refetchers.delete(doFetch);
@@ -936,13 +976,13 @@ function query(key, fetcher, options = {}) {
936
976
  if (oldEntry.subscribers <= 0 && cacheTime >= 0) {
937
977
  const oldKey = currentKey;
938
978
  if (oldEntry.gcTimer !== null) clearTimeout(oldEntry.gcTimer);
939
- oldEntry.gcTimer = setTimeout(() => queryCache.delete(oldKey), cacheTime);
979
+ oldEntry.gcTimer = setTimeout(() => cache.delete(oldKey), cacheTime);
940
980
  }
941
981
  }
942
982
  }
943
983
  const keyChanged = currentKey !== key2;
944
984
  currentKey = key2;
945
- const entry = getOrCreateEntry(key2, initialData);
985
+ const entry = getOrCreateEntry(cache, key2, initialData);
946
986
  if (keyChanged) entry.subscribers++;
947
987
  if (entry.gcTimer !== null) {
948
988
  clearTimeout(entry.gcTimer);
@@ -996,7 +1036,7 @@ function query(key, fetcher, options = {}) {
996
1036
  effectCleanup();
997
1037
  if (intervalTimer) clearInterval(intervalTimer);
998
1038
  if (currentKey) {
999
- const entry = queryCache.get(currentKey);
1039
+ const entry = cache.get(currentKey);
1000
1040
  if (entry) {
1001
1041
  entry.listeners.delete(onCacheUpdate);
1002
1042
  entry.refetchers.delete(doFetch);
@@ -1004,7 +1044,7 @@ function query(key, fetcher, options = {}) {
1004
1044
  if (entry.subscribers <= 0 && cacheTime >= 0) {
1005
1045
  const key2 = currentKey;
1006
1046
  if (entry.gcTimer !== null) clearTimeout(entry.gcTimer);
1007
- entry.gcTimer = setTimeout(() => queryCache.delete(key2), cacheTime);
1047
+ entry.gcTimer = setTimeout(() => cache.delete(key2), cacheTime);
1008
1048
  }
1009
1049
  }
1010
1050
  }
@@ -1027,7 +1067,7 @@ function query(key, fetcher, options = {}) {
1027
1067
  }
1028
1068
  function invalidateQueries(keyOrPredicate) {
1029
1069
  const predicate = typeof keyOrPredicate === "function" ? keyOrPredicate : (k) => k === keyOrPredicate;
1030
- for (const [key, entry] of queryCache.entries()) {
1070
+ for (const [key, entry] of getActiveQueryCache().entries()) {
1031
1071
  if (predicate(key)) {
1032
1072
  entry.dataUpdatedAt = 0;
1033
1073
  for (const refetcher of entry.refetchers) refetcher();
@@ -1035,10 +1075,10 @@ function invalidateQueries(keyOrPredicate) {
1035
1075
  }
1036
1076
  }
1037
1077
  function getQueryData(key) {
1038
- return queryCache.get(key)?.data;
1078
+ return getActiveQueryCache().get(key)?.data;
1039
1079
  }
1040
1080
  function setQueryData(key, data) {
1041
- const entry = queryCache.get(key);
1081
+ const entry = getActiveQueryCache().get(key);
1042
1082
  if (!entry) return;
1043
1083
  const newData = typeof data === "function" ? data(entry.data) : data;
1044
1084
  entry.data = newData;
@@ -1048,14 +1088,15 @@ function setQueryData(key, data) {
1048
1088
  function clearQueryCache() {
1049
1089
  const activeListeners = [];
1050
1090
  const activeRefetchers = [];
1051
- for (const entry of queryCache.values()) {
1091
+ const activeCache = getActiveQueryCache();
1092
+ for (const entry of activeCache.values()) {
1052
1093
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
1053
1094
  if (entry.subscribers > 0) {
1054
1095
  for (const listener of entry.listeners) activeListeners.push(listener);
1055
1096
  for (const refetcher of entry.refetchers) activeRefetchers.push(refetcher);
1056
1097
  }
1057
1098
  }
1058
- queryCache.clear();
1099
+ activeCache.clear();
1059
1100
  for (const listener of activeListeners) listener();
1060
1101
  for (const refetcher of activeRefetchers) {
1061
1102
  refetcher().catch((err) => {
@@ -1066,10 +1107,11 @@ function clearQueryCache() {
1066
1107
  }
1067
1108
  }
1068
1109
  function __resetQueryCache() {
1069
- for (const entry of queryCache.values()) {
1110
+ const activeCache = getActiveQueryCache();
1111
+ for (const entry of activeCache.values()) {
1070
1112
  if (entry.gcTimer) clearTimeout(entry.gcTimer);
1071
1113
  }
1072
- queryCache.clear();
1114
+ activeCache.clear();
1073
1115
  }
1074
1116
 
1075
1117
  // src/data/mutation.ts
@@ -1081,7 +1123,11 @@ function mutation(mutationFn, options = {}) {
1081
1123
  const isSuccess = derived(() => status() === "success");
1082
1124
  const isIdle = derived(() => status() === "idle");
1083
1125
  let runId = 0;
1126
+ let abortController = null;
1084
1127
  async function execute(variables) {
1128
+ abortController?.abort();
1129
+ abortController = new AbortController();
1130
+ const signal2 = abortController.signal;
1085
1131
  const myRun = ++runId;
1086
1132
  let context2;
1087
1133
  batch(() => {
@@ -1093,7 +1139,7 @@ function mutation(mutationFn, options = {}) {
1093
1139
  if (options.onMutate) {
1094
1140
  context2 = await options.onMutate(variables);
1095
1141
  }
1096
- const result = await withRetry(() => mutationFn(variables), options.retry);
1142
+ const result = await withRetry(() => mutationFn(variables, signal2), options.retry, void 0, signal2);
1097
1143
  if (myRun !== runId) return result;
1098
1144
  batch(() => {
1099
1145
  setData(result);
@@ -1105,6 +1151,7 @@ function mutation(mutationFn, options = {}) {
1105
1151
  return result;
1106
1152
  } catch (err) {
1107
1153
  const errorObj = err instanceof Error ? err : new Error(String(err));
1154
+ if (errorObj instanceof DOMException && errorObj.name === "AbortError") throw errorObj;
1108
1155
  if (myRun !== runId) throw errorObj;
1109
1156
  batch(() => {
1110
1157
  setError(errorObj);
@@ -1118,6 +1165,8 @@ function mutation(mutationFn, options = {}) {
1118
1165
  }
1119
1166
  function reset() {
1120
1167
  runId++;
1168
+ abortController?.abort();
1169
+ abortController = null;
1121
1170
  batch(() => {
1122
1171
  setData(void 0);
1123
1172
  setError(void 0);
@@ -1133,6 +1182,7 @@ function mutation(mutationFn, options = {}) {
1133
1182
  isIdle,
1134
1183
  mutate: (variables) => {
1135
1184
  execute(variables).catch((err) => {
1185
+ if (err instanceof DOMException && err.name === "AbortError") return;
1136
1186
  if (typeof console !== "undefined") {
1137
1187
  console.warn("[SibuJS mutation] mutate() failed; check `.error()` signal or onError option.", err);
1138
1188
  }
@@ -1149,6 +1199,7 @@ function infiniteQuery(key, fetcher, options) {
1149
1199
  getNextPageParam,
1150
1200
  getPreviousPageParam,
1151
1201
  initialPageParam = 0,
1202
+ maxPages,
1152
1203
  enabled = true,
1153
1204
  retry: retryOptions,
1154
1205
  onSuccess,
@@ -1172,8 +1223,9 @@ function infiniteQuery(key, fetcher, options) {
1172
1223
  let abortController = null;
1173
1224
  let disposed = false;
1174
1225
  let runId = 0;
1175
- async function fetchPage(pageParam, direction) {
1176
- if (disposed) return;
1226
+ let inFlight = null;
1227
+ function fetchPage(pageParam, direction) {
1228
+ if (disposed) return Promise.resolve();
1177
1229
  abortController?.abort();
1178
1230
  abortController = new AbortController();
1179
1231
  const signal2 = abortController.signal;
@@ -1184,39 +1236,44 @@ function infiniteQuery(key, fetcher, options) {
1184
1236
  if (direction === "prev") setIsFetchingPrev(true);
1185
1237
  setError(void 0);
1186
1238
  });
1187
- try {
1188
- const page = await withRetry(() => fetcher({ signal: signal2, pageParam }), retryOptions, void 0, signal2);
1189
- if (disposed || myRun !== runId) return;
1190
- const currentPages = pages();
1191
- let newPages;
1192
- if (direction === "prev") {
1193
- newPages = [page, ...currentPages];
1194
- } else {
1195
- 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);
1196
1270
  }
1197
- const nextParam = getNextPageParam(page, newPages);
1198
- const prevParam = getPreviousPageParam?.(newPages[0], newPages);
1199
- batch(() => {
1200
- setPages(newPages);
1201
- setNextPageParam(nextParam);
1202
- setPrevPageParam(prevParam);
1203
- setIsFetching(false);
1204
- setIsFetchingNext(false);
1205
- setIsFetchingPrev(false);
1206
- });
1207
- onSuccess?.(newPages);
1208
- } catch (err) {
1209
- if (disposed || myRun !== runId) return;
1210
- if (err instanceof DOMException && err.name === "AbortError") return;
1211
- const errorObj = err instanceof Error ? err : new Error(String(err));
1212
- batch(() => {
1213
- setError(errorObj);
1214
- setIsFetching(false);
1215
- setIsFetchingNext(false);
1216
- setIsFetchingPrev(false);
1217
- });
1218
- onError?.(errorObj);
1219
- }
1271
+ })();
1272
+ inFlight = promise;
1273
+ void promise.finally(() => {
1274
+ if (inFlight === promise) inFlight = null;
1275
+ });
1276
+ return promise;
1220
1277
  }
1221
1278
  const effectCleanup = effect(() => {
1222
1279
  resolveKey();
@@ -1231,11 +1288,13 @@ function infiniteQuery(key, fetcher, options) {
1231
1288
  }
1232
1289
  });
1233
1290
  function fetchNextPage() {
1291
+ if (inFlight) return inFlight;
1234
1292
  const param = nextPageParam();
1235
1293
  if (param === void 0) return Promise.resolve();
1236
1294
  return fetchPage(param, "next");
1237
1295
  }
1238
1296
  function fetchPreviousPage() {
1297
+ if (inFlight) return inFlight;
1239
1298
  const param = prevPageParam();
1240
1299
  if (param === void 0) return Promise.resolve();
1241
1300
  return fetchPage(param, "prev");
@@ -1274,13 +1333,17 @@ function infiniteQuery(key, fetcher, options) {
1274
1333
  function previous(getter) {
1275
1334
  const [previous2, setPrevious] = signal(void 0);
1276
1335
  let current = getter();
1277
- effect(() => {
1336
+ const stop = effect(() => {
1278
1337
  const next = getter();
1279
1338
  if (!Object.is(next, current)) {
1280
1339
  setPrevious(current);
1281
1340
  current = next;
1282
1341
  }
1283
1342
  });
1343
+ Object.defineProperty(previous2, "dispose", {
1344
+ value: stop,
1345
+ enumerable: false
1346
+ });
1284
1347
  return previous2;
1285
1348
  }
1286
1349
 
@@ -1288,7 +1351,7 @@ function previous(getter) {
1288
1351
  function debounce(getter, delay) {
1289
1352
  const [debounced, setDebounced] = signal(getter());
1290
1353
  let timer = null;
1291
- effect(() => {
1354
+ const stop = effect(() => {
1292
1355
  const value = getter();
1293
1356
  if (timer !== null) clearTimeout(timer);
1294
1357
  timer = setTimeout(() => {
@@ -1296,6 +1359,16 @@ function debounce(getter, delay) {
1296
1359
  timer = null;
1297
1360
  }, delay);
1298
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
+ });
1299
1372
  return debounced;
1300
1373
  }
1301
1374
 
@@ -1305,7 +1378,8 @@ function throttle(getter, interval) {
1305
1378
  let cooldown = false;
1306
1379
  let pending = null;
1307
1380
  let lastEmitted = getter();
1308
- effect(() => {
1381
+ let timer = null;
1382
+ const stop = effect(() => {
1309
1383
  const value = getter();
1310
1384
  if (!cooldown) {
1311
1385
  if (!Object.is(value, lastEmitted)) {
@@ -1313,7 +1387,8 @@ function throttle(getter, interval) {
1313
1387
  lastEmitted = value;
1314
1388
  cooldown = true;
1315
1389
  pending = null;
1316
- setTimeout(() => {
1390
+ timer = setTimeout(() => {
1391
+ timer = null;
1317
1392
  cooldown = false;
1318
1393
  if (pending !== null) {
1319
1394
  const trailingValue = pending.value;
@@ -1327,6 +1402,16 @@ function throttle(getter, interval) {
1327
1402
  pending = { value };
1328
1403
  }
1329
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
+ });
1330
1415
  return throttled;
1331
1416
  }
1332
1417
 
@@ -1455,28 +1540,45 @@ function idbGet(db, store, key) {
1455
1540
  req.onerror = () => reject(req.error);
1456
1541
  });
1457
1542
  }
1458
- function idbPut(db, store, item) {
1543
+ function idbPut(db, store, item, key) {
1459
1544
  return new Promise((resolve, reject) => {
1460
1545
  const tx = db.transaction(store, "readwrite");
1461
- tx.objectStore(store).put(item);
1546
+ if (key !== void 0) tx.objectStore(store).put(item, key);
1547
+ else tx.objectStore(store).put(item);
1462
1548
  tx.oncomplete = () => resolve();
1463
1549
  tx.onerror = () => reject(tx.error);
1464
1550
  });
1465
1551
  }
1466
- 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) {
1467
1569
  return new Promise((resolve, reject) => {
1468
1570
  const tx = db.transaction(["items", "_changes"], "readwrite");
1469
1571
  tx.objectStore("items").put(item);
1470
- tx.objectStore("_changes").put(change);
1572
+ coalesceAndAddChange(tx, change, keyPath);
1471
1573
  tx.oncomplete = () => resolve();
1472
1574
  tx.onerror = () => reject(tx.error);
1473
1575
  });
1474
1576
  }
1475
- function idbDeleteWithChange(db, key, change) {
1577
+ function idbDeleteWithChange(db, key, change, keyPath) {
1476
1578
  return new Promise((resolve, reject) => {
1477
1579
  const tx = db.transaction(["items", "_changes"], "readwrite");
1478
1580
  tx.objectStore("items").delete(key);
1479
- tx.objectStore("_changes").put(change);
1581
+ coalesceAndAddChange(tx, change, keyPath);
1480
1582
  tx.oncomplete = () => resolve();
1481
1583
  tx.onerror = () => reject(tx.error);
1482
1584
  });
@@ -1536,13 +1638,18 @@ async function offlineStore(options) {
1536
1638
  setPendingCount(changes.length);
1537
1639
  }
1538
1640
  async function put(item) {
1539
- await idbPutWithChange(db, item, { type: "put", item, timestamp: Date.now() });
1641
+ await idbPutWithChange(db, item, { type: "put", item, timestamp: Date.now() }, keyPath);
1540
1642
  await refreshData();
1541
1643
  }
1542
1644
  async function remove(key) {
1543
1645
  const existing = await idbGet(db, "items", key);
1544
1646
  if (existing) {
1545
- 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
+ );
1546
1653
  await refreshData();
1547
1654
  }
1548
1655
  }
@@ -1568,6 +1675,8 @@ async function offlineStore(options) {
1568
1675
  snapshot.map((e) => e.key)
1569
1676
  );
1570
1677
  if (closed) return;
1678
+ } else if (typeof console !== "undefined") {
1679
+ console.warn(`[offlineStore] push rejected by adapter${result.error ? `: ${result.error}` : ""}`);
1571
1680
  }
1572
1681
  }
1573
1682
  const remoteItems = await adapter.pull(lastSynced());
@@ -1586,7 +1695,7 @@ async function offlineStore(options) {
1586
1695
  await idbPutMany(db, "items", safeRemote);
1587
1696
  if (closed) return;
1588
1697
  const now = Date.now();
1589
- await idbPut(db, "_meta", now);
1698
+ await idbPut(db, "_meta", now, "lastSynced");
1590
1699
  if (closed) return;
1591
1700
  setLastSynced(now);
1592
1701
  await refreshData();
@@ -1685,14 +1794,20 @@ function loaderData() {
1685
1794
  async function preloadRoute(route, context2, callerSignal) {
1686
1795
  if (!route.loader) return void 0;
1687
1796
  const controller = new AbortController();
1797
+ let onAbort = null;
1688
1798
  if (callerSignal) {
1689
1799
  if (callerSignal.aborted) {
1690
1800
  controller.abort();
1691
1801
  } else {
1692
- callerSignal.addEventListener("abort", () => controller.abort(), { once: true });
1802
+ onAbort = () => controller.abort();
1803
+ callerSignal.addEventListener("abort", onAbort, { once: true });
1693
1804
  }
1694
1805
  }
1695
- 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
+ }
1696
1811
  }
1697
1812
 
1698
1813
  // src/ui/socket.ts
@@ -1798,9 +1913,12 @@ function socket(url, options) {
1798
1913
  }
1799
1914
 
1800
1915
  // src/utils/sanitize.ts
1916
+ function stripControlChars(value) {
1917
+ return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
1918
+ }
1801
1919
  var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
1802
1920
  function sanitizeUrl(url) {
1803
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
1921
+ const trimmed = stripControlChars(url).trim();
1804
1922
  if (!trimmed) return "";
1805
1923
  const lower = trimmed.toLowerCase();
1806
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 */