sibujs 3.4.0 → 4.0.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 (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -15
  3. package/dist/browser.cjs +410 -101
  4. package/dist/browser.d.cts +50 -5
  5. package/dist/browser.d.ts +50 -5
  6. package/dist/browser.js +8 -6
  7. package/dist/build.cjs +777 -385
  8. package/dist/build.js +15 -15
  9. package/dist/cdn.global.js +11 -7
  10. package/dist/{chunk-JCO7AHNU.js → chunk-3EKJK5FZ.js} +73 -34
  11. package/dist/chunk-4MFZMLUU.js +147 -0
  12. package/dist/chunk-52XFPGSN.js +123 -0
  13. package/dist/chunk-5INI7D2L.js +194 -0
  14. package/dist/{chunk-OAVXAFAY.js → chunk-6LG2T7LO.js} +5 -5
  15. package/dist/{chunk-OOUQUCNN.js → chunk-ACBT6UZS.js} +57 -22
  16. package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
  17. package/dist/chunk-DHDZ7IVN.js +109 -0
  18. package/dist/{chunk-JNGGM2XE.js → chunk-DQVUBVNX.js} +16 -25
  19. package/dist/{chunk-AXFWGY32.js → chunk-DR7H6UGM.js} +232 -67
  20. package/dist/{chunk-WSGLJYFI.js → chunk-E62UADKL.js} +9 -3
  21. package/dist/{chunk-3Z2NGWQI.js → chunk-ES2GERY2.js} +1 -1
  22. package/dist/{chunk-S3V6ZXPI.js → chunk-EWEE7NBF.js} +115 -41
  23. package/dist/{chunk-3DVGCW7O.js → chunk-F64ZPCFE.js} +8 -8
  24. package/dist/{chunk-APFRCIRI.js → chunk-GCSGGA3Q.js} +295 -198
  25. package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
  26. package/dist/{chunk-ELXMRSVN.js → chunk-NF3LYCQH.js} +259 -124
  27. package/dist/chunk-O3QQ3XOL.js +61 -0
  28. package/dist/{chunk-BUKYCWHX.js → chunk-PBQEW5VW.js} +14 -10
  29. package/dist/{chunk-H5FI5WDP.js → chunk-RRIDJQ5C.js} +3 -3
  30. package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
  31. package/dist/{chunk-ODXVQ3CS.js → chunk-VFHIVEPF.js} +309 -188
  32. package/dist/chunk-VPP2FONR.js +142 -0
  33. package/dist/{chunk-KZBEVFXZ.js → chunk-W55YHPEP.js} +5 -3
  34. package/dist/{chunk-FKQG7FTY.js → chunk-WN6WDFCB.js} +4 -4
  35. package/dist/{chunk-CEBWDD63.js → chunk-WOOMUJFT.js} +228 -86
  36. package/dist/{chunk-ITWCTZYH.js → chunk-Z2CE42DB.js} +19 -25
  37. package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.cts} +6 -6
  38. package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.ts} +6 -6
  39. package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.cts} +73 -11
  40. package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.ts} +73 -11
  41. package/dist/data.cjs +483 -205
  42. package/dist/data.d.cts +139 -7
  43. package/dist/data.d.ts +139 -7
  44. package/dist/data.js +9 -9
  45. package/dist/devtools.cjs +222 -87
  46. package/dist/devtools.js +8 -8
  47. package/dist/dispose-4FZ6OFXR.js +19 -0
  48. package/dist/ecosystem.cjs +419 -175
  49. package/dist/ecosystem.js +11 -11
  50. package/dist/extras.cjs +1981 -765
  51. package/dist/extras.d.cts +7 -7
  52. package/dist/extras.d.ts +7 -7
  53. package/dist/extras.js +29 -26
  54. package/dist/index.cjs +784 -385
  55. package/dist/index.d.cts +299 -28
  56. package/dist/index.d.ts +299 -28
  57. package/dist/index.js +33 -18
  58. package/dist/motion.cjs +180 -50
  59. package/dist/motion.d.cts +26 -0
  60. package/dist/motion.d.ts +26 -0
  61. package/dist/motion.js +5 -5
  62. package/dist/patterns.cjs +437 -170
  63. package/dist/patterns.d.cts +60 -13
  64. package/dist/patterns.d.ts +60 -13
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.cjs +333 -148
  67. package/dist/performance.d.cts +55 -7
  68. package/dist/performance.d.ts +55 -7
  69. package/dist/performance.js +9 -9
  70. package/dist/plugins.cjs +1374 -576
  71. package/dist/plugins.d.cts +227 -9
  72. package/dist/plugins.d.ts +227 -9
  73. package/dist/plugins.js +613 -321
  74. package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
  75. package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
  76. package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
  77. package/dist/ssr.cjs +1104 -443
  78. package/dist/ssr.d.cts +230 -20
  79. package/dist/ssr.d.ts +230 -20
  80. package/dist/ssr.js +15 -12
  81. package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
  82. package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
  83. package/dist/testing.cjs +205 -63
  84. package/dist/testing.d.cts +1 -1
  85. package/dist/testing.d.ts +1 -1
  86. package/dist/testing.js +8 -10
  87. package/dist/ui.cjs +640 -199
  88. package/dist/ui.d.cts +2 -2
  89. package/dist/ui.d.ts +2 -2
  90. package/dist/ui.js +10 -10
  91. package/dist/widgets.cjs +207 -82
  92. package/dist/widgets.js +9 -9
  93. package/package.json +12 -7
  94. package/dist/chunk-5VGSK6D2.js +0 -80
  95. package/dist/chunk-COY6PUD2.js +0 -34
  96. package/dist/chunk-K35BAR4P.js +0 -96
  97. package/dist/chunk-L3GAGWCC.js +0 -111
  98. package/dist/chunk-RT3IYFN2.js +0 -38
@@ -252,8 +252,95 @@ function devWarn(message) {
252
252
  }
253
253
  }
254
254
 
255
+ // src/core/errors.ts
256
+ var ERROR_STATE_KEY = /* @__PURE__ */ Symbol.for("sibujs.runtime-errors.v1");
257
+ function state() {
258
+ const g = globalThis;
259
+ const existing = g[ERROR_STATE_KEY];
260
+ if (existing) return existing;
261
+ const created = { handler: null, handlerDepth: 0 };
262
+ g[ERROR_STATE_KEY] = created;
263
+ return created;
264
+ }
265
+ function normalizeError(value) {
266
+ if (value instanceof Error) return value;
267
+ const message = typeof value === "string" ? value : `Non-Error value thrown: ${safeStringify(value)}`;
268
+ return new Error(message, { cause: value });
269
+ }
270
+ function safeStringify(value) {
271
+ if (typeof value === "symbol") return value.toString();
272
+ try {
273
+ return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
274
+ } catch {
275
+ return Object.prototype.toString.call(value);
276
+ }
277
+ }
278
+ function describe(context) {
279
+ return context.name ? `${context.phase} "${context.name}"` : context.phase;
280
+ }
281
+ var REACTIVE_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
282
+ function untrackedRun(fn) {
283
+ const api = globalThis[REACTIVE_REGISTRY_KEY];
284
+ return api?.untracked ? api.untracked(fn) : fn();
285
+ }
286
+ var PROPAGATE_EVENT = "sibu:error-propagate";
287
+ function claimedByBoundary(error, context) {
288
+ const raw = context.node;
289
+ if (!raw || typeof CustomEvent === "undefined") return false;
290
+ const target = resolveEventTarget(raw);
291
+ if (!target) return false;
292
+ try {
293
+ const event = new CustomEvent(PROPAGATE_EVENT, {
294
+ bubbles: true,
295
+ cancelable: true,
296
+ detail: { error, context }
297
+ });
298
+ return target.dispatchEvent(event) === false;
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+ function resolveEventTarget(node) {
304
+ const ELEMENT_NODE = 1;
305
+ let current = node;
306
+ while (current) {
307
+ if (current.nodeType === ELEMENT_NODE && typeof current.dispatchEvent === "function") {
308
+ return current;
309
+ }
310
+ current = current.parentNode;
311
+ }
312
+ const self = node;
313
+ return typeof self.dispatchEvent === "function" ? self : null;
314
+ }
315
+ function defaultReport(error, context) {
316
+ if (typeof console === "undefined") return;
317
+ console.error(`[SibuJS] Uncaught error during ${describe(context)}: ${error.message}`, error);
318
+ }
319
+ function reportError(error, context) {
320
+ const err = normalizeError(error);
321
+ untrackedRun(() => {
322
+ if (claimedByBoundary(err, context)) return;
323
+ const s = state();
324
+ const handler = s.handler;
325
+ if (handler && s.handlerDepth === 0) {
326
+ s.handlerDepth++;
327
+ try {
328
+ handler(error instanceof Error ? error : err, context);
329
+ return;
330
+ } catch (handlerError) {
331
+ if (typeof console !== "undefined") {
332
+ const detail = handlerError instanceof Error ? handlerError.message : String(handlerError);
333
+ console.error(`[SibuJS] runtime error handler itself threw: ${detail}`, handlerError);
334
+ }
335
+ } finally {
336
+ s.handlerDepth--;
337
+ }
338
+ }
339
+ defaultReport(err, context);
340
+ });
341
+ }
342
+
255
343
  // src/reactivity/track-core.ts
256
- var _isDev2 = isDev();
257
344
  var POOL_MAX = 4096;
258
345
  var nodePool = [];
259
346
  function createNode() {
@@ -261,6 +348,7 @@ function createNode() {
261
348
  sig: null,
262
349
  sub: null,
263
350
  epoch: 0,
351
+ depVersion: 0,
264
352
  sigPrev: null,
265
353
  sigNext: null,
266
354
  subPrev: null,
@@ -345,9 +433,28 @@ function safeInvoke(sub) {
345
433
  try {
346
434
  sub();
347
435
  } catch (err) {
348
- if (_isDev2) devWarn(`Subscriber threw during notification: ${err instanceof Error ? err.message : String(err)}`);
436
+ const s = sub;
437
+ reportError(err, { phase: s._errorPhase ?? "effect", name: s.__name, node: s._errorNode });
349
438
  }
350
439
  }
440
+ function depsChanged(sub) {
441
+ let node = sub.depsHead ?? null;
442
+ if (node === null) return true;
443
+ while (node !== null) {
444
+ const sig = node.sig;
445
+ if (sig === null) return true;
446
+ if (sig._d === true && sig._validate !== void 0) {
447
+ try {
448
+ sig._validate();
449
+ } catch {
450
+ return true;
451
+ }
452
+ }
453
+ if (node.depVersion !== sig.__v) return true;
454
+ node = node.subNext;
455
+ }
456
+ return false;
457
+ }
351
458
  var suspendDepth = 0;
352
459
  var trackingSuspended = false;
353
460
  function suspendTracking() {
@@ -428,7 +535,7 @@ function track(effectFn, subscriber) {
428
535
  const sub = subscriber;
429
536
  return sub._dispose ?? (sub._dispose = () => cleanup(subscriber));
430
537
  }
431
- function reactiveBinding(commit) {
538
+ function reactiveBinding(commit, ownerNode) {
432
539
  const run = () => {
433
540
  const s = subscriber;
434
541
  if (s._disposed || s._reentrant) return;
@@ -444,13 +551,17 @@ function reactiveBinding(commit) {
444
551
  subscriber.depsTail = null;
445
552
  subscriber._epoch = 0;
446
553
  subscriber._structDirty = false;
554
+ subscriber._hasComputedDep = false;
447
555
  subscriber._runEpoch = 0;
448
556
  subscriber._runs = 0;
449
557
  subscriber._reentrant = false;
450
558
  subscriber._disposed = false;
559
+ subscriber._errorPhase = "binding";
560
+ subscriber._errorNode = ownerNode;
451
561
  run();
452
562
  return subscriber._dispose ?? (subscriber._dispose = () => {
453
563
  subscriber._disposed = true;
564
+ subscriber._errorNode = void 0;
454
565
  cleanup(subscriber);
455
566
  });
456
567
  }
@@ -462,9 +573,12 @@ function recordDependency(signal2) {
462
573
  const active = sig.__activeNode ?? null;
463
574
  if (active !== null && active.sub === sub) {
464
575
  active.epoch = epoch;
576
+ active.depVersion = sig.__v ?? Number.NaN;
465
577
  return;
466
578
  }
579
+ if (sig._validate !== void 0) sub._hasComputedDep = true;
467
580
  const node = allocNode(signal2, sub, epoch);
581
+ node.depVersion = sig.__v ?? Number.NaN;
468
582
  node.prevActive = active;
469
583
  sig.__activeNode = node;
470
584
  linkSub(sub, node);
@@ -483,9 +597,10 @@ function cleanup(subscriber) {
483
597
  node = next;
484
598
  }
485
599
  }
486
- var maxSubscriberRepeats = 50;
600
+ var maxSubscriberRepeats = 1e3;
487
601
  var maxDrainIterations = 1e6;
488
602
  var drainEpoch = 0;
603
+ var quarantined = /* @__PURE__ */ new Set();
489
604
  function setMaxSubscriberRepeats(n) {
490
605
  const prev = maxSubscriberRepeats;
491
606
  if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
@@ -507,19 +622,21 @@ function tickRepeat(sub) {
507
622
  return s._runs > maxSubscriberRepeats;
508
623
  }
509
624
  function cycleError(sub) {
510
- if (typeof console !== "undefined") {
511
- const name = sub.__name ?? "<unnamed>";
512
- console.error(
513
- `[SibuJS] subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. Breaking to prevent infinite loop.`
514
- );
515
- }
625
+ const name = sub.__name ?? "<unnamed>";
626
+ reportError(
627
+ new Error(
628
+ `subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. This subscriber is quarantined for the rest of this update; other pending work still runs.`
629
+ ),
630
+ { phase: "scheduler", name }
631
+ );
516
632
  }
517
633
  function absoluteDrainError() {
518
- if (typeof console !== "undefined") {
519
- console.error(
520
- `[SibuJS] Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
521
- );
522
- }
634
+ reportError(
635
+ new Error(
636
+ `Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
637
+ ),
638
+ { phase: "scheduler" }
639
+ );
523
640
  }
524
641
  function drainQueue() {
525
642
  let i = 0;
@@ -529,11 +646,14 @@ function drainQueue() {
529
646
  break;
530
647
  }
531
648
  const sub = pendingQueue[i++];
649
+ pendingSet.delete(sub);
650
+ if (quarantined.size > 0 && quarantined.has(sub)) continue;
651
+ if (sub._hasComputedDep === true && !depsChanged(sub)) continue;
532
652
  if (tickRepeat(sub)) {
533
653
  cycleError(sub);
534
- break;
654
+ quarantined.add(sub);
655
+ continue;
535
656
  }
536
- pendingSet.delete(sub);
537
657
  safeInvoke(sub);
538
658
  }
539
659
  }
@@ -548,6 +668,7 @@ function drainNotificationQueue() {
548
668
  if (notifyDepth === 0) {
549
669
  pendingQueue.length = 0;
550
670
  pendingSet.clear();
671
+ if (quarantined.size > 0) quarantined.clear();
551
672
  }
552
673
  }
553
674
  }
@@ -641,6 +762,7 @@ function notifySubscribers(signal2) {
641
762
  if (notifyDepth === 0) {
642
763
  pendingQueue.length = 0;
643
764
  pendingSet.clear();
765
+ if (quarantined.size > 0) quarantined.clear();
644
766
  }
645
767
  }
646
768
  }
@@ -667,14 +789,14 @@ function forEachSubscriber(signal2, visit) {
667
789
  }
668
790
 
669
791
  // src/reactivity/track.ts
670
- var _isDev3 = isDev();
671
- var _runtimeVersion = true ? "3.4.0" : "dev";
792
+ var _isDev2 = isDev();
793
+ var _runtimeVersion = true ? "4.0.0" : "dev";
672
794
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
673
795
  function resolveReactiveApi() {
674
796
  const g = globalThis;
675
797
  const existing = g[REGISTRY_KEY];
676
798
  if (existing) {
677
- if (_isDev3 && !existing.__dupWarned) {
799
+ if (_isDev2 && !existing.__dupWarned) {
678
800
  existing.__dupWarned = true;
679
801
  devWarn(
680
802
  `Multiple instances of the reactive runtime detected on this page (active: ${existing.version}, duplicate: ${_runtimeVersion}). Reactivity still works \u2014 all copies share the first one \u2014 but de-duplicate sibujs in your bundler (e.g. Vite optimizeDeps.exclude: ['sibujs'] or resolve.dedupe: ['sibujs']).`
@@ -734,20 +856,24 @@ function detectSSRShared() {
734
856
  const getBuiltin = process.getBuiltinModule;
735
857
  if (typeof getBuiltin === "function") {
736
858
  mod = getBuiltin("node:async_hooks");
737
- } else {
738
- const req = Function("return typeof require==='function'?require:null")();
739
- if (req) mod = req("node:async_hooks");
859
+ } else if (typeof require === "function") {
860
+ mod = require("async_hooks");
740
861
  }
741
862
  if (mod) detected = new mod.AsyncLocalStorage();
742
863
  }
743
864
  } catch {
744
865
  detected = null;
745
866
  }
746
- return { als: detected, fallbackStore: { ssr: false, suspenseIdCounter: 0 } };
867
+ return {
868
+ als: detected,
869
+ fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
870
+ fallbackDepth: 0
871
+ };
747
872
  }
748
873
  var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
749
874
  var als = _shared.als;
750
875
  var fallbackStore = _shared.fallbackStore;
876
+ _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
751
877
  function getSSRStore() {
752
878
  if (als) {
753
879
  const s = als.getStore();
@@ -770,7 +896,7 @@ function flushUserCleanups(ctx) {
770
896
  try {
771
897
  list[i]();
772
898
  } catch (err) {
773
- if (typeof console !== "undefined") console.warn("[SibuJS effect] onCleanup threw:", err);
899
+ reportError(err, { phase: "cleanup" });
774
900
  }
775
901
  }
776
902
  }
@@ -783,11 +909,12 @@ function drainReruns(ctx) {
783
909
  } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
784
910
  if (ctx.rerunPending) {
785
911
  ctx.rerunPending = false;
786
- if (_g.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
787
- console.error(
788
- `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
789
- );
790
- }
912
+ reportError(
913
+ new Error(
914
+ `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
915
+ ),
916
+ { phase: "effect" }
917
+ );
791
918
  }
792
919
  }
793
920
  function disposeEffect(ctx) {
@@ -864,6 +991,8 @@ function effect(effectFn, options) {
864
991
  sub.depsTail = null;
865
992
  sub._epoch = 0;
866
993
  sub._structDirty = false;
994
+ sub._hasComputedDep = false;
995
+ sub._errorPhase = "effect";
867
996
  sub._runEpoch = 0;
868
997
  sub._runs = 0;
869
998
  ctx.subscriber = sub;
@@ -932,70 +1061,72 @@ var isBatching = API2.isBatching;
932
1061
 
933
1062
  // src/core/signals/signal.ts
934
1063
  var _g2 = globalThis;
935
- var _isDev4 = isDev();
1064
+ var _isDev3 = isDev();
936
1065
  function signal(initial, options) {
937
- const state = {
1066
+ const state2 = {
938
1067
  value: initial,
939
1068
  __v: 0,
940
1069
  __sc: 0,
941
1070
  subsHead: null,
942
1071
  subsTail: null,
943
1072
  __activeNode: null,
944
- __name: void 0
1073
+ __name: void 0,
1074
+ _d: false,
1075
+ _validate: void 0
945
1076
  };
946
- const debugName = _isDev4 ? options?.name : void 0;
1077
+ const debugName = _isDev3 ? options?.name : void 0;
947
1078
  const equalsFn = options?.equals;
948
- if (debugName) state.__name = debugName;
1079
+ if (debugName) state2.__name = debugName;
949
1080
  function get() {
950
- recordDependency2(state);
951
- return state.value;
1081
+ recordDependency2(state2);
1082
+ return state2.value;
952
1083
  }
953
- get.__signal = state;
1084
+ get.__signal = state2;
954
1085
  if (debugName) get.__name = debugName;
955
1086
  let set;
956
1087
  if (equalsFn) {
957
1088
  set = (next) => {
958
- const prev = state.value;
1089
+ const prev = state2.value;
959
1090
  const newValue = typeof next === "function" ? next(prev) : next;
960
1091
  if (equalsFn(prev, newValue)) return;
961
- state.value = newValue;
962
- state.__v++;
963
- if (_isDev4) {
1092
+ state2.value = newValue;
1093
+ state2.__v++;
1094
+ if (_isDev3) {
964
1095
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
965
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
1096
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
966
1097
  }
967
- if (!enqueueBatchedSignal(state)) {
968
- notifySubscribers2(state);
1098
+ if (!enqueueBatchedSignal(state2)) {
1099
+ notifySubscribers2(state2);
969
1100
  }
970
1101
  };
971
- } else if (_isDev4) {
1102
+ } else if (_isDev3) {
972
1103
  set = (next) => {
973
- const prev = state.value;
1104
+ const prev = state2.value;
974
1105
  const newValue = typeof next === "function" ? next(prev) : next;
975
1106
  if (Object.is(newValue, prev)) return;
976
- state.value = newValue;
977
- state.__v++;
1107
+ state2.value = newValue;
1108
+ state2.__v++;
978
1109
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
979
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
980
- if (!enqueueBatchedSignal(state)) {
981
- notifySubscribers2(state);
1110
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1111
+ if (!enqueueBatchedSignal(state2)) {
1112
+ notifySubscribers2(state2);
982
1113
  }
983
1114
  };
984
1115
  } else {
985
1116
  set = (next) => {
986
- const prev = state.value;
1117
+ const prev = state2.value;
987
1118
  const newValue = typeof next === "function" ? next(prev) : next;
988
1119
  if (Object.is(newValue, prev)) return;
989
- state.value = newValue;
990
- state.__v++;
991
- if (!enqueueBatchedSignal(state)) {
992
- notifySubscribers2(state);
1120
+ state2.value = newValue;
1121
+ state2.__v++;
1122
+ if (!enqueueBatchedSignal(state2)) {
1123
+ notifySubscribers2(state2);
993
1124
  }
994
1125
  };
995
1126
  }
996
- if (_isDev4) {
1127
+ if (_isDev3) {
997
1128
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
998
- if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial });
1129
+ if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
999
1130
  }
1000
1131
  return [get, set];
1001
1132
  }
@@ -1041,15 +1172,84 @@ function uniqueId(suffix) {
1041
1172
  return suffix ? `${id}-${suffix}` : id;
1042
1173
  }
1043
1174
 
1175
+ // src/core/rendering/dispose.ts
1176
+ var elementDisposers = /* @__PURE__ */ new WeakMap();
1177
+ var MAX_DRAIN_TEARDOWNS = 1e4;
1178
+ function reportDrainRunaway(label, executed, remaining) {
1179
+ if (typeof console === "undefined") return;
1180
+ console.error(
1181
+ `[SibuJS ${label}] runaway cleanup: stopped after running ${executed} teardowns with ${remaining} still queued. Cleanup production did not stabilize before the teardown safety ceiling was reached \u2014 the remaining work was NOT run.`,
1182
+ { executed, remaining }
1183
+ );
1184
+ }
1185
+ var _isDev4 = isDev();
1186
+ var activeBindingCount = 0;
1187
+ function dispose(node) {
1188
+ const stack = [node];
1189
+ const order = [];
1190
+ while (stack.length > 0) {
1191
+ const current = stack.pop();
1192
+ order.push(current);
1193
+ const children = Array.from(current.childNodes);
1194
+ for (let i = 0; i < children.length; i++) {
1195
+ stack.push(children[i]);
1196
+ }
1197
+ }
1198
+ for (let i = order.length - 1; i >= 0; i--) {
1199
+ const current = order[i];
1200
+ if (elementDisposers.has(current)) {
1201
+ let executed = 0;
1202
+ let runaway = false;
1203
+ while (!runaway) {
1204
+ const pending = elementDisposers.get(current);
1205
+ if (!pending || pending.length === 0) break;
1206
+ const snapshot = pending.slice();
1207
+ elementDisposers.delete(current);
1208
+ if (_isDev4) activeBindingCount -= snapshot.length;
1209
+ for (let i2 = 0; i2 < snapshot.length; i2++) {
1210
+ if (executed >= MAX_DRAIN_TEARDOWNS) {
1211
+ const rest = snapshot.slice(i2);
1212
+ const added = elementDisposers.get(current);
1213
+ elementDisposers.set(current, added ? rest.concat(added) : rest);
1214
+ if (_isDev4) activeBindingCount += rest.length;
1215
+ reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
1216
+ runaway = true;
1217
+ break;
1218
+ }
1219
+ executed++;
1220
+ try {
1221
+ snapshot[i2]();
1222
+ } catch (err) {
1223
+ reportError(err, { phase: "cleanup", name: "disposer" });
1224
+ }
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+ function replaceChildrenSafely(parent, ...next) {
1231
+ for (let i = 0; i < next.length; i++) {
1232
+ const node = next[i];
1233
+ node.parentNode?.removeChild(node);
1234
+ }
1235
+ const current = Array.from(parent.childNodes);
1236
+ for (let i = 0; i < current.length; i++) {
1237
+ dispose(current[i]);
1238
+ }
1239
+ parent.replaceChildren(...next);
1240
+ }
1241
+
1044
1242
  // src/utils/sanitize.ts
1045
1243
  function stripControlChars(value) {
1046
1244
  return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
1047
1245
  }
1048
1246
  var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
1247
+ var OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
1049
1248
  function sanitizeUrl(url) {
1050
- const trimmed = stripControlChars(url).trim();
1051
- if (!trimmed) return "";
1052
- const lower = trimmed.toLowerCase();
1249
+ const probe = stripControlChars(url).trim();
1250
+ if (!probe) return "";
1251
+ const output = url.replace(OUTER_TRIM, "");
1252
+ const lower = probe.toLowerCase();
1053
1253
  let schemeEnd = -1;
1054
1254
  for (let i = 0; i < lower.length; i++) {
1055
1255
  const ch = lower.charCodeAt(i);
@@ -1059,11 +1259,11 @@ function sanitizeUrl(url) {
1059
1259
  }
1060
1260
  if (ch === 47 || ch === 63 || ch === 35) break;
1061
1261
  }
1062
- if (schemeEnd === -1) return trimmed;
1262
+ if (schemeEnd === -1) return output;
1063
1263
  const scheme = lower.slice(0, schemeEnd + 1);
1064
- if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return trimmed;
1264
+ if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output;
1065
1265
  if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
1066
- return trimmed;
1266
+ return output;
1067
1267
  }
1068
1268
 
1069
1269
  // src/performance/domRecycler.ts
@@ -1106,7 +1306,7 @@ var DOMPool = class {
1106
1306
  this.pools.set(tag, pool);
1107
1307
  }
1108
1308
  if (pool.length >= this.maxSize) return;
1109
- element.innerHTML = "";
1309
+ replaceChildrenSafely(element);
1110
1310
  element.className = "";
1111
1311
  element.removeAttribute("style");
1112
1312
  element.removeAttribute("id");
@@ -1244,8 +1444,9 @@ function normalizedStore(schema) {
1244
1444
  ids: [],
1245
1445
  entities: {}
1246
1446
  });
1447
+ const idOf = (entity) => String(entity[idKey]);
1247
1448
  function add(entity) {
1248
- const id = String(entity[idKey]);
1449
+ const id = idOf(entity);
1249
1450
  setState((prev) => {
1250
1451
  const ids = prev.ids.includes(id) ? prev.ids : [...prev.ids, id];
1251
1452
  return {
@@ -1259,7 +1460,7 @@ function normalizedStore(schema) {
1259
1460
  const nextIds = [...prev.ids];
1260
1461
  const nextEntities = { ...prev.entities };
1261
1462
  for (const entity of entities) {
1262
- const id = String(entity[idKey]);
1463
+ const id = idOf(entity);
1263
1464
  if (!nextIds.includes(id)) {
1264
1465
  nextIds.push(id);
1265
1466
  }
@@ -1269,12 +1470,12 @@ function normalizedStore(schema) {
1269
1470
  });
1270
1471
  }
1271
1472
  function get(id) {
1272
- const state = getState();
1273
- return state.entities[id];
1473
+ const state2 = getState();
1474
+ return state2.entities[id];
1274
1475
  }
1275
1476
  function getAll() {
1276
- const state = getState();
1277
- return state.ids.map((id) => state.entities[id]);
1477
+ const state2 = getState();
1478
+ return state2.ids.map((id) => state2.entities[id]);
1278
1479
  }
1279
1480
  function update(id, partial) {
1280
1481
  setState((prev) => {
@@ -1300,8 +1501,8 @@ function normalizedStore(schema) {
1300
1501
  });
1301
1502
  }
1302
1503
  function select(predicate) {
1303
- const state = getState();
1304
- return state.ids.map((id) => state.entities[id]).filter(predicate);
1504
+ const state2 = getState();
1505
+ return state2.ids.map((id) => state2.entities[id]).filter(predicate);
1305
1506
  }
1306
1507
  return { add, addMany, get, getAll, update, remove, select, getState };
1307
1508
  }
@@ -1361,58 +1562,6 @@ function denormalize(id, entities, schema) {
1361
1562
  return result;
1362
1563
  }
1363
1564
 
1364
- // src/core/rendering/dispose.ts
1365
- var elementDisposers = /* @__PURE__ */ new WeakMap();
1366
- var _isDev7 = isDev();
1367
- var activeBindingCount = 0;
1368
- function dispose(node) {
1369
- const stack = [node];
1370
- const order = [];
1371
- while (stack.length > 0) {
1372
- const current = stack.pop();
1373
- order.push(current);
1374
- const children = Array.from(current.childNodes);
1375
- for (let i = 0; i < children.length; i++) {
1376
- stack.push(children[i]);
1377
- }
1378
- }
1379
- for (let i = order.length - 1; i >= 0; i--) {
1380
- const current = order[i];
1381
- const disposers = elementDisposers.get(current);
1382
- if (disposers) {
1383
- const snapshot = disposers.slice();
1384
- elementDisposers.delete(current);
1385
- if (_isDev7) activeBindingCount -= snapshot.length;
1386
- for (const d of snapshot) {
1387
- try {
1388
- d();
1389
- } catch (err) {
1390
- if (_isDev7 && typeof console !== "undefined") {
1391
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1392
- }
1393
- }
1394
- }
1395
- let extraPasses = 0;
1396
- while (extraPasses++ < 8) {
1397
- const added = elementDisposers.get(current);
1398
- if (!added || added.length === 0) break;
1399
- const moreSnapshot = added.slice();
1400
- elementDisposers.delete(current);
1401
- if (_isDev7) activeBindingCount -= moreSnapshot.length;
1402
- for (const d of moreSnapshot) {
1403
- try {
1404
- d();
1405
- } catch (err) {
1406
- if (_isDev7 && typeof console !== "undefined") {
1407
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
1408
- }
1409
- }
1410
- }
1411
- }
1412
- }
1413
- }
1414
- }
1415
-
1416
1565
  // src/performance/chunkLoader.ts
1417
1566
  function clearChildren(el) {
1418
1567
  while (el.firstChild) {
@@ -1433,7 +1582,14 @@ function createChunkRegistry(config = {}) {
1433
1582
  } = config;
1434
1583
  const cache = /* @__PURE__ */ new Map();
1435
1584
  const pending = /* @__PURE__ */ new Map();
1436
- const preloaded = /* @__PURE__ */ new Set();
1585
+ const preloaded = /* @__PURE__ */ new Map();
1586
+ function notify(name, run) {
1587
+ try {
1588
+ run();
1589
+ } catch (err) {
1590
+ reportError(err, { phase: "async", name: `chunkRegistry(${name})` });
1591
+ }
1592
+ }
1437
1593
  function evict() {
1438
1594
  while (cache.size >= maxCacheSize) {
1439
1595
  let lru = null;
@@ -1488,30 +1644,47 @@ function createChunkRegistry(config = {}) {
1488
1644
  cached.lastAccess = Date.now();
1489
1645
  return cached.value;
1490
1646
  }
1491
- const pendingLoad = pending.get(id);
1492
- if (pendingLoad) return pendingLoad;
1493
- onLoadStart?.(id);
1494
- const loadPromise = loadWithRetry(id, loader).then((value) => {
1495
- evict();
1496
- const now = Date.now();
1497
- cache.set(id, { value, timestamp: now, lastAccess: now, accessCount: 1 });
1498
- pending.delete(id);
1499
- onLoadEnd?.(id);
1500
- return value;
1501
- }).catch((err) => {
1502
- pending.delete(id);
1503
- const error = err instanceof Error ? err : new Error(String(err));
1504
- onLoadError?.(id, error);
1505
- throw error;
1647
+ const existing = pending.get(id);
1648
+ if (existing) return existing.promise;
1649
+ let resolveOuter;
1650
+ let rejectOuter;
1651
+ const outcome = new Promise((resolve, reject) => {
1652
+ resolveOuter = resolve;
1653
+ rejectOuter = reject;
1506
1654
  });
1507
- pending.set(id, loadPromise);
1508
- return loadPromise;
1655
+ const entry = { promise: outcome };
1656
+ pending.set(id, entry);
1657
+ const owns = () => pending.get(id) === entry;
1658
+ notify("onLoadStart", () => onLoadStart?.(id));
1659
+ void loadWithRetry(id, loader).then(
1660
+ (value) => {
1661
+ if (owns()) {
1662
+ pending.delete(id);
1663
+ evict();
1664
+ const now = Date.now();
1665
+ cache.set(id, { value, timestamp: now, lastAccess: now, accessCount: 1 });
1666
+ }
1667
+ notify("onLoadEnd", () => onLoadEnd?.(id));
1668
+ resolveOuter(value);
1669
+ },
1670
+ (err) => {
1671
+ if (owns()) pending.delete(id);
1672
+ const error = err instanceof Error ? err : new Error(String(err));
1673
+ notify("onLoadError", () => onLoadError?.(id, error));
1674
+ rejectOuter(error);
1675
+ }
1676
+ ).catch((internal) => {
1677
+ reportError(internal, { phase: "async", name: "chunkRegistry(settlement)" });
1678
+ rejectOuter(internal);
1679
+ });
1680
+ return outcome;
1509
1681
  }
1510
1682
  function preloadFn(id, loader) {
1511
1683
  if (cache.has(id) || pending.has(id) || preloaded.has(id)) return;
1512
- preloaded.add(id);
1684
+ const marker = {};
1685
+ preloaded.set(id, marker);
1513
1686
  loadFn(id, loader).catch(() => {
1514
- preloaded.delete(id);
1687
+ if (preloaded.get(id) === marker) preloaded.delete(id);
1515
1688
  });
1516
1689
  }
1517
1690
  return {
@@ -1545,21 +1718,33 @@ function createChunkRegistry(config = {}) {
1545
1718
  return void 0;
1546
1719
  },
1547
1720
  /**
1548
- * Invalidate a cached chunk.
1721
+ * Invalidate a cached chunk, and supersede any load still in flight for it.
1722
+ *
1723
+ * In-flight work is not cancelled — the loader API takes no abort signal, so
1724
+ * claiming cancellation would be a lie. It is *superseded*: it still settles
1725
+ * for whoever called `load()`, but it can no longer write to the cache, and
1726
+ * a subsequent `load(id, …)` starts fresh work rather than adopting it.
1549
1727
  */
1550
1728
  invalidate(id) {
1551
1729
  cache.delete(id);
1552
1730
  preloaded.delete(id);
1731
+ pending.delete(id);
1553
1732
  },
1554
1733
  /**
1555
- * Clear all cached chunks.
1734
+ * Clear all cached chunks, and supersede every load still in flight.
1735
+ *
1736
+ * Same barrier as `invalidate`, applied to every key at once.
1556
1737
  */
1557
1738
  clear() {
1558
1739
  cache.clear();
1559
1740
  preloaded.clear();
1741
+ pending.clear();
1560
1742
  },
1561
1743
  /**
1562
1744
  * Get cache statistics.
1745
+ *
1746
+ * Describes currently OWNED state: superseded loads are already absent from
1747
+ * `pending`, so the counts never include work whose result will be discarded.
1563
1748
  */
1564
1749
  stats() {
1565
1750
  return {