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
package/dist/extras.cjs CHANGED
@@ -175,6 +175,7 @@ __export(extras_exports, {
175
175
  lazyModule: () => lazyModule,
176
176
  loadRemoteModule: () => loadRemoteModule,
177
177
  loadWasmModule: () => loadWasmModule,
178
+ loadWasmModuleWithOptions: () => loadWasmModuleWithOptions,
178
179
  loaderData: () => loaderData,
179
180
  machine: () => machine,
180
181
  matchesPattern: () => matchesPattern,
@@ -338,8 +339,95 @@ function devWarn(message) {
338
339
  }
339
340
  }
340
341
 
342
+ // src/core/errors.ts
343
+ var ERROR_STATE_KEY = /* @__PURE__ */ Symbol.for("sibujs.runtime-errors.v1");
344
+ function state() {
345
+ const g2 = globalThis;
346
+ const existing = g2[ERROR_STATE_KEY];
347
+ if (existing) return existing;
348
+ const created = { handler: null, handlerDepth: 0 };
349
+ g2[ERROR_STATE_KEY] = created;
350
+ return created;
351
+ }
352
+ function normalizeError(value) {
353
+ if (value instanceof Error) return value;
354
+ const message = typeof value === "string" ? value : `Non-Error value thrown: ${safeStringify(value)}`;
355
+ return new Error(message, { cause: value });
356
+ }
357
+ function safeStringify(value) {
358
+ if (typeof value === "symbol") return value.toString();
359
+ try {
360
+ return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
361
+ } catch {
362
+ return Object.prototype.toString.call(value);
363
+ }
364
+ }
365
+ function describe(context2) {
366
+ return context2.name ? `${context2.phase} "${context2.name}"` : context2.phase;
367
+ }
368
+ var REACTIVE_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
369
+ function untrackedRun(fn) {
370
+ const api = globalThis[REACTIVE_REGISTRY_KEY];
371
+ return api?.untracked ? api.untracked(fn) : fn();
372
+ }
373
+ var PROPAGATE_EVENT = "sibu:error-propagate";
374
+ function claimedByBoundary(error, context2) {
375
+ const raw = context2.node;
376
+ if (!raw || typeof CustomEvent === "undefined") return false;
377
+ const target = resolveEventTarget(raw);
378
+ if (!target) return false;
379
+ try {
380
+ const event = new CustomEvent(PROPAGATE_EVENT, {
381
+ bubbles: true,
382
+ cancelable: true,
383
+ detail: { error, context: context2 }
384
+ });
385
+ return target.dispatchEvent(event) === false;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+ function resolveEventTarget(node) {
391
+ const ELEMENT_NODE = 1;
392
+ let current = node;
393
+ while (current) {
394
+ if (current.nodeType === ELEMENT_NODE && typeof current.dispatchEvent === "function") {
395
+ return current;
396
+ }
397
+ current = current.parentNode;
398
+ }
399
+ const self2 = node;
400
+ return typeof self2.dispatchEvent === "function" ? self2 : null;
401
+ }
402
+ function defaultReport(error, context2) {
403
+ if (typeof console === "undefined") return;
404
+ console.error(`[SibuJS] Uncaught error during ${describe(context2)}: ${error.message}`, error);
405
+ }
406
+ function reportError(error, context2) {
407
+ const err = normalizeError(error);
408
+ untrackedRun(() => {
409
+ if (claimedByBoundary(err, context2)) return;
410
+ const s2 = state();
411
+ const handler = s2.handler;
412
+ if (handler && s2.handlerDepth === 0) {
413
+ s2.handlerDepth++;
414
+ try {
415
+ handler(error instanceof Error ? error : err, context2);
416
+ return;
417
+ } catch (handlerError) {
418
+ if (typeof console !== "undefined") {
419
+ const detail = handlerError instanceof Error ? handlerError.message : String(handlerError);
420
+ console.error(`[SibuJS] runtime error handler itself threw: ${detail}`, handlerError);
421
+ }
422
+ } finally {
423
+ s2.handlerDepth--;
424
+ }
425
+ }
426
+ defaultReport(err, context2);
427
+ });
428
+ }
429
+
341
430
  // src/reactivity/track-core.ts
342
- var _isDev2 = isDev();
343
431
  var POOL_MAX = 4096;
344
432
  var nodePool = [];
345
433
  function createNode() {
@@ -347,6 +435,7 @@ function createNode() {
347
435
  sig: null,
348
436
  sub: null,
349
437
  epoch: 0,
438
+ depVersion: 0,
350
439
  sigPrev: null,
351
440
  sigNext: null,
352
441
  subPrev: null,
@@ -431,8 +520,27 @@ function safeInvoke(sub2) {
431
520
  try {
432
521
  sub2();
433
522
  } catch (err) {
434
- if (_isDev2) devWarn(`Subscriber threw during notification: ${err instanceof Error ? err.message : String(err)}`);
523
+ const s2 = sub2;
524
+ reportError(err, { phase: s2._errorPhase ?? "effect", name: s2.__name, node: s2._errorNode });
525
+ }
526
+ }
527
+ function depsChanged(sub2) {
528
+ let node = sub2.depsHead ?? null;
529
+ if (node === null) return true;
530
+ while (node !== null) {
531
+ const sig = node.sig;
532
+ if (sig === null) return true;
533
+ if (sig._d === true && sig._validate !== void 0) {
534
+ try {
535
+ sig._validate();
536
+ } catch {
537
+ return true;
538
+ }
539
+ }
540
+ if (node.depVersion !== sig.__v) return true;
541
+ node = node.subNext;
435
542
  }
543
+ return false;
436
544
  }
437
545
  var suspendDepth = 0;
438
546
  var trackingSuspended = false;
@@ -514,7 +622,7 @@ function track(effectFn, subscriber) {
514
622
  const sub2 = subscriber;
515
623
  return sub2._dispose ?? (sub2._dispose = () => cleanup(subscriber));
516
624
  }
517
- function reactiveBinding(commit) {
625
+ function reactiveBinding(commit, ownerNode) {
518
626
  const run = () => {
519
627
  const s2 = subscriber;
520
628
  if (s2._disposed || s2._reentrant) return;
@@ -530,13 +638,17 @@ function reactiveBinding(commit) {
530
638
  subscriber.depsTail = null;
531
639
  subscriber._epoch = 0;
532
640
  subscriber._structDirty = false;
641
+ subscriber._hasComputedDep = false;
533
642
  subscriber._runEpoch = 0;
534
643
  subscriber._runs = 0;
535
644
  subscriber._reentrant = false;
536
645
  subscriber._disposed = false;
646
+ subscriber._errorPhase = "binding";
647
+ subscriber._errorNode = ownerNode;
537
648
  run();
538
649
  return subscriber._dispose ?? (subscriber._dispose = () => {
539
650
  subscriber._disposed = true;
651
+ subscriber._errorNode = void 0;
540
652
  cleanup(subscriber);
541
653
  });
542
654
  }
@@ -548,9 +660,12 @@ function recordDependency(signal2) {
548
660
  const active = sig.__activeNode ?? null;
549
661
  if (active !== null && active.sub === sub2) {
550
662
  active.epoch = epoch;
663
+ active.depVersion = sig.__v ?? Number.NaN;
551
664
  return;
552
665
  }
666
+ if (sig._validate !== void 0) sub2._hasComputedDep = true;
553
667
  const node = allocNode(signal2, sub2, epoch);
668
+ node.depVersion = sig.__v ?? Number.NaN;
554
669
  node.prevActive = active;
555
670
  sig.__activeNode = node;
556
671
  linkSub(sub2, node);
@@ -569,9 +684,10 @@ function cleanup(subscriber) {
569
684
  node = next;
570
685
  }
571
686
  }
572
- var maxSubscriberRepeats = 50;
687
+ var maxSubscriberRepeats = 1e3;
573
688
  var maxDrainIterations = 1e6;
574
689
  var drainEpoch = 0;
690
+ var quarantined = /* @__PURE__ */ new Set();
575
691
  function setMaxSubscriberRepeats(n) {
576
692
  const prev = maxSubscriberRepeats;
577
693
  if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
@@ -593,19 +709,21 @@ function tickRepeat(sub2) {
593
709
  return s2._runs > maxSubscriberRepeats;
594
710
  }
595
711
  function cycleError(sub2) {
596
- if (typeof console !== "undefined") {
597
- const name = sub2.__name ?? "<unnamed>";
598
- console.error(
599
- `[SibuJS] subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. Breaking to prevent infinite loop.`
600
- );
601
- }
712
+ const name = sub2.__name ?? "<unnamed>";
713
+ reportError(
714
+ new Error(
715
+ `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.`
716
+ ),
717
+ { phase: "scheduler", name }
718
+ );
602
719
  }
603
720
  function absoluteDrainError() {
604
- if (typeof console !== "undefined") {
605
- console.error(
606
- `[SibuJS] Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
607
- );
608
- }
721
+ reportError(
722
+ new Error(
723
+ `Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
724
+ ),
725
+ { phase: "scheduler" }
726
+ );
609
727
  }
610
728
  function drainQueue() {
611
729
  let i2 = 0;
@@ -615,11 +733,14 @@ function drainQueue() {
615
733
  break;
616
734
  }
617
735
  const sub2 = pendingQueue[i2++];
736
+ pendingSet.delete(sub2);
737
+ if (quarantined.size > 0 && quarantined.has(sub2)) continue;
738
+ if (sub2._hasComputedDep === true && !depsChanged(sub2)) continue;
618
739
  if (tickRepeat(sub2)) {
619
740
  cycleError(sub2);
620
- break;
741
+ quarantined.add(sub2);
742
+ continue;
621
743
  }
622
- pendingSet.delete(sub2);
623
744
  safeInvoke(sub2);
624
745
  }
625
746
  }
@@ -634,6 +755,7 @@ function drainNotificationQueue() {
634
755
  if (notifyDepth === 0) {
635
756
  pendingQueue.length = 0;
636
757
  pendingSet.clear();
758
+ if (quarantined.size > 0) quarantined.clear();
637
759
  }
638
760
  }
639
761
  }
@@ -727,6 +849,7 @@ function notifySubscribers(signal2) {
727
849
  if (notifyDepth === 0) {
728
850
  pendingQueue.length = 0;
729
851
  pendingSet.clear();
852
+ if (quarantined.size > 0) quarantined.clear();
730
853
  }
731
854
  }
732
855
  }
@@ -753,14 +876,14 @@ function forEachSubscriber(signal2, visit) {
753
876
  }
754
877
 
755
878
  // src/reactivity/track.ts
756
- var _isDev3 = isDev();
757
- var _runtimeVersion = true ? "3.4.0" : "dev";
879
+ var _isDev2 = isDev();
880
+ var _runtimeVersion = true ? "4.0.0" : "dev";
758
881
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
759
882
  function resolveReactiveApi() {
760
883
  const g2 = globalThis;
761
884
  const existing = g2[REGISTRY_KEY];
762
885
  if (existing) {
763
- if (_isDev3 && !existing.__dupWarned) {
886
+ if (_isDev2 && !existing.__dupWarned) {
764
887
  existing.__dupWarned = true;
765
888
  devWarn(
766
889
  `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']).`
@@ -845,6 +968,21 @@ function derived(getter, options) {
845
968
  }, markDirty);
846
969
  const hook = globalThis.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
847
970
  let evaluating = false;
971
+ const validate = () => {
972
+ if (!cs._d) return;
973
+ const oldValue = cs._v;
974
+ evaluating = true;
975
+ try {
976
+ retrack2(recompute, markDirty);
977
+ if (!Object.is(oldValue, cs._v)) cs.__v++;
978
+ } finally {
979
+ evaluating = false;
980
+ }
981
+ if (hook && !Object.is(oldValue, cs._v)) {
982
+ hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
983
+ }
984
+ };
985
+ cs._validate = validate;
848
986
  function computedGetter() {
849
987
  if (evaluating) {
850
988
  throw new Error(
@@ -852,32 +990,11 @@ function derived(getter, options) {
852
990
  );
853
991
  }
854
992
  if (isTrackingSuspended2()) {
855
- if (cs._d) {
856
- const prev = cs._v;
857
- evaluating = true;
858
- try {
859
- retrack2(recompute, markDirty);
860
- if (!Object.is(prev, cs._v)) cs.__v++;
861
- } finally {
862
- evaluating = false;
863
- }
864
- }
993
+ if (cs._d) validate();
865
994
  return cs._v;
866
995
  }
996
+ if (cs._d) validate();
867
997
  recordDependency2(cs);
868
- if (cs._d) {
869
- const oldValue = cs._v;
870
- evaluating = true;
871
- try {
872
- retrack2(recompute, markDirty);
873
- if (!Object.is(oldValue, cs._v)) cs.__v++;
874
- } finally {
875
- evaluating = false;
876
- }
877
- if (hook && oldValue !== cs._v) {
878
- hook.emit("computed:update", { signal: cs, oldValue, newValue: cs._v });
879
- }
880
- }
881
998
  return cs._v;
882
999
  }
883
1000
  if (debugName) {
@@ -899,20 +1016,24 @@ function detectSSRShared() {
899
1016
  const getBuiltin = process.getBuiltinModule;
900
1017
  if (typeof getBuiltin === "function") {
901
1018
  mod = getBuiltin("node:async_hooks");
902
- } else {
903
- const req = Function("return typeof require==='function'?require:null")();
904
- if (req) mod = req("node:async_hooks");
1019
+ } else if (typeof require === "function") {
1020
+ mod = require("async_hooks");
905
1021
  }
906
1022
  if (mod) detected = new mod.AsyncLocalStorage();
907
1023
  }
908
1024
  } catch {
909
1025
  detected = null;
910
1026
  }
911
- return { als: detected, fallbackStore: { ssr: false, suspenseIdCounter: 0 } };
1027
+ return {
1028
+ als: detected,
1029
+ fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
1030
+ fallbackDepth: 0
1031
+ };
912
1032
  }
913
1033
  var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
914
1034
  var als = _shared.als;
915
1035
  var fallbackStore = _shared.fallbackStore;
1036
+ _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
916
1037
  function getSSRStore() {
917
1038
  if (als) {
918
1039
  const s2 = als.getStore();
@@ -946,7 +1067,7 @@ function flushUserCleanups(ctx) {
946
1067
  try {
947
1068
  list[i2]();
948
1069
  } catch (err) {
949
- if (typeof console !== "undefined") console.warn("[SibuJS effect] onCleanup threw:", err);
1070
+ reportError(err, { phase: "cleanup" });
950
1071
  }
951
1072
  }
952
1073
  }
@@ -959,11 +1080,12 @@ function drainReruns(ctx) {
959
1080
  } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
960
1081
  if (ctx.rerunPending) {
961
1082
  ctx.rerunPending = false;
962
- if (_g.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
963
- console.error(
964
- `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
965
- );
966
- }
1083
+ reportError(
1084
+ new Error(
1085
+ `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
1086
+ ),
1087
+ { phase: "effect" }
1088
+ );
967
1089
  }
968
1090
  }
969
1091
  function disposeEffect(ctx) {
@@ -1040,6 +1162,8 @@ function effect(effectFn, options) {
1040
1162
  sub2.depsTail = null;
1041
1163
  sub2._epoch = 0;
1042
1164
  sub2._structDirty = false;
1165
+ sub2._hasComputedDep = false;
1166
+ sub2._errorPhase = "effect";
1043
1167
  sub2._runEpoch = 0;
1044
1168
  sub2._runs = 0;
1045
1169
  ctx.subscriber = sub2;
@@ -1108,70 +1232,72 @@ var isBatching = API2.isBatching;
1108
1232
 
1109
1233
  // src/core/signals/signal.ts
1110
1234
  var _g2 = globalThis;
1111
- var _isDev4 = isDev();
1235
+ var _isDev3 = isDev();
1112
1236
  function signal(initial, options) {
1113
- const state = {
1237
+ const state2 = {
1114
1238
  value: initial,
1115
1239
  __v: 0,
1116
1240
  __sc: 0,
1117
1241
  subsHead: null,
1118
1242
  subsTail: null,
1119
1243
  __activeNode: null,
1120
- __name: void 0
1244
+ __name: void 0,
1245
+ _d: false,
1246
+ _validate: void 0
1121
1247
  };
1122
- const debugName = _isDev4 ? options?.name : void 0;
1248
+ const debugName = _isDev3 ? options?.name : void 0;
1123
1249
  const equalsFn = options?.equals;
1124
- if (debugName) state.__name = debugName;
1250
+ if (debugName) state2.__name = debugName;
1125
1251
  function get() {
1126
- recordDependency2(state);
1127
- return state.value;
1252
+ recordDependency2(state2);
1253
+ return state2.value;
1128
1254
  }
1129
- get.__signal = state;
1255
+ get.__signal = state2;
1130
1256
  if (debugName) get.__name = debugName;
1131
1257
  let set;
1132
1258
  if (equalsFn) {
1133
1259
  set = (next) => {
1134
- const prev = state.value;
1260
+ const prev = state2.value;
1135
1261
  const newValue = typeof next === "function" ? next(prev) : next;
1136
1262
  if (equalsFn(prev, newValue)) return;
1137
- state.value = newValue;
1138
- state.__v++;
1139
- if (_isDev4) {
1263
+ state2.value = newValue;
1264
+ state2.__v++;
1265
+ if (_isDev3) {
1140
1266
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1141
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
1267
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1142
1268
  }
1143
- if (!enqueueBatchedSignal(state)) {
1144
- notifySubscribers2(state);
1269
+ if (!enqueueBatchedSignal(state2)) {
1270
+ notifySubscribers2(state2);
1145
1271
  }
1146
1272
  };
1147
- } else if (_isDev4) {
1273
+ } else if (_isDev3) {
1148
1274
  set = (next) => {
1149
- const prev = state.value;
1275
+ const prev = state2.value;
1150
1276
  const newValue = typeof next === "function" ? next(prev) : next;
1151
1277
  if (Object.is(newValue, prev)) return;
1152
- state.value = newValue;
1153
- state.__v++;
1278
+ state2.value = newValue;
1279
+ state2.__v++;
1154
1280
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1155
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
1156
- if (!enqueueBatchedSignal(state)) {
1157
- notifySubscribers2(state);
1281
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1282
+ if (!enqueueBatchedSignal(state2)) {
1283
+ notifySubscribers2(state2);
1158
1284
  }
1159
1285
  };
1160
1286
  } else {
1161
1287
  set = (next) => {
1162
- const prev = state.value;
1288
+ const prev = state2.value;
1163
1289
  const newValue = typeof next === "function" ? next(prev) : next;
1164
1290
  if (Object.is(newValue, prev)) return;
1165
- state.value = newValue;
1166
- state.__v++;
1167
- if (!enqueueBatchedSignal(state)) {
1168
- notifySubscribers2(state);
1291
+ state2.value = newValue;
1292
+ state2.__v++;
1293
+ if (!enqueueBatchedSignal(state2)) {
1294
+ notifySubscribers2(state2);
1169
1295
  }
1170
1296
  };
1171
1297
  }
1172
- if (_isDev4) {
1298
+ if (_isDev3) {
1173
1299
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1174
- if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial });
1300
+ if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1175
1301
  }
1176
1302
  return [get, set];
1177
1303
  }
@@ -1182,6 +1308,50 @@ function globalSingleton(key, create) {
1182
1308
  return g2[key] ?? (g2[key] = create());
1183
1309
  }
1184
1310
 
1311
+ // src/data/abort.ts
1312
+ function isAbortError(error) {
1313
+ return typeof error === "object" && error !== null && error.name === "AbortError";
1314
+ }
1315
+ function createAbortError(message = "Aborted") {
1316
+ if (typeof DOMException !== "undefined") {
1317
+ return new DOMException(message, "AbortError");
1318
+ }
1319
+ const error = new Error(message);
1320
+ error.name = "AbortError";
1321
+ return error;
1322
+ }
1323
+
1324
+ // src/data/callbacks.ts
1325
+ function reportCallbackError(label2, error) {
1326
+ if (typeof console !== "undefined") {
1327
+ console.error(`[SibuJS data] ${label2} threw. The operation's own status is unaffected.`, error);
1328
+ }
1329
+ }
1330
+ function runCallback(label2, fn) {
1331
+ try {
1332
+ fn();
1333
+ } catch (error) {
1334
+ reportCallbackError(label2, error);
1335
+ }
1336
+ }
1337
+ function runSelect(label2, fn) {
1338
+ try {
1339
+ return { ok: true, value: fn() };
1340
+ } catch (error) {
1341
+ reportCallbackError(label2, error);
1342
+ return { ok: false };
1343
+ }
1344
+ }
1345
+ function notifyListeners(listeners, label2) {
1346
+ for (const listener of Array.from(listeners)) {
1347
+ try {
1348
+ listener();
1349
+ } catch (error) {
1350
+ reportCallbackError(label2, error);
1351
+ }
1352
+ }
1353
+ }
1354
+
1185
1355
  // src/data/retry.ts
1186
1356
  function calculateDelay(attempt, strategy, baseDelay, maxDelay, jitter) {
1187
1357
  let delay;
@@ -1214,39 +1384,54 @@ async function withRetry(fn, options, onRetry, signal2) {
1214
1384
  const shouldRetry = options?.shouldRetry ?? (() => true);
1215
1385
  let lastError;
1216
1386
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1217
- if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
1387
+ if (signal2?.aborted) throw createAbortError();
1218
1388
  try {
1219
- if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
1220
1389
  return await fn();
1221
1390
  } catch (error) {
1222
1391
  lastError = error;
1392
+ if (signal2?.aborted) throw createAbortError();
1393
+ if (isAbortError(error)) throw error;
1223
1394
  if (attempt >= maxRetries || !shouldRetry(error, attempt)) throw error;
1224
1395
  const delay = calculateDelay(attempt, strategy, baseDelay, maxDelay, jitter);
1225
1396
  onRetry?.(error, attempt, delay);
1226
- await new Promise((resolve, reject) => {
1227
- let onAbort = null;
1228
- const timer = setTimeout(() => {
1229
- if (onAbort && signal2) signal2.removeEventListener("abort", onAbort);
1230
- resolve();
1231
- }, delay);
1232
- if (signal2) {
1233
- onAbort = () => {
1234
- clearTimeout(timer);
1235
- reject(new DOMException("Aborted", "AbortError"));
1236
- };
1237
- signal2.addEventListener("abort", onAbort, { once: true });
1238
- }
1239
- });
1397
+ await waitForRetryDelay(delay, signal2);
1240
1398
  }
1241
1399
  }
1242
1400
  throw lastError;
1243
1401
  }
1402
+ function waitForRetryDelay(delay, signal2) {
1403
+ if (signal2?.aborted) return Promise.reject(createAbortError());
1404
+ return new Promise((resolve, reject) => {
1405
+ let settled = false;
1406
+ const finish = (action) => {
1407
+ if (settled) return;
1408
+ settled = true;
1409
+ clearTimeout(timer);
1410
+ if (signal2) signal2.removeEventListener("abort", onAbort);
1411
+ action();
1412
+ };
1413
+ const onAbort = () => finish(() => reject(createAbortError()));
1414
+ const timer = setTimeout(() => finish(resolve), delay);
1415
+ if (signal2) {
1416
+ signal2.addEventListener("abort", onAbort, { once: true });
1417
+ if (signal2.aborted) onAbort();
1418
+ }
1419
+ });
1420
+ }
1244
1421
 
1245
1422
  // src/data/query.ts
1246
1423
  var globalQueryCache = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.query.cache.v1"), () => /* @__PURE__ */ new Map());
1247
1424
  function getActiveQueryCache() {
1248
1425
  return getRequestScopedCache("query") ?? globalQueryCache;
1249
1426
  }
1427
+ function abandonEntry(entry) {
1428
+ if (entry.gcTimer) clearTimeout(entry.gcTimer);
1429
+ entry.gcTimer = null;
1430
+ entry.controller?.abort();
1431
+ entry.controller = null;
1432
+ entry.promise = null;
1433
+ entry.generation++;
1434
+ }
1250
1435
  function getOrCreateEntry(cache, key, initialData) {
1251
1436
  let entry = cache.get(key);
1252
1437
  if (!entry) {
@@ -1258,7 +1443,9 @@ function getOrCreateEntry(cache, key, initialData) {
1258
1443
  gcTimer: null,
1259
1444
  promise: null,
1260
1445
  listeners: /* @__PURE__ */ new Set(),
1261
- refetchers: /* @__PURE__ */ new Set()
1446
+ refetchers: /* @__PURE__ */ new Set(),
1447
+ controller: null,
1448
+ generation: 0
1262
1449
  };
1263
1450
  cache.set(key, entry);
1264
1451
  }
@@ -1284,9 +1471,10 @@ function query(key, fetcher, options = {}) {
1284
1471
  const [data2, setData] = signal(initialData);
1285
1472
  const [isFetching, setIsFetching] = signal(false);
1286
1473
  const [error, setError] = signal(void 0);
1287
- let abortController = null;
1288
1474
  let disposed = false;
1289
1475
  let currentKey = null;
1476
+ let attachedEntry = null;
1477
+ let attachedKey = null;
1290
1478
  let intervalTimer = null;
1291
1479
  const loading = derived(() => isFetching() && data2() === void 0);
1292
1480
  const isStale = derived(() => {
@@ -1296,82 +1484,128 @@ function query(key, fetcher, options = {}) {
1296
1484
  if (!entry || entry.dataUpdatedAt === 0) return true;
1297
1485
  return Date.now() - entry.dataUpdatedAt >= staleTime;
1298
1486
  });
1487
+ function detachFromEntry() {
1488
+ const entry = attachedEntry;
1489
+ const key2 = attachedKey;
1490
+ if (!entry) return;
1491
+ attachedEntry = null;
1492
+ attachedKey = null;
1493
+ entry.listeners.delete(onCacheUpdate);
1494
+ entry.refetchers.delete(doFetch);
1495
+ entry.subscribers = Math.max(0, entry.subscribers - 1);
1496
+ if (entry.subscribers > 0 || cacheTime < 0 || key2 === null) return;
1497
+ if (cache.get(key2) !== entry) return;
1498
+ if (entry.gcTimer !== null) clearTimeout(entry.gcTimer);
1499
+ entry.gcTimer = setTimeout(() => {
1500
+ const current = cache.get(key2);
1501
+ if (current === entry && current.subscribers <= 0) {
1502
+ abandonEntry(current);
1503
+ cache.delete(key2);
1504
+ }
1505
+ }, cacheTime);
1506
+ entry.gcTimer.unref?.();
1507
+ }
1508
+ function attachToEntry(entry, key2) {
1509
+ if (attachedEntry === entry) {
1510
+ if (entry.gcTimer !== null) {
1511
+ clearTimeout(entry.gcTimer);
1512
+ entry.gcTimer = null;
1513
+ }
1514
+ return;
1515
+ }
1516
+ detachFromEntry();
1517
+ attachedEntry = entry;
1518
+ attachedKey = key2;
1519
+ entry.subscribers++;
1520
+ entry.listeners.add(onCacheUpdate);
1521
+ entry.refetchers.add(doFetch);
1522
+ if (entry.gcTimer !== null) {
1523
+ clearTimeout(entry.gcTimer);
1524
+ entry.gcTimer = null;
1525
+ }
1526
+ }
1299
1527
  async function doFetch() {
1300
1528
  if (disposed || !currentKey || !enabled) return;
1301
1529
  const key2 = currentKey;
1302
- let entry = cache.get(key2);
1303
- if (!entry) {
1304
- entry = getOrCreateEntry(cache, key2);
1305
- entry.listeners.add(onCacheUpdate);
1306
- entry.refetchers.add(doFetch);
1307
- }
1530
+ const entry = getOrCreateEntry(cache, key2);
1531
+ attachToEntry(entry, key2);
1308
1532
  if (entry.promise) {
1309
1533
  setIsFetching(true);
1310
1534
  const captured = entry.promise;
1311
1535
  try {
1312
1536
  await captured;
1313
- if (disposed || currentKey !== key2) return;
1314
- if (entry.promise === captured) {
1315
- onCacheUpdate();
1316
- if (entry.error) onError?.(entry.error);
1317
- else if (entry.data !== void 0) onSuccess?.(entry.data);
1318
- }
1319
1537
  } catch {
1320
- if (disposed || currentKey !== key2) return;
1321
- if (entry.promise === captured) {
1538
+ } finally {
1539
+ if (!disposed && currentKey === key2) {
1322
1540
  onCacheUpdate();
1323
- if (entry.error) onError?.(entry.error);
1541
+ if (entry.error) {
1542
+ const settledError = entry.error;
1543
+ runCallback("query onError", () => onError?.(settledError));
1544
+ } else if (entry.data !== void 0) {
1545
+ const settledData = entry.data;
1546
+ runCallback("query onSuccess", () => onSuccess?.(settledData));
1547
+ }
1548
+ runCallback("query onSettled", () => onSettled?.());
1324
1549
  }
1325
- } finally {
1326
- if (!disposed && currentKey === key2) onSettled?.();
1327
1550
  }
1328
1551
  return;
1329
1552
  }
1330
- abortController?.abort();
1331
- abortController = new AbortController();
1332
- const signal2 = abortController.signal;
1553
+ entry.controller = new AbortController();
1554
+ const signal2 = entry.controller.signal;
1555
+ const generation = ++entry.generation;
1333
1556
  setIsFetching(true);
1334
1557
  let promise;
1335
1558
  try {
1336
1559
  promise = withRetry(() => fetcher({ signal: signal2, key: key2 }), retryOptions, void 0, signal2);
1337
1560
  } catch (err) {
1338
1561
  setIsFetching(false);
1562
+ if (isAbortError(err)) return;
1339
1563
  const errorObj = err instanceof Error ? err : new Error(String(err));
1340
1564
  entry.error = errorObj;
1341
- onError?.(errorObj);
1342
- onSettled?.();
1565
+ runCallback("query onError", () => onError?.(errorObj));
1566
+ runCallback("query onSettled", () => onSettled?.());
1343
1567
  return;
1344
1568
  }
1345
1569
  entry.promise = promise;
1346
1570
  try {
1347
1571
  const result = await promise;
1572
+ if (entry.generation !== generation) return;
1348
1573
  entry.promise = null;
1349
- if (disposed || currentKey !== key2) return;
1574
+ entry.controller = null;
1350
1575
  entry.data = result;
1351
1576
  entry.dataUpdatedAt = Date.now();
1352
1577
  entry.error = void 0;
1353
- const selected = select3 ? select3(result) : result;
1578
+ notifyListeners(entry.listeners, "query cache listener");
1579
+ if (disposed || currentKey !== key2) return;
1580
+ const selected = runSelect("query select", () => select3 ? select3(result) : result);
1354
1581
  batch(() => {
1355
- setData(selected);
1582
+ if (selected.ok) setData(selected.value);
1356
1583
  setIsFetching(false);
1357
1584
  setError(void 0);
1358
1585
  });
1359
- for (const listener of entry.listeners) listener();
1360
- onSuccess?.(result);
1586
+ runCallback("query onSuccess", () => onSuccess?.(result));
1361
1587
  } catch (err) {
1588
+ if (entry.generation !== generation) return;
1362
1589
  entry.promise = null;
1363
- if (disposed || currentKey !== key2) return;
1364
- if (err instanceof DOMException && err.name === "AbortError") return;
1590
+ entry.controller = null;
1591
+ if (isAbortError(err)) {
1592
+ notifyListeners(entry.listeners, "query cache listener");
1593
+ if (!disposed && currentKey === key2) setIsFetching(false);
1594
+ return;
1595
+ }
1365
1596
  const errorObj = err instanceof Error ? err : new Error(String(err));
1366
1597
  entry.error = errorObj;
1598
+ notifyListeners(entry.listeners, "query cache listener");
1599
+ if (disposed || currentKey !== key2) return;
1367
1600
  batch(() => {
1368
1601
  setError(errorObj);
1369
1602
  setIsFetching(false);
1370
1603
  });
1371
- for (const listener of entry.listeners) listener();
1372
- onError?.(errorObj);
1604
+ runCallback("query onError", () => onError?.(errorObj));
1373
1605
  } finally {
1374
- if (!disposed && currentKey === key2) onSettled?.();
1606
+ if (!disposed && currentKey === key2 && entry.generation === generation) {
1607
+ runCallback("query onSettled", () => onSettled?.());
1608
+ }
1375
1609
  }
1376
1610
  }
1377
1611
  function onCacheUpdate() {
@@ -1386,43 +1620,24 @@ function query(key, fetcher, options = {}) {
1386
1620
  return;
1387
1621
  }
1388
1622
  const raw = entry.data;
1389
- const selected = raw !== void 0 && select3 ? select3(raw) : raw;
1623
+ const selected = raw !== void 0 && select3 ? runSelect("query select", () => select3(raw)) : { ok: true, value: raw };
1390
1624
  batch(() => {
1391
- setData(selected);
1625
+ if (selected.ok) setData(selected.value);
1392
1626
  setError(entry.error);
1393
1627
  if (!entry.promise) setIsFetching(false);
1394
1628
  });
1395
1629
  }
1396
1630
  const effectCleanup = effect(() => {
1397
1631
  const key2 = resolveKey();
1398
- if (currentKey !== null && currentKey !== key2) {
1399
- const oldEntry = cache.get(currentKey);
1400
- if (oldEntry) {
1401
- oldEntry.listeners.delete(onCacheUpdate);
1402
- oldEntry.refetchers.delete(doFetch);
1403
- oldEntry.subscribers--;
1404
- if (oldEntry.subscribers <= 0 && cacheTime >= 0) {
1405
- const oldKey = currentKey;
1406
- if (oldEntry.gcTimer !== null) clearTimeout(oldEntry.gcTimer);
1407
- oldEntry.gcTimer = setTimeout(() => cache.delete(oldKey), cacheTime);
1408
- }
1409
- }
1410
- }
1411
1632
  const keyChanged = currentKey !== key2;
1412
1633
  currentKey = key2;
1413
1634
  const entry = getOrCreateEntry(cache, key2, initialData);
1414
- if (keyChanged) entry.subscribers++;
1415
- if (entry.gcTimer !== null) {
1416
- clearTimeout(entry.gcTimer);
1417
- entry.gcTimer = null;
1418
- }
1419
- entry.listeners.add(onCacheUpdate);
1420
- entry.refetchers.add(doFetch);
1635
+ attachToEntry(entry, key2);
1421
1636
  if (entry.data !== void 0) {
1422
1637
  const raw = entry.data;
1423
- const selected = select3 ? select3(raw) : raw;
1638
+ const selected = runSelect("query select", () => select3 ? select3(raw) : raw);
1424
1639
  batch(() => {
1425
- setData(selected);
1640
+ if (selected.ok) setData(selected.value);
1426
1641
  setError(entry.error);
1427
1642
  });
1428
1643
  }
@@ -1460,22 +1675,9 @@ function query(key, fetcher, options = {}) {
1460
1675
  function dispose2() {
1461
1676
  if (disposed) return;
1462
1677
  disposed = true;
1463
- abortController?.abort();
1464
1678
  effectCleanup();
1465
1679
  if (intervalTimer) clearInterval(intervalTimer);
1466
- if (currentKey) {
1467
- const entry = cache.get(currentKey);
1468
- if (entry) {
1469
- entry.listeners.delete(onCacheUpdate);
1470
- entry.refetchers.delete(doFetch);
1471
- entry.subscribers--;
1472
- if (entry.subscribers <= 0 && cacheTime >= 0) {
1473
- const key2 = currentKey;
1474
- if (entry.gcTimer !== null) clearTimeout(entry.gcTimer);
1475
- entry.gcTimer = setTimeout(() => cache.delete(key2), cacheTime);
1476
- }
1477
- }
1478
- }
1680
+ detachFromEntry();
1479
1681
  if (focusHandler && typeof globalThis.removeEventListener === "function") {
1480
1682
  globalThis.removeEventListener("focus", focusHandler);
1481
1683
  }
@@ -1511,21 +1713,21 @@ function setQueryData(key, data2) {
1511
1713
  const newData = typeof data2 === "function" ? data2(entry.data) : data2;
1512
1714
  entry.data = newData;
1513
1715
  entry.dataUpdatedAt = Date.now();
1514
- for (const listener of entry.listeners) listener();
1716
+ notifyListeners(entry.listeners, "query cache listener");
1515
1717
  }
1516
1718
  function clearQueryCache() {
1517
1719
  const activeListeners = [];
1518
1720
  const activeRefetchers = [];
1519
1721
  const activeCache = getActiveQueryCache();
1520
1722
  for (const entry of activeCache.values()) {
1521
- if (entry.gcTimer) clearTimeout(entry.gcTimer);
1522
1723
  if (entry.subscribers > 0) {
1523
1724
  for (const listener of entry.listeners) activeListeners.push(listener);
1524
1725
  for (const refetcher of entry.refetchers) activeRefetchers.push(refetcher);
1525
1726
  }
1727
+ abandonEntry(entry);
1526
1728
  }
1527
1729
  activeCache.clear();
1528
- for (const listener of activeListeners) listener();
1730
+ notifyListeners(activeListeners, "query cache listener");
1529
1731
  for (const refetcher of activeRefetchers) {
1530
1732
  refetcher().catch((err) => {
1531
1733
  if (typeof console !== "undefined") {
@@ -1537,7 +1739,7 @@ function clearQueryCache() {
1537
1739
  function __resetQueryCache() {
1538
1740
  const activeCache = getActiveQueryCache();
1539
1741
  for (const entry of activeCache.values()) {
1540
- if (entry.gcTimer) clearTimeout(entry.gcTimer);
1742
+ abandonEntry(entry);
1541
1743
  }
1542
1744
  activeCache.clear();
1543
1745
  }
@@ -1552,6 +1754,8 @@ function mutation(mutationFn, options = {}) {
1552
1754
  const isIdle = derived(() => status() === "idle");
1553
1755
  let runId = 0;
1554
1756
  let abortController = null;
1757
+ let terminalStatus = "idle";
1758
+ let terminalError;
1555
1759
  async function execute(variables) {
1556
1760
  abortController?.abort();
1557
1761
  abortController = new AbortController();
@@ -1569,25 +1773,38 @@ function mutation(mutationFn, options = {}) {
1569
1773
  }
1570
1774
  const result = await withRetry(() => mutationFn(variables, signal2), options.retry, void 0, signal2);
1571
1775
  if (myRun !== runId) return result;
1776
+ terminalStatus = "success";
1777
+ terminalError = void 0;
1572
1778
  batch(() => {
1573
1779
  setData(result);
1574
1780
  setLoading(false);
1575
1781
  setStatus("success");
1576
1782
  });
1577
- options.onSuccess?.(result, variables, context2);
1578
- options.onSettled?.(result, void 0, variables, context2);
1783
+ runCallback("mutation onSuccess", () => options.onSuccess?.(result, variables, context2));
1784
+ runCallback("mutation onSettled", () => options.onSettled?.(result, void 0, variables, context2));
1579
1785
  return result;
1580
1786
  } catch (err) {
1787
+ if (isAbortError(err)) {
1788
+ if (myRun === runId) {
1789
+ batch(() => {
1790
+ setLoading(false);
1791
+ setError(terminalError);
1792
+ setStatus(terminalStatus);
1793
+ });
1794
+ }
1795
+ throw err;
1796
+ }
1581
1797
  const errorObj = err instanceof Error ? err : new Error(String(err));
1582
- if (errorObj instanceof DOMException && errorObj.name === "AbortError") throw errorObj;
1583
1798
  if (myRun !== runId) throw errorObj;
1799
+ terminalStatus = "error";
1800
+ terminalError = errorObj;
1584
1801
  batch(() => {
1585
1802
  setError(errorObj);
1586
1803
  setLoading(false);
1587
1804
  setStatus("error");
1588
1805
  });
1589
- options.onError?.(errorObj, variables, context2);
1590
- options.onSettled?.(void 0, errorObj, variables, context2);
1806
+ runCallback("mutation onError", () => options.onError?.(errorObj, variables, context2));
1807
+ runCallback("mutation onSettled", () => options.onSettled?.(void 0, errorObj, variables, context2));
1591
1808
  throw errorObj;
1592
1809
  }
1593
1810
  }
@@ -1595,6 +1812,8 @@ function mutation(mutationFn, options = {}) {
1595
1812
  runId++;
1596
1813
  abortController?.abort();
1597
1814
  abortController = null;
1815
+ terminalStatus = "idle";
1816
+ terminalError = void 0;
1598
1817
  batch(() => {
1599
1818
  setData(void 0);
1600
1819
  setError(void 0);
@@ -1610,7 +1829,7 @@ function mutation(mutationFn, options = {}) {
1610
1829
  isIdle,
1611
1830
  mutate: (variables) => {
1612
1831
  execute(variables).catch((err) => {
1613
- if (err instanceof DOMException && err.name === "AbortError") return;
1832
+ if (isAbortError(err)) return;
1614
1833
  if (typeof console !== "undefined") {
1615
1834
  console.warn("[SibuJS mutation] mutate() failed; check `.error()` signal or onError option.", err);
1616
1835
  }
@@ -1683,10 +1902,17 @@ function infiniteQuery(key, fetcher, options) {
1683
1902
  setIsFetchingNext(false);
1684
1903
  setIsFetchingPrev(false);
1685
1904
  });
1686
- onSuccess?.(newPages);
1905
+ runCallback("infiniteQuery onSuccess", () => onSuccess?.(newPages));
1687
1906
  } catch (err) {
1688
1907
  if (disposed || myRun !== runId) return;
1689
- if (err instanceof DOMException && err.name === "AbortError") return;
1908
+ if (isAbortError(err)) {
1909
+ batch(() => {
1910
+ setIsFetching(false);
1911
+ setIsFetchingNext(false);
1912
+ setIsFetchingPrev(false);
1913
+ });
1914
+ return;
1915
+ }
1690
1916
  const errorObj = err instanceof Error ? err : new Error(String(err));
1691
1917
  batch(() => {
1692
1918
  setError(errorObj);
@@ -1694,7 +1920,7 @@ function infiniteQuery(key, fetcher, options) {
1694
1920
  setIsFetchingNext(false);
1695
1921
  setIsFetchingPrev(false);
1696
1922
  });
1697
- onError?.(errorObj);
1923
+ runCallback("infiniteQuery onError", () => onError?.(errorObj));
1698
1924
  }
1699
1925
  })();
1700
1926
  inFlight = promise;
@@ -1876,7 +2102,7 @@ function resource(sourceOrFetcher, fetcherOrOptions, maybeOptions) {
1876
2102
  setLoading(true);
1877
2103
  setError(void 0);
1878
2104
  });
1879
- options.onStart?.();
2105
+ runCallback("resource onStart", () => options.onStart?.());
1880
2106
  try {
1881
2107
  const result = await withRetry(() => fetcher(sourceValue, { signal: signal2, prev }), options.retry, void 0, signal2);
1882
2108
  if (version !== fetchVersion || disposed) return;
@@ -1885,10 +2111,10 @@ function resource(sourceOrFetcher, fetcherOrOptions, maybeOptions) {
1885
2111
  setData(result);
1886
2112
  setLoading(false);
1887
2113
  });
1888
- options.onSuccess?.(result);
2114
+ runCallback("resource onSuccess", () => options.onSuccess?.(result));
1889
2115
  } catch (err) {
1890
2116
  if (version !== fetchVersion || disposed) return;
1891
- if (err instanceof DOMException && err.name === "AbortError") {
2117
+ if (isAbortError(err)) {
1892
2118
  if (version === fetchVersion) setLoading(false);
1893
2119
  return;
1894
2120
  }
@@ -1897,10 +2123,10 @@ function resource(sourceOrFetcher, fetcherOrOptions, maybeOptions) {
1897
2123
  setError(errorObj);
1898
2124
  setLoading(false);
1899
2125
  });
1900
- options.onError?.(errorObj);
2126
+ runCallback("resource onError", () => options.onError?.(errorObj));
1901
2127
  } finally {
1902
2128
  if (version === fetchVersion) {
1903
- options.onSettled?.();
2129
+ runCallback("resource onSettled", () => options.onSettled?.());
1904
2130
  }
1905
2131
  }
1906
2132
  }
@@ -2088,13 +2314,14 @@ async function offlineStore(options) {
2088
2314
  return data2().filter(filter);
2089
2315
  }
2090
2316
  async function sync() {
2091
- if (!adapter || isSyncing() || closed) return;
2317
+ const syncAdapter2 = adapter;
2318
+ if (!syncAdapter2 || isSyncing() || closed) return;
2092
2319
  setIsSyncing(true);
2093
2320
  try {
2094
2321
  const snapshot = await idbGetAllWithKeys(db, "_changes");
2095
2322
  if (closed) return;
2096
2323
  if (snapshot.length > 0) {
2097
- const result = await adapter.push(snapshot.map((e) => e.value));
2324
+ const result = await syncAdapter2.push(snapshot.map((e) => e.value));
2098
2325
  if (closed) return;
2099
2326
  if (result.ok) {
2100
2327
  await idbDeleteKeys(
@@ -2107,20 +2334,18 @@ async function offlineStore(options) {
2107
2334
  console.warn(`[offlineStore] push rejected by adapter${result.error ? `: ${result.error}` : ""}`);
2108
2335
  }
2109
2336
  }
2110
- const remoteItems = await adapter.pull(lastSynced());
2337
+ const remoteItems = await syncAdapter2.pull(lastSynced());
2111
2338
  if (closed) return;
2112
2339
  const pendingChanges = await idbGetAll(db, "_changes");
2113
2340
  if (closed) return;
2114
- const pendingKeys = /* @__PURE__ */ new Set();
2341
+ const pendingByKey = /* @__PURE__ */ new Map();
2115
2342
  for (const c of pendingChanges) {
2116
2343
  const k = c.item[keyPath];
2117
- if (k != null) pendingKeys.add(k);
2344
+ if (k != null) pendingByKey.set(k, c);
2118
2345
  }
2119
- const safeRemote = remoteItems.filter((item) => {
2120
- const k = item[keyPath];
2121
- return k == null || !pendingKeys.has(k);
2122
- });
2123
- await idbPutMany(db, "items", safeRemote);
2346
+ const resolved = await resolveRemoteItems(syncAdapter2, remoteItems, pendingByKey);
2347
+ if (closed) return;
2348
+ await idbPutMany(db, "items", resolved);
2124
2349
  if (closed) return;
2125
2350
  const now = Date.now();
2126
2351
  await idbPut(db, "_meta", now, "lastSynced");
@@ -2133,6 +2358,42 @@ async function offlineStore(options) {
2133
2358
  setIsSyncing(false);
2134
2359
  }
2135
2360
  }
2361
+ async function resolveRemoteItems(activeAdapter, remoteItems, pendingByKey) {
2362
+ const strategy = activeAdapter.conflictStrategy ?? "client-wins";
2363
+ const out = [];
2364
+ for (const remote of remoteItems) {
2365
+ const key = remote[keyPath];
2366
+ const pending = key == null ? void 0 : pendingByKey.get(key);
2367
+ if (!pending) {
2368
+ out.push(remote);
2369
+ continue;
2370
+ }
2371
+ if (strategy === "server-wins") {
2372
+ out.push(remote);
2373
+ continue;
2374
+ }
2375
+ if (strategy === "manual") {
2376
+ if (!activeAdapter.resolveConflict) {
2377
+ if (typeof console !== "undefined") {
2378
+ console.warn(
2379
+ '[offlineStore] conflictStrategy is "manual" but the adapter has no resolveConflict(); keeping the local record for this key (client-wins).'
2380
+ );
2381
+ }
2382
+ continue;
2383
+ }
2384
+ try {
2385
+ const local = pending.type === "delete" ? void 0 : await idbGet(db, "items", key);
2386
+ const choice = await activeAdapter.resolveConflict({ key, local, remote, pending });
2387
+ if (choice !== void 0) out.push(choice);
2388
+ } catch (err) {
2389
+ if (typeof console !== "undefined") {
2390
+ console.warn("[offlineStore] resolveConflict threw; keeping the local record", err);
2391
+ }
2392
+ }
2393
+ }
2394
+ }
2395
+ return out;
2396
+ }
2136
2397
  function attach(newAdapter) {
2137
2398
  adapter = newAdapter;
2138
2399
  }
@@ -2171,10 +2432,18 @@ function syncAdapter(config) {
2171
2432
  }
2172
2433
 
2173
2434
  // src/core/rendering/context.ts
2435
+ var _isDev4 = isDev();
2174
2436
  function context(defaultValue) {
2175
2437
  const [getValue, setValue] = signal(defaultValue);
2438
+ const warnIfSSR = (method) => {
2439
+ if (!_isDev4 || !isSSR()) return;
2440
+ devWarn(
2441
+ `context.${method}() called during SSR. SibuJS context() is application-global \u2014 it is NOT isolated per request, so a concurrent request can observe this value. Do not use context() for request-specific SSR data; pass it explicitly or use request-scoped storage instead.`
2442
+ );
2443
+ };
2176
2444
  const ctx = {
2177
2445
  provide(value) {
2446
+ warnIfSSR("provide");
2178
2447
  const previous2 = getValue();
2179
2448
  setValue(value);
2180
2449
  return () => setValue(previous2);
@@ -2186,13 +2455,21 @@ function context(defaultValue) {
2186
2455
  return getValue();
2187
2456
  },
2188
2457
  set(value) {
2458
+ warnIfSSR("set");
2189
2459
  setValue(value);
2190
2460
  },
2191
2461
  withContext(value, fn) {
2462
+ warnIfSSR("withContext");
2192
2463
  const previous2 = getValue();
2193
2464
  setValue(value);
2194
2465
  try {
2195
- return fn();
2466
+ const result = fn();
2467
+ if (_isDev4 && result !== null && typeof result?.then === "function") {
2468
+ devWarn(
2469
+ "context.withContext() received an async callback. Scoping is synchronous only \u2014 the previous value is restored as soon as the callback returns its promise, so anything after an `await` runs outside the scope."
2470
+ );
2471
+ }
2472
+ return result;
2196
2473
  } finally {
2197
2474
  setValue(previous2);
2198
2475
  }
@@ -2348,16 +2625,21 @@ function socket(url, options) {
2348
2625
  function stripControlChars(value) {
2349
2626
  return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
2350
2627
  }
2628
+ function canonicalAttrName(name) {
2629
+ return name.replace(/[A-Z]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 32));
2630
+ }
2351
2631
  function isEventHandlerAttr(name) {
2352
2632
  if (name.length < 3) return false;
2353
2633
  const lower = name.toLowerCase();
2354
2634
  return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
2355
2635
  }
2356
2636
  var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
2637
+ var OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
2357
2638
  function sanitizeUrl(url) {
2358
- const trimmed = stripControlChars(url).trim();
2359
- if (!trimmed) return "";
2360
- const lower = trimmed.toLowerCase();
2639
+ const probe = stripControlChars(url).trim();
2640
+ if (!probe) return "";
2641
+ const output2 = url.replace(OUTER_TRIM, "");
2642
+ const lower = probe.toLowerCase();
2361
2643
  let schemeEnd = -1;
2362
2644
  for (let i2 = 0; i2 < lower.length; i2++) {
2363
2645
  const ch = lower.charCodeAt(i2);
@@ -2367,12 +2649,13 @@ function sanitizeUrl(url) {
2367
2649
  }
2368
2650
  if (ch === 47 || ch === 63 || ch === 35) break;
2369
2651
  }
2370
- if (schemeEnd === -1) return trimmed;
2652
+ if (schemeEnd === -1) return output2;
2371
2653
  const scheme = lower.slice(0, schemeEnd + 1);
2372
- if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return trimmed;
2654
+ if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output2;
2373
2655
  if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
2374
- return trimmed;
2656
+ return output2;
2375
2657
  }
2658
+ var SRCSET_DESCRIPTOR = /^\s+\d+(\.\d+)?[wx]$/;
2376
2659
  function sanitizeSrcset(value) {
2377
2660
  const parts = value.split(",");
2378
2661
  const out = [];
@@ -2381,6 +2664,7 @@ function sanitizeSrcset(value) {
2381
2664
  if (!part) continue;
2382
2665
  const m = part.match(/^(\S+)(\s+.+)?$/);
2383
2666
  if (!m) continue;
2667
+ if (m[2] !== void 0 && !SRCSET_DESCRIPTOR.test(m[2])) continue;
2384
2668
  const safe = sanitizeUrl(m[1]);
2385
2669
  if (!safe) continue;
2386
2670
  out.push(m[2] ? `${safe}${m[2]}` : safe);
@@ -2388,23 +2672,83 @@ function sanitizeSrcset(value) {
2388
2672
  return out.join(", ");
2389
2673
  }
2390
2674
  var CSS_DANGER_GATE = /[(:@\\]/;
2675
+ function isCssWhitespace(ch) {
2676
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
2677
+ }
2678
+ function isHexDigit(ch) {
2679
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
2680
+ }
2681
+ function decodeCssEscapes(value) {
2682
+ let out = "";
2683
+ for (let i2 = 0; i2 < value.length; i2++) {
2684
+ const ch = value[i2];
2685
+ if (ch !== "\\") {
2686
+ out += ch;
2687
+ continue;
2688
+ }
2689
+ if (i2 + 1 >= value.length) break;
2690
+ const next = value[i2 + 1];
2691
+ if (next === "\n" || next === "\f") {
2692
+ i2 += 1;
2693
+ continue;
2694
+ }
2695
+ if (next === "\r") {
2696
+ i2 += value[i2 + 2] === "\n" ? 2 : 1;
2697
+ continue;
2698
+ }
2699
+ if (isHexDigit(next)) {
2700
+ let hex = "";
2701
+ let j = i2 + 1;
2702
+ while (j < value.length && hex.length < 6 && isHexDigit(value[j])) {
2703
+ hex += value[j];
2704
+ j++;
2705
+ }
2706
+ if (j < value.length && isCssWhitespace(value[j])) {
2707
+ j += value[j] === "\r" && value[j + 1] === "\n" ? 2 : 1;
2708
+ }
2709
+ const code2 = Number.parseInt(hex, 16);
2710
+ const invalid = code2 === 0 || code2 > 1114111 || code2 >= 55296 && code2 <= 57343;
2711
+ out += invalid ? "\uFFFD" : String.fromCodePoint(code2);
2712
+ i2 = j - 1;
2713
+ continue;
2714
+ }
2715
+ out += next;
2716
+ i2 += 1;
2717
+ }
2718
+ return out;
2719
+ }
2391
2720
  function sanitizeCSSValue(value) {
2392
2721
  if (!CSS_DANGER_GATE.test(value)) return value;
2393
- const decoded = value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_m, hex) => {
2394
- const code2 = Number.parseInt(hex, 16);
2395
- if (!Number.isFinite(code2) || code2 < 0 || code2 > 1114111) return "";
2396
- try {
2397
- return String.fromCodePoint(code2);
2398
- } catch {
2399
- return "";
2400
- }
2401
- });
2402
- const lower = decoded.toLowerCase().replace(/\s+/g, "");
2722
+ const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
2723
+ const lower = normalized.toLowerCase().replace(/\s+/g, "");
2403
2724
  if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("vbscript:") || lower.includes("-moz-binding") || lower.includes("behavior:") || lower.includes("@import") || lower.includes("image-set(") || lower.includes("filter:progid")) {
2404
2725
  return "";
2405
2726
  }
2406
2727
  return value;
2407
2728
  }
2729
+ function sanitizeStyleAttribute(cssText) {
2730
+ const input2 = String(cssText);
2731
+ if (input2.trim() === "") return "";
2732
+ if (typeof document === "undefined") {
2733
+ return sanitizeCSSValue(input2) === "" ? "" : input2;
2734
+ }
2735
+ const probe = document.createElement("div");
2736
+ try {
2737
+ probe.style.cssText = input2;
2738
+ } catch {
2739
+ return "";
2740
+ }
2741
+ const declarations = [];
2742
+ for (let i2 = 0; i2 < probe.style.length; i2++) {
2743
+ const property = probe.style[i2];
2744
+ const value = probe.style.getPropertyValue(property);
2745
+ if (sanitizeCSSValue(value) === "") continue;
2746
+ if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
2747
+ const priority = probe.style.getPropertyPriority(property);
2748
+ declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
2749
+ }
2750
+ return declarations.join("; ");
2751
+ }
2408
2752
  function stripHtml(html2) {
2409
2753
  const input2 = String(html2);
2410
2754
  if (typeof DOMParser !== "undefined") {
@@ -2433,12 +2777,25 @@ var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
2433
2777
  "background",
2434
2778
  "srcset",
2435
2779
  "ping",
2436
- "data"
2780
+ "data",
2781
+ // `<html manifest>` is a URL sink too. It was carried only by the SSR
2782
+ // serializers' private copy of this set; folding that copy into this one would
2783
+ // otherwise have silently dropped its coverage.
2784
+ "manifest"
2437
2785
  ]);
2786
+ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
2787
+ function isHtmlContentAttribute(name) {
2788
+ return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
2789
+ }
2790
+ function isPolicyAttribute(name) {
2791
+ const lower = name.toLowerCase();
2792
+ return lower === "style" || URL_ATTRIBUTES.has(lower);
2793
+ }
2438
2794
  function sanitizeAttributeString(attr, value) {
2439
2795
  const lower = attr.toLowerCase();
2440
2796
  if (lower === "srcset") return sanitizeSrcset(value);
2441
2797
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
2798
+ if (lower === "style") return sanitizeStyleAttribute(value);
2442
2799
  return value;
2443
2800
  }
2444
2801
 
@@ -2771,13 +3128,13 @@ function idle(timeout = 6e4) {
2771
3128
 
2772
3129
  // src/browser/permissions.ts
2773
3130
  function permissions(name) {
2774
- const [state, setState] = signal("prompt");
3131
+ const [state2, setState] = signal("prompt");
2775
3132
  let permissionStatus = null;
2776
3133
  let onChange = null;
2777
3134
  let disposed = false;
2778
3135
  if (typeof navigator === "undefined" || !navigator.permissions) {
2779
3136
  setState("unsupported");
2780
- return { state, dispose: () => {
3137
+ return { state: state2, dispose: () => {
2781
3138
  } };
2782
3139
  }
2783
3140
  navigator.permissions.query({ name }).then((status) => {
@@ -2789,6 +3146,7 @@ function permissions(name) {
2789
3146
  };
2790
3147
  status.addEventListener("change", onChange);
2791
3148
  }).catch(() => {
3149
+ if (disposed) return;
2792
3150
  setState("unsupported");
2793
3151
  });
2794
3152
  function dispose2() {
@@ -2799,7 +3157,7 @@ function permissions(name) {
2799
3157
  onChange = null;
2800
3158
  }
2801
3159
  }
2802
- return { state, dispose: dispose2 };
3160
+ return { state: state2, dispose: dispose2 };
2803
3161
  }
2804
3162
 
2805
3163
  // src/browser/clipboard.ts
@@ -2807,20 +3165,25 @@ function clipboard() {
2807
3165
  const [text2, setText] = signal("");
2808
3166
  const [copied, setCopied] = signal(false);
2809
3167
  let copiedTimer = null;
3168
+ let disposed = false;
2810
3169
  async function copy(value) {
3170
+ if (disposed) return;
2811
3171
  if (typeof navigator === "undefined" || !navigator.clipboard) {
2812
3172
  return;
2813
3173
  }
2814
3174
  await navigator.clipboard.writeText(value);
3175
+ if (disposed) return;
2815
3176
  setText(value);
2816
3177
  setCopied(true);
2817
3178
  if (copiedTimer !== null) clearTimeout(copiedTimer);
2818
3179
  copiedTimer = setTimeout(() => {
3180
+ if (disposed) return;
2819
3181
  setCopied(false);
2820
3182
  copiedTimer = null;
2821
3183
  }, 2e3);
2822
3184
  }
2823
3185
  function dispose2() {
3186
+ disposed = true;
2824
3187
  if (copiedTimer !== null) {
2825
3188
  clearTimeout(copiedTimer);
2826
3189
  copiedTimer = null;
@@ -2953,26 +3316,141 @@ function dropZone(element, options) {
2953
3316
  return { isOver, dispose: dispose2 };
2954
3317
  }
2955
3318
 
3319
+ // src/utils/singletonResource.ts
3320
+ function singletonResource(io) {
3321
+ let original = null;
3322
+ let captured = false;
3323
+ const stack = [];
3324
+ function apply() {
3325
+ if (stack.length > 0) {
3326
+ io.write(stack[stack.length - 1].value);
3327
+ return;
3328
+ }
3329
+ if (captured) {
3330
+ io.write(original);
3331
+ captured = false;
3332
+ original = null;
3333
+ }
3334
+ }
3335
+ return {
3336
+ acquire(value) {
3337
+ if (!captured) {
3338
+ original = io.read();
3339
+ captured = true;
3340
+ }
3341
+ const entry = { value, released: false };
3342
+ stack.push(entry);
3343
+ apply();
3344
+ return {
3345
+ set(next) {
3346
+ if (entry.released) return;
3347
+ entry.value = next;
3348
+ apply();
3349
+ },
3350
+ release() {
3351
+ if (entry.released) return;
3352
+ entry.released = true;
3353
+ const index = stack.indexOf(entry);
3354
+ if (index !== -1) stack.splice(index, 1);
3355
+ apply();
3356
+ }
3357
+ };
3358
+ },
3359
+ ownerCount() {
3360
+ return stack.length;
3361
+ }
3362
+ };
3363
+ }
3364
+
3365
+ // src/utils/documentResources.ts
3366
+ var INERT_LEASE = {
3367
+ set() {
3368
+ },
3369
+ release() {
3370
+ }
3371
+ };
3372
+ function inertLease() {
3373
+ return INERT_LEASE;
3374
+ }
3375
+ var titleResource = globalSingleton(
3376
+ /* @__PURE__ */ Symbol.for("sibujs.documentTitle.v1"),
3377
+ () => singletonResource({
3378
+ read: () => document.title,
3379
+ write: (value) => {
3380
+ document.title = value;
3381
+ }
3382
+ })
3383
+ );
3384
+ var baseResource = globalSingleton(
3385
+ /* @__PURE__ */ Symbol.for("sibujs.documentBase.v1"),
3386
+ () => singletonResource({
3387
+ read: () => {
3388
+ const el = document.head.querySelector("base");
3389
+ if (!el) return null;
3390
+ return {
3391
+ href: el.getAttribute("href") ?? void 0,
3392
+ target: el.getAttribute("target") ?? void 0
3393
+ };
3394
+ },
3395
+ write: (spec) => {
3396
+ const existing = document.head.querySelector("base");
3397
+ if (spec === null) {
3398
+ existing?.remove();
3399
+ return;
3400
+ }
3401
+ const el = existing ?? document.head.appendChild(document.createElement("base"));
3402
+ if (spec.href === void 0) el.removeAttribute("href");
3403
+ else el.setAttribute("href", spec.href);
3404
+ if (spec.target === void 0) el.removeAttribute("target");
3405
+ else el.setAttribute("target", spec.target);
3406
+ }
3407
+ })
3408
+ );
3409
+ var scrollRestorationResource = globalSingleton(
3410
+ /* @__PURE__ */ Symbol.for("sibujs.historyScrollRestoration.v1"),
3411
+ () => singletonResource({
3412
+ read: () => history.scrollRestoration,
3413
+ write: (mode) => {
3414
+ history.scrollRestoration = mode;
3415
+ }
3416
+ })
3417
+ );
3418
+ function acquireScrollRestorationMode(mode) {
3419
+ if (typeof history === "undefined" || !("scrollRestoration" in history)) {
3420
+ return inertLease();
3421
+ }
3422
+ return scrollRestorationResource.acquire(mode);
3423
+ }
3424
+ function acquireTitle(value) {
3425
+ if (typeof document === "undefined") return inertLease();
3426
+ return titleResource.acquire(value);
3427
+ }
3428
+ function acquireBase(spec) {
3429
+ if (typeof document === "undefined") return inertLease();
3430
+ return baseResource.acquire(spec);
3431
+ }
3432
+
2956
3433
  // src/browser/title.ts
2957
3434
  function title(value) {
2958
3435
  if (typeof document === "undefined") {
2959
3436
  return () => {
2960
3437
  };
2961
3438
  }
2962
- const previousTitle = document.title;
2963
3439
  if (typeof value === "function") {
3440
+ let lease2 = null;
2964
3441
  const cleanup3 = effect(() => {
2965
- document.title = value();
3442
+ const next = value();
3443
+ if (lease2) lease2.set(next);
3444
+ else lease2 = acquireTitle(next);
2966
3445
  });
2967
3446
  return () => {
2968
3447
  cleanup3();
2969
- document.title = previousTitle;
3448
+ lease2?.release();
3449
+ lease2 = null;
2970
3450
  };
2971
3451
  }
2972
- document.title = value;
2973
- return () => {
2974
- document.title = previousTitle;
2975
- };
3452
+ const lease = acquireTitle(value);
3453
+ return () => lease.release();
2976
3454
  }
2977
3455
 
2978
3456
  // src/browser/colorScheme.ts
@@ -3286,51 +3764,112 @@ function wakeLock() {
3286
3764
  };
3287
3765
  }
3288
3766
  const api = navigator.wakeLock;
3289
- let sentinel = null;
3767
+ let current = null;
3768
+ let desire = false;
3769
+ let generation = 0;
3770
+ let inflight = null;
3290
3771
  let disposed = false;
3291
- async function request() {
3772
+ function publishActive(value) {
3292
3773
  if (disposed) return;
3293
- try {
3294
- const s2 = await api.request("screen");
3295
- if (disposed) {
3296
- s2.release().catch(() => {
3297
- });
3298
- return;
3299
- }
3300
- sentinel = s2;
3301
- setActive(true);
3302
- sentinel.addEventListener("release", () => {
3303
- setActive(false);
3304
- });
3305
- } catch {
3306
- setActive(false);
3774
+ setActive(value);
3775
+ }
3776
+ function syncReleasedState() {
3777
+ if (current?.released) {
3778
+ current = null;
3779
+ publishActive(false);
3780
+ }
3781
+ }
3782
+ function discard(sentinel) {
3783
+ if (sentinel.released) return;
3784
+ sentinel.release().catch((err) => {
3785
+ reportError(err, { phase: "async", name: "wakeLock(discard)" });
3786
+ });
3787
+ }
3788
+ async function request() {
3789
+ if (disposed) return;
3790
+ desire = true;
3791
+ syncReleasedState();
3792
+ if (current && !current.released) {
3793
+ publishActive(true);
3794
+ return;
3307
3795
  }
3796
+ if (inflight) return inflight;
3797
+ const myGeneration = ++generation;
3798
+ const attempt = (async () => {
3799
+ try {
3800
+ const sentinel = await api.request("screen");
3801
+ if (disposed || !desire || generation !== myGeneration) {
3802
+ discard(sentinel);
3803
+ return;
3804
+ }
3805
+ if (sentinel.released) {
3806
+ publishActive(false);
3807
+ return;
3808
+ }
3809
+ try {
3810
+ sentinel.addEventListener("release", () => {
3811
+ if (current !== sentinel) return;
3812
+ current = null;
3813
+ publishActive(false);
3814
+ });
3815
+ } catch (listenerError) {
3816
+ discard(sentinel);
3817
+ publishActive(false);
3818
+ reportError(listenerError, { phase: "async", name: "wakeLock(listener registration)" });
3819
+ return;
3820
+ }
3821
+ if (sentinel.released) {
3822
+ publishActive(false);
3823
+ return;
3824
+ }
3825
+ current = sentinel;
3826
+ publishActive(true);
3827
+ } catch (err) {
3828
+ if (generation === myGeneration) publishActive(false);
3829
+ reportError(err, { phase: "async", name: "wakeLock(request)" });
3830
+ }
3831
+ })();
3832
+ inflight = attempt;
3833
+ void attempt.finally(() => {
3834
+ if (inflight === attempt) inflight = null;
3835
+ }).catch(() => {
3836
+ });
3837
+ return attempt;
3308
3838
  }
3309
3839
  async function release() {
3310
- if (sentinel && !sentinel.released) {
3311
- await sentinel.release();
3840
+ desire = false;
3841
+ generation++;
3842
+ inflight = null;
3843
+ const held = current;
3844
+ current = null;
3845
+ publishActive(false);
3846
+ if (held && !held.released) {
3847
+ await held.release();
3312
3848
  }
3313
- sentinel = null;
3314
- setActive(false);
3315
3849
  }
3316
3850
  const onVisibility = () => {
3317
- if (sentinel?.released && !document.hidden) {
3318
- request().catch((err) => {
3319
- if (typeof console !== "undefined") {
3320
- console.warn("[SibuJS wakeLock] re-acquire failed:", err);
3321
- }
3322
- });
3323
- }
3851
+ syncReleasedState();
3852
+ if (!desire || current || document.hidden) return;
3853
+ request().catch((err) => {
3854
+ reportError(err, { phase: "async", name: "wakeLock(visibility re-acquire)" });
3855
+ });
3324
3856
  };
3325
3857
  document.addEventListener("visibilitychange", onVisibility);
3326
3858
  function dispose2() {
3327
- disposed = true;
3859
+ if (disposed) return;
3860
+ desire = false;
3861
+ generation++;
3862
+ inflight = null;
3328
3863
  document.removeEventListener("visibilitychange", onVisibility);
3329
- release().catch((err) => {
3330
- if (typeof console !== "undefined") {
3331
- console.warn("[SibuJS wakeLock] release failed:", err);
3332
- }
3333
- });
3864
+ const held = current;
3865
+ current = null;
3866
+ setActive(false);
3867
+ disposed = true;
3868
+ if (held && !held.released) {
3869
+ held.release().catch((err) => {
3870
+ reportError(err, { phase: "async", name: "wakeLock(dispose release)" });
3871
+ });
3872
+ }
3334
3873
  }
3335
3874
  return { active, request, release, dispose: dispose2 };
3336
3875
  }
@@ -3845,14 +4384,14 @@ function imageLoader(src) {
3845
4384
 
3846
4385
  // src/patterns/machine.ts
3847
4386
  function machine(config) {
3848
- const [state, setState] = signal(config.initial);
4387
+ const [state2, setState] = signal(config.initial);
3849
4388
  const [context2, setContext] = signal(config.context || {});
3850
4389
  const initialDef = config.states[config.initial];
3851
4390
  if (initialDef?.entry) {
3852
4391
  initialDef.entry(context2());
3853
4392
  }
3854
4393
  function send(event) {
3855
- const currentState = state();
4394
+ const currentState = state2();
3856
4395
  const stateDef = config.states[currentState];
3857
4396
  if (!stateDef?.on) return;
3858
4397
  const transition2 = stateDef.on[event];
@@ -3884,10 +4423,10 @@ function machine(config) {
3884
4423
  }
3885
4424
  }
3886
4425
  function matches(s2) {
3887
- return state() === s2;
4426
+ return state2() === s2;
3888
4427
  }
3889
4428
  function can(event) {
3890
- const currentState = state();
4429
+ const currentState = state2();
3891
4430
  const stateDef = config.states[currentState];
3892
4431
  if (!stateDef?.on) return false;
3893
4432
  const transition2 = stateDef.on[event];
@@ -3898,7 +4437,7 @@ function machine(config) {
3898
4437
  }
3899
4438
  return true;
3900
4439
  }
3901
- return { state, context: context2, send, matches, can };
4440
+ return { state: state2, context: context2, send, matches, can };
3902
4441
  }
3903
4442
 
3904
4443
  // src/patterns/persist.ts
@@ -4007,116 +4546,258 @@ function optimistic(initialValue) {
4007
4546
  }
4008
4547
  } finally {
4009
4548
  inflightCount--;
4010
- if (inflightCount === 0) setPending(false);
4549
+ if (inflightCount <= 0) {
4550
+ inflightCount = 0;
4551
+ setPending(false);
4552
+ }
4011
4553
  }
4012
4554
  }
4013
4555
  return { value, pending, update };
4014
4556
  }
4015
4557
  function optimisticList(initialValue) {
4016
- const [items, setItems] = signal([...initialValue]);
4558
+ let nextRowId = 0;
4559
+ let nextKey = 0;
4560
+ let nextOpId = 0;
4561
+ const records = /* @__PURE__ */ new Map();
4562
+ let order = [];
4563
+ for (const value of initialValue) {
4564
+ const row = { id: nextRowId++, key: nextKey++, base: value, claims: [], visible: true, refs: 0 };
4565
+ records.set(row.id, row);
4566
+ order.push(row.id);
4567
+ }
4568
+ function effectiveValue(row) {
4569
+ const claims = row.claims;
4570
+ return claims.length > 0 ? claims[claims.length - 1].value : row.base;
4571
+ }
4572
+ function valueBelow(row, opId) {
4573
+ const claims = row.claims;
4574
+ for (let i2 = claims.length - 1; i2 >= 0; i2--) {
4575
+ if (claims[i2].opId < opId) return claims[i2].value;
4576
+ }
4577
+ return row.base;
4578
+ }
4579
+ function stake(row, opId, value) {
4580
+ const claim = { opId, value, settled: false };
4581
+ const claims = row.claims;
4582
+ let at = claims.length;
4583
+ while (at > 0 && claims[at - 1].opId > opId) at--;
4584
+ claims.splice(at, 0, claim);
4585
+ return claim;
4586
+ }
4587
+ function dropClaim(row, claim) {
4588
+ const at = row.claims.indexOf(claim);
4589
+ if (at !== -1) row.claims.splice(at, 1);
4590
+ }
4591
+ function collapse(row) {
4592
+ const claims = row.claims;
4593
+ let i2 = 0;
4594
+ while (i2 < claims.length && claims[i2].settled) {
4595
+ row.base = claims[i2].value;
4596
+ i2++;
4597
+ }
4598
+ if (i2 > 0) claims.splice(0, i2);
4599
+ }
4600
+ const [items, setItems] = signal(order.map((id) => effectiveValue(records.get(id))));
4017
4601
  const [pending, setPending] = signal(false);
4018
- let inflightCount = 0;
4019
- let version = 0;
4020
- let tempIdCounter = 0;
4021
- const itemIds = /* @__PURE__ */ new WeakMap();
4022
- const idToItem = /* @__PURE__ */ new Map();
4023
- function tagItem(item) {
4024
- const id = ++tempIdCounter;
4025
- if (item !== null && typeof item === "object") {
4026
- itemIds.set(item, id);
4027
- }
4028
- idToItem.set(id, item);
4029
- return id;
4602
+ let inflight = 0;
4603
+ const retiredRowIds = /* @__PURE__ */ new Set();
4604
+ function project() {
4605
+ const out = new Array(order.length);
4606
+ for (let i2 = 0; i2 < order.length; i2++) out[i2] = effectiveValue(records.get(order[i2]));
4607
+ return out;
4030
4608
  }
4031
- function findIndexById(list, id) {
4032
- for (let i2 = 0; i2 < list.length; i2++) {
4033
- const it = list[i2];
4034
- if (it !== null && typeof it === "object" && itemIds.get(it) === id) {
4035
- return i2;
4609
+ function commit(mutate) {
4610
+ batch(() => {
4611
+ let visibleChanged = false;
4612
+ try {
4613
+ visibleChanged = mutate();
4614
+ } finally {
4615
+ setPending(inflight > 0);
4616
+ if (visibleChanged) setItems(project());
4036
4617
  }
4037
- if (Object.is(it, idToItem.get(id))) return i2;
4618
+ });
4619
+ }
4620
+ function hide(id) {
4621
+ const row = records.get(id);
4622
+ if (!row || !row.visible) return false;
4623
+ row.visible = false;
4624
+ order = order.filter((x) => x !== id);
4625
+ return true;
4626
+ }
4627
+ function restore(ids) {
4628
+ const restorable = [];
4629
+ for (const id of ids) {
4630
+ const row = records.get(id);
4631
+ if (!row || row.visible || retiredRowIds.has(id)) continue;
4632
+ restorable.push(id);
4633
+ }
4634
+ if (restorable.length === 0) return false;
4635
+ const merged = new Array(order.length + restorable.length);
4636
+ let i2 = 0;
4637
+ let j = 0;
4638
+ let w = 0;
4639
+ while (i2 < order.length && j < restorable.length) {
4640
+ const a2 = records.get(order[i2]).key;
4641
+ const b2 = records.get(restorable[j]).key;
4642
+ merged[w++] = a2 <= b2 ? order[i2++] : restorable[j++];
4643
+ }
4644
+ while (i2 < order.length) merged[w++] = order[i2++];
4645
+ while (j < restorable.length) merged[w++] = restorable[j++];
4646
+ for (const id of restorable) records.get(id).visible = true;
4647
+ order = merged;
4648
+ return true;
4649
+ }
4650
+ function retain(ids) {
4651
+ for (const id of ids) {
4652
+ const row = records.get(id);
4653
+ if (row) row.refs++;
4038
4654
  }
4039
- return -1;
4040
4655
  }
4041
- function begin() {
4042
- const v = ++version;
4043
- inflightCount++;
4044
- setPending(true);
4045
- return v;
4656
+ function release(ids) {
4657
+ for (const id of ids) {
4658
+ const row = records.get(id);
4659
+ if (!row) continue;
4660
+ row.refs--;
4661
+ if (row.refs <= 0 && !row.visible) {
4662
+ records.delete(id);
4663
+ retiredRowIds.delete(id);
4664
+ }
4665
+ }
4046
4666
  }
4047
- function end(myVersion, revertFn) {
4048
- if (revertFn && version === myVersion) {
4049
- revertFn();
4667
+ function settle(heldIds) {
4668
+ release(heldIds);
4669
+ inflight--;
4670
+ if (inflight <= 0) {
4671
+ inflight = 0;
4672
+ retiredRowIds.clear();
4673
+ if (records.size !== order.length) {
4674
+ for (const [id, row] of records) {
4675
+ if (!row.visible) records.delete(id);
4676
+ }
4677
+ }
4050
4678
  }
4051
- inflightCount--;
4052
- if (inflightCount === 0) setPending(false);
4053
4679
  }
4054
4680
  async function add(item, asyncAction) {
4055
- const prev = items();
4056
- const id = tagItem(item);
4057
- setItems([...prev, item]);
4058
- const myVersion = begin();
4681
+ const opId = ++nextOpId;
4682
+ const row = { id: nextRowId++, key: nextKey++, base: item, claims: [], visible: false, refs: 1 };
4683
+ const claim = stake(row, opId, item);
4684
+ commit(() => {
4685
+ records.set(row.id, row);
4686
+ row.visible = true;
4687
+ order = [...order, row.id];
4688
+ inflight++;
4689
+ return true;
4690
+ });
4691
+ let result;
4692
+ let failed = false;
4059
4693
  try {
4060
- const result = await asyncAction();
4061
- setItems((current) => {
4062
- const idx = findIndexById(current, id);
4063
- if (idx >= 0) {
4064
- const next = [...current];
4065
- next[idx] = result;
4066
- return next;
4067
- }
4068
- return [...current, result];
4069
- });
4070
- idToItem.delete(id);
4071
- end(myVersion);
4694
+ result = await asyncAction();
4072
4695
  } catch {
4073
- idToItem.delete(id);
4074
- end(myVersion, () => setItems(prev));
4696
+ failed = true;
4075
4697
  }
4698
+ commit(() => {
4699
+ try {
4700
+ if (failed) {
4701
+ retiredRowIds.add(row.id);
4702
+ dropClaim(row, claim);
4703
+ return hide(row.id);
4704
+ }
4705
+ const before = effectiveValue(row);
4706
+ claim.value = result;
4707
+ claim.settled = true;
4708
+ collapse(row);
4709
+ return row.visible && !Object.is(before, effectiveValue(row));
4710
+ } finally {
4711
+ settle([row.id]);
4712
+ }
4713
+ });
4076
4714
  }
4077
4715
  async function remove(predicate, asyncAction) {
4078
- const prev = items();
4079
- setItems(prev.filter((item) => !predicate(item)));
4080
- const myVersion = begin();
4716
+ const opId = ++nextOpId;
4717
+ const matchedIds = [];
4718
+ for (const id of order) {
4719
+ if (predicate(valueBelow(records.get(id), opId))) matchedIds.push(id);
4720
+ }
4721
+ commit(() => {
4722
+ retain(matchedIds);
4723
+ inflight++;
4724
+ if (matchedIds.length === 0) return false;
4725
+ const removing = new Set(matchedIds);
4726
+ for (const id of matchedIds) {
4727
+ const row = records.get(id);
4728
+ if (row) row.visible = false;
4729
+ }
4730
+ order = order.filter((id) => !removing.has(id));
4731
+ return true;
4732
+ });
4733
+ let failed = false;
4081
4734
  try {
4082
4735
  await asyncAction();
4083
- end(myVersion);
4084
4736
  } catch {
4085
- end(myVersion, () => setItems(prev));
4737
+ failed = true;
4086
4738
  }
4739
+ commit(() => {
4740
+ try {
4741
+ if (!failed) {
4742
+ for (const id of matchedIds) retiredRowIds.add(id);
4743
+ return false;
4744
+ }
4745
+ return restore(matchedIds);
4746
+ } finally {
4747
+ settle(matchedIds);
4748
+ }
4749
+ });
4087
4750
  }
4088
4751
  async function updateItem(predicate, patch, asyncAction) {
4089
- const prev = items();
4090
- const patchedIds = [];
4091
- setItems(
4092
- prev.map((item) => {
4093
- if (predicate(item)) {
4094
- const patched = { ...item, ...patch };
4095
- const id = tagItem(patched);
4096
- patchedIds.push(id);
4097
- return patched;
4098
- }
4099
- return item;
4100
- })
4101
- );
4102
- const myVersion = begin();
4752
+ const opId = ++nextOpId;
4753
+ const prepared = [];
4754
+ for (const id of order) {
4755
+ const row = records.get(id);
4756
+ const seen = valueBelow(row, opId);
4757
+ if (!predicate(seen)) continue;
4758
+ prepared.push({ row, nextValue: { ...seen, ...patch } });
4759
+ }
4760
+ const preparedIds = prepared.map((p2) => p2.row.id);
4761
+ const claims = [];
4762
+ commit(() => {
4763
+ retain(preparedIds);
4764
+ inflight++;
4765
+ let visibleChanged = false;
4766
+ for (const p2 of prepared) {
4767
+ const before = effectiveValue(p2.row);
4768
+ claims.push(stake(p2.row, opId, p2.nextValue));
4769
+ if (p2.row.visible && !Object.is(before, effectiveValue(p2.row))) visibleChanged = true;
4770
+ }
4771
+ return visibleChanged;
4772
+ });
4773
+ let result;
4774
+ let failed = false;
4103
4775
  try {
4104
- const result = await asyncAction();
4105
- setItems(
4106
- (current) => current.map((item) => {
4107
- if (item !== null && typeof item === "object") {
4108
- const existingId = itemIds.get(item);
4109
- if (existingId !== void 0 && patchedIds.includes(existingId)) return result;
4110
- }
4111
- return item;
4112
- })
4113
- );
4114
- for (const id of patchedIds) idToItem.delete(id);
4115
- end(myVersion);
4776
+ result = await asyncAction();
4116
4777
  } catch {
4117
- for (const id of patchedIds) idToItem.delete(id);
4118
- end(myVersion, () => setItems(prev));
4778
+ failed = true;
4119
4779
  }
4780
+ commit(() => {
4781
+ try {
4782
+ let visibleChanged = false;
4783
+ for (let i2 = 0; i2 < claims.length; i2++) {
4784
+ const row = prepared[i2].row;
4785
+ const claim = claims[i2];
4786
+ const before = effectiveValue(row);
4787
+ if (failed) {
4788
+ dropClaim(row, claim);
4789
+ } else {
4790
+ claim.value = result;
4791
+ claim.settled = true;
4792
+ }
4793
+ collapse(row);
4794
+ if (row.visible && !Object.is(before, effectiveValue(row))) visibleChanged = true;
4795
+ }
4796
+ return visibleChanged;
4797
+ } finally {
4798
+ settle(preparedIds);
4799
+ }
4800
+ });
4120
4801
  }
4121
4802
  return {
4122
4803
  items,
@@ -4129,16 +4810,16 @@ function optimisticList(initialValue) {
4129
4810
 
4130
4811
  // src/patterns/timeTravel.ts
4131
4812
  function timeline(initial, maxHistory = 100) {
4132
- const [history, setHistory] = signal([initial]);
4813
+ const [history2, setHistory] = signal([initial]);
4133
4814
  const [index, setIndex] = signal(0);
4134
- const value = derived(() => history()[index()]);
4815
+ const value = derived(() => history2()[index()]);
4135
4816
  const canUndo = derived(() => index() > 0);
4136
- const canRedo = derived(() => index() < history().length - 1);
4817
+ const canRedo = derived(() => index() < history2().length - 1);
4137
4818
  function set(next) {
4138
4819
  const current = value();
4139
4820
  const newValue = typeof next === "function" ? next(current) : next;
4140
4821
  if (Object.is(newValue, current)) return;
4141
- const hist = history();
4822
+ const hist = history2();
4142
4823
  const idx = index();
4143
4824
  const newHistory = hist.slice(0, idx + 1);
4144
4825
  newHistory.push(newValue);
@@ -4170,12 +4851,12 @@ function timeline(initial, maxHistory = 100) {
4170
4851
  });
4171
4852
  }
4172
4853
  function jumpTo(targetIndex) {
4173
- const hist = history();
4854
+ const hist = history2();
4174
4855
  if (targetIndex >= 0 && targetIndex < hist.length) {
4175
4856
  setIndex(targetIndex);
4176
4857
  }
4177
4858
  }
4178
- return { value, set, undo, redo, canUndo, canRedo, history, index, reset, jumpTo };
4859
+ return { value, set, undo, redo, canUndo, canRedo, history: history2, index, reset, jumpTo };
4179
4860
  }
4180
4861
 
4181
4862
  // src/patterns/globalStore.ts
@@ -4422,6 +5103,14 @@ function createGuard(validator) {
4422
5103
 
4423
5104
  // src/core/rendering/dispose.ts
4424
5105
  var elementDisposers = /* @__PURE__ */ new WeakMap();
5106
+ var MAX_DRAIN_TEARDOWNS = 1e4;
5107
+ function reportDrainRunaway(label2, executed, remaining) {
5108
+ if (typeof console === "undefined") return;
5109
+ console.error(
5110
+ `[SibuJS ${label2}] 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.`,
5111
+ { executed, remaining }
5112
+ );
5113
+ }
4425
5114
  var _isDev5 = isDev();
4426
5115
  var activeBindingCount = 0;
4427
5116
  function registerDisposer(node, teardown) {
@@ -4446,40 +5135,47 @@ function dispose(node) {
4446
5135
  }
4447
5136
  for (let i2 = order.length - 1; i2 >= 0; i2--) {
4448
5137
  const current = order[i2];
4449
- const disposers = elementDisposers.get(current);
4450
- if (disposers) {
4451
- const snapshot = disposers.slice();
4452
- elementDisposers.delete(current);
4453
- if (_isDev5) activeBindingCount -= snapshot.length;
4454
- for (const d of snapshot) {
4455
- try {
4456
- d();
4457
- } catch (err) {
4458
- if (_isDev5 && typeof console !== "undefined") {
4459
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
4460
- }
4461
- }
4462
- }
4463
- let extraPasses = 0;
4464
- while (extraPasses++ < 8) {
4465
- const added = elementDisposers.get(current);
4466
- if (!added || added.length === 0) break;
4467
- const moreSnapshot = added.slice();
5138
+ if (elementDisposers.has(current)) {
5139
+ let executed = 0;
5140
+ let runaway = false;
5141
+ while (!runaway) {
5142
+ const pending = elementDisposers.get(current);
5143
+ if (!pending || pending.length === 0) break;
5144
+ const snapshot = pending.slice();
4468
5145
  elementDisposers.delete(current);
4469
- if (_isDev5) activeBindingCount -= moreSnapshot.length;
4470
- for (const d of moreSnapshot) {
5146
+ if (_isDev5) activeBindingCount -= snapshot.length;
5147
+ for (let i3 = 0; i3 < snapshot.length; i3++) {
5148
+ if (executed >= MAX_DRAIN_TEARDOWNS) {
5149
+ const rest = snapshot.slice(i3);
5150
+ const added = elementDisposers.get(current);
5151
+ elementDisposers.set(current, added ? rest.concat(added) : rest);
5152
+ if (_isDev5) activeBindingCount += rest.length;
5153
+ reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
5154
+ runaway = true;
5155
+ break;
5156
+ }
5157
+ executed++;
4471
5158
  try {
4472
- d();
5159
+ snapshot[i3]();
4473
5160
  } catch (err) {
4474
- if (_isDev5 && typeof console !== "undefined") {
4475
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
4476
- }
5161
+ reportError(err, { phase: "cleanup", name: "disposer" });
4477
5162
  }
4478
5163
  }
4479
5164
  }
4480
5165
  }
4481
5166
  }
4482
5167
  }
5168
+ function replaceChildrenSafely(parent, ...next) {
5169
+ for (let i2 = 0; i2 < next.length; i2++) {
5170
+ const node = next[i2];
5171
+ node.parentNode?.removeChild(node);
5172
+ }
5173
+ const current = Array.from(parent.childNodes);
5174
+ for (let i2 = 0; i2 < current.length; i2++) {
5175
+ dispose(current[i2]);
5176
+ }
5177
+ parent.replaceChildren(...next);
5178
+ }
4483
5179
 
4484
5180
  // src/ui/transition.ts
4485
5181
  function transition(element, options = {}) {
@@ -4811,7 +5507,9 @@ function TransitionGroup(options) {
4811
5507
  // src/ui/viewTransition.ts
4812
5508
  function viewTransition(callback) {
4813
5509
  const [isTransitioning, setIsTransitioning] = signal(false);
5510
+ let inflight = 0;
4814
5511
  async function start() {
5512
+ inflight++;
4815
5513
  setIsTransitioning(true);
4816
5514
  try {
4817
5515
  if (typeof document !== "undefined" && "startViewTransition" in document && typeof document.startViewTransition === "function") {
@@ -4821,7 +5519,11 @@ function viewTransition(callback) {
4821
5519
  await callback();
4822
5520
  }
4823
5521
  } finally {
4824
- setIsTransitioning(false);
5522
+ inflight--;
5523
+ if (inflight <= 0) {
5524
+ inflight = 0;
5525
+ setIsTransitioning(false);
5526
+ }
4825
5527
  }
4826
5528
  }
4827
5529
  return { start, isTransitioning };
@@ -4982,6 +5684,8 @@ function bindField(field, extras) {
4982
5684
  }
4983
5685
  };
4984
5686
  return {
5687
+ // The field's own getter, no longer widened to `unknown` — that widening is
5688
+ // what forced every consumer to cast. (TYPE-010)
4985
5689
  value: field.value,
4986
5690
  on: mergedOn,
4987
5691
  onElement,
@@ -5534,7 +6238,7 @@ function announce(message, priority = "polite") {
5534
6238
 
5535
6239
  // src/ui/scopedStyle.ts
5536
6240
  var _scope = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.scopedStyle.v1"), () => ({ n: 0 }));
5537
- function decodeCssEscapes(css) {
6241
+ function decodeCssEscapes2(css) {
5538
6242
  return css.replace(/\\([0-9a-f]{1,6})[ \t\n\r\f]?|\\([^\n])/gi, (_match, hex, ch) => {
5539
6243
  if (hex) {
5540
6244
  const code2 = Number.parseInt(hex, 16);
@@ -5551,7 +6255,7 @@ function decodeCssEscapes(css) {
5551
6255
  });
5552
6256
  }
5553
6257
  function sanitizeCSS(css) {
5554
- let sanitized = decodeCssEscapes(css);
6258
+ let sanitized = decodeCssEscapes2(css);
5555
6259
  sanitized = sanitized.replace(/@import\s+[^;]+;/gi, "/* @import removed */");
5556
6260
  sanitized = sanitized.replace(/url\s*\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "/* url() removed */");
5557
6261
  sanitized = sanitized.replace(/expression\s*\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "/* expression() removed */");
@@ -5559,21 +6263,169 @@ function sanitizeCSS(css) {
5559
6263
  sanitized = sanitized.replace(/behavior\s*:[^;]+;/gi, "/* behavior removed */");
5560
6264
  return sanitized;
5561
6265
  }
6266
+ function firstCompoundEnd(selector) {
6267
+ let parens = 0;
6268
+ let brackets = 0;
6269
+ let quote = null;
6270
+ for (let i2 = 0; i2 < selector.length; i2++) {
6271
+ const ch = selector[i2];
6272
+ if (quote) {
6273
+ if (ch === "\\") i2++;
6274
+ else if (ch === quote) quote = null;
6275
+ continue;
6276
+ }
6277
+ if (ch === '"' || ch === "'") {
6278
+ quote = ch;
6279
+ continue;
6280
+ }
6281
+ if (ch === "\\") {
6282
+ i2++;
6283
+ continue;
6284
+ }
6285
+ if (ch === "(") parens++;
6286
+ else if (ch === ")") parens--;
6287
+ else if (ch === "[") brackets++;
6288
+ else if (ch === "]") brackets--;
6289
+ if (parens > 0 || brackets > 0) continue;
6290
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f") return i2;
6291
+ if (ch === ">" || ch === "+" || ch === "~") return i2;
6292
+ if (ch === ":" && selector[i2 + 1] === ":") return i2;
6293
+ }
6294
+ return selector.length;
6295
+ }
6296
+ function scopeSelector(selector, attr) {
6297
+ const end = firstCompoundEnd(selector);
6298
+ const selfForm = `${selector.slice(0, end)}[${attr}]${selector.slice(end)}`;
6299
+ const descendantForm = `[${attr}] ${selector}`;
6300
+ return `${descendantForm}, ${selfForm}`;
6301
+ }
6302
+ function skipNonCode(text2, i2) {
6303
+ const ch = text2[i2];
6304
+ if (ch === "\\") return i2 + 2;
6305
+ if (ch === '"' || ch === "'") {
6306
+ let j = i2 + 1;
6307
+ while (j < text2.length) {
6308
+ if (text2[j] === "\\") {
6309
+ j += 2;
6310
+ continue;
6311
+ }
6312
+ if (text2[j] === ch) return j + 1;
6313
+ j++;
6314
+ }
6315
+ return text2.length;
6316
+ }
6317
+ if (ch === "/" && text2[i2 + 1] === "*") {
6318
+ const end = text2.indexOf("*/", i2 + 2);
6319
+ return end === -1 ? text2.length : end + 2;
6320
+ }
6321
+ return -1;
6322
+ }
6323
+ function splitSelectorList(prelude) {
6324
+ const parts = [];
6325
+ let parens = 0;
6326
+ let brackets = 0;
6327
+ let start = 0;
6328
+ for (let i2 = 0; i2 < prelude.length; i2++) {
6329
+ const skip = skipNonCode(prelude, i2);
6330
+ if (skip !== -1) {
6331
+ i2 = skip - 1;
6332
+ continue;
6333
+ }
6334
+ const ch = prelude[i2];
6335
+ if (ch === "(") parens++;
6336
+ else if (ch === ")") parens--;
6337
+ else if (ch === "[") brackets++;
6338
+ else if (ch === "]") brackets--;
6339
+ else if (ch === "," && parens === 0 && brackets === 0) {
6340
+ parts.push(prelude.slice(start, i2));
6341
+ start = i2 + 1;
6342
+ }
6343
+ }
6344
+ parts.push(prelude.slice(start));
6345
+ return parts;
6346
+ }
6347
+ function findBlockEnd(css, openBrace) {
6348
+ let depth = 0;
6349
+ for (let i2 = openBrace; i2 < css.length; i2++) {
6350
+ const skip = skipNonCode(css, i2);
6351
+ if (skip !== -1) {
6352
+ i2 = skip - 1;
6353
+ continue;
6354
+ }
6355
+ const ch = css[i2];
6356
+ if (ch === "{") depth++;
6357
+ else if (ch === "}") {
6358
+ depth--;
6359
+ if (depth === 0) return i2;
6360
+ }
6361
+ }
6362
+ return css.length;
6363
+ }
6364
+ var NESTED_AT_RULE = /^@(media|supports|layer|container|scope|document)\b/i;
6365
+ function scopeStylesheet(css, attr) {
6366
+ let out = "";
6367
+ let preludeStart = 0;
6368
+ let parens = 0;
6369
+ let brackets = 0;
6370
+ let i2 = 0;
6371
+ while (i2 < css.length) {
6372
+ const skip = skipNonCode(css, i2);
6373
+ if (skip !== -1) {
6374
+ i2 = skip;
6375
+ continue;
6376
+ }
6377
+ const ch = css[i2];
6378
+ if (ch === "(") {
6379
+ parens++;
6380
+ i2++;
6381
+ continue;
6382
+ }
6383
+ if (ch === ")") {
6384
+ parens--;
6385
+ i2++;
6386
+ continue;
6387
+ }
6388
+ if (ch === "[") {
6389
+ brackets++;
6390
+ i2++;
6391
+ continue;
6392
+ }
6393
+ if (ch === "]") {
6394
+ brackets--;
6395
+ i2++;
6396
+ continue;
6397
+ }
6398
+ if (ch === "{" && parens === 0 && brackets === 0) {
6399
+ const prelude = css.slice(preludeStart, i2);
6400
+ const bodyEnd = findBlockEnd(css, i2);
6401
+ const body2 = css.slice(i2 + 1, bodyEnd);
6402
+ const trimmed = prelude.trim();
6403
+ const lead = prelude.slice(0, prelude.length - prelude.trimStart().length);
6404
+ if (trimmed === "") {
6405
+ out += `${prelude}{${body2}}`;
6406
+ } else if (trimmed.startsWith("@")) {
6407
+ out += NESTED_AT_RULE.test(trimmed) ? `${prelude}{${scopeStylesheet(body2, attr)}}` : `${prelude}{${body2}}`;
6408
+ } else {
6409
+ const scoped = splitSelectorList(trimmed).map((part) => {
6410
+ const selector = part.trim();
6411
+ return selector ? scopeSelector(selector, attr) : part;
6412
+ }).join(", ");
6413
+ out += `${lead}${scoped}{${body2}}`;
6414
+ }
6415
+ i2 = bodyEnd + 1;
6416
+ preludeStart = i2;
6417
+ continue;
6418
+ }
6419
+ i2++;
6420
+ }
6421
+ out += css.slice(preludeStart);
6422
+ return out;
6423
+ }
5562
6424
  function scopedStyle(css) {
5563
6425
  const id = `sibu-s${_scope.n++}`;
5564
6426
  const attr = `data-${id}`;
5565
6427
  const safeCss = sanitizeCSS(css);
5566
- const scopedCSS = safeCss.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g, (match, selector, delimiter) => {
5567
- const trimmed = selector.trim();
5568
- if (trimmed.startsWith("@") || trimmed.startsWith("from") || trimmed.startsWith("to") || /^\d+%$/.test(trimmed)) {
5569
- return match;
5570
- }
5571
- const pseudoIdx = trimmed.indexOf("::");
5572
- if (pseudoIdx >= 0) {
5573
- return `${trimmed.slice(0, pseudoIdx)}[${attr}]${trimmed.slice(pseudoIdx)}${delimiter}`;
5574
- }
5575
- return `${trimmed}[${attr}]${delimiter}`;
5576
- });
6428
+ const scopedCSS = scopeStylesheet(safeCss, attr);
5577
6429
  if (typeof document !== "undefined") {
5578
6430
  const styleEl = document.createElement("style");
5579
6431
  styleEl.setAttribute("data-sibu-scope", id);
@@ -5589,34 +6441,95 @@ function withScopedStyle(css, component) {
5589
6441
  style2 = scopedStyle(css);
5590
6442
  }
5591
6443
  const el = component(props);
5592
- applyScopeRecursive(el, style2.attr);
6444
+ el.setAttribute(style2.attr, "");
5593
6445
  return el;
5594
6446
  };
5595
6447
  }
5596
- function applyScopeRecursive(element, attr) {
5597
- element.setAttribute(attr, "");
5598
- for (const child of Array.from(element.children)) {
5599
- if (child instanceof HTMLElement) {
5600
- applyScopeRecursive(child, attr);
5601
- }
5602
- }
5603
- }
5604
6448
  function removeScopedStyle(scopeId) {
5605
6449
  const el = document.head.querySelector(`style[data-sibu-scope="${scopeId}"]`);
5606
6450
  if (el) el.remove();
5607
6451
  }
5608
6452
 
5609
- // src/reactivity/bindAttribute.ts
6453
+ // src/utils/setSafeAttribute.ts
5610
6454
  var _isDev6 = isDev();
6455
+ var HTML_NS = "http://www.w3.org/1999/xhtml";
6456
+ var SVG_NS = "http://www.w3.org/2000/svg";
6457
+ var XLINK_NS = "http://www.w3.org/1999/xlink";
6458
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
6459
+ var BOOLEAN_IDL_ATTRS = /* @__PURE__ */ new Set(["checked", "disabled", "selected"]);
6460
+ function isHtmlElement(el) {
6461
+ return el.namespaceURI === null || el.namespaceURI === HTML_NS;
6462
+ }
5611
6463
  function setProp(el, key, val) {
5612
6464
  el[key] = val;
5613
6465
  }
5614
- function bindAttribute(el, attr, getter) {
5615
- if (isEventHandlerAttr(attr)) {
5616
- if (_isDev6)
6466
+ function namespaceFor(el, name) {
6467
+ if (el.namespaceURI !== SVG_NS) return null;
6468
+ const lower = name.toLowerCase();
6469
+ if (lower.startsWith("xlink:")) return XLINK_NS;
6470
+ if (lower.startsWith("xml:")) return XML_NS;
6471
+ return null;
6472
+ }
6473
+ function setSafeAttribute(el, name, value, options = {}) {
6474
+ const ns = namespaceFor(el, name);
6475
+ const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
6476
+ if (isEventHandlerAttr(name)) {
6477
+ if (_isDev6) {
6478
+ devWarn(
6479
+ `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
6480
+ );
6481
+ }
6482
+ if (ns) el.removeAttributeNS(ns, localName);
6483
+ else el.removeAttribute(name);
6484
+ return false;
6485
+ }
6486
+ if (value == null) {
6487
+ if (ns) el.removeAttributeNS(ns, localName);
6488
+ else el.removeAttribute(name);
6489
+ return true;
6490
+ }
6491
+ if (isHtmlContentAttribute(name)) {
6492
+ if (_isDev6) {
5617
6493
  devWarn(
5618
- `bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
6494
+ `${options.label ?? "setSafeAttribute"}: refusing to set "${name}". The browser parses this attribute as a nested HTML document, so a generic string value cannot be made safe \u2014 attribute escaping is undone before the parse. Build the frame's content another way.`
5619
6495
  );
6496
+ }
6497
+ if (ns) el.removeAttributeNS(ns, localName);
6498
+ else el.removeAttribute(name);
6499
+ return false;
6500
+ }
6501
+ const html2 = isHtmlElement(el);
6502
+ const idlName = html2 ? name.toLowerCase() : name;
6503
+ if (typeof value === "boolean") {
6504
+ if (html2 && BOOLEAN_IDL_ATTRS.has(idlName) && idlName in el) {
6505
+ setProp(el, idlName, value);
6506
+ } else if (value) {
6507
+ if (ns) el.setAttributeNS(ns, name, "");
6508
+ else el.setAttribute(name, "");
6509
+ } else if (ns) {
6510
+ el.removeAttributeNS(ns, localName);
6511
+ } else {
6512
+ el.removeAttribute(name);
6513
+ }
6514
+ return true;
6515
+ }
6516
+ const str = String(value);
6517
+ if (options.syncValueProperty !== false && html2 && (idlName === "value" || idlName === "checked") && idlName in el) {
6518
+ setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
6519
+ return true;
6520
+ }
6521
+ const safe = sanitizeAttributeString(name, str);
6522
+ const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
6523
+ if (current === safe) return true;
6524
+ if (ns) el.setAttributeNS(ns, name, safe);
6525
+ else el.setAttribute(name, safe);
6526
+ return true;
6527
+ }
6528
+
6529
+ // src/reactivity/bindAttribute.ts
6530
+ function bindAttribute(el, attr, getter) {
6531
+ if (isEventHandlerAttr(attr)) {
6532
+ setSafeAttribute(el, attr, "", { label: `bindAttribute("${attr}")` });
5620
6533
  return () => {
5621
6534
  };
5622
6535
  }
@@ -5625,28 +6538,12 @@ function bindAttribute(el, attr, getter) {
5625
6538
  try {
5626
6539
  value = getter();
5627
6540
  } catch (err) {
5628
- if (_isDev6)
5629
- devWarn(`bindAttribute: getter for "${attr}" threw: ${err instanceof Error ? err.message : String(err)}`);
6541
+ reportError(err, { phase: "binding", name: `bindAttribute("${attr}")`, node: el });
5630
6542
  return;
5631
6543
  }
5632
- if (typeof value === "boolean") {
5633
- if (attr in el && (attr === "checked" || attr === "disabled" || attr === "selected")) {
5634
- setProp(el, attr, value);
5635
- } else if (value) {
5636
- el.setAttribute(attr, "");
5637
- } else {
5638
- el.removeAttribute(attr);
5639
- }
5640
- return;
5641
- }
5642
- const str = String(value);
5643
- if ((attr === "value" || attr === "checked") && attr in el) {
5644
- setProp(el, attr, attr === "checked" ? Boolean(value) : str);
5645
- } else {
5646
- el.setAttribute(attr, sanitizeAttributeString(attr, str));
5647
- }
6544
+ setSafeAttribute(el, attr, value, { label: `bindAttribute("${attr}")` });
5648
6545
  }
5649
- return reactiveBinding2(commit);
6546
+ return reactiveBinding2(commit, el);
5650
6547
  }
5651
6548
 
5652
6549
  // src/ui/reactiveAttr.ts
@@ -5656,14 +6553,8 @@ function bindAttrs(el, attrs) {
5656
6553
  if (typeof value === "function") {
5657
6554
  const teardown = bindAttribute(el, attr, value);
5658
6555
  teardowns.push(teardown);
5659
- } else if (typeof value === "boolean") {
5660
- if (value) {
5661
- el.setAttribute(attr, "");
5662
- } else {
5663
- el.removeAttribute(attr);
5664
- }
5665
6556
  } else {
5666
- el.setAttribute(attr, String(value));
6557
+ setSafeAttribute(el, attr, value, { label: "bindAttrs" });
5667
6558
  }
5668
6559
  }
5669
6560
  return () => {
@@ -5674,11 +6565,7 @@ function bindAttrs(el, attrs) {
5674
6565
  }
5675
6566
  function bindBoolAttr(el, attr, getter) {
5676
6567
  if (typeof getter !== "function") {
5677
- if (getter) {
5678
- el.setAttribute(attr, "");
5679
- } else {
5680
- el.removeAttribute(attr);
5681
- }
6568
+ setSafeAttribute(el, attr, getter, { label: "bindBoolAttr" });
5682
6569
  return () => {
5683
6570
  };
5684
6571
  }
@@ -5690,11 +6577,7 @@ function bindBoolAttr(el, attr, getter) {
5690
6577
  } catch {
5691
6578
  return;
5692
6579
  }
5693
- if (value) {
5694
- el.setAttribute(attr, "");
5695
- } else {
5696
- el.removeAttribute(attr);
5697
- }
6580
+ setSafeAttribute(el, attr, Boolean(value), { label: "bindBoolAttr" });
5698
6581
  }
5699
6582
  const teardown = track2(commit);
5700
6583
  return teardown;
@@ -5702,7 +6585,7 @@ function bindBoolAttr(el, attr, getter) {
5702
6585
  function bindData(el, key, getter) {
5703
6586
  const dataAttr = `data-${key}`;
5704
6587
  if (typeof getter !== "function") {
5705
- el.setAttribute(dataAttr, String(getter));
6588
+ setSafeAttribute(el, dataAttr, getter, { label: "bindData" });
5706
6589
  return () => {
5707
6590
  };
5708
6591
  }
@@ -5842,31 +6725,58 @@ function infiniteScroll(options) {
5842
6725
  const sentinelRef = { current: null };
5843
6726
  let observer = null;
5844
6727
  let disposed = false;
6728
+ let generation = 0;
6729
+ function safeHasMore() {
6730
+ try {
6731
+ return hasMore();
6732
+ } catch (err) {
6733
+ reportError(err, { phase: "async", name: "infiniteScroll(hasMore)" });
6734
+ return false;
6735
+ }
6736
+ }
6737
+ function ownsAttachment(owner) {
6738
+ return !disposed && generation === owner.generation && observer === owner.observer && _current === owner.sentinel;
6739
+ }
5845
6740
  function createObserver() {
5846
6741
  if (typeof IntersectionObserver === "undefined") return;
5847
- observer = new IntersectionObserver(
6742
+ let createdObserver;
6743
+ createdObserver = new IntersectionObserver(
5848
6744
  (entries) => {
5849
6745
  const entry = entries[0];
5850
- if (entry?.isIntersecting && !loading() && hasMore() && !disposed) {
5851
- loadMore();
5852
- }
6746
+ if (!entry?.isIntersecting) return;
6747
+ const sentinel = _current;
6748
+ if (!sentinel) return;
6749
+ const owner = { generation, observer: createdObserver, sentinel };
6750
+ if (!ownsAttachment(owner) || loading()) return;
6751
+ const shouldLoad = safeHasMore();
6752
+ if (!shouldLoad || !ownsAttachment(owner)) return;
6753
+ void loadMore(owner);
5853
6754
  },
5854
6755
  { threshold }
5855
6756
  );
6757
+ observer = createdObserver;
5856
6758
  if (sentinelRef.current) {
5857
- observer.observe(sentinelRef.current);
6759
+ createdObserver.observe(sentinelRef.current);
5858
6760
  }
5859
6761
  }
5860
- async function loadMore() {
5861
- setLoading(true);
6762
+ async function loadMore(owner) {
5862
6763
  try {
5863
- await onLoadMore();
5864
- } finally {
5865
- setLoading(false);
5866
- if (!disposed && observer && _current && hasMore()) {
5867
- observer.unobserve(_current);
5868
- observer.observe(_current);
6764
+ setLoading(true);
6765
+ if (!ownsAttachment(owner)) return;
6766
+ try {
6767
+ await onLoadMore();
6768
+ } catch (err) {
6769
+ reportError(err, { phase: "async", name: "infiniteScroll(onLoadMore)" });
5869
6770
  }
6771
+ if (!ownsAttachment(owner)) return;
6772
+ setLoading(false);
6773
+ if (!ownsAttachment(owner)) return;
6774
+ const shouldContinue = safeHasMore();
6775
+ if (!shouldContinue || !ownsAttachment(owner)) return;
6776
+ owner.observer.unobserve(owner.sentinel);
6777
+ owner.observer.observe(owner.sentinel);
6778
+ } catch (err) {
6779
+ reportError(err, { phase: "async", name: "infiniteScroll(loadMore)" });
5870
6780
  }
5871
6781
  }
5872
6782
  const originalRef = sentinelRef;
@@ -5876,7 +6786,10 @@ function infiniteScroll(options) {
5876
6786
  return _current;
5877
6787
  },
5878
6788
  set(el) {
6789
+ if (el === _current) return;
5879
6790
  _current = el;
6791
+ generation++;
6792
+ setLoading(false);
5880
6793
  if (observer) {
5881
6794
  observer.disconnect();
5882
6795
  observer = null;
@@ -5889,10 +6802,12 @@ function infiniteScroll(options) {
5889
6802
  });
5890
6803
  function dispose2() {
5891
6804
  disposed = true;
6805
+ generation++;
5892
6806
  if (observer) {
5893
6807
  observer.disconnect();
5894
6808
  observer = null;
5895
6809
  }
6810
+ setLoading(false);
5896
6811
  }
5897
6812
  return { sentinelRef: originalRef, loading, dispose: dispose2 };
5898
6813
  }
@@ -6214,7 +7129,7 @@ var DOMPool = class {
6214
7129
  this.pools.set(tag, pool);
6215
7130
  }
6216
7131
  if (pool.length >= this.maxSize) return;
6217
- element.innerHTML = "";
7132
+ replaceChildrenSafely(element);
6218
7133
  element.className = "";
6219
7134
  element.removeAttribute("style");
6220
7135
  element.removeAttribute("id");
@@ -6313,29 +7228,178 @@ function noSideEffect(fn) {
6313
7228
  return fn;
6314
7229
  }
6315
7230
 
7231
+ // src/utils/metaRefresh.ts
7232
+ function isAsciiWhitespace(ch) {
7233
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
7234
+ }
7235
+ function trimAsciiWhitespace(value) {
7236
+ let start = 0;
7237
+ let end = value.length;
7238
+ while (start < end && isAsciiWhitespace(value[start])) start++;
7239
+ while (end > start && isAsciiWhitespace(value[end - 1])) end--;
7240
+ return value.slice(start, end);
7241
+ }
7242
+ var forbidden = (reason) => ({ kind: "forbidden", reason });
7243
+ function parseMetaRefreshContent(content) {
7244
+ const raw = trimAsciiWhitespace(stripControlChars(content));
7245
+ if (raw === "") return forbidden("empty content");
7246
+ let i2 = 0;
7247
+ while (i2 < raw.length && raw[i2] >= "0" && raw[i2] <= "9") i2++;
7248
+ if (i2 === 0) return forbidden("missing or non-numeric delay");
7249
+ const delay = Number.parseInt(raw.slice(0, i2), 10);
7250
+ if (!Number.isFinite(delay)) return forbidden("unparseable delay");
7251
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
7252
+ if (i2 === raw.length) return { kind: "delay-only", delay };
7253
+ if (raw[i2] !== ";" && raw[i2] !== ",") return forbidden("unexpected text after delay");
7254
+ i2++;
7255
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
7256
+ const keyStart = i2;
7257
+ while (i2 < raw.length && !isAsciiWhitespace(raw[i2]) && raw[i2] !== "=") i2++;
7258
+ const key = raw.slice(keyStart, i2).toLowerCase();
7259
+ if (key !== "url") return forbidden(`unsupported refresh key ${JSON.stringify(key)}`);
7260
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
7261
+ if (raw[i2] !== "=") return forbidden("missing '=' after url");
7262
+ i2++;
7263
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
7264
+ let destination;
7265
+ const quote = raw[i2];
7266
+ if (quote === '"' || quote === "'") {
7267
+ const close = raw.indexOf(quote, i2 + 1);
7268
+ if (close === -1) return forbidden("unterminated quoted destination");
7269
+ destination = raw.slice(i2 + 1, close);
7270
+ const rest = trimAsciiWhitespace(raw.slice(close + 1));
7271
+ if (rest !== "") return forbidden("trailing text after quoted destination");
7272
+ } else {
7273
+ destination = trimAsciiWhitespace(raw.slice(i2));
7274
+ if (/[;,]/.test(destination)) return forbidden("multiple refresh assignments");
7275
+ }
7276
+ if (destination === "") return forbidden("empty destination");
7277
+ const sanitizedDestination = sanitizeUrl(destination);
7278
+ if (sanitizedDestination === "") return forbidden("destination uses a disallowed protocol");
7279
+ return { kind: "allowed", delay, destination, sanitizedDestination };
7280
+ }
7281
+ function resolveMetaRefreshPolicy(attributes) {
7282
+ let httpEquiv;
7283
+ let content;
7284
+ let duplicateName;
7285
+ const seen = /* @__PURE__ */ new Set();
7286
+ for (const [key, value] of attributes) {
7287
+ const canonical = canonicalAttrName(key);
7288
+ if (seen.has(canonical)) duplicateName = canonical;
7289
+ seen.add(canonical);
7290
+ if (canonical === "http-equiv") httpEquiv = value;
7291
+ else if (canonical === "content") content = value;
7292
+ }
7293
+ if (duplicateName !== void 0) {
7294
+ return forbidden(`duplicate case-insensitive attribute ${JSON.stringify(duplicateName)}`);
7295
+ }
7296
+ if (typeof httpEquiv !== "string") return { kind: "not-refresh" };
7297
+ if (trimAsciiWhitespace(stripControlChars(httpEquiv)).toLowerCase() !== "refresh") {
7298
+ return { kind: "not-refresh" };
7299
+ }
7300
+ if (typeof content !== "string") return { kind: "not-refresh" };
7301
+ return parseMetaRefreshContent(content);
7302
+ }
7303
+ function normalizeMetaAttributes(props) {
7304
+ const out = /* @__PURE__ */ new Map();
7305
+ const seen = /* @__PURE__ */ new Set();
7306
+ for (const key of Object.keys(props)) {
7307
+ const canonical = canonicalAttrName(key);
7308
+ if (seen.has(canonical)) return null;
7309
+ seen.add(canonical);
7310
+ out.set(key, props[key]);
7311
+ }
7312
+ return out;
7313
+ }
7314
+ function isForbiddenMetaEntry(props) {
7315
+ const normalized = normalizeMetaAttributes(props);
7316
+ if (normalized === null) return true;
7317
+ return resolveMetaRefreshPolicy(normalized).kind === "forbidden";
7318
+ }
7319
+
7320
+ // src/utils/headEntry.ts
7321
+ var SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
7322
+ function isEmittableHeadAttrName(name) {
7323
+ if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
7324
+ const canonical = canonicalAttrName(name);
7325
+ if (isEventHandlerAttr(canonical)) return false;
7326
+ if (isHtmlContentAttribute(canonical)) return false;
7327
+ return true;
7328
+ }
7329
+ function sanitizeHeadAttrValue(canonical, value) {
7330
+ if (!isPolicyAttribute(canonical)) return value;
7331
+ const safe = sanitizeAttributeString(canonical, value);
7332
+ return safe === "" ? null : safe;
7333
+ }
7334
+ function findDuplicateAttributeName(names) {
7335
+ const seen = /* @__PURE__ */ new Set();
7336
+ for (const name of names) {
7337
+ const canonical = canonicalAttrName(name);
7338
+ if (seen.has(canonical)) return canonical;
7339
+ seen.add(canonical);
7340
+ }
7341
+ return null;
7342
+ }
7343
+ function planEntry(props, resolver, applyRefreshPolicy) {
7344
+ const rawNames = Object.keys(props);
7345
+ const duplicate = findDuplicateAttributeName(rawNames);
7346
+ if (duplicate !== null) {
7347
+ return { kind: "drop", reason: `duplicate case-insensitive attribute ${JSON.stringify(duplicate)}` };
7348
+ }
7349
+ const attributes = /* @__PURE__ */ new Map();
7350
+ let reactive = false;
7351
+ for (const name of rawNames) {
7352
+ if (!Object.hasOwn(props, name)) continue;
7353
+ if (!isEmittableHeadAttrName(name)) continue;
7354
+ const authored = props[name];
7355
+ if (resolver.isReactiveValue?.(authored)) reactive = true;
7356
+ const canonical = canonicalAttrName(name);
7357
+ const sanitized = sanitizeHeadAttrValue(canonical, resolver.resolveValue(name, authored));
7358
+ if (sanitized === null) continue;
7359
+ attributes.set(canonical, sanitized);
7360
+ }
7361
+ if (attributes.size === 0) return { kind: "drop", reason: "no effective attributes remain" };
7362
+ const refresh = applyRefreshPolicy ? resolveMetaRefreshPolicy(attributes) : { kind: "not-refresh" };
7363
+ if (refresh.kind === "forbidden") return { kind: "drop", reason: refresh.reason };
7364
+ if (reactive && (refresh.kind === "allowed" || refresh.kind === "delay-only")) {
7365
+ return { kind: "drop", reason: "a reactive meta entry may not publish a native refresh directive" };
7366
+ }
7367
+ return { kind: "publish", attributes, refresh };
7368
+ }
7369
+ function planMetaEntry(props, resolver) {
7370
+ return planEntry(props, resolver, true);
7371
+ }
7372
+ function planHeadElementEntry(props, resolver) {
7373
+ return planEntry(props, resolver, false);
7374
+ }
7375
+ var STATIC_VALUE_RESOLVER = {
7376
+ resolveValue: (_name, value) => String(value)
7377
+ };
7378
+ function escapeHeadAttrValue(value) {
7379
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7380
+ }
7381
+ function serializeHeadEntry(tag, attrs) {
7382
+ const plan = tag === "meta" ? planMetaEntry(attrs, STATIC_VALUE_RESOLVER) : planHeadElementEntry(attrs, STATIC_VALUE_RESOLVER);
7383
+ if (plan.kind === "drop") return "";
7384
+ const pairs = Array.from(plan.attributes, ([name, value]) => `${name}="${escapeHeadAttrValue(value)}"`).join(" ");
7385
+ return `<${tag} ${pairs} />`;
7386
+ }
7387
+
6316
7388
  // src/platform/ssr.ts
6317
- function sanitizeUrlAttr(name, value) {
6318
- return name === "srcset" ? sanitizeSrcset(value) : sanitizeUrl(value);
7389
+ function isPolicyAttr(lowerName) {
7390
+ return isPolicyAttribute(lowerName);
7391
+ }
7392
+ function isForbiddenSsrAttr(lowerName) {
7393
+ return isHtmlContentAttribute(lowerName);
7394
+ }
7395
+ function sanitizeSsrAttributeValue(lowerName, value) {
7396
+ return sanitizeAttributeString(lowerName, value);
6319
7397
  }
6320
7398
  var _isDev8 = isDev();
6321
7399
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
6322
7400
  function isSafeAttrName(name) {
6323
7401
  return SAFE_ATTR_NAME.test(name);
6324
7402
  }
6325
- var URL_ATTRS = /* @__PURE__ */ new Set([
6326
- "href",
6327
- "src",
6328
- "action",
6329
- "formaction",
6330
- "cite",
6331
- "poster",
6332
- "background",
6333
- "srcset",
6334
- "ping",
6335
- "manifest",
6336
- "data",
6337
- "xlink:href"
6338
- ]);
6339
7403
  function ssrErrorComment(err) {
6340
7404
  if (_isDev8) {
6341
7405
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
@@ -6393,10 +7457,11 @@ function renderToString(element) {
6393
7457
  const rawName = attr.name;
6394
7458
  if (!isSafeAttrName(rawName)) continue;
6395
7459
  if (isEventHandlerAttr(rawName)) continue;
7460
+ if (isForbiddenSsrAttr(rawName)) continue;
6396
7461
  const lowerName = rawName.toLowerCase();
6397
7462
  let value = attr.value;
6398
- if (URL_ATTRS.has(lowerName)) {
6399
- value = sanitizeUrlAttr(lowerName, value);
7463
+ if (isPolicyAttr(lowerName)) {
7464
+ value = sanitizeSsrAttributeValue(lowerName, value);
6400
7465
  if (!value) continue;
6401
7466
  }
6402
7467
  html2 += ` ${rawName}="${escapeAttr(value)}"`;
@@ -6437,9 +7502,20 @@ function hydrate(component, container, options = {}) {
6437
7502
  }
6438
7503
  }
6439
7504
  }
6440
- container.replaceChildren(clientTree);
7505
+ replaceChildrenSafely(container, clientTree);
6441
7506
  container.setAttribute("data-sibu-hydrated", "true");
6442
7507
  }
7508
+ function directTextOf(node) {
7509
+ let out = "";
7510
+ for (const child of Array.from(node.childNodes)) {
7511
+ if (child.nodeType === 3) out += child.nodeValue ?? "";
7512
+ }
7513
+ return out.replace(/\s+/g, " ").trim();
7514
+ }
7515
+ function disposeAndReplace(outgoing, next) {
7516
+ dispose(outgoing);
7517
+ outgoing.replaceWith(next);
7518
+ }
6443
7519
  function collectMismatches(serverNode, clientNode, path2, out, max2 = 5) {
6444
7520
  if (out.length >= max2) return;
6445
7521
  const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
@@ -6515,6 +7591,18 @@ function collectMismatches(serverNode, clientNode, path2, out, max2 = 5) {
6515
7591
  });
6516
7592
  }
6517
7593
  }
7594
+ const serverText = directTextOf(serverNode);
7595
+ const clientText = directTextOf(clientNode);
7596
+ if (serverText !== clientText) {
7597
+ out.push({
7598
+ kind: "text",
7599
+ path: nodePath,
7600
+ serverValue: serverText,
7601
+ clientValue: clientText,
7602
+ message: "Text content differs between server and client."
7603
+ });
7604
+ if (out.length >= max2) return;
7605
+ }
6518
7606
  const serverChildren = Array.from(serverNode.children);
6519
7607
  const clientChildren = Array.from(clientNode.children);
6520
7608
  const max22 = Math.max(serverChildren.length, clientChildren.length);
@@ -6534,10 +7622,11 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
6534
7622
  if (!Object.hasOwn(attrs, rawKey)) continue;
6535
7623
  if (!isSafeAttrName(rawKey)) continue;
6536
7624
  if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
7625
+ if (isForbiddenSsrAttr(rawKey)) continue;
6537
7626
  const lowerKey = rawKey.toLowerCase();
6538
7627
  let value = String(attrs[rawKey]);
6539
- if (URL_ATTRS.has(lowerKey)) {
6540
- value = sanitizeUrlAttr(lowerKey, value);
7628
+ if (isPolicyAttr(lowerKey)) {
7629
+ value = sanitizeSsrAttributeValue(lowerKey, value);
6541
7630
  if (!value) continue;
6542
7631
  }
6543
7632
  out.push(`${rawKey}="${escapeAttr(value)}"`);
@@ -6545,18 +7634,7 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
6545
7634
  return out.join(" ");
6546
7635
  }
6547
7636
  function isDangerousMetaRefresh(metaProps) {
6548
- let httpEquiv;
6549
- let content;
6550
- for (const k in metaProps) {
6551
- const lk = k.toLowerCase();
6552
- if (lk === "http-equiv") httpEquiv = metaProps[k];
6553
- else if (lk === "content") content = metaProps[k];
6554
- }
6555
- if (typeof httpEquiv !== "string") return false;
6556
- if (httpEquiv.toLowerCase() !== "refresh") return false;
6557
- if (typeof content !== "string") return false;
6558
- const normalized = stripControlChars(content).toLowerCase();
6559
- return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
7637
+ return isForbiddenMetaEntry(metaProps);
6560
7638
  }
6561
7639
  function renderToDocument(component, options = {}) {
6562
7640
  let content;
@@ -6565,15 +7643,8 @@ function renderToDocument(component, options = {}) {
6565
7643
  } catch (err) {
6566
7644
  content = ssrErrorComment(err);
6567
7645
  }
6568
- const metaTags = (options.meta || []).map((attrs) => {
6569
- if (isDangerousMetaRefresh(attrs)) return "";
6570
- const pairs = buildAttrString(attrs);
6571
- return pairs ? `<meta ${pairs} />` : "";
6572
- }).filter(Boolean).join("\n ");
6573
- const linkTags = (options.links || []).map((attrs) => {
6574
- const pairs = buildAttrString(attrs);
6575
- return pairs ? `<link ${pairs} />` : "";
6576
- }).filter(Boolean).join("\n ");
7646
+ const metaTags = (options.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
7647
+ const linkTags = (options.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
6577
7648
  const scriptTags = (options.scripts || []).map((src) => {
6578
7649
  const safe = sanitizeUrl(String(src));
6579
7650
  if (!safe) return "";
@@ -6634,14 +7705,18 @@ async function* renderToStream(element) {
6634
7705
  const rawName = attr.name;
6635
7706
  if (!isSafeAttrName(rawName)) continue;
6636
7707
  if (isEventHandlerAttr(rawName)) continue;
7708
+ if (isForbiddenSsrAttr(rawName)) continue;
6637
7709
  const lowerName = rawName.toLowerCase();
6638
7710
  let value = attr.value;
6639
- if (URL_ATTRS.has(lowerName)) {
6640
- value = sanitizeUrlAttr(lowerName, value);
7711
+ if (isPolicyAttr(lowerName)) {
7712
+ value = sanitizeSsrAttributeValue(lowerName, value);
6641
7713
  if (!value) continue;
6642
7714
  }
6643
7715
  openTag += ` ${rawName}="${escapeAttr(value)}"`;
6644
7716
  }
7717
+ if (element.dataset && !element.dataset.sibuHydrate) {
7718
+ openTag += ` data-sibu-ssr="true"`;
7719
+ }
6645
7720
  if (VOID_ELEMENTS.has(tag)) {
6646
7721
  yield `${openTag} />`;
6647
7722
  return;
@@ -6688,9 +7763,13 @@ function island(id, component) {
6688
7763
  el.setAttribute("data-sibu-island", id);
6689
7764
  return el;
6690
7765
  }
7766
+ function isAlreadyHydrated(element) {
7767
+ return element.hasAttribute("data-sibu-hydrated");
7768
+ }
6691
7769
  function hydrateIslands(container, islands) {
6692
7770
  const markers = container.querySelectorAll("[data-sibu-island]");
6693
7771
  for (const marker2 of Array.from(markers)) {
7772
+ if (isAlreadyHydrated(marker2)) continue;
6694
7773
  const id = marker2.getAttribute("data-sibu-island") ?? "";
6695
7774
  if (!Object.hasOwn(islands, id)) continue;
6696
7775
  const factory = islands[id];
@@ -6698,7 +7777,7 @@ function hydrateIslands(container, islands) {
6698
7777
  const clientTree = factory();
6699
7778
  clientTree.setAttribute("data-sibu-island", id);
6700
7779
  clientTree.setAttribute("data-sibu-hydrated", "true");
6701
- marker2.replaceWith(clientTree);
7780
+ disposeAndReplace(marker2, clientTree);
6702
7781
  }
6703
7782
  container.setAttribute("data-sibu-hydrated", "partial");
6704
7783
  }
@@ -6706,6 +7785,7 @@ function hydrateProgressively(container, islands, options) {
6706
7785
  const markers = container.querySelectorAll("[data-sibu-island]");
6707
7786
  const cleanups = [];
6708
7787
  for (const marker2 of Array.from(markers)) {
7788
+ if (isAlreadyHydrated(marker2)) continue;
6709
7789
  const id = marker2.getAttribute("data-sibu-island") ?? "";
6710
7790
  if (!Object.hasOwn(islands, id)) continue;
6711
7791
  const factory = islands[id];
@@ -6715,10 +7795,11 @@ function hydrateProgressively(container, islands, options) {
6715
7795
  for (const entry of entries) {
6716
7796
  if (entry.isIntersecting) {
6717
7797
  observer.disconnect();
7798
+ if (isAlreadyHydrated(marker2) || marker2.parentNode === null) break;
6718
7799
  const clientTree = factory();
6719
7800
  clientTree.setAttribute("data-sibu-island", id);
6720
7801
  clientTree.setAttribute("data-sibu-hydrated", "true");
6721
- marker2.replaceWith(clientTree);
7802
+ disposeAndReplace(marker2, clientTree);
6722
7803
  break;
6723
7804
  }
6724
7805
  }
@@ -6774,7 +7855,7 @@ function suspenseSwapScript(id, nonce) {
6774
7855
  );
6775
7856
  }
6776
7857
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
6777
- return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
7858
+ return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(f.firstChild)f.removeChild(f.firstChild);while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
6778
7859
  }
6779
7860
  async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
6780
7861
  yield* renderToStream(element);
@@ -6792,8 +7873,8 @@ function escapeScriptJson(json) {
6792
7873
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6793
7874
  }
6794
7875
  var DEFAULT_MAX_SSR_BYTES = 1024 * 1024;
6795
- function serializeState(state, nonce, options) {
6796
- const rawJson = JSON.stringify(state);
7876
+ function serializeState(state2, nonce, options) {
7877
+ const rawJson = JSON.stringify(state2);
6797
7878
  const maxBytes = options?.maxBytes ?? DEFAULT_MAX_SSR_BYTES;
6798
7879
  const byteLen = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(rawJson).byteLength : Buffer.byteLength(rawJson, "utf8");
6799
7880
  if (byteLen > maxBytes) {
@@ -6855,8 +7936,9 @@ function normalizedStore(schema) {
6855
7936
  ids: [],
6856
7937
  entities: {}
6857
7938
  });
7939
+ const idOf = (entity) => String(entity[idKey]);
6858
7940
  function add(entity) {
6859
- const id = String(entity[idKey]);
7941
+ const id = idOf(entity);
6860
7942
  setState((prev) => {
6861
7943
  const ids = prev.ids.includes(id) ? prev.ids : [...prev.ids, id];
6862
7944
  return {
@@ -6870,7 +7952,7 @@ function normalizedStore(schema) {
6870
7952
  const nextIds = [...prev.ids];
6871
7953
  const nextEntities = { ...prev.entities };
6872
7954
  for (const entity of entities) {
6873
- const id = String(entity[idKey]);
7955
+ const id = idOf(entity);
6874
7956
  if (!nextIds.includes(id)) {
6875
7957
  nextIds.push(id);
6876
7958
  }
@@ -6880,12 +7962,12 @@ function normalizedStore(schema) {
6880
7962
  });
6881
7963
  }
6882
7964
  function get(id) {
6883
- const state = getState();
6884
- return state.entities[id];
7965
+ const state2 = getState();
7966
+ return state2.entities[id];
6885
7967
  }
6886
7968
  function getAll() {
6887
- const state = getState();
6888
- return state.ids.map((id) => state.entities[id]);
7969
+ const state2 = getState();
7970
+ return state2.ids.map((id) => state2.entities[id]);
6889
7971
  }
6890
7972
  function update(id, partial) {
6891
7973
  setState((prev) => {
@@ -6911,8 +7993,8 @@ function normalizedStore(schema) {
6911
7993
  });
6912
7994
  }
6913
7995
  function select3(predicate) {
6914
- const state = getState();
6915
- return state.ids.map((id) => state.entities[id]).filter(predicate);
7996
+ const state2 = getState();
7997
+ return state2.ids.map((id) => state2.entities[id]).filter(predicate);
6916
7998
  }
6917
7999
  return { add, addMany, get, getAll, update, remove, select: select3, getState };
6918
8000
  }
@@ -6992,7 +8074,14 @@ function createChunkRegistry(config = {}) {
6992
8074
  } = config;
6993
8075
  const cache = /* @__PURE__ */ new Map();
6994
8076
  const pending = /* @__PURE__ */ new Map();
6995
- const preloaded = /* @__PURE__ */ new Set();
8077
+ const preloaded = /* @__PURE__ */ new Map();
8078
+ function notify(name, run) {
8079
+ try {
8080
+ run();
8081
+ } catch (err) {
8082
+ reportError(err, { phase: "async", name: `chunkRegistry(${name})` });
8083
+ }
8084
+ }
6996
8085
  function evict() {
6997
8086
  while (cache.size >= maxCacheSize) {
6998
8087
  let lru = null;
@@ -7047,30 +8136,47 @@ function createChunkRegistry(config = {}) {
7047
8136
  cached.lastAccess = Date.now();
7048
8137
  return cached.value;
7049
8138
  }
7050
- const pendingLoad = pending.get(id);
7051
- if (pendingLoad) return pendingLoad;
7052
- onLoadStart?.(id);
7053
- const loadPromise = loadWithRetry(id, loader).then((value) => {
7054
- evict();
7055
- const now = Date.now();
7056
- cache.set(id, { value, timestamp: now, lastAccess: now, accessCount: 1 });
7057
- pending.delete(id);
7058
- onLoadEnd?.(id);
7059
- return value;
7060
- }).catch((err) => {
7061
- pending.delete(id);
7062
- const error = err instanceof Error ? err : new Error(String(err));
7063
- onLoadError?.(id, error);
7064
- throw error;
8139
+ const existing = pending.get(id);
8140
+ if (existing) return existing.promise;
8141
+ let resolveOuter;
8142
+ let rejectOuter;
8143
+ const outcome = new Promise((resolve, reject) => {
8144
+ resolveOuter = resolve;
8145
+ rejectOuter = reject;
7065
8146
  });
7066
- pending.set(id, loadPromise);
7067
- return loadPromise;
8147
+ const entry = { promise: outcome };
8148
+ pending.set(id, entry);
8149
+ const owns = () => pending.get(id) === entry;
8150
+ notify("onLoadStart", () => onLoadStart?.(id));
8151
+ void loadWithRetry(id, loader).then(
8152
+ (value) => {
8153
+ if (owns()) {
8154
+ pending.delete(id);
8155
+ evict();
8156
+ const now = Date.now();
8157
+ cache.set(id, { value, timestamp: now, lastAccess: now, accessCount: 1 });
8158
+ }
8159
+ notify("onLoadEnd", () => onLoadEnd?.(id));
8160
+ resolveOuter(value);
8161
+ },
8162
+ (err) => {
8163
+ if (owns()) pending.delete(id);
8164
+ const error = err instanceof Error ? err : new Error(String(err));
8165
+ notify("onLoadError", () => onLoadError?.(id, error));
8166
+ rejectOuter(error);
8167
+ }
8168
+ ).catch((internal) => {
8169
+ reportError(internal, { phase: "async", name: "chunkRegistry(settlement)" });
8170
+ rejectOuter(internal);
8171
+ });
8172
+ return outcome;
7068
8173
  }
7069
8174
  function preloadFn(id, loader) {
7070
8175
  if (cache.has(id) || pending.has(id) || preloaded.has(id)) return;
7071
- preloaded.add(id);
8176
+ const marker2 = {};
8177
+ preloaded.set(id, marker2);
7072
8178
  loadFn(id, loader).catch(() => {
7073
- preloaded.delete(id);
8179
+ if (preloaded.get(id) === marker2) preloaded.delete(id);
7074
8180
  });
7075
8181
  }
7076
8182
  return {
@@ -7104,21 +8210,33 @@ function createChunkRegistry(config = {}) {
7104
8210
  return void 0;
7105
8211
  },
7106
8212
  /**
7107
- * Invalidate a cached chunk.
8213
+ * Invalidate a cached chunk, and supersede any load still in flight for it.
8214
+ *
8215
+ * In-flight work is not cancelled — the loader API takes no abort signal, so
8216
+ * claiming cancellation would be a lie. It is *superseded*: it still settles
8217
+ * for whoever called `load()`, but it can no longer write to the cache, and
8218
+ * a subsequent `load(id, …)` starts fresh work rather than adopting it.
7108
8219
  */
7109
8220
  invalidate(id) {
7110
8221
  cache.delete(id);
7111
8222
  preloaded.delete(id);
8223
+ pending.delete(id);
7112
8224
  },
7113
8225
  /**
7114
- * Clear all cached chunks.
8226
+ * Clear all cached chunks, and supersede every load still in flight.
8227
+ *
8228
+ * Same barrier as `invalidate`, applied to every key at once.
7115
8229
  */
7116
8230
  clear() {
7117
8231
  cache.clear();
7118
8232
  preloaded.clear();
8233
+ pending.clear();
7119
8234
  },
7120
8235
  /**
7121
8236
  * Get cache statistics.
8237
+ *
8238
+ * Describes currently OWNED state: superseded loads are already absent from
8239
+ * `pending`, so the counts never include work whose result will be discarded.
7122
8240
  */
7123
8241
  stats() {
7124
8242
  return {
@@ -7171,38 +8289,57 @@ function preloadModules(urls) {
7171
8289
  }
7172
8290
 
7173
8291
  // src/platform/head.ts
7174
- var HEAD_URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
7175
- function sanitizeHeadAttr(key, value) {
7176
- if (HEAD_URL_ATTRS.has(key)) return sanitizeUrl(value);
7177
- return value;
7178
- }
7179
- function isDangerousRefreshContent(content) {
7180
- const normalized = stripControlChars(content).toLowerCase();
7181
- return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
7182
- }
7183
- function getMetaAttr(metaProps, name) {
7184
- for (const k in metaProps) {
7185
- if (k.toLowerCase() === name) return metaProps[k];
7186
- }
7187
- return void 0;
7188
- }
7189
- function isDangerousMetaRefresh2(metaProps) {
7190
- const httpEquiv = getMetaAttr(metaProps, "http-equiv");
7191
- if (typeof httpEquiv !== "string") return false;
7192
- if (httpEquiv.toLowerCase() !== "refresh") return false;
7193
- const content = getMetaAttr(metaProps, "content");
7194
- if (typeof content !== "string") return false;
7195
- return isDangerousRefreshContent(content);
7196
- }
7197
- var SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
7198
- function isSafeHeadAttr(name) {
7199
- if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
7200
- if (isEventHandlerAttr(name)) return false;
7201
- return true;
7202
- }
7203
8292
  function escapeScriptJsonLocal(json) {
7204
8293
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
7205
8294
  }
8295
+ var CLIENT_RESOLVER = {
8296
+ resolveValue: (_name, value) => typeof value === "function" ? String(value()) : String(value),
8297
+ isReactiveValue: (value) => typeof value === "function"
8298
+ };
8299
+ function applyMetaEntry(metaProps, managedElements) {
8300
+ let current = null;
8301
+ const retract = () => {
8302
+ if (!current) return;
8303
+ const index = managedElements.indexOf(current);
8304
+ if (index !== -1) managedElements.splice(index, 1);
8305
+ current.remove();
8306
+ current = null;
8307
+ };
8308
+ const alreadyPublished = (attributes) => {
8309
+ if (!current || current.attributes.length !== attributes.size) return false;
8310
+ for (const [name, value] of attributes) {
8311
+ if (current.getAttribute(name) !== value) return false;
8312
+ }
8313
+ return true;
8314
+ };
8315
+ const publish = (attributes) => {
8316
+ if (alreadyPublished(attributes)) return;
8317
+ const next = document.createElement("meta");
8318
+ for (const [name, value] of attributes) next.setAttribute(name, value);
8319
+ const index = current ? managedElements.indexOf(current) : -1;
8320
+ if (current?.parentNode) {
8321
+ current.replaceWith(next);
8322
+ } else {
8323
+ current?.remove();
8324
+ document.head.appendChild(next);
8325
+ }
8326
+ if (index !== -1) managedElements[index] = next;
8327
+ else managedElements.push(next);
8328
+ current = next;
8329
+ };
8330
+ const stopEffect = effect(() => {
8331
+ const plan = planMetaEntry(metaProps, CLIENT_RESOLVER);
8332
+ if (plan.kind === "drop") {
8333
+ retract();
8334
+ return;
8335
+ }
8336
+ publish(plan.attributes);
8337
+ });
8338
+ return () => {
8339
+ stopEffect();
8340
+ retract();
8341
+ };
8342
+ }
7206
8343
  function Head(props) {
7207
8344
  const anchor = document.createComment("sibu-head");
7208
8345
  const managedElements = [];
@@ -7219,78 +8356,52 @@ function Head(props) {
7219
8356
  cleanup3();
7220
8357
  if (props.title) {
7221
8358
  if (typeof props.title === "function") {
7222
- const cleanupFn = effect(() => {
7223
- document.title = props.title();
8359
+ const getter = props.title;
8360
+ let lease = null;
8361
+ const stopEffect = effect(() => {
8362
+ const next = getter();
8363
+ if (lease) lease.set(next);
8364
+ else lease = acquireTitle(next);
8365
+ });
8366
+ effectCleanups.push(() => {
8367
+ stopEffect();
8368
+ lease?.release();
8369
+ lease = null;
7224
8370
  });
7225
- effectCleanups.push(cleanupFn);
7226
8371
  } else {
7227
- document.title = props.title;
8372
+ const lease = acquireTitle(props.title);
8373
+ effectCleanups.push(() => lease.release());
7228
8374
  }
7229
8375
  }
7230
8376
  if (props.meta) {
7231
8377
  for (const metaProps of props.meta) {
7232
- if (isDangerousMetaRefresh2(metaProps)) continue;
7233
- const httpEquiv = getMetaAttr(metaProps, "http-equiv");
7234
- const isRefreshNow = () => {
7235
- const eq = typeof httpEquiv === "function" ? httpEquiv() : httpEquiv;
7236
- return typeof eq === "string" && eq.toLowerCase() === "refresh";
7237
- };
7238
- const el = document.createElement("meta");
7239
- for (const [key, value] of Object.entries(metaProps)) {
7240
- if (!isSafeHeadAttr(key)) continue;
7241
- const isContent = key.toLowerCase() === "content";
7242
- if (typeof value === "function") {
7243
- const cleanupFn = effect(() => {
7244
- const resolved = value();
7245
- if (isContent && isRefreshNow() && isDangerousRefreshContent(resolved)) {
7246
- el.removeAttribute(key);
7247
- return;
7248
- }
7249
- el.setAttribute(key, sanitizeHeadAttr(key, resolved));
7250
- });
7251
- effectCleanups.push(cleanupFn);
7252
- } else {
7253
- if (isContent && isRefreshNow() && isDangerousRefreshContent(value)) continue;
7254
- el.setAttribute(key, sanitizeHeadAttr(key, value));
7255
- }
7256
- }
7257
- document.head.appendChild(el);
7258
- managedElements.push(el);
7259
- }
7260
- }
7261
- if (props.link) {
7262
- for (const linkProps of props.link) {
7263
- const el = document.createElement("link");
7264
- for (const [key, value] of Object.entries(linkProps)) {
7265
- if (!isSafeHeadAttr(key)) continue;
7266
- el.setAttribute(key, sanitizeHeadAttr(key, value));
7267
- }
7268
- document.head.appendChild(el);
7269
- managedElements.push(el);
7270
- }
7271
- }
7272
- if (props.script) {
7273
- for (const scriptProps of props.script) {
7274
- const el = document.createElement("script");
7275
- for (const [key, value] of Object.entries(scriptProps)) {
7276
- if (!isSafeHeadAttr(key)) continue;
7277
- el.setAttribute(key, sanitizeHeadAttr(key, value));
7278
- }
8378
+ const stop2 = applyMetaEntry(metaProps, managedElements);
8379
+ effectCleanups.push(stop2);
8380
+ }
8381
+ }
8382
+ for (const [tag, entries] of [
8383
+ ["link", props.link],
8384
+ ["script", props.script]
8385
+ ]) {
8386
+ if (!entries) continue;
8387
+ for (const entryProps of entries) {
8388
+ const plan = planHeadElementEntry(entryProps, STATIC_VALUE_RESOLVER);
8389
+ if (plan.kind === "drop") continue;
8390
+ const el = document.createElement(tag);
8391
+ for (const [name, value] of plan.attributes) el.setAttribute(name, value);
7279
8392
  document.head.appendChild(el);
7280
8393
  managedElements.push(el);
7281
8394
  }
7282
8395
  }
7283
8396
  if (props.base) {
7284
- const existing = document.head.querySelector("base");
7285
- if (existing) existing.remove();
7286
- const el = document.createElement("base");
8397
+ const spec = {};
7287
8398
  if (props.base.href) {
7288
8399
  const safeHref = sanitizeUrl(props.base.href);
7289
- if (safeHref) el.href = safeHref;
8400
+ if (safeHref) spec.href = safeHref;
7290
8401
  }
7291
- if (props.base.target) el.target = props.base.target;
7292
- document.head.appendChild(el);
7293
- managedElements.push(el);
8402
+ if (props.base.target) spec.target = props.base.target;
8403
+ const lease = acquireBase(spec);
8404
+ effectCleanups.push(() => lease.release());
7294
8405
  }
7295
8406
  };
7296
8407
  apply();
@@ -7313,10 +8424,13 @@ function setCanonical(url) {
7313
8424
  link2.rel = "canonical";
7314
8425
  document.head.appendChild(link2);
7315
8426
  }
7316
- link2.href = sanitizeUrl(url);
8427
+ const safe = sanitizeUrl(url);
8428
+ if (safe) link2.setAttribute("href", safe);
8429
+ else link2.removeAttribute("href");
7317
8430
  }
7318
8431
 
7319
8432
  // src/platform/customElement.ts
8433
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
7320
8434
  function defineElement(name, component, options = {}) {
7321
8435
  if (customElements.get(name)) return;
7322
8436
  const observed = options.observedAttributes || [];
@@ -7345,11 +8459,8 @@ function defineElement(name, component, options = {}) {
7345
8459
  }
7346
8460
  }
7347
8461
  _teardown() {
7348
- if (this._rendered) {
7349
- dispose(this._rendered);
7350
- this._rendered = null;
7351
- }
7352
- this._root.replaceChildren();
8462
+ this._rendered = null;
8463
+ replaceChildrenSafely(this._root);
7353
8464
  }
7354
8465
  _render() {
7355
8466
  this._teardown();
@@ -7374,14 +8485,13 @@ function defineElement(name, component, options = {}) {
7374
8485
  customElements.define(name, SibuElement);
7375
8486
  }
7376
8487
  function svgElement(tag, props = {}, ...nodes) {
7377
- const SVG_NS2 = "http://www.w3.org/2000/svg";
7378
8488
  const el = document.createElementNS(SVG_NS2, tag);
7379
8489
  for (const [key, value] of Object.entries(props)) {
7380
8490
  if (key === "nodes") continue;
7381
- if (typeof value === "function" && key.startsWith("on")) {
8491
+ if (typeof value === "function" && isEventHandlerAttr(key)) {
7382
8492
  el.addEventListener(key.slice(2).toLowerCase(), value);
7383
8493
  } else if (value != null) {
7384
- el.setAttribute(key, String(value));
8494
+ setSafeAttribute(el, key, value, { label: "svgElement" });
7385
8495
  }
7386
8496
  }
7387
8497
  for (const child of nodes) {
@@ -7617,22 +8727,34 @@ var instanceCache = globalSingleton(
7617
8727
  /* @__PURE__ */ Symbol.for("sibujs.wasm.instanceCache.v1"),
7618
8728
  () => /* @__PURE__ */ new Map()
7619
8729
  );
8730
+ var inFlightCache = globalSingleton(
8731
+ /* @__PURE__ */ Symbol.for("sibujs.wasm.inFlightCache.v1"),
8732
+ () => /* @__PURE__ */ new Map()
8733
+ );
8734
+ var cacheState = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.wasm.cacheState.v1"), () => ({ generation: 0 }));
8735
+ function isCurrentGeneration(generation) {
8736
+ return generation === cacheState.generation;
8737
+ }
7620
8738
  function wasm(source2, config = {}) {
7621
8739
  const [instance, setInstance] = signal(null);
7622
8740
  const [loading, setLoading] = signal(true);
7623
8741
  const [error, setError] = signal(null);
7624
8742
  const cacheKey = config.cacheKey || (typeof source2 === "string" ? source2 : void 0);
8743
+ let runId = 0;
7625
8744
  async function load() {
8745
+ const myRun = ++runId;
7626
8746
  setLoading(true);
7627
8747
  setError(null);
7628
8748
  setInstance(null);
7629
8749
  try {
7630
- const wasmInstance = await loadWasmModule(source2, config.imports, cacheKey);
8750
+ const wasmInstance = await loadWasmModuleWithOptions(source2, { ...config, cacheKey });
8751
+ if (myRun !== runId) return;
7631
8752
  setInstance(wasmInstance.exports);
7632
8753
  } catch (err) {
8754
+ if (myRun !== runId) return;
7633
8755
  setError(err instanceof Error ? err : new Error(String(err)));
7634
8756
  } finally {
7635
- setLoading(false);
8757
+ if (myRun === runId) setLoading(false);
7636
8758
  }
7637
8759
  }
7638
8760
  load();
@@ -7645,9 +8767,9 @@ function wasm(source2, config = {}) {
7645
8767
  };
7646
8768
  }
7647
8769
  async function loadWasmModule(source2, imports, cacheKey) {
7648
- const isOptionsBag = !!(imports && ("allowedOrigins" in imports || "unsafelyAllowAnyOrigin" in imports));
7649
- const opts = isOptionsBag ? imports : { imports, cacheKey };
7650
- const wasmImports = opts.imports;
8770
+ return loadWasmModuleWithOptions(source2, { imports, cacheKey });
8771
+ }
8772
+ async function loadWasmModuleWithOptions(source2, opts = {}) {
7651
8773
  const key = opts.cacheKey || (typeof source2 === "string" ? source2 : void 0);
7652
8774
  if (typeof source2 === "string") {
7653
8775
  const allowed = opts.allowedOrigins ?? [];
@@ -7667,44 +8789,50 @@ async function loadWasmModule(source2, imports, cacheKey) {
7667
8789
  );
7668
8790
  }
7669
8791
  }
7670
- if (key) {
7671
- const cachedInstance = instanceCache.get(key);
7672
- if (cachedInstance) {
7673
- return cachedInstance;
7674
- }
7675
- }
7676
- let module2;
8792
+ const generation = cacheState.generation;
8793
+ if (!key) return instantiateWasm(source2, opts.imports, void 0, generation);
8794
+ const cachedInstance = instanceCache.get(key);
8795
+ if (cachedInstance) return cachedInstance;
8796
+ const inFlight = inFlightCache.get(key);
8797
+ if (inFlight) return inFlight;
8798
+ const pending = instantiateWasm(source2, opts.imports, key, generation).then((instance) => {
8799
+ if (isCurrentGeneration(generation)) instanceCache.set(key, instance);
8800
+ return instance;
8801
+ });
8802
+ inFlightCache.set(key, pending);
8803
+ void pending.catch(() => {
8804
+ }).then(() => {
8805
+ if (inFlightCache.get(key) === pending) inFlightCache.delete(key);
8806
+ });
8807
+ return pending;
8808
+ }
8809
+ async function instantiateWasm(source2, wasmImports, key, generation) {
7677
8810
  const cachedModule = key ? moduleCache.get(key) : void 0;
7678
8811
  if (cachedModule) {
7679
- module2 = cachedModule;
8812
+ return WebAssembly.instantiate(cachedModule, wasmImports || {});
8813
+ }
8814
+ let bytes;
8815
+ if (typeof source2 === "string") {
8816
+ if (typeof WebAssembly.instantiateStreaming === "function") {
8817
+ const response2 = fetch(source2);
8818
+ const result = await WebAssembly.instantiateStreaming(response2, wasmImports || {});
8819
+ if (key && isCurrentGeneration(generation)) moduleCache.set(key, result.module);
8820
+ return result.instance;
8821
+ }
8822
+ const response = await fetch(source2);
8823
+ bytes = await response.arrayBuffer();
8824
+ } else if (source2 instanceof Uint8Array) {
8825
+ bytes = source2.buffer.slice(source2.byteOffset, source2.byteOffset + source2.byteLength);
7680
8826
  } else {
7681
- let bytes;
7682
- if (typeof source2 === "string") {
7683
- if (typeof WebAssembly.instantiateStreaming === "function") {
7684
- const response2 = fetch(source2);
7685
- const result = await WebAssembly.instantiateStreaming(response2, wasmImports || {});
7686
- if (key) {
7687
- moduleCache.set(key, result.module);
7688
- instanceCache.set(key, result.instance);
7689
- }
7690
- return result.instance;
7691
- }
7692
- const response = await fetch(source2);
7693
- bytes = await response.arrayBuffer();
7694
- } else if (source2 instanceof Uint8Array) {
7695
- bytes = source2.buffer.slice(source2.byteOffset, source2.byteOffset + source2.byteLength);
7696
- } else {
7697
- bytes = source2;
7698
- }
7699
- module2 = await WebAssembly.compile(bytes);
7700
- if (key) moduleCache.set(key, module2);
8827
+ bytes = source2;
7701
8828
  }
7702
- const instance = await WebAssembly.instantiate(module2, wasmImports || {});
7703
- if (key) instanceCache.set(key, instance);
7704
- return instance;
8829
+ const module2 = await WebAssembly.compile(bytes);
8830
+ if (key && isCurrentGeneration(generation)) moduleCache.set(key, module2);
8831
+ return WebAssembly.instantiate(module2, wasmImports || {});
7705
8832
  }
7706
8833
  async function preloadWasm(url, options = {}) {
7707
8834
  if (moduleCache.has(url)) return;
8835
+ const generation = cacheState.generation;
7708
8836
  const allowed = options.allowedOrigins ?? [];
7709
8837
  if (allowed.length > 0) {
7710
8838
  let parsed;
@@ -7729,7 +8857,7 @@ async function preloadWasm(url, options = {}) {
7729
8857
  const bytes = await response.arrayBuffer();
7730
8858
  module2 = await WebAssembly.compile(bytes);
7731
8859
  }
7732
- moduleCache.set(url, module2);
8860
+ if (isCurrentGeneration(generation)) moduleCache.set(url, module2);
7733
8861
  }
7734
8862
  function createWasmBridge(instance) {
7735
8863
  const exports2 = instance.exports;
@@ -7769,8 +8897,10 @@ function createWasmBridge(instance) {
7769
8897
  };
7770
8898
  }
7771
8899
  function clearWasmCache() {
8900
+ cacheState.generation++;
7772
8901
  moduleCache.clear();
7773
8902
  instanceCache.clear();
8903
+ inFlightCache.clear();
7774
8904
  }
7775
8905
  function isWasmCached(key) {
7776
8906
  return moduleCache.has(key);
@@ -7785,7 +8915,7 @@ function bindChildNode(placeholder, getter) {
7785
8915
  try {
7786
8916
  result = getter();
7787
8917
  } catch (err) {
7788
- if (_isDev9) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
8918
+ reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
7789
8919
  return;
7790
8920
  }
7791
8921
  if (result == null || typeof result === "boolean") {
@@ -7846,11 +8976,11 @@ function bindChildNode(placeholder, getter) {
7846
8976
  }
7847
8977
  lastNodes = newNodes;
7848
8978
  }
7849
- return reactiveBinding2(commit);
8979
+ return reactiveBinding2(commit, placeholder);
7850
8980
  }
7851
8981
 
7852
8982
  // src/core/rendering/tagFactory.ts
7853
- var SVG_NS = "http://www.w3.org/2000/svg";
8983
+ var SVG_NS3 = "http://www.w3.org/2000/svg";
7854
8984
  var _isDev10 = isDev();
7855
8985
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
7856
8986
  function isBlockedTag(tag) {
@@ -7869,9 +8999,6 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
7869
8999
  "self",
7870
9000
  "frames"
7871
9001
  ]);
7872
- function setProp2(el, key, val) {
7873
- el[key] = val;
7874
- }
7875
9002
  function looksLikeClassList(s2) {
7876
9003
  const t = s2.trim();
7877
9004
  if (!t) return false;
@@ -7895,13 +9022,13 @@ function toKebab(prop) {
7895
9022
  function applyStyle(el, style2) {
7896
9023
  if (typeof style2 === "function") {
7897
9024
  const teardown = track2(() => {
7898
- el.setAttribute("style", style2());
9025
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
7899
9026
  });
7900
9027
  registerDisposer(el, teardown);
7901
9028
  return;
7902
9029
  }
7903
9030
  if (typeof style2 === "string") {
7904
- el.setAttribute("style", style2);
9031
+ el.setAttribute("style", sanitizeStyleAttribute(style2));
7905
9032
  return;
7906
9033
  }
7907
9034
  const htmlEl = el;
@@ -8086,16 +9213,8 @@ var tagFactory = (tag, ns) => {
8086
9213
  if (isEventHandlerAttr(key)) continue;
8087
9214
  if (typeof value === "function") {
8088
9215
  registerDisposer(el, bindAttribute(el, key, value));
8089
- } else if (typeof value === "boolean") {
8090
- if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
8091
- setProp2(el, key, value);
8092
- } else if (value) {
8093
- el.setAttribute(key, "");
8094
- } else {
8095
- el.removeAttribute(key);
8096
- }
8097
9216
  } else {
8098
- el.setAttribute(key, sanitizeAttributeString(key, String(value)));
9217
+ setSafeAttribute(el, key, value, { syncValueProperty: false, label: "tagFactory" });
8099
9218
  }
8100
9219
  }
8101
9220
  }
@@ -8180,7 +9299,7 @@ var param = tagFactory("param");
8180
9299
  var picture = tagFactory("picture");
8181
9300
  var portal = tagFactory("portal");
8182
9301
  var source = tagFactory("source");
8183
- var svg = tagFactory("svg", SVG_NS);
9302
+ var svg = tagFactory("svg", SVG_NS3);
8184
9303
  var math = tagFactory("math");
8185
9304
  var canvas = tagFactory("canvas");
8186
9305
  var noscript = tagFactory("noscript");
@@ -8221,26 +9340,26 @@ var base = tagFactory("base");
8221
9340
  var link = tagFactory("link");
8222
9341
  var meta = tagFactory("meta");
8223
9342
  var style = tagFactory("style");
8224
- var circle = tagFactory("circle", SVG_NS);
8225
- var ellipse = tagFactory("ellipse", SVG_NS);
8226
- var g = tagFactory("g", SVG_NS);
8227
- var line = tagFactory("line", SVG_NS);
8228
- var path = tagFactory("path", SVG_NS);
8229
- var polygon = tagFactory("polygon", SVG_NS);
8230
- var polyline = tagFactory("polyline", SVG_NS);
8231
- var rect = tagFactory("rect", SVG_NS);
8232
- var text = tagFactory("text", SVG_NS);
8233
- var tspan = tagFactory("tspan", SVG_NS);
8234
- var defs = tagFactory("defs", SVG_NS);
8235
- var clipPath = tagFactory("clipPath", SVG_NS);
8236
- var mask = tagFactory("mask", SVG_NS);
8237
- var pattern = tagFactory("pattern", SVG_NS);
8238
- var linearGradient = tagFactory("linearGradient", SVG_NS);
8239
- var radialGradient = tagFactory("radialGradient", SVG_NS);
8240
- var stop = tagFactory("stop", SVG_NS);
8241
- var use = tagFactory("use", SVG_NS);
8242
- var symbol = tagFactory("symbol", SVG_NS);
8243
- var marker = tagFactory("marker", SVG_NS);
9343
+ var circle = tagFactory("circle", SVG_NS3);
9344
+ var ellipse = tagFactory("ellipse", SVG_NS3);
9345
+ var g = tagFactory("g", SVG_NS3);
9346
+ var line = tagFactory("line", SVG_NS3);
9347
+ var path = tagFactory("path", SVG_NS3);
9348
+ var polygon = tagFactory("polygon", SVG_NS3);
9349
+ var polyline = tagFactory("polyline", SVG_NS3);
9350
+ var rect = tagFactory("rect", SVG_NS3);
9351
+ var text = tagFactory("text", SVG_NS3);
9352
+ var tspan = tagFactory("tspan", SVG_NS3);
9353
+ var defs = tagFactory("defs", SVG_NS3);
9354
+ var clipPath = tagFactory("clipPath", SVG_NS3);
9355
+ var mask = tagFactory("mask", SVG_NS3);
9356
+ var pattern = tagFactory("pattern", SVG_NS3);
9357
+ var linearGradient = tagFactory("linearGradient", SVG_NS3);
9358
+ var radialGradient = tagFactory("radialGradient", SVG_NS3);
9359
+ var stop = tagFactory("stop", SVG_NS3);
9360
+ var use = tagFactory("use", SVG_NS3);
9361
+ var symbol = tagFactory("symbol", SVG_NS3);
9362
+ var marker = tagFactory("marker", SVG_NS3);
8244
9363
  var center = tagFactory("center");
8245
9364
  var font = tagFactory("font");
8246
9365
  var marquee = tagFactory("marquee");
@@ -8261,14 +9380,13 @@ function createMicroApp(config) {
8261
9380
  }
8262
9381
  let mounted = false;
8263
9382
  function mount(component) {
8264
- root.replaceChildren();
8265
9383
  const el = component();
8266
- root.appendChild(el);
9384
+ replaceChildrenSafely(root, el);
8267
9385
  mounted = true;
8268
9386
  }
8269
9387
  function unmount() {
8270
9388
  if (!mounted) return;
8271
- root.replaceChildren();
9389
+ replaceChildrenSafely(root);
8272
9390
  mounted = false;
8273
9391
  }
8274
9392
  return { mount, unmount, element: host };
@@ -8315,18 +9433,21 @@ function defineRemoteComponent(name, loader) {
8315
9433
  class: "sibu-remote",
8316
9434
  "data-remote-component": name
8317
9435
  });
8318
- container.appendChild(span({ class: "sibu-remote-loading", nodes: "Loading..." }));
9436
+ container.appendChild(span({ class: "sibu-remote-loading" }, "Loading..."));
9437
+ let disposed = false;
9438
+ registerDisposer(container, () => {
9439
+ disposed = true;
9440
+ });
8319
9441
  loader().then((mod) => {
8320
9442
  cached = mod.default;
8321
- const rendered = cached();
8322
- container.replaceChildren(rendered);
9443
+ if (disposed) return;
9444
+ replaceChildrenSafely(container, cached());
8323
9445
  }).catch((err) => {
9446
+ if (disposed) return;
8324
9447
  const message = err instanceof Error ? err.message : String(err);
8325
- container.replaceChildren(
8326
- div({
8327
- class: "sibu-remote-error",
8328
- nodes: `Failed to load remote component "${name}": ${message}`
8329
- })
9448
+ replaceChildrenSafely(
9449
+ container,
9450
+ div({ class: "sibu-remote-error" }, `Failed to load remote component "${name}": ${message}`)
8330
9451
  );
8331
9452
  });
8332
9453
  return container;
@@ -8382,11 +9503,16 @@ function serviceWorker(scriptUrl, options) {
8382
9503
  const [isReady, setIsReady] = signal(false);
8383
9504
  const [isUpdateAvailable, setIsUpdateAvailable] = signal(false);
8384
9505
  const [error, setError] = signal(null);
8385
- let disposed = false;
9506
+ let unregistered = false;
9507
+ let unregisterRequested = false;
9508
+ let registrationPromise = null;
9509
+ let arrivedRegistration = null;
9510
+ let pendingUnregister = null;
8386
9511
  let updateFoundHandler = null;
8387
9512
  let stateChangeHandler = null;
8388
9513
  let trackedWorker = null;
8389
9514
  let trackedReg = null;
9515
+ const supported = typeof navigator !== "undefined" && !!navigator.serviceWorker;
8390
9516
  function detachListeners() {
8391
9517
  if (trackedReg && updateFoundHandler) {
8392
9518
  trackedReg.removeEventListener("updatefound", updateFoundHandler);
@@ -8399,33 +9525,53 @@ function serviceWorker(scriptUrl, options) {
8399
9525
  trackedWorker = null;
8400
9526
  trackedReg = null;
8401
9527
  }
8402
- if ("serviceWorker" in navigator) {
8403
- navigator.serviceWorker.register(scriptUrl, options).then((reg) => {
8404
- if (disposed) return;
8405
- setRegistration(reg);
8406
- setIsReady(true);
8407
- trackedReg = reg;
8408
- updateFoundHandler = () => {
8409
- if (disposed) return;
8410
- const newWorker = reg.installing;
8411
- if (newWorker) {
8412
- if (trackedWorker && stateChangeHandler) {
8413
- trackedWorker.removeEventListener("statechange", stateChangeHandler);
8414
- }
8415
- trackedWorker = newWorker;
8416
- stateChangeHandler = () => {
8417
- if (disposed) return;
8418
- if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
8419
- setIsUpdateAvailable(true);
8420
- }
8421
- };
8422
- newWorker.addEventListener("statechange", stateChangeHandler);
9528
+ function attachListeners(reg) {
9529
+ trackedReg = reg;
9530
+ updateFoundHandler = () => {
9531
+ if (unregistered) return;
9532
+ const newWorker = reg.installing;
9533
+ if (newWorker) {
9534
+ if (trackedWorker && stateChangeHandler) {
9535
+ trackedWorker.removeEventListener("statechange", stateChangeHandler);
8423
9536
  }
8424
- };
8425
- reg.addEventListener("updatefound", updateFoundHandler);
8426
- }).catch((err) => {
8427
- if (disposed) return;
8428
- setError(err instanceof Error ? err : new Error(String(err)));
9537
+ trackedWorker = newWorker;
9538
+ stateChangeHandler = () => {
9539
+ if (unregistered) return;
9540
+ if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
9541
+ setIsUpdateAvailable(true);
9542
+ }
9543
+ };
9544
+ newWorker.addEventListener("statechange", stateChangeHandler);
9545
+ }
9546
+ };
9547
+ reg.addEventListener("updatefound", updateFoundHandler);
9548
+ }
9549
+ function activate(reg) {
9550
+ setRegistration(reg);
9551
+ setIsReady(true);
9552
+ attachListeners(reg);
9553
+ }
9554
+ function recoverFailedUnregister() {
9555
+ unregisterRequested = false;
9556
+ if (!registration() && arrivedRegistration) {
9557
+ activate(arrivedRegistration);
9558
+ arrivedRegistration = null;
9559
+ }
9560
+ }
9561
+ function adopt(reg) {
9562
+ if (unregisterRequested) {
9563
+ arrivedRegistration = reg;
9564
+ return null;
9565
+ }
9566
+ activate(reg);
9567
+ return reg;
9568
+ }
9569
+ if (supported) {
9570
+ registrationPromise = navigator.serviceWorker.register(scriptUrl, options).then(adopt).catch((err) => {
9571
+ if (!unregistered) {
9572
+ setError(err instanceof Error ? err : new Error(String(err)));
9573
+ }
9574
+ return null;
8429
9575
  });
8430
9576
  }
8431
9577
  async function update() {
@@ -8434,19 +9580,43 @@ function serviceWorker(scriptUrl, options) {
8434
9580
  await reg.update();
8435
9581
  }
8436
9582
  }
8437
- async function unregister() {
8438
- disposed = true;
8439
- detachListeners();
8440
- const reg = registration();
8441
- if (reg) {
8442
- const result = await reg.unregister();
8443
- if (result) {
8444
- setRegistration(null);
8445
- setIsReady(false);
8446
- }
8447
- return result;
9583
+ async function performUnregister() {
9584
+ unregisterRequested = true;
9585
+ if (registrationPromise) await registrationPromise.catch(() => null);
9586
+ const reg = registration() ?? arrivedRegistration;
9587
+ if (!reg) {
9588
+ unregisterRequested = false;
9589
+ return false;
8448
9590
  }
8449
- return false;
9591
+ let result;
9592
+ try {
9593
+ result = await reg.unregister();
9594
+ } catch (err) {
9595
+ recoverFailedUnregister();
9596
+ throw err;
9597
+ }
9598
+ if (result) {
9599
+ detachListeners();
9600
+ setRegistration(null);
9601
+ setIsReady(false);
9602
+ setIsUpdateAvailable(false);
9603
+ arrivedRegistration = null;
9604
+ unregistered = true;
9605
+ } else {
9606
+ recoverFailedUnregister();
9607
+ }
9608
+ return result;
9609
+ }
9610
+ async function unregister() {
9611
+ if (!supported || unregistered) return false;
9612
+ if (pendingUnregister) return pendingUnregister;
9613
+ const request = performUnregister();
9614
+ pendingUnregister = request;
9615
+ void request.catch(() => {
9616
+ }).then(() => {
9617
+ if (pendingUnregister === request) pendingUnregister = null;
9618
+ });
9619
+ return request;
8450
9620
  }
8451
9621
  return { registration, isReady, isUpdateAvailable, error, update, unregister };
8452
9622
  }
@@ -8551,17 +9721,39 @@ function createAction(actionFn) {
8551
9721
  }
8552
9722
 
8553
9723
  // src/platform/scrollRestoration.ts
9724
+ var DEFAULT_STATE_KEY = "__sibuScrollKey";
8554
9725
  function scrollRestoration(options) {
8555
9726
  const mode = options?.mode ?? "auto";
9727
+ const stateKey = options?.stateKey ?? DEFAULT_STATE_KEY;
8556
9728
  const positions = /* @__PURE__ */ new Map();
8557
9729
  let popstateHandler = null;
8558
9730
  let currentKey = null;
8559
- const save = (key) => {
9731
+ if (options?.getKey) {
9732
+ try {
9733
+ currentKey = options.getKey();
9734
+ } catch {
9735
+ currentKey = null;
9736
+ }
9737
+ }
9738
+ const record = (key) => {
8560
9739
  if (typeof window !== "undefined") {
8561
9740
  positions.set(key, { x: window.scrollX, y: window.scrollY });
8562
9741
  }
8563
9742
  currentKey = key;
8564
9743
  };
9744
+ const tagCurrentEntry = (key) => {
9745
+ if (typeof window === "undefined" || typeof history === "undefined") return;
9746
+ try {
9747
+ const existing = history.state ?? {};
9748
+ if (existing[stateKey] === key) return;
9749
+ history.replaceState({ ...existing, [stateKey]: key }, "");
9750
+ } catch {
9751
+ }
9752
+ };
9753
+ const save = (key) => {
9754
+ record(key);
9755
+ if (mode === "auto") tagCurrentEntry(key);
9756
+ };
8565
9757
  const restore = (key) => {
8566
9758
  const pos = positions.get(key);
8567
9759
  if (pos && typeof window !== "undefined") {
@@ -8572,11 +9764,32 @@ function scrollRestoration(options) {
8572
9764
  const getPosition = (key) => {
8573
9765
  return positions.get(key);
8574
9766
  };
9767
+ const onNavigation = (key) => {
9768
+ if (mode !== "auto") return;
9769
+ if (currentKey !== null && currentKey !== key) record(currentKey);
9770
+ currentKey = key;
9771
+ tagCurrentEntry(key);
9772
+ };
9773
+ let nativeModeLease = null;
8575
9774
  if (mode === "auto" && typeof window !== "undefined") {
8576
- popstateHandler = () => {
8577
- if (currentKey) {
8578
- save(currentKey);
9775
+ nativeModeLease = acquireScrollRestorationMode("manual");
9776
+ if (currentKey !== null) tagCurrentEntry(currentKey);
9777
+ popstateHandler = (event) => {
9778
+ const state2 = event.state ?? null;
9779
+ const rawDestination = state2 ? state2[stateKey] : void 0;
9780
+ const destination = typeof rawDestination === "string" ? rawDestination : null;
9781
+ let outgoing = currentKey;
9782
+ if (outgoing === null && options?.getKey) {
9783
+ try {
9784
+ outgoing = options.getKey();
9785
+ } catch {
9786
+ outgoing = null;
9787
+ }
9788
+ }
9789
+ if (outgoing !== null && outgoing !== destination) {
9790
+ record(outgoing);
8579
9791
  }
9792
+ if (destination !== null) restore(destination);
8580
9793
  };
8581
9794
  window.addEventListener("popstate", popstateHandler);
8582
9795
  }
@@ -8585,9 +9798,11 @@ function scrollRestoration(options) {
8585
9798
  window.removeEventListener("popstate", popstateHandler);
8586
9799
  popstateHandler = null;
8587
9800
  }
9801
+ nativeModeLease?.release();
9802
+ nativeModeLease = null;
8588
9803
  positions.clear();
8589
9804
  };
8590
- return { save, restore, getPosition, dispose: dispose2 };
9805
+ return { save, restore, onNavigation, getPosition, dispose: dispose2 };
8591
9806
  }
8592
9807
 
8593
9808
  // src/platform/routeMiddleware.ts
@@ -9762,8 +10977,8 @@ function initDevTools(config) {
9762
10977
  function clearEvents() {
9763
10978
  eventLog.length = 0;
9764
10979
  }
9765
- function registerComponent(name, element, state) {
9766
- hook.components.set(name, { element, state });
10980
+ function registerComponent(name, element, state2) {
10981
+ hook.components.set(name, { element, state: state2 });
9767
10982
  emit();
9768
10983
  }
9769
10984
  function unregisterComponent(name) {
@@ -12114,9 +13329,9 @@ function zustandAdapter(options) {
12114
13329
  return createPlugin("sibu-zustand", (ctx) => {
12115
13330
  const { store } = options;
12116
13331
  const [getState, setSibuState] = signal(store.getState());
12117
- const unsubscribe = store.subscribe((state) => {
13332
+ const unsubscribe = store.subscribe((state2) => {
12118
13333
  batch(() => {
12119
- setSibuState(state);
13334
+ setSibuState(state2);
12120
13335
  });
12121
13336
  });
12122
13337
  function select3(selector) {
@@ -12548,6 +13763,7 @@ var materialAdapter = componentAdapter(materialConfig);
12548
13763
  lazyModule,
12549
13764
  loadRemoteModule,
12550
13765
  loadWasmModule,
13766
+ loadWasmModuleWithOptions,
12551
13767
  loaderData,
12552
13768
  machine,
12553
13769
  matchesPattern,