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/ssr.cjs CHANGED
@@ -43,6 +43,7 @@ __export(ssr_exports, {
43
43
  island: () => island,
44
44
  loadRemoteModule: () => loadRemoteModule,
45
45
  loadWasmModule: () => loadWasmModule,
46
+ loadWasmModuleWithOptions: () => loadWasmModuleWithOptions,
46
47
  preloadWasm: () => preloadWasm,
47
48
  renderToDocument: () => renderToDocument,
48
49
  renderToReadableStream: () => renderToReadableStream,
@@ -86,6 +87,170 @@ function devWarn(message) {
86
87
  }
87
88
  }
88
89
 
90
+ // src/core/errors.ts
91
+ var ERROR_STATE_KEY = /* @__PURE__ */ Symbol.for("sibujs.runtime-errors.v1");
92
+ function state() {
93
+ const g2 = globalThis;
94
+ const existing = g2[ERROR_STATE_KEY];
95
+ if (existing) return existing;
96
+ const created = { handler: null, handlerDepth: 0 };
97
+ g2[ERROR_STATE_KEY] = created;
98
+ return created;
99
+ }
100
+ function normalizeError(value) {
101
+ if (value instanceof Error) return value;
102
+ const message = typeof value === "string" ? value : `Non-Error value thrown: ${safeStringify(value)}`;
103
+ return new Error(message, { cause: value });
104
+ }
105
+ function safeStringify(value) {
106
+ if (typeof value === "symbol") return value.toString();
107
+ try {
108
+ return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
109
+ } catch {
110
+ return Object.prototype.toString.call(value);
111
+ }
112
+ }
113
+ function describe(context) {
114
+ return context.name ? `${context.phase} "${context.name}"` : context.phase;
115
+ }
116
+ var REACTIVE_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
117
+ function untrackedRun(fn) {
118
+ const api = globalThis[REACTIVE_REGISTRY_KEY];
119
+ return api?.untracked ? api.untracked(fn) : fn();
120
+ }
121
+ var PROPAGATE_EVENT = "sibu:error-propagate";
122
+ function claimedByBoundary(error, context) {
123
+ const raw = context.node;
124
+ if (!raw || typeof CustomEvent === "undefined") return false;
125
+ const target = resolveEventTarget(raw);
126
+ if (!target) return false;
127
+ try {
128
+ const event = new CustomEvent(PROPAGATE_EVENT, {
129
+ bubbles: true,
130
+ cancelable: true,
131
+ detail: { error, context }
132
+ });
133
+ return target.dispatchEvent(event) === false;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+ function resolveEventTarget(node) {
139
+ const ELEMENT_NODE = 1;
140
+ let current = node;
141
+ while (current) {
142
+ if (current.nodeType === ELEMENT_NODE && typeof current.dispatchEvent === "function") {
143
+ return current;
144
+ }
145
+ current = current.parentNode;
146
+ }
147
+ const self = node;
148
+ return typeof self.dispatchEvent === "function" ? self : null;
149
+ }
150
+ function defaultReport(error, context) {
151
+ if (typeof console === "undefined") return;
152
+ console.error(`[SibuJS] Uncaught error during ${describe(context)}: ${error.message}`, error);
153
+ }
154
+ function reportError(error, context) {
155
+ const err = normalizeError(error);
156
+ untrackedRun(() => {
157
+ if (claimedByBoundary(err, context)) return;
158
+ const s2 = state();
159
+ const handler = s2.handler;
160
+ if (handler && s2.handlerDepth === 0) {
161
+ s2.handlerDepth++;
162
+ try {
163
+ handler(error instanceof Error ? error : err, context);
164
+ return;
165
+ } catch (handlerError) {
166
+ if (typeof console !== "undefined") {
167
+ const detail = handlerError instanceof Error ? handlerError.message : String(handlerError);
168
+ console.error(`[SibuJS] runtime error handler itself threw: ${detail}`, handlerError);
169
+ }
170
+ } finally {
171
+ s2.handlerDepth--;
172
+ }
173
+ }
174
+ defaultReport(err, context);
175
+ });
176
+ }
177
+
178
+ // src/core/rendering/dispose.ts
179
+ var elementDisposers = /* @__PURE__ */ new WeakMap();
180
+ var MAX_DRAIN_TEARDOWNS = 1e4;
181
+ function reportDrainRunaway(label2, executed, remaining) {
182
+ if (typeof console === "undefined") return;
183
+ console.error(
184
+ `[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.`,
185
+ { executed, remaining }
186
+ );
187
+ }
188
+ var _isDev2 = isDev();
189
+ var activeBindingCount = 0;
190
+ function registerDisposer(node, teardown) {
191
+ let disposers = elementDisposers.get(node);
192
+ if (!disposers) {
193
+ disposers = [];
194
+ elementDisposers.set(node, disposers);
195
+ }
196
+ disposers.push(teardown);
197
+ if (_isDev2) activeBindingCount++;
198
+ }
199
+ function dispose(node) {
200
+ const stack = [node];
201
+ const order = [];
202
+ while (stack.length > 0) {
203
+ const current = stack.pop();
204
+ order.push(current);
205
+ const children = Array.from(current.childNodes);
206
+ for (let i2 = 0; i2 < children.length; i2++) {
207
+ stack.push(children[i2]);
208
+ }
209
+ }
210
+ for (let i2 = order.length - 1; i2 >= 0; i2--) {
211
+ const current = order[i2];
212
+ if (elementDisposers.has(current)) {
213
+ let executed = 0;
214
+ let runaway = false;
215
+ while (!runaway) {
216
+ const pending = elementDisposers.get(current);
217
+ if (!pending || pending.length === 0) break;
218
+ const snapshot = pending.slice();
219
+ elementDisposers.delete(current);
220
+ if (_isDev2) activeBindingCount -= snapshot.length;
221
+ for (let i3 = 0; i3 < snapshot.length; i3++) {
222
+ if (executed >= MAX_DRAIN_TEARDOWNS) {
223
+ const rest = snapshot.slice(i3);
224
+ const added = elementDisposers.get(current);
225
+ elementDisposers.set(current, added ? rest.concat(added) : rest);
226
+ if (_isDev2) activeBindingCount += rest.length;
227
+ reportDrainRunaway("dispose", executed, rest.length + (added?.length ?? 0));
228
+ runaway = true;
229
+ break;
230
+ }
231
+ executed++;
232
+ try {
233
+ snapshot[i3]();
234
+ } catch (err) {
235
+ reportError(err, { phase: "cleanup", name: "disposer" });
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ function replaceChildrenSafely(parent, ...next) {
243
+ for (let i2 = 0; i2 < next.length; i2++) {
244
+ const node = next[i2];
245
+ node.parentNode?.removeChild(node);
246
+ }
247
+ const current = Array.from(parent.childNodes);
248
+ for (let i2 = 0; i2 < current.length; i2++) {
249
+ dispose(current[i2]);
250
+ }
251
+ parent.replaceChildren(...next);
252
+ }
253
+
89
254
  // src/core/ssr-context.ts
90
255
  var SSR_KEY = /* @__PURE__ */ Symbol.for("sibujs.ssr.v1");
91
256
  function detectSSRShared() {
@@ -96,20 +261,24 @@ function detectSSRShared() {
96
261
  const getBuiltin = process.getBuiltinModule;
97
262
  if (typeof getBuiltin === "function") {
98
263
  mod = getBuiltin("node:async_hooks");
99
- } else {
100
- const req = Function("return typeof require==='function'?require:null")();
101
- if (req) mod = req("node:async_hooks");
264
+ } else if (typeof require === "function") {
265
+ mod = require("async_hooks");
102
266
  }
103
267
  if (mod) detected = new mod.AsyncLocalStorage();
104
268
  }
105
269
  } catch {
106
270
  detected = null;
107
271
  }
108
- return { als: detected, fallbackStore: { ssr: false, suspenseIdCounter: 0 } };
272
+ return {
273
+ als: detected,
274
+ fallbackStore: { ssr: false, suspenseIdCounter: 0, locale: void 0 },
275
+ fallbackDepth: 0
276
+ };
109
277
  }
110
278
  var _shared = globalThis[SSR_KEY] ?? (globalThis[SSR_KEY] = detectSSRShared());
111
279
  var als = _shared.als;
112
280
  var fallbackStore = _shared.fallbackStore;
281
+ _shared.fallbackDepth ?? (_shared.fallbackDepth = 0);
113
282
  function getSSRStore() {
114
283
  if (als) {
115
284
  const s2 = als.getStore();
@@ -125,16 +294,21 @@ function isSSR() {
125
294
  function stripControlChars(value) {
126
295
  return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
127
296
  }
297
+ function canonicalAttrName(name) {
298
+ return name.replace(/[A-Z]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 32));
299
+ }
128
300
  function isEventHandlerAttr(name) {
129
301
  if (name.length < 3) return false;
130
302
  const lower = name.toLowerCase();
131
303
  return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
132
304
  }
133
305
  var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
306
+ var OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
134
307
  function sanitizeUrl(url) {
135
- const trimmed = stripControlChars(url).trim();
136
- if (!trimmed) return "";
137
- const lower = trimmed.toLowerCase();
308
+ const probe = stripControlChars(url).trim();
309
+ if (!probe) return "";
310
+ const output2 = url.replace(OUTER_TRIM, "");
311
+ const lower = probe.toLowerCase();
138
312
  let schemeEnd = -1;
139
313
  for (let i2 = 0; i2 < lower.length; i2++) {
140
314
  const ch = lower.charCodeAt(i2);
@@ -144,12 +318,13 @@ function sanitizeUrl(url) {
144
318
  }
145
319
  if (ch === 47 || ch === 63 || ch === 35) break;
146
320
  }
147
- if (schemeEnd === -1) return trimmed;
321
+ if (schemeEnd === -1) return output2;
148
322
  const scheme = lower.slice(0, schemeEnd + 1);
149
- if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return trimmed;
323
+ if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output2;
150
324
  if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
151
- return trimmed;
325
+ return output2;
152
326
  }
327
+ var SRCSET_DESCRIPTOR = /^\s+\d+(\.\d+)?[wx]$/;
153
328
  function sanitizeSrcset(value) {
154
329
  const parts = value.split(",");
155
330
  const out = [];
@@ -158,6 +333,7 @@ function sanitizeSrcset(value) {
158
333
  if (!part) continue;
159
334
  const m = part.match(/^(\S+)(\s+.+)?$/);
160
335
  if (!m) continue;
336
+ if (m[2] !== void 0 && !SRCSET_DESCRIPTOR.test(m[2])) continue;
161
337
  const safe = sanitizeUrl(m[1]);
162
338
  if (!safe) continue;
163
339
  out.push(m[2] ? `${safe}${m[2]}` : safe);
@@ -165,23 +341,83 @@ function sanitizeSrcset(value) {
165
341
  return out.join(", ");
166
342
  }
167
343
  var CSS_DANGER_GATE = /[(:@\\]/;
344
+ function isCssWhitespace(ch) {
345
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
346
+ }
347
+ function isHexDigit(ch) {
348
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
349
+ }
350
+ function decodeCssEscapes(value) {
351
+ let out = "";
352
+ for (let i2 = 0; i2 < value.length; i2++) {
353
+ const ch = value[i2];
354
+ if (ch !== "\\") {
355
+ out += ch;
356
+ continue;
357
+ }
358
+ if (i2 + 1 >= value.length) break;
359
+ const next = value[i2 + 1];
360
+ if (next === "\n" || next === "\f") {
361
+ i2 += 1;
362
+ continue;
363
+ }
364
+ if (next === "\r") {
365
+ i2 += value[i2 + 2] === "\n" ? 2 : 1;
366
+ continue;
367
+ }
368
+ if (isHexDigit(next)) {
369
+ let hex = "";
370
+ let j = i2 + 1;
371
+ while (j < value.length && hex.length < 6 && isHexDigit(value[j])) {
372
+ hex += value[j];
373
+ j++;
374
+ }
375
+ if (j < value.length && isCssWhitespace(value[j])) {
376
+ j += value[j] === "\r" && value[j + 1] === "\n" ? 2 : 1;
377
+ }
378
+ const code2 = Number.parseInt(hex, 16);
379
+ const invalid = code2 === 0 || code2 > 1114111 || code2 >= 55296 && code2 <= 57343;
380
+ out += invalid ? "\uFFFD" : String.fromCodePoint(code2);
381
+ i2 = j - 1;
382
+ continue;
383
+ }
384
+ out += next;
385
+ i2 += 1;
386
+ }
387
+ return out;
388
+ }
168
389
  function sanitizeCSSValue(value) {
169
390
  if (!CSS_DANGER_GATE.test(value)) return value;
170
- const decoded = value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_m, hex) => {
171
- const code2 = Number.parseInt(hex, 16);
172
- if (!Number.isFinite(code2) || code2 < 0 || code2 > 1114111) return "";
173
- try {
174
- return String.fromCodePoint(code2);
175
- } catch {
176
- return "";
177
- }
178
- });
179
- const lower = decoded.toLowerCase().replace(/\s+/g, "");
391
+ const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
392
+ const lower = normalized.toLowerCase().replace(/\s+/g, "");
180
393
  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")) {
181
394
  return "";
182
395
  }
183
396
  return value;
184
397
  }
398
+ function sanitizeStyleAttribute(cssText) {
399
+ const input2 = String(cssText);
400
+ if (input2.trim() === "") return "";
401
+ if (typeof document === "undefined") {
402
+ return sanitizeCSSValue(input2) === "" ? "" : input2;
403
+ }
404
+ const probe = document.createElement("div");
405
+ try {
406
+ probe.style.cssText = input2;
407
+ } catch {
408
+ return "";
409
+ }
410
+ const declarations = [];
411
+ for (let i2 = 0; i2 < probe.style.length; i2++) {
412
+ const property = probe.style[i2];
413
+ const value = probe.style.getPropertyValue(property);
414
+ if (sanitizeCSSValue(value) === "") continue;
415
+ if (sanitizeCSSValue(`${property}:${value}`) === "") continue;
416
+ const priority = probe.style.getPropertyPriority(property);
417
+ declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
418
+ }
419
+ return declarations.join("; ");
420
+ }
185
421
  var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
186
422
  "href",
187
423
  "xlink:href",
@@ -194,40 +430,202 @@ var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
194
430
  "background",
195
431
  "srcset",
196
432
  "ping",
197
- "data"
433
+ "data",
434
+ // `<html manifest>` is a URL sink too. It was carried only by the SSR
435
+ // serializers' private copy of this set; folding that copy into this one would
436
+ // otherwise have silently dropped its coverage.
437
+ "manifest"
198
438
  ]);
439
+ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
440
+ function isHtmlContentAttribute(name) {
441
+ return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
442
+ }
443
+ function isPolicyAttribute(name) {
444
+ const lower = name.toLowerCase();
445
+ return lower === "style" || URL_ATTRIBUTES.has(lower);
446
+ }
199
447
  function sanitizeAttributeString(attr, value) {
200
448
  const lower = attr.toLowerCase();
201
449
  if (lower === "srcset") return sanitizeSrcset(value);
202
450
  if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
451
+ if (lower === "style") return sanitizeStyleAttribute(value);
203
452
  return value;
204
453
  }
205
454
 
455
+ // src/utils/metaRefresh.ts
456
+ function isAsciiWhitespace(ch) {
457
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
458
+ }
459
+ function trimAsciiWhitespace(value) {
460
+ let start = 0;
461
+ let end = value.length;
462
+ while (start < end && isAsciiWhitespace(value[start])) start++;
463
+ while (end > start && isAsciiWhitespace(value[end - 1])) end--;
464
+ return value.slice(start, end);
465
+ }
466
+ var forbidden = (reason) => ({ kind: "forbidden", reason });
467
+ function parseMetaRefreshContent(content) {
468
+ const raw = trimAsciiWhitespace(stripControlChars(content));
469
+ if (raw === "") return forbidden("empty content");
470
+ let i2 = 0;
471
+ while (i2 < raw.length && raw[i2] >= "0" && raw[i2] <= "9") i2++;
472
+ if (i2 === 0) return forbidden("missing or non-numeric delay");
473
+ const delay = Number.parseInt(raw.slice(0, i2), 10);
474
+ if (!Number.isFinite(delay)) return forbidden("unparseable delay");
475
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
476
+ if (i2 === raw.length) return { kind: "delay-only", delay };
477
+ if (raw[i2] !== ";" && raw[i2] !== ",") return forbidden("unexpected text after delay");
478
+ i2++;
479
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
480
+ const keyStart = i2;
481
+ while (i2 < raw.length && !isAsciiWhitespace(raw[i2]) && raw[i2] !== "=") i2++;
482
+ const key = raw.slice(keyStart, i2).toLowerCase();
483
+ if (key !== "url") return forbidden(`unsupported refresh key ${JSON.stringify(key)}`);
484
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
485
+ if (raw[i2] !== "=") return forbidden("missing '=' after url");
486
+ i2++;
487
+ while (i2 < raw.length && isAsciiWhitespace(raw[i2])) i2++;
488
+ let destination;
489
+ const quote = raw[i2];
490
+ if (quote === '"' || quote === "'") {
491
+ const close = raw.indexOf(quote, i2 + 1);
492
+ if (close === -1) return forbidden("unterminated quoted destination");
493
+ destination = raw.slice(i2 + 1, close);
494
+ const rest = trimAsciiWhitespace(raw.slice(close + 1));
495
+ if (rest !== "") return forbidden("trailing text after quoted destination");
496
+ } else {
497
+ destination = trimAsciiWhitespace(raw.slice(i2));
498
+ if (/[;,]/.test(destination)) return forbidden("multiple refresh assignments");
499
+ }
500
+ if (destination === "") return forbidden("empty destination");
501
+ const sanitizedDestination = sanitizeUrl(destination);
502
+ if (sanitizedDestination === "") return forbidden("destination uses a disallowed protocol");
503
+ return { kind: "allowed", delay, destination, sanitizedDestination };
504
+ }
505
+ function resolveMetaRefreshPolicy(attributes) {
506
+ let httpEquiv;
507
+ let content;
508
+ let duplicateName;
509
+ const seen = /* @__PURE__ */ new Set();
510
+ for (const [key, value] of attributes) {
511
+ const canonical = canonicalAttrName(key);
512
+ if (seen.has(canonical)) duplicateName = canonical;
513
+ seen.add(canonical);
514
+ if (canonical === "http-equiv") httpEquiv = value;
515
+ else if (canonical === "content") content = value;
516
+ }
517
+ if (duplicateName !== void 0) {
518
+ return forbidden(`duplicate case-insensitive attribute ${JSON.stringify(duplicateName)}`);
519
+ }
520
+ if (typeof httpEquiv !== "string") return { kind: "not-refresh" };
521
+ if (trimAsciiWhitespace(stripControlChars(httpEquiv)).toLowerCase() !== "refresh") {
522
+ return { kind: "not-refresh" };
523
+ }
524
+ if (typeof content !== "string") return { kind: "not-refresh" };
525
+ return parseMetaRefreshContent(content);
526
+ }
527
+ function normalizeMetaAttributes(props) {
528
+ const out = /* @__PURE__ */ new Map();
529
+ const seen = /* @__PURE__ */ new Set();
530
+ for (const key of Object.keys(props)) {
531
+ const canonical = canonicalAttrName(key);
532
+ if (seen.has(canonical)) return null;
533
+ seen.add(canonical);
534
+ out.set(key, props[key]);
535
+ }
536
+ return out;
537
+ }
538
+ function isForbiddenMetaEntry(props) {
539
+ const normalized = normalizeMetaAttributes(props);
540
+ if (normalized === null) return true;
541
+ return resolveMetaRefreshPolicy(normalized).kind === "forbidden";
542
+ }
543
+
544
+ // src/utils/headEntry.ts
545
+ var SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
546
+ function isEmittableHeadAttrName(name) {
547
+ if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
548
+ const canonical = canonicalAttrName(name);
549
+ if (isEventHandlerAttr(canonical)) return false;
550
+ if (isHtmlContentAttribute(canonical)) return false;
551
+ return true;
552
+ }
553
+ function sanitizeHeadAttrValue(canonical, value) {
554
+ if (!isPolicyAttribute(canonical)) return value;
555
+ const safe = sanitizeAttributeString(canonical, value);
556
+ return safe === "" ? null : safe;
557
+ }
558
+ function findDuplicateAttributeName(names) {
559
+ const seen = /* @__PURE__ */ new Set();
560
+ for (const name of names) {
561
+ const canonical = canonicalAttrName(name);
562
+ if (seen.has(canonical)) return canonical;
563
+ seen.add(canonical);
564
+ }
565
+ return null;
566
+ }
567
+ function planEntry(props, resolver, applyRefreshPolicy) {
568
+ const rawNames = Object.keys(props);
569
+ const duplicate = findDuplicateAttributeName(rawNames);
570
+ if (duplicate !== null) {
571
+ return { kind: "drop", reason: `duplicate case-insensitive attribute ${JSON.stringify(duplicate)}` };
572
+ }
573
+ const attributes = /* @__PURE__ */ new Map();
574
+ let reactive = false;
575
+ for (const name of rawNames) {
576
+ if (!Object.hasOwn(props, name)) continue;
577
+ if (!isEmittableHeadAttrName(name)) continue;
578
+ const authored = props[name];
579
+ if (resolver.isReactiveValue?.(authored)) reactive = true;
580
+ const canonical = canonicalAttrName(name);
581
+ const sanitized = sanitizeHeadAttrValue(canonical, resolver.resolveValue(name, authored));
582
+ if (sanitized === null) continue;
583
+ attributes.set(canonical, sanitized);
584
+ }
585
+ if (attributes.size === 0) return { kind: "drop", reason: "no effective attributes remain" };
586
+ const refresh = applyRefreshPolicy ? resolveMetaRefreshPolicy(attributes) : { kind: "not-refresh" };
587
+ if (refresh.kind === "forbidden") return { kind: "drop", reason: refresh.reason };
588
+ if (reactive && (refresh.kind === "allowed" || refresh.kind === "delay-only")) {
589
+ return { kind: "drop", reason: "a reactive meta entry may not publish a native refresh directive" };
590
+ }
591
+ return { kind: "publish", attributes, refresh };
592
+ }
593
+ function planMetaEntry(props, resolver) {
594
+ return planEntry(props, resolver, true);
595
+ }
596
+ function planHeadElementEntry(props, resolver) {
597
+ return planEntry(props, resolver, false);
598
+ }
599
+ var STATIC_VALUE_RESOLVER = {
600
+ resolveValue: (_name, value) => String(value)
601
+ };
602
+ function escapeHeadAttrValue(value) {
603
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
604
+ }
605
+ function serializeHeadEntry(tag, attrs) {
606
+ const plan = tag === "meta" ? planMetaEntry(attrs, STATIC_VALUE_RESOLVER) : planHeadElementEntry(attrs, STATIC_VALUE_RESOLVER);
607
+ if (plan.kind === "drop") return "";
608
+ const pairs = Array.from(plan.attributes, ([name, value]) => `${name}="${escapeHeadAttrValue(value)}"`).join(" ");
609
+ return `<${tag} ${pairs} />`;
610
+ }
611
+
206
612
  // src/platform/ssr.ts
207
- function sanitizeUrlAttr(name, value) {
208
- return name === "srcset" ? sanitizeSrcset(value) : sanitizeUrl(value);
613
+ function isPolicyAttr(lowerName) {
614
+ return isPolicyAttribute(lowerName);
209
615
  }
210
- var _isDev2 = isDev();
616
+ function isForbiddenSsrAttr(lowerName) {
617
+ return isHtmlContentAttribute(lowerName);
618
+ }
619
+ function sanitizeSsrAttributeValue(lowerName, value) {
620
+ return sanitizeAttributeString(lowerName, value);
621
+ }
622
+ var _isDev3 = isDev();
211
623
  var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
212
624
  function isSafeAttrName(name) {
213
625
  return SAFE_ATTR_NAME.test(name);
214
626
  }
215
- var URL_ATTRS = /* @__PURE__ */ new Set([
216
- "href",
217
- "src",
218
- "action",
219
- "formaction",
220
- "cite",
221
- "poster",
222
- "background",
223
- "srcset",
224
- "ping",
225
- "manifest",
226
- "data",
227
- "xlink:href"
228
- ]);
229
627
  function ssrErrorComment(err) {
230
- if (_isDev2) {
628
+ if (_isDev3) {
231
629
  const msg = escapeHtml(err instanceof Error ? err.message : String(err));
232
630
  return `<!--SSR error: ${safeCommentText(msg)}-->`;
233
631
  }
@@ -273,20 +671,21 @@ function renderToString(element) {
273
671
  }
274
672
  const tag = element.tagName.toLowerCase();
275
673
  if (tag === "script" || tag === "style") {
276
- return _isDev2 ? `<!--ssr:${tag}-stripped-->` : "";
674
+ return _isDev3 ? `<!--ssr:${tag}-stripped-->` : "";
277
675
  }
278
676
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
279
- return _isDev2 ? "<!--ssr:invalid-tag-->" : "";
677
+ return _isDev3 ? "<!--ssr:invalid-tag-->" : "";
280
678
  }
281
679
  let html2 = `<${tag}`;
282
680
  for (const attr of Array.from(element.attributes)) {
283
681
  const rawName = attr.name;
284
682
  if (!isSafeAttrName(rawName)) continue;
285
683
  if (isEventHandlerAttr(rawName)) continue;
684
+ if (isForbiddenSsrAttr(rawName)) continue;
286
685
  const lowerName = rawName.toLowerCase();
287
686
  let value = attr.value;
288
- if (URL_ATTRS.has(lowerName)) {
289
- value = sanitizeUrlAttr(lowerName, value);
687
+ if (isPolicyAttr(lowerName)) {
688
+ value = sanitizeSsrAttributeValue(lowerName, value);
290
689
  if (!value) continue;
291
690
  }
292
691
  html2 += ` ${rawName}="${escapeAttr(value)}"`;
@@ -317,7 +716,7 @@ function hydrate(component, container, options = {}) {
317
716
  const first = mismatches[0];
318
717
  if (options.onMismatch) {
319
718
  options.onMismatch(first);
320
- } else if (_isDev2) {
719
+ } else if (_isDev3) {
321
720
  console.warn(
322
721
  `[SibuJS hydration] ${first.message}
323
722
  at ${first.path}
@@ -327,9 +726,20 @@ function hydrate(component, container, options = {}) {
327
726
  }
328
727
  }
329
728
  }
330
- container.replaceChildren(clientTree);
729
+ replaceChildrenSafely(container, clientTree);
331
730
  container.setAttribute("data-sibu-hydrated", "true");
332
731
  }
732
+ function directTextOf(node) {
733
+ let out = "";
734
+ for (const child of Array.from(node.childNodes)) {
735
+ if (child.nodeType === 3) out += child.nodeValue ?? "";
736
+ }
737
+ return out.replace(/\s+/g, " ").trim();
738
+ }
739
+ function disposeAndReplace(outgoing, next) {
740
+ dispose(outgoing);
741
+ outgoing.replaceWith(next);
742
+ }
333
743
  function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
334
744
  if (out.length >= max) return;
335
745
  const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
@@ -405,6 +815,18 @@ function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
405
815
  });
406
816
  }
407
817
  }
818
+ const serverText = directTextOf(serverNode);
819
+ const clientText = directTextOf(clientNode);
820
+ if (serverText !== clientText) {
821
+ out.push({
822
+ kind: "text",
823
+ path: nodePath,
824
+ serverValue: serverText,
825
+ clientValue: clientText,
826
+ message: "Text content differs between server and client."
827
+ });
828
+ if (out.length >= max) return;
829
+ }
408
830
  const serverChildren = Array.from(serverNode.children);
409
831
  const clientChildren = Array.from(clientNode.children);
410
832
  const max2 = Math.max(serverChildren.length, clientChildren.length);
@@ -424,10 +846,11 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
424
846
  if (!Object.hasOwn(attrs, rawKey)) continue;
425
847
  if (!isSafeAttrName(rawKey)) continue;
426
848
  if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
849
+ if (isForbiddenSsrAttr(rawKey)) continue;
427
850
  const lowerKey = rawKey.toLowerCase();
428
851
  let value = String(attrs[rawKey]);
429
- if (URL_ATTRS.has(lowerKey)) {
430
- value = sanitizeUrlAttr(lowerKey, value);
852
+ if (isPolicyAttr(lowerKey)) {
853
+ value = sanitizeSsrAttributeValue(lowerKey, value);
431
854
  if (!value) continue;
432
855
  }
433
856
  out.push(`${rawKey}="${escapeAttr(value)}"`);
@@ -435,18 +858,7 @@ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
435
858
  return out.join(" ");
436
859
  }
437
860
  function isDangerousMetaRefresh(metaProps) {
438
- let httpEquiv;
439
- let content;
440
- for (const k in metaProps) {
441
- const lk = k.toLowerCase();
442
- if (lk === "http-equiv") httpEquiv = metaProps[k];
443
- else if (lk === "content") content = metaProps[k];
444
- }
445
- if (typeof httpEquiv !== "string") return false;
446
- if (httpEquiv.toLowerCase() !== "refresh") return false;
447
- if (typeof content !== "string") return false;
448
- const normalized = stripControlChars(content).toLowerCase();
449
- return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
861
+ return isForbiddenMetaEntry(metaProps);
450
862
  }
451
863
  function renderToDocument(component, options = {}) {
452
864
  let content;
@@ -455,15 +867,8 @@ function renderToDocument(component, options = {}) {
455
867
  } catch (err) {
456
868
  content = ssrErrorComment(err);
457
869
  }
458
- const metaTags = (options.meta || []).map((attrs) => {
459
- if (isDangerousMetaRefresh(attrs)) return "";
460
- const pairs = buildAttrString(attrs);
461
- return pairs ? `<meta ${pairs} />` : "";
462
- }).filter(Boolean).join("\n ");
463
- const linkTags = (options.links || []).map((attrs) => {
464
- const pairs = buildAttrString(attrs);
465
- return pairs ? `<link ${pairs} />` : "";
466
- }).filter(Boolean).join("\n ");
870
+ const metaTags = (options.meta || []).map((attrs) => serializeHeadEntry("meta", attrs)).filter(Boolean).join("\n ");
871
+ const linkTags = (options.links || []).map((attrs) => serializeHeadEntry("link", attrs)).filter(Boolean).join("\n ");
467
872
  const scriptTags = (options.scripts || []).map((src) => {
468
873
  const safe = sanitizeUrl(String(src));
469
874
  if (!safe) return "";
@@ -512,11 +917,11 @@ async function* renderToStream(element) {
512
917
  }
513
918
  const tag = element.tagName.toLowerCase();
514
919
  if (tag === "script" || tag === "style") {
515
- if (_isDev2) yield `<!--ssr:${tag}-stripped-->`;
920
+ if (_isDev3) yield `<!--ssr:${tag}-stripped-->`;
516
921
  return;
517
922
  }
518
923
  if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
519
- if (_isDev2) yield "<!--ssr:invalid-tag-->";
924
+ if (_isDev3) yield "<!--ssr:invalid-tag-->";
520
925
  return;
521
926
  }
522
927
  let openTag = `<${tag}`;
@@ -524,14 +929,18 @@ async function* renderToStream(element) {
524
929
  const rawName = attr.name;
525
930
  if (!isSafeAttrName(rawName)) continue;
526
931
  if (isEventHandlerAttr(rawName)) continue;
932
+ if (isForbiddenSsrAttr(rawName)) continue;
527
933
  const lowerName = rawName.toLowerCase();
528
934
  let value = attr.value;
529
- if (URL_ATTRS.has(lowerName)) {
530
- value = sanitizeUrlAttr(lowerName, value);
935
+ if (isPolicyAttr(lowerName)) {
936
+ value = sanitizeSsrAttributeValue(lowerName, value);
531
937
  if (!value) continue;
532
938
  }
533
939
  openTag += ` ${rawName}="${escapeAttr(value)}"`;
534
940
  }
941
+ if (element.dataset && !element.dataset.sibuHydrate) {
942
+ openTag += ` data-sibu-ssr="true"`;
943
+ }
535
944
  if (VOID_ELEMENTS.has(tag)) {
536
945
  yield `${openTag} />`;
537
946
  return;
@@ -578,9 +987,13 @@ function island(id, component) {
578
987
  el.setAttribute("data-sibu-island", id);
579
988
  return el;
580
989
  }
990
+ function isAlreadyHydrated(element) {
991
+ return element.hasAttribute("data-sibu-hydrated");
992
+ }
581
993
  function hydrateIslands(container, islands) {
582
994
  const markers = container.querySelectorAll("[data-sibu-island]");
583
995
  for (const marker2 of Array.from(markers)) {
996
+ if (isAlreadyHydrated(marker2)) continue;
584
997
  const id = marker2.getAttribute("data-sibu-island") ?? "";
585
998
  if (!Object.hasOwn(islands, id)) continue;
586
999
  const factory = islands[id];
@@ -588,7 +1001,7 @@ function hydrateIslands(container, islands) {
588
1001
  const clientTree = factory();
589
1002
  clientTree.setAttribute("data-sibu-island", id);
590
1003
  clientTree.setAttribute("data-sibu-hydrated", "true");
591
- marker2.replaceWith(clientTree);
1004
+ disposeAndReplace(marker2, clientTree);
592
1005
  }
593
1006
  container.setAttribute("data-sibu-hydrated", "partial");
594
1007
  }
@@ -596,6 +1009,7 @@ function hydrateProgressively(container, islands, options) {
596
1009
  const markers = container.querySelectorAll("[data-sibu-island]");
597
1010
  const cleanups = [];
598
1011
  for (const marker2 of Array.from(markers)) {
1012
+ if (isAlreadyHydrated(marker2)) continue;
599
1013
  const id = marker2.getAttribute("data-sibu-island") ?? "";
600
1014
  if (!Object.hasOwn(islands, id)) continue;
601
1015
  const factory = islands[id];
@@ -605,10 +1019,11 @@ function hydrateProgressively(container, islands, options) {
605
1019
  for (const entry of entries) {
606
1020
  if (entry.isIntersecting) {
607
1021
  observer.disconnect();
1022
+ if (isAlreadyHydrated(marker2) || marker2.parentNode === null) break;
608
1023
  const clientTree = factory();
609
1024
  clientTree.setAttribute("data-sibu-island", id);
610
1025
  clientTree.setAttribute("data-sibu-hydrated", "true");
611
- marker2.replaceWith(clientTree);
1026
+ disposeAndReplace(marker2, clientTree);
612
1027
  break;
613
1028
  }
614
1029
  }
@@ -649,7 +1064,7 @@ function ssrSuspense(props) {
649
1064
  },
650
1065
  (err) => {
651
1066
  if (timer) clearTimeout(timer);
652
- if (_isDev2) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
1067
+ if (_isDev3) console.warn("[SibuJS SSR] ssrSuspense rejected:", err);
653
1068
  return { id, html: fallbackHtml };
654
1069
  }
655
1070
  );
@@ -664,7 +1079,7 @@ function suspenseSwapScript(id, nonce) {
664
1079
  );
665
1080
  }
666
1081
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
667
- 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>`;
1082
+ 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>`;
668
1083
  }
669
1084
  async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
670
1085
  yield* renderToStream(element);
@@ -682,8 +1097,8 @@ function escapeScriptJson(json) {
682
1097
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
683
1098
  }
684
1099
  var DEFAULT_MAX_SSR_BYTES = 1024 * 1024;
685
- function serializeState(state, nonce, options) {
686
- const rawJson = JSON.stringify(state);
1100
+ function serializeState(state2, nonce, options) {
1101
+ const rawJson = JSON.stringify(state2);
687
1102
  const maxBytes = options?.maxBytes ?? DEFAULT_MAX_SSR_BYTES;
688
1103
  const byteLen = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(rawJson).byteLength : Buffer.byteLength(rawJson, "utf8");
689
1104
  if (byteLen > maxBytes) {
@@ -695,7 +1110,7 @@ function serializeState(state, nonce, options) {
695
1110
  }
696
1111
  function deserializeState(validate) {
697
1112
  if (typeof window === "undefined") return void 0;
698
- if (_isDev2 && !validate) {
1113
+ if (_isDev3 && !validate) {
699
1114
  console.warn(
700
1115
  "[SibuJS SSR] deserializeState() called without a validate guard \u2014 tampered SSR payloads will not be detected."
701
1116
  );
@@ -713,69 +1128,7 @@ function escapeAttr(str) {
713
1128
  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
714
1129
  }
715
1130
 
716
- // src/core/rendering/dispose.ts
717
- var elementDisposers = /* @__PURE__ */ new WeakMap();
718
- var _isDev3 = isDev();
719
- var activeBindingCount = 0;
720
- function registerDisposer(node, teardown) {
721
- let disposers = elementDisposers.get(node);
722
- if (!disposers) {
723
- disposers = [];
724
- elementDisposers.set(node, disposers);
725
- }
726
- disposers.push(teardown);
727
- if (_isDev3) activeBindingCount++;
728
- }
729
- function dispose(node) {
730
- const stack = [node];
731
- const order = [];
732
- while (stack.length > 0) {
733
- const current = stack.pop();
734
- order.push(current);
735
- const children = Array.from(current.childNodes);
736
- for (let i2 = 0; i2 < children.length; i2++) {
737
- stack.push(children[i2]);
738
- }
739
- }
740
- for (let i2 = order.length - 1; i2 >= 0; i2--) {
741
- const current = order[i2];
742
- const disposers = elementDisposers.get(current);
743
- if (disposers) {
744
- const snapshot = disposers.slice();
745
- elementDisposers.delete(current);
746
- if (_isDev3) activeBindingCount -= snapshot.length;
747
- for (const d of snapshot) {
748
- try {
749
- d();
750
- } catch (err) {
751
- if (_isDev3 && typeof console !== "undefined") {
752
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
753
- }
754
- }
755
- }
756
- let extraPasses = 0;
757
- while (extraPasses++ < 8) {
758
- const added = elementDisposers.get(current);
759
- if (!added || added.length === 0) break;
760
- const moreSnapshot = added.slice();
761
- elementDisposers.delete(current);
762
- if (_isDev3) activeBindingCount -= moreSnapshot.length;
763
- for (const d of moreSnapshot) {
764
- try {
765
- d();
766
- } catch (err) {
767
- if (_isDev3 && typeof console !== "undefined") {
768
- console.warn("[SibuJS] Disposer threw during cleanup:", err);
769
- }
770
- }
771
- }
772
- }
773
- }
774
- }
775
- }
776
-
777
1131
  // src/reactivity/track-core.ts
778
- var _isDev4 = isDev();
779
1132
  var POOL_MAX = 4096;
780
1133
  var nodePool = [];
781
1134
  function createNode() {
@@ -783,6 +1136,7 @@ function createNode() {
783
1136
  sig: null,
784
1137
  sub: null,
785
1138
  epoch: 0,
1139
+ depVersion: 0,
786
1140
  sigPrev: null,
787
1141
  sigNext: null,
788
1142
  subPrev: null,
@@ -867,8 +1221,27 @@ function safeInvoke(sub2) {
867
1221
  try {
868
1222
  sub2();
869
1223
  } catch (err) {
870
- if (_isDev4) devWarn(`Subscriber threw during notification: ${err instanceof Error ? err.message : String(err)}`);
1224
+ const s2 = sub2;
1225
+ reportError(err, { phase: s2._errorPhase ?? "effect", name: s2.__name, node: s2._errorNode });
1226
+ }
1227
+ }
1228
+ function depsChanged(sub2) {
1229
+ let node = sub2.depsHead ?? null;
1230
+ if (node === null) return true;
1231
+ while (node !== null) {
1232
+ const sig = node.sig;
1233
+ if (sig === null) return true;
1234
+ if (sig._d === true && sig._validate !== void 0) {
1235
+ try {
1236
+ sig._validate();
1237
+ } catch {
1238
+ return true;
1239
+ }
1240
+ }
1241
+ if (node.depVersion !== sig.__v) return true;
1242
+ node = node.subNext;
871
1243
  }
1244
+ return false;
872
1245
  }
873
1246
  var suspendDepth = 0;
874
1247
  var trackingSuspended = false;
@@ -950,7 +1323,7 @@ function track(effectFn, subscriber) {
950
1323
  const sub2 = subscriber;
951
1324
  return sub2._dispose ?? (sub2._dispose = () => cleanup(subscriber));
952
1325
  }
953
- function reactiveBinding(commit) {
1326
+ function reactiveBinding(commit, ownerNode) {
954
1327
  const run = () => {
955
1328
  const s2 = subscriber;
956
1329
  if (s2._disposed || s2._reentrant) return;
@@ -966,13 +1339,17 @@ function reactiveBinding(commit) {
966
1339
  subscriber.depsTail = null;
967
1340
  subscriber._epoch = 0;
968
1341
  subscriber._structDirty = false;
1342
+ subscriber._hasComputedDep = false;
969
1343
  subscriber._runEpoch = 0;
970
1344
  subscriber._runs = 0;
971
1345
  subscriber._reentrant = false;
972
1346
  subscriber._disposed = false;
1347
+ subscriber._errorPhase = "binding";
1348
+ subscriber._errorNode = ownerNode;
973
1349
  run();
974
1350
  return subscriber._dispose ?? (subscriber._dispose = () => {
975
1351
  subscriber._disposed = true;
1352
+ subscriber._errorNode = void 0;
976
1353
  cleanup(subscriber);
977
1354
  });
978
1355
  }
@@ -984,9 +1361,12 @@ function recordDependency(signal2) {
984
1361
  const active = sig.__activeNode ?? null;
985
1362
  if (active !== null && active.sub === sub2) {
986
1363
  active.epoch = epoch;
1364
+ active.depVersion = sig.__v ?? Number.NaN;
987
1365
  return;
988
1366
  }
1367
+ if (sig._validate !== void 0) sub2._hasComputedDep = true;
989
1368
  const node = allocNode(signal2, sub2, epoch);
1369
+ node.depVersion = sig.__v ?? Number.NaN;
990
1370
  node.prevActive = active;
991
1371
  sig.__activeNode = node;
992
1372
  linkSub(sub2, node);
@@ -1005,9 +1385,10 @@ function cleanup(subscriber) {
1005
1385
  node = next;
1006
1386
  }
1007
1387
  }
1008
- var maxSubscriberRepeats = 50;
1388
+ var maxSubscriberRepeats = 1e3;
1009
1389
  var maxDrainIterations = 1e6;
1010
1390
  var drainEpoch = 0;
1391
+ var quarantined = /* @__PURE__ */ new Set();
1011
1392
  function setMaxSubscriberRepeats(n) {
1012
1393
  const prev = maxSubscriberRepeats;
1013
1394
  if (Number.isFinite(n) && n > 0) maxSubscriberRepeats = Math.floor(n);
@@ -1029,19 +1410,21 @@ function tickRepeat(sub2) {
1029
1410
  return s2._runs > maxSubscriberRepeats;
1030
1411
  }
1031
1412
  function cycleError(sub2) {
1032
- if (typeof console !== "undefined") {
1033
- const name = sub2.__name ?? "<unnamed>";
1034
- console.error(
1035
- `[SibuJS] subscriber "${name}" fired more than ${maxSubscriberRepeats} times \u2014 likely a write-reads-self cycle between effects/signals. Breaking to prevent infinite loop.`
1036
- );
1037
- }
1413
+ const name = sub2.__name ?? "<unnamed>";
1414
+ reportError(
1415
+ new Error(
1416
+ `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.`
1417
+ ),
1418
+ { phase: "scheduler", name }
1419
+ );
1038
1420
  }
1039
1421
  function absoluteDrainError() {
1040
- if (typeof console !== "undefined") {
1041
- console.error(
1042
- `[SibuJS] Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
1043
- );
1044
- }
1422
+ reportError(
1423
+ new Error(
1424
+ `Notification drain exceeded ${maxDrainIterations} iterations \u2014 absolute safety net tripped. Breaking to prevent infinite loop.`
1425
+ ),
1426
+ { phase: "scheduler" }
1427
+ );
1045
1428
  }
1046
1429
  function drainQueue() {
1047
1430
  let i2 = 0;
@@ -1051,11 +1434,14 @@ function drainQueue() {
1051
1434
  break;
1052
1435
  }
1053
1436
  const sub2 = pendingQueue[i2++];
1437
+ pendingSet.delete(sub2);
1438
+ if (quarantined.size > 0 && quarantined.has(sub2)) continue;
1439
+ if (sub2._hasComputedDep === true && !depsChanged(sub2)) continue;
1054
1440
  if (tickRepeat(sub2)) {
1055
1441
  cycleError(sub2);
1056
- break;
1442
+ quarantined.add(sub2);
1443
+ continue;
1057
1444
  }
1058
- pendingSet.delete(sub2);
1059
1445
  safeInvoke(sub2);
1060
1446
  }
1061
1447
  }
@@ -1070,6 +1456,7 @@ function drainNotificationQueue() {
1070
1456
  if (notifyDepth === 0) {
1071
1457
  pendingQueue.length = 0;
1072
1458
  pendingSet.clear();
1459
+ if (quarantined.size > 0) quarantined.clear();
1073
1460
  }
1074
1461
  }
1075
1462
  }
@@ -1163,6 +1550,7 @@ function notifySubscribers(signal2) {
1163
1550
  if (notifyDepth === 0) {
1164
1551
  pendingQueue.length = 0;
1165
1552
  pendingSet.clear();
1553
+ if (quarantined.size > 0) quarantined.clear();
1166
1554
  }
1167
1555
  }
1168
1556
  }
@@ -1189,14 +1577,14 @@ function forEachSubscriber(signal2, visit) {
1189
1577
  }
1190
1578
 
1191
1579
  // src/reactivity/track.ts
1192
- var _isDev5 = isDev();
1193
- var _runtimeVersion = true ? "3.4.0" : "dev";
1580
+ var _isDev4 = isDev();
1581
+ var _runtimeVersion = true ? "4.0.0" : "dev";
1194
1582
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
1195
1583
  function resolveReactiveApi() {
1196
1584
  const g2 = globalThis;
1197
1585
  const existing = g2[REGISTRY_KEY];
1198
1586
  if (existing) {
1199
- if (_isDev5 && !existing.__dupWarned) {
1587
+ if (_isDev4 && !existing.__dupWarned) {
1200
1588
  existing.__dupWarned = true;
1201
1589
  devWarn(
1202
1590
  `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']).`
@@ -1257,7 +1645,7 @@ function flushUserCleanups(ctx) {
1257
1645
  try {
1258
1646
  list[i2]();
1259
1647
  } catch (err) {
1260
- if (typeof console !== "undefined") console.warn("[SibuJS effect] onCleanup threw:", err);
1648
+ reportError(err, { phase: "cleanup" });
1261
1649
  }
1262
1650
  }
1263
1651
  }
@@ -1270,11 +1658,12 @@ function drainReruns(ctx) {
1270
1658
  } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
1271
1659
  if (ctx.rerunPending) {
1272
1660
  ctx.rerunPending = false;
1273
- if (_g.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
1274
- console.error(
1275
- `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
1276
- );
1277
- }
1661
+ reportError(
1662
+ new Error(
1663
+ `effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
1664
+ ),
1665
+ { phase: "effect" }
1666
+ );
1278
1667
  }
1279
1668
  }
1280
1669
  function disposeEffect(ctx) {
@@ -1351,6 +1740,8 @@ function effect(effectFn, options) {
1351
1740
  sub2.depsTail = null;
1352
1741
  sub2._epoch = 0;
1353
1742
  sub2._structDirty = false;
1743
+ sub2._hasComputedDep = false;
1744
+ sub2._errorPhase = "effect";
1354
1745
  sub2._runEpoch = 0;
1355
1746
  sub2._runs = 0;
1356
1747
  ctx.subscriber = sub2;
@@ -1367,39 +1758,178 @@ function effect(effectFn, options) {
1367
1758
  return () => disposeEffect(ctx);
1368
1759
  }
1369
1760
 
1370
- // src/platform/head.ts
1371
- var HEAD_URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
1372
- function sanitizeHeadAttr(key, value) {
1373
- if (HEAD_URL_ATTRS.has(key)) return sanitizeUrl(value);
1374
- return value;
1761
+ // src/utils/globalSingleton.ts
1762
+ function globalSingleton(key, create) {
1763
+ const g2 = globalThis;
1764
+ return g2[key] ?? (g2[key] = create());
1375
1765
  }
1376
- function isDangerousRefreshContent(content) {
1377
- const normalized = stripControlChars(content).toLowerCase();
1378
- return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
1766
+
1767
+ // src/utils/singletonResource.ts
1768
+ function singletonResource(io) {
1769
+ let original = null;
1770
+ let captured = false;
1771
+ const stack = [];
1772
+ function apply() {
1773
+ if (stack.length > 0) {
1774
+ io.write(stack[stack.length - 1].value);
1775
+ return;
1776
+ }
1777
+ if (captured) {
1778
+ io.write(original);
1779
+ captured = false;
1780
+ original = null;
1781
+ }
1782
+ }
1783
+ return {
1784
+ acquire(value) {
1785
+ if (!captured) {
1786
+ original = io.read();
1787
+ captured = true;
1788
+ }
1789
+ const entry = { value, released: false };
1790
+ stack.push(entry);
1791
+ apply();
1792
+ return {
1793
+ set(next) {
1794
+ if (entry.released) return;
1795
+ entry.value = next;
1796
+ apply();
1797
+ },
1798
+ release() {
1799
+ if (entry.released) return;
1800
+ entry.released = true;
1801
+ const index = stack.indexOf(entry);
1802
+ if (index !== -1) stack.splice(index, 1);
1803
+ apply();
1804
+ }
1805
+ };
1806
+ },
1807
+ ownerCount() {
1808
+ return stack.length;
1809
+ }
1810
+ };
1811
+ }
1812
+
1813
+ // src/utils/documentResources.ts
1814
+ var INERT_LEASE = {
1815
+ set() {
1816
+ },
1817
+ release() {
1818
+ }
1819
+ };
1820
+ function inertLease() {
1821
+ return INERT_LEASE;
1379
1822
  }
1380
- function getMetaAttr(metaProps, name) {
1381
- for (const k in metaProps) {
1382
- if (k.toLowerCase() === name) return metaProps[k];
1823
+ var titleResource = globalSingleton(
1824
+ /* @__PURE__ */ Symbol.for("sibujs.documentTitle.v1"),
1825
+ () => singletonResource({
1826
+ read: () => document.title,
1827
+ write: (value) => {
1828
+ document.title = value;
1829
+ }
1830
+ })
1831
+ );
1832
+ var baseResource = globalSingleton(
1833
+ /* @__PURE__ */ Symbol.for("sibujs.documentBase.v1"),
1834
+ () => singletonResource({
1835
+ read: () => {
1836
+ const el = document.head.querySelector("base");
1837
+ if (!el) return null;
1838
+ return {
1839
+ href: el.getAttribute("href") ?? void 0,
1840
+ target: el.getAttribute("target") ?? void 0
1841
+ };
1842
+ },
1843
+ write: (spec) => {
1844
+ const existing = document.head.querySelector("base");
1845
+ if (spec === null) {
1846
+ existing?.remove();
1847
+ return;
1848
+ }
1849
+ const el = existing ?? document.head.appendChild(document.createElement("base"));
1850
+ if (spec.href === void 0) el.removeAttribute("href");
1851
+ else el.setAttribute("href", spec.href);
1852
+ if (spec.target === void 0) el.removeAttribute("target");
1853
+ else el.setAttribute("target", spec.target);
1854
+ }
1855
+ })
1856
+ );
1857
+ var scrollRestorationResource = globalSingleton(
1858
+ /* @__PURE__ */ Symbol.for("sibujs.historyScrollRestoration.v1"),
1859
+ () => singletonResource({
1860
+ read: () => history.scrollRestoration,
1861
+ write: (mode) => {
1862
+ history.scrollRestoration = mode;
1863
+ }
1864
+ })
1865
+ );
1866
+ function acquireScrollRestorationMode(mode) {
1867
+ if (typeof history === "undefined" || !("scrollRestoration" in history)) {
1868
+ return inertLease();
1383
1869
  }
1384
- return void 0;
1870
+ return scrollRestorationResource.acquire(mode);
1385
1871
  }
1386
- function isDangerousMetaRefresh2(metaProps) {
1387
- const httpEquiv = getMetaAttr(metaProps, "http-equiv");
1388
- if (typeof httpEquiv !== "string") return false;
1389
- if (httpEquiv.toLowerCase() !== "refresh") return false;
1390
- const content = getMetaAttr(metaProps, "content");
1391
- if (typeof content !== "string") return false;
1392
- return isDangerousRefreshContent(content);
1872
+ function acquireTitle(value) {
1873
+ if (typeof document === "undefined") return inertLease();
1874
+ return titleResource.acquire(value);
1393
1875
  }
1394
- var SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
1395
- function isSafeHeadAttr(name) {
1396
- if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
1397
- if (isEventHandlerAttr(name)) return false;
1398
- return true;
1876
+ function acquireBase(spec) {
1877
+ if (typeof document === "undefined") return inertLease();
1878
+ return baseResource.acquire(spec);
1399
1879
  }
1880
+
1881
+ // src/platform/head.ts
1400
1882
  function escapeScriptJsonLocal(json) {
1401
1883
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1402
1884
  }
1885
+ var CLIENT_RESOLVER = {
1886
+ resolveValue: (_name, value) => typeof value === "function" ? String(value()) : String(value),
1887
+ isReactiveValue: (value) => typeof value === "function"
1888
+ };
1889
+ function applyMetaEntry(metaProps, managedElements) {
1890
+ let current = null;
1891
+ const retract = () => {
1892
+ if (!current) return;
1893
+ const index = managedElements.indexOf(current);
1894
+ if (index !== -1) managedElements.splice(index, 1);
1895
+ current.remove();
1896
+ current = null;
1897
+ };
1898
+ const alreadyPublished = (attributes) => {
1899
+ if (!current || current.attributes.length !== attributes.size) return false;
1900
+ for (const [name, value] of attributes) {
1901
+ if (current.getAttribute(name) !== value) return false;
1902
+ }
1903
+ return true;
1904
+ };
1905
+ const publish = (attributes) => {
1906
+ if (alreadyPublished(attributes)) return;
1907
+ const next = document.createElement("meta");
1908
+ for (const [name, value] of attributes) next.setAttribute(name, value);
1909
+ const index = current ? managedElements.indexOf(current) : -1;
1910
+ if (current?.parentNode) {
1911
+ current.replaceWith(next);
1912
+ } else {
1913
+ current?.remove();
1914
+ document.head.appendChild(next);
1915
+ }
1916
+ if (index !== -1) managedElements[index] = next;
1917
+ else managedElements.push(next);
1918
+ current = next;
1919
+ };
1920
+ const stopEffect = effect(() => {
1921
+ const plan = planMetaEntry(metaProps, CLIENT_RESOLVER);
1922
+ if (plan.kind === "drop") {
1923
+ retract();
1924
+ return;
1925
+ }
1926
+ publish(plan.attributes);
1927
+ });
1928
+ return () => {
1929
+ stopEffect();
1930
+ retract();
1931
+ };
1932
+ }
1403
1933
  function Head(props) {
1404
1934
  const anchor = document.createComment("sibu-head");
1405
1935
  const managedElements = [];
@@ -1416,78 +1946,52 @@ function Head(props) {
1416
1946
  cleanup3();
1417
1947
  if (props.title) {
1418
1948
  if (typeof props.title === "function") {
1419
- const cleanupFn = effect(() => {
1420
- document.title = props.title();
1949
+ const getter = props.title;
1950
+ let lease = null;
1951
+ const stopEffect = effect(() => {
1952
+ const next = getter();
1953
+ if (lease) lease.set(next);
1954
+ else lease = acquireTitle(next);
1955
+ });
1956
+ effectCleanups.push(() => {
1957
+ stopEffect();
1958
+ lease?.release();
1959
+ lease = null;
1421
1960
  });
1422
- effectCleanups.push(cleanupFn);
1423
1961
  } else {
1424
- document.title = props.title;
1962
+ const lease = acquireTitle(props.title);
1963
+ effectCleanups.push(() => lease.release());
1425
1964
  }
1426
1965
  }
1427
1966
  if (props.meta) {
1428
1967
  for (const metaProps of props.meta) {
1429
- if (isDangerousMetaRefresh2(metaProps)) continue;
1430
- const httpEquiv = getMetaAttr(metaProps, "http-equiv");
1431
- const isRefreshNow = () => {
1432
- const eq = typeof httpEquiv === "function" ? httpEquiv() : httpEquiv;
1433
- return typeof eq === "string" && eq.toLowerCase() === "refresh";
1434
- };
1435
- const el = document.createElement("meta");
1436
- for (const [key, value] of Object.entries(metaProps)) {
1437
- if (!isSafeHeadAttr(key)) continue;
1438
- const isContent = key.toLowerCase() === "content";
1439
- if (typeof value === "function") {
1440
- const cleanupFn = effect(() => {
1441
- const resolved = value();
1442
- if (isContent && isRefreshNow() && isDangerousRefreshContent(resolved)) {
1443
- el.removeAttribute(key);
1444
- return;
1445
- }
1446
- el.setAttribute(key, sanitizeHeadAttr(key, resolved));
1447
- });
1448
- effectCleanups.push(cleanupFn);
1449
- } else {
1450
- if (isContent && isRefreshNow() && isDangerousRefreshContent(value)) continue;
1451
- el.setAttribute(key, sanitizeHeadAttr(key, value));
1452
- }
1453
- }
1454
- document.head.appendChild(el);
1455
- managedElements.push(el);
1968
+ const stop2 = applyMetaEntry(metaProps, managedElements);
1969
+ effectCleanups.push(stop2);
1456
1970
  }
1457
1971
  }
1458
- if (props.link) {
1459
- for (const linkProps of props.link) {
1460
- const el = document.createElement("link");
1461
- for (const [key, value] of Object.entries(linkProps)) {
1462
- if (!isSafeHeadAttr(key)) continue;
1463
- el.setAttribute(key, sanitizeHeadAttr(key, value));
1464
- }
1465
- document.head.appendChild(el);
1466
- managedElements.push(el);
1467
- }
1468
- }
1469
- if (props.script) {
1470
- for (const scriptProps of props.script) {
1471
- const el = document.createElement("script");
1472
- for (const [key, value] of Object.entries(scriptProps)) {
1473
- if (!isSafeHeadAttr(key)) continue;
1474
- el.setAttribute(key, sanitizeHeadAttr(key, value));
1475
- }
1972
+ for (const [tag, entries] of [
1973
+ ["link", props.link],
1974
+ ["script", props.script]
1975
+ ]) {
1976
+ if (!entries) continue;
1977
+ for (const entryProps of entries) {
1978
+ const plan = planHeadElementEntry(entryProps, STATIC_VALUE_RESOLVER);
1979
+ if (plan.kind === "drop") continue;
1980
+ const el = document.createElement(tag);
1981
+ for (const [name, value] of plan.attributes) el.setAttribute(name, value);
1476
1982
  document.head.appendChild(el);
1477
1983
  managedElements.push(el);
1478
1984
  }
1479
1985
  }
1480
1986
  if (props.base) {
1481
- const existing = document.head.querySelector("base");
1482
- if (existing) existing.remove();
1483
- const el = document.createElement("base");
1987
+ const spec = {};
1484
1988
  if (props.base.href) {
1485
1989
  const safeHref = sanitizeUrl(props.base.href);
1486
- if (safeHref) el.href = safeHref;
1990
+ if (safeHref) spec.href = safeHref;
1487
1991
  }
1488
- if (props.base.target) el.target = props.base.target;
1489
- document.head.appendChild(el);
1490
- managedElements.push(el);
1992
+ if (props.base.target) spec.target = props.base.target;
1993
+ const lease = acquireBase(spec);
1994
+ effectCleanups.push(() => lease.release());
1491
1995
  }
1492
1996
  };
1493
1997
  apply();
@@ -1510,7 +2014,9 @@ function setCanonical(url) {
1510
2014
  link2.rel = "canonical";
1511
2015
  document.head.appendChild(link2);
1512
2016
  }
1513
- link2.href = sanitizeUrl(url);
2017
+ const safe = sanitizeUrl(url);
2018
+ if (safe) link2.setAttribute("href", safe);
2019
+ else link2.removeAttribute("href");
1514
2020
  }
1515
2021
 
1516
2022
  // src/platform/staticSiteGenerator.ts
@@ -1584,70 +2090,72 @@ var isBatching = API2.isBatching;
1584
2090
 
1585
2091
  // src/core/signals/signal.ts
1586
2092
  var _g2 = globalThis;
1587
- var _isDev6 = isDev();
2093
+ var _isDev5 = isDev();
1588
2094
  function signal(initial, options) {
1589
- const state = {
2095
+ const state2 = {
1590
2096
  value: initial,
1591
2097
  __v: 0,
1592
2098
  __sc: 0,
1593
2099
  subsHead: null,
1594
2100
  subsTail: null,
1595
2101
  __activeNode: null,
1596
- __name: void 0
2102
+ __name: void 0,
2103
+ _d: false,
2104
+ _validate: void 0
1597
2105
  };
1598
- const debugName = _isDev6 ? options?.name : void 0;
2106
+ const debugName = _isDev5 ? options?.name : void 0;
1599
2107
  const equalsFn = options?.equals;
1600
- if (debugName) state.__name = debugName;
2108
+ if (debugName) state2.__name = debugName;
1601
2109
  function get() {
1602
- recordDependency2(state);
1603
- return state.value;
2110
+ recordDependency2(state2);
2111
+ return state2.value;
1604
2112
  }
1605
- get.__signal = state;
2113
+ get.__signal = state2;
1606
2114
  if (debugName) get.__name = debugName;
1607
2115
  let set;
1608
2116
  if (equalsFn) {
1609
2117
  set = (next) => {
1610
- const prev = state.value;
2118
+ const prev = state2.value;
1611
2119
  const newValue = typeof next === "function" ? next(prev) : next;
1612
2120
  if (equalsFn(prev, newValue)) return;
1613
- state.value = newValue;
1614
- state.__v++;
1615
- if (_isDev6) {
2121
+ state2.value = newValue;
2122
+ state2.__v++;
2123
+ if (_isDev5) {
1616
2124
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1617
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
2125
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
1618
2126
  }
1619
- if (!enqueueBatchedSignal(state)) {
1620
- notifySubscribers2(state);
2127
+ if (!enqueueBatchedSignal(state2)) {
2128
+ notifySubscribers2(state2);
1621
2129
  }
1622
2130
  };
1623
- } else if (_isDev6) {
2131
+ } else if (_isDev5) {
1624
2132
  set = (next) => {
1625
- const prev = state.value;
2133
+ const prev = state2.value;
1626
2134
  const newValue = typeof next === "function" ? next(prev) : next;
1627
2135
  if (Object.is(newValue, prev)) return;
1628
- state.value = newValue;
1629
- state.__v++;
2136
+ state2.value = newValue;
2137
+ state2.__v++;
1630
2138
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1631
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
1632
- if (!enqueueBatchedSignal(state)) {
1633
- notifySubscribers2(state);
2139
+ if (hook) hook.emit("signal:update", { signal: state2, name: debugName, oldValue: prev, newValue });
2140
+ if (!enqueueBatchedSignal(state2)) {
2141
+ notifySubscribers2(state2);
1634
2142
  }
1635
2143
  };
1636
2144
  } else {
1637
2145
  set = (next) => {
1638
- const prev = state.value;
2146
+ const prev = state2.value;
1639
2147
  const newValue = typeof next === "function" ? next(prev) : next;
1640
2148
  if (Object.is(newValue, prev)) return;
1641
- state.value = newValue;
1642
- state.__v++;
1643
- if (!enqueueBatchedSignal(state)) {
1644
- notifySubscribers2(state);
2149
+ state2.value = newValue;
2150
+ state2.__v++;
2151
+ if (!enqueueBatchedSignal(state2)) {
2152
+ notifySubscribers2(state2);
1645
2153
  }
1646
2154
  };
1647
2155
  }
1648
- if (_isDev6) {
2156
+ if (_isDev5) {
1649
2157
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
1650
- if (hook) hook.emit("signal:create", { signal: state, name: debugName, getter: get, initial });
2158
+ if (hook) hook.emit("signal:create", { signal: state2, name: debugName, getter: get, initial });
1651
2159
  }
1652
2160
  return [get, set];
1653
2161
  }
@@ -1733,17 +2241,39 @@ function createAction(actionFn) {
1733
2241
  }
1734
2242
 
1735
2243
  // src/platform/scrollRestoration.ts
2244
+ var DEFAULT_STATE_KEY = "__sibuScrollKey";
1736
2245
  function scrollRestoration(options) {
1737
2246
  const mode = options?.mode ?? "auto";
2247
+ const stateKey = options?.stateKey ?? DEFAULT_STATE_KEY;
1738
2248
  const positions = /* @__PURE__ */ new Map();
1739
2249
  let popstateHandler = null;
1740
2250
  let currentKey = null;
1741
- const save = (key) => {
2251
+ if (options?.getKey) {
2252
+ try {
2253
+ currentKey = options.getKey();
2254
+ } catch {
2255
+ currentKey = null;
2256
+ }
2257
+ }
2258
+ const record = (key) => {
1742
2259
  if (typeof window !== "undefined") {
1743
2260
  positions.set(key, { x: window.scrollX, y: window.scrollY });
1744
2261
  }
1745
2262
  currentKey = key;
1746
2263
  };
2264
+ const tagCurrentEntry = (key) => {
2265
+ if (typeof window === "undefined" || typeof history === "undefined") return;
2266
+ try {
2267
+ const existing = history.state ?? {};
2268
+ if (existing[stateKey] === key) return;
2269
+ history.replaceState({ ...existing, [stateKey]: key }, "");
2270
+ } catch {
2271
+ }
2272
+ };
2273
+ const save = (key) => {
2274
+ record(key);
2275
+ if (mode === "auto") tagCurrentEntry(key);
2276
+ };
1747
2277
  const restore = (key) => {
1748
2278
  const pos = positions.get(key);
1749
2279
  if (pos && typeof window !== "undefined") {
@@ -1754,11 +2284,32 @@ function scrollRestoration(options) {
1754
2284
  const getPosition = (key) => {
1755
2285
  return positions.get(key);
1756
2286
  };
2287
+ const onNavigation = (key) => {
2288
+ if (mode !== "auto") return;
2289
+ if (currentKey !== null && currentKey !== key) record(currentKey);
2290
+ currentKey = key;
2291
+ tagCurrentEntry(key);
2292
+ };
2293
+ let nativeModeLease = null;
1757
2294
  if (mode === "auto" && typeof window !== "undefined") {
1758
- popstateHandler = () => {
1759
- if (currentKey) {
1760
- save(currentKey);
2295
+ nativeModeLease = acquireScrollRestorationMode("manual");
2296
+ if (currentKey !== null) tagCurrentEntry(currentKey);
2297
+ popstateHandler = (event) => {
2298
+ const state2 = event.state ?? null;
2299
+ const rawDestination = state2 ? state2[stateKey] : void 0;
2300
+ const destination = typeof rawDestination === "string" ? rawDestination : null;
2301
+ let outgoing = currentKey;
2302
+ if (outgoing === null && options?.getKey) {
2303
+ try {
2304
+ outgoing = options.getKey();
2305
+ } catch {
2306
+ outgoing = null;
2307
+ }
2308
+ }
2309
+ if (outgoing !== null && outgoing !== destination) {
2310
+ record(outgoing);
1761
2311
  }
2312
+ if (destination !== null) restore(destination);
1762
2313
  };
1763
2314
  window.addEventListener("popstate", popstateHandler);
1764
2315
  }
@@ -1767,9 +2318,11 @@ function scrollRestoration(options) {
1767
2318
  window.removeEventListener("popstate", popstateHandler);
1768
2319
  popstateHandler = null;
1769
2320
  }
2321
+ nativeModeLease?.release();
2322
+ nativeModeLease = null;
1770
2323
  positions.clear();
1771
2324
  };
1772
- return { save, restore, getPosition, dispose: dispose2 };
2325
+ return { save, restore, onNavigation, getPosition, dispose: dispose2 };
1773
2326
  }
1774
2327
 
1775
2328
  // src/platform/routeMiddleware.ts
@@ -1803,17 +2356,86 @@ function createMiddlewareChain() {
1803
2356
  return { use: use2, run };
1804
2357
  }
1805
2358
 
1806
- // src/reactivity/bindAttribute.ts
1807
- var _isDev7 = isDev();
2359
+ // src/utils/setSafeAttribute.ts
2360
+ var _isDev6 = isDev();
2361
+ var HTML_NS = "http://www.w3.org/1999/xhtml";
2362
+ var SVG_NS = "http://www.w3.org/2000/svg";
2363
+ var XLINK_NS = "http://www.w3.org/1999/xlink";
2364
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
2365
+ var BOOLEAN_IDL_ATTRS = /* @__PURE__ */ new Set(["checked", "disabled", "selected"]);
2366
+ function isHtmlElement(el) {
2367
+ return el.namespaceURI === null || el.namespaceURI === HTML_NS;
2368
+ }
1808
2369
  function setProp(el, key, val) {
1809
2370
  el[key] = val;
1810
2371
  }
1811
- function bindAttribute(el, attr, getter) {
1812
- if (isEventHandlerAttr(attr)) {
1813
- if (_isDev7)
2372
+ function namespaceFor(el, name) {
2373
+ if (el.namespaceURI !== SVG_NS) return null;
2374
+ const lower = name.toLowerCase();
2375
+ if (lower.startsWith("xlink:")) return XLINK_NS;
2376
+ if (lower.startsWith("xml:")) return XML_NS;
2377
+ return null;
2378
+ }
2379
+ function setSafeAttribute(el, name, value, options = {}) {
2380
+ const ns = namespaceFor(el, name);
2381
+ const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
2382
+ if (isEventHandlerAttr(name)) {
2383
+ if (_isDev6) {
1814
2384
  devWarn(
1815
- `bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
2385
+ `${options.label ?? "setSafeAttribute"}: refusing to set event-handler attribute "${name}". Its value would be evaluated as JavaScript. Use on:{ ${name.slice(2)}: fn } instead.`
1816
2386
  );
2387
+ }
2388
+ if (ns) el.removeAttributeNS(ns, localName);
2389
+ else el.removeAttribute(name);
2390
+ return false;
2391
+ }
2392
+ if (value == null) {
2393
+ if (ns) el.removeAttributeNS(ns, localName);
2394
+ else el.removeAttribute(name);
2395
+ return true;
2396
+ }
2397
+ if (isHtmlContentAttribute(name)) {
2398
+ if (_isDev6) {
2399
+ devWarn(
2400
+ `${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.`
2401
+ );
2402
+ }
2403
+ if (ns) el.removeAttributeNS(ns, localName);
2404
+ else el.removeAttribute(name);
2405
+ return false;
2406
+ }
2407
+ const html2 = isHtmlElement(el);
2408
+ const idlName = html2 ? name.toLowerCase() : name;
2409
+ if (typeof value === "boolean") {
2410
+ if (html2 && BOOLEAN_IDL_ATTRS.has(idlName) && idlName in el) {
2411
+ setProp(el, idlName, value);
2412
+ } else if (value) {
2413
+ if (ns) el.setAttributeNS(ns, name, "");
2414
+ else el.setAttribute(name, "");
2415
+ } else if (ns) {
2416
+ el.removeAttributeNS(ns, localName);
2417
+ } else {
2418
+ el.removeAttribute(name);
2419
+ }
2420
+ return true;
2421
+ }
2422
+ const str = String(value);
2423
+ if (options.syncValueProperty !== false && html2 && (idlName === "value" || idlName === "checked") && idlName in el) {
2424
+ setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
2425
+ return true;
2426
+ }
2427
+ const safe = sanitizeAttributeString(name, str);
2428
+ const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
2429
+ if (current === safe) return true;
2430
+ if (ns) el.setAttributeNS(ns, name, safe);
2431
+ else el.setAttribute(name, safe);
2432
+ return true;
2433
+ }
2434
+
2435
+ // src/reactivity/bindAttribute.ts
2436
+ function bindAttribute(el, attr, getter) {
2437
+ if (isEventHandlerAttr(attr)) {
2438
+ setSafeAttribute(el, attr, "", { label: `bindAttribute("${attr}")` });
1817
2439
  return () => {
1818
2440
  };
1819
2441
  }
@@ -1822,32 +2444,16 @@ function bindAttribute(el, attr, getter) {
1822
2444
  try {
1823
2445
  value = getter();
1824
2446
  } catch (err) {
1825
- if (_isDev7)
1826
- devWarn(`bindAttribute: getter for "${attr}" threw: ${err instanceof Error ? err.message : String(err)}`);
2447
+ reportError(err, { phase: "binding", name: `bindAttribute("${attr}")`, node: el });
1827
2448
  return;
1828
2449
  }
1829
- if (typeof value === "boolean") {
1830
- if (attr in el && (attr === "checked" || attr === "disabled" || attr === "selected")) {
1831
- setProp(el, attr, value);
1832
- } else if (value) {
1833
- el.setAttribute(attr, "");
1834
- } else {
1835
- el.removeAttribute(attr);
1836
- }
1837
- return;
1838
- }
1839
- const str = String(value);
1840
- if ((attr === "value" || attr === "checked") && attr in el) {
1841
- setProp(el, attr, attr === "checked" ? Boolean(value) : str);
1842
- } else {
1843
- el.setAttribute(attr, sanitizeAttributeString(attr, str));
1844
- }
2450
+ setSafeAttribute(el, attr, value, { label: `bindAttribute("${attr}")` });
1845
2451
  }
1846
- return reactiveBinding2(commit);
2452
+ return reactiveBinding2(commit, el);
1847
2453
  }
1848
2454
 
1849
2455
  // src/reactivity/bindChildNode.ts
1850
- var _isDev8 = isDev();
2456
+ var _isDev7 = isDev();
1851
2457
  function bindChildNode(placeholder, getter) {
1852
2458
  let lastNodes = [];
1853
2459
  function commit() {
@@ -1855,7 +2461,7 @@ function bindChildNode(placeholder, getter) {
1855
2461
  try {
1856
2462
  result = getter();
1857
2463
  } catch (err) {
1858
- if (_isDev8) devWarn(`bindChildNode: getter threw: ${err instanceof Error ? err.message : String(err)}`);
2464
+ reportError(err, { phase: "binding", name: "bindChildNode", node: placeholder });
1859
2465
  return;
1860
2466
  }
1861
2467
  if (result == null || typeof result === "boolean") {
@@ -1881,7 +2487,7 @@ function bindChildNode(placeholder, getter) {
1881
2487
  if (item == null || typeof item === "boolean") continue;
1882
2488
  const node = item instanceof Node ? item : document.createTextNode(String(item));
1883
2489
  if (seen.has(node)) {
1884
- if (_isDev8)
2490
+ if (_isDev7)
1885
2491
  devWarn("bindChildNode: duplicate node reference in array \u2014 only the first occurrence is rendered.");
1886
2492
  continue;
1887
2493
  }
@@ -1916,12 +2522,12 @@ function bindChildNode(placeholder, getter) {
1916
2522
  }
1917
2523
  lastNodes = newNodes;
1918
2524
  }
1919
- return reactiveBinding2(commit);
2525
+ return reactiveBinding2(commit, placeholder);
1920
2526
  }
1921
2527
 
1922
2528
  // src/core/rendering/tagFactory.ts
1923
- var SVG_NS = "http://www.w3.org/2000/svg";
1924
- var _isDev9 = isDev();
2529
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
2530
+ var _isDev8 = isDev();
1925
2531
  var BLOCKED_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
1926
2532
  function isBlockedTag(tag) {
1927
2533
  return BLOCKED_TAGS.has(tag.toLowerCase());
@@ -1939,9 +2545,6 @@ var CLOBBER_RISKY_IDS = /* @__PURE__ */ new Set([
1939
2545
  "self",
1940
2546
  "frames"
1941
2547
  ]);
1942
- function setProp2(el, key, val) {
1943
- el[key] = val;
1944
- }
1945
2548
  function looksLikeClassList(s2) {
1946
2549
  const t = s2.trim();
1947
2550
  if (!t) return false;
@@ -1965,13 +2568,13 @@ function toKebab(prop) {
1965
2568
  function applyStyle(el, style2) {
1966
2569
  if (typeof style2 === "function") {
1967
2570
  const teardown = track2(() => {
1968
- el.setAttribute("style", style2());
2571
+ el.setAttribute("style", sanitizeStyleAttribute(String(style2())));
1969
2572
  });
1970
2573
  registerDisposer(el, teardown);
1971
2574
  return;
1972
2575
  }
1973
2576
  if (typeof style2 === "string") {
1974
- el.setAttribute("style", style2);
2577
+ el.setAttribute("style", sanitizeStyleAttribute(style2));
1975
2578
  return;
1976
2579
  }
1977
2580
  const htmlEl = el;
@@ -2092,7 +2695,7 @@ var tagFactory = (tag, ns) => {
2092
2695
  appendChildren(el, second);
2093
2696
  return el;
2094
2697
  }
2095
- if (_isDev9 && looksLikeClassList(first)) {
2698
+ if (_isDev8 && looksLikeClassList(first)) {
2096
2699
  devWarn(
2097
2700
  `tagFactory: lone string "${first}" looks like a class list but is being rendered as TEXT. For a class, use ${tag}({ class: "${first}" }) \u2014 or ${tag}("${first}", children) to set the class AND add children.`
2098
2701
  );
@@ -2113,7 +2716,7 @@ var tagFactory = (tag, ns) => {
2113
2716
  if (pClass != null) applyClass(el, pClass);
2114
2717
  const pId = props.id;
2115
2718
  if (pId != null) {
2116
- if (_isDev9 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2719
+ if (_isDev8 && typeof pId === "string" && CLOBBER_RISKY_IDS.has(pId.toLowerCase())) {
2117
2720
  devWarn(
2118
2721
  `tagFactory: element id="${pId}" matches a common global and may cause DOM clobbering. Avoid setting ids from untrusted input.`
2119
2722
  );
@@ -2128,7 +2731,7 @@ var tagFactory = (tag, ns) => {
2128
2731
  const handler = pOn[ev];
2129
2732
  if (typeof handler === "function") {
2130
2733
  el.addEventListener(ev, handler);
2131
- } else if (_isDev9) {
2734
+ } else if (_isDev8) {
2132
2735
  devWarn(
2133
2736
  `tagFactory: on.${ev} handler is not a function (got ${typeof handler}). Event listener was not attached.`
2134
2737
  );
@@ -2156,16 +2759,8 @@ var tagFactory = (tag, ns) => {
2156
2759
  if (isEventHandlerAttr(key)) continue;
2157
2760
  if (typeof value === "function") {
2158
2761
  registerDisposer(el, bindAttribute(el, key, value));
2159
- } else if (typeof value === "boolean") {
2160
- if (key in el && (key === "checked" || key === "disabled" || key === "selected")) {
2161
- setProp2(el, key, value);
2162
- } else if (value) {
2163
- el.setAttribute(key, "");
2164
- } else {
2165
- el.removeAttribute(key);
2166
- }
2167
2762
  } else {
2168
- el.setAttribute(key, sanitizeAttributeString(key, String(value)));
2763
+ setSafeAttribute(el, key, value, { syncValueProperty: false, label: "tagFactory" });
2169
2764
  }
2170
2765
  }
2171
2766
  }
@@ -2250,7 +2845,7 @@ var param = tagFactory("param");
2250
2845
  var picture = tagFactory("picture");
2251
2846
  var portal = tagFactory("portal");
2252
2847
  var source = tagFactory("source");
2253
- var svg = tagFactory("svg", SVG_NS);
2848
+ var svg = tagFactory("svg", SVG_NS2);
2254
2849
  var math = tagFactory("math");
2255
2850
  var canvas = tagFactory("canvas");
2256
2851
  var noscript = tagFactory("noscript");
@@ -2291,36 +2886,30 @@ var base = tagFactory("base");
2291
2886
  var link = tagFactory("link");
2292
2887
  var meta = tagFactory("meta");
2293
2888
  var style = tagFactory("style");
2294
- var circle = tagFactory("circle", SVG_NS);
2295
- var ellipse = tagFactory("ellipse", SVG_NS);
2296
- var g = tagFactory("g", SVG_NS);
2297
- var line = tagFactory("line", SVG_NS);
2298
- var path = tagFactory("path", SVG_NS);
2299
- var polygon = tagFactory("polygon", SVG_NS);
2300
- var polyline = tagFactory("polyline", SVG_NS);
2301
- var rect = tagFactory("rect", SVG_NS);
2302
- var text = tagFactory("text", SVG_NS);
2303
- var tspan = tagFactory("tspan", SVG_NS);
2304
- var defs = tagFactory("defs", SVG_NS);
2305
- var clipPath = tagFactory("clipPath", SVG_NS);
2306
- var mask = tagFactory("mask", SVG_NS);
2307
- var pattern = tagFactory("pattern", SVG_NS);
2308
- var linearGradient = tagFactory("linearGradient", SVG_NS);
2309
- var radialGradient = tagFactory("radialGradient", SVG_NS);
2310
- var stop = tagFactory("stop", SVG_NS);
2311
- var use = tagFactory("use", SVG_NS);
2312
- var symbol = tagFactory("symbol", SVG_NS);
2313
- var marker = tagFactory("marker", SVG_NS);
2889
+ var circle = tagFactory("circle", SVG_NS2);
2890
+ var ellipse = tagFactory("ellipse", SVG_NS2);
2891
+ var g = tagFactory("g", SVG_NS2);
2892
+ var line = tagFactory("line", SVG_NS2);
2893
+ var path = tagFactory("path", SVG_NS2);
2894
+ var polygon = tagFactory("polygon", SVG_NS2);
2895
+ var polyline = tagFactory("polyline", SVG_NS2);
2896
+ var rect = tagFactory("rect", SVG_NS2);
2897
+ var text = tagFactory("text", SVG_NS2);
2898
+ var tspan = tagFactory("tspan", SVG_NS2);
2899
+ var defs = tagFactory("defs", SVG_NS2);
2900
+ var clipPath = tagFactory("clipPath", SVG_NS2);
2901
+ var mask = tagFactory("mask", SVG_NS2);
2902
+ var pattern = tagFactory("pattern", SVG_NS2);
2903
+ var linearGradient = tagFactory("linearGradient", SVG_NS2);
2904
+ var radialGradient = tagFactory("radialGradient", SVG_NS2);
2905
+ var stop = tagFactory("stop", SVG_NS2);
2906
+ var use = tagFactory("use", SVG_NS2);
2907
+ var symbol = tagFactory("symbol", SVG_NS2);
2908
+ var marker = tagFactory("marker", SVG_NS2);
2314
2909
  var center = tagFactory("center");
2315
2910
  var font = tagFactory("font");
2316
2911
  var marquee = tagFactory("marquee");
2317
2912
 
2318
- // src/utils/globalSingleton.ts
2319
- function globalSingleton(key, create) {
2320
- const g2 = globalThis;
2321
- return g2[key] ?? (g2[key] = create());
2322
- }
2323
-
2324
2913
  // src/platform/microfrontend.ts
2325
2914
  var moduleCache = globalSingleton(
2326
2915
  /* @__PURE__ */ Symbol.for("sibujs.microfrontend.moduleCache.v1"),
@@ -2337,14 +2926,13 @@ function createMicroApp(config) {
2337
2926
  }
2338
2927
  let mounted = false;
2339
2928
  function mount(component) {
2340
- root.replaceChildren();
2341
2929
  const el = component();
2342
- root.appendChild(el);
2930
+ replaceChildrenSafely(root, el);
2343
2931
  mounted = true;
2344
2932
  }
2345
2933
  function unmount() {
2346
2934
  if (!mounted) return;
2347
- root.replaceChildren();
2935
+ replaceChildrenSafely(root);
2348
2936
  mounted = false;
2349
2937
  }
2350
2938
  return { mount, unmount, element: host };
@@ -2391,18 +2979,21 @@ function defineRemoteComponent(name, loader) {
2391
2979
  class: "sibu-remote",
2392
2980
  "data-remote-component": name
2393
2981
  });
2394
- container.appendChild(span({ class: "sibu-remote-loading", nodes: "Loading..." }));
2982
+ container.appendChild(span({ class: "sibu-remote-loading" }, "Loading..."));
2983
+ let disposed = false;
2984
+ registerDisposer(container, () => {
2985
+ disposed = true;
2986
+ });
2395
2987
  loader().then((mod) => {
2396
2988
  cached = mod.default;
2397
- const rendered = cached();
2398
- container.replaceChildren(rendered);
2989
+ if (disposed) return;
2990
+ replaceChildrenSafely(container, cached());
2399
2991
  }).catch((err) => {
2992
+ if (disposed) return;
2400
2993
  const message = err instanceof Error ? err.message : String(err);
2401
- container.replaceChildren(
2402
- div({
2403
- class: "sibu-remote-error",
2404
- nodes: `Failed to load remote component "${name}": ${message}`
2405
- })
2994
+ replaceChildrenSafely(
2995
+ container,
2996
+ div({ class: "sibu-remote-error" }, `Failed to load remote component "${name}": ${message}`)
2406
2997
  );
2407
2998
  });
2408
2999
  return container;
@@ -2458,11 +3049,16 @@ function serviceWorker(scriptUrl, options) {
2458
3049
  const [isReady, setIsReady] = signal(false);
2459
3050
  const [isUpdateAvailable, setIsUpdateAvailable] = signal(false);
2460
3051
  const [error, setError] = signal(null);
2461
- let disposed = false;
3052
+ let unregistered = false;
3053
+ let unregisterRequested = false;
3054
+ let registrationPromise = null;
3055
+ let arrivedRegistration = null;
3056
+ let pendingUnregister = null;
2462
3057
  let updateFoundHandler = null;
2463
3058
  let stateChangeHandler = null;
2464
3059
  let trackedWorker = null;
2465
3060
  let trackedReg = null;
3061
+ const supported = typeof navigator !== "undefined" && !!navigator.serviceWorker;
2466
3062
  function detachListeners() {
2467
3063
  if (trackedReg && updateFoundHandler) {
2468
3064
  trackedReg.removeEventListener("updatefound", updateFoundHandler);
@@ -2475,33 +3071,53 @@ function serviceWorker(scriptUrl, options) {
2475
3071
  trackedWorker = null;
2476
3072
  trackedReg = null;
2477
3073
  }
2478
- if ("serviceWorker" in navigator) {
2479
- navigator.serviceWorker.register(scriptUrl, options).then((reg) => {
2480
- if (disposed) return;
2481
- setRegistration(reg);
2482
- setIsReady(true);
2483
- trackedReg = reg;
2484
- updateFoundHandler = () => {
2485
- if (disposed) return;
2486
- const newWorker = reg.installing;
2487
- if (newWorker) {
2488
- if (trackedWorker && stateChangeHandler) {
2489
- trackedWorker.removeEventListener("statechange", stateChangeHandler);
2490
- }
2491
- trackedWorker = newWorker;
2492
- stateChangeHandler = () => {
2493
- if (disposed) return;
2494
- if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
2495
- setIsUpdateAvailable(true);
2496
- }
2497
- };
2498
- newWorker.addEventListener("statechange", stateChangeHandler);
3074
+ function attachListeners(reg) {
3075
+ trackedReg = reg;
3076
+ updateFoundHandler = () => {
3077
+ if (unregistered) return;
3078
+ const newWorker = reg.installing;
3079
+ if (newWorker) {
3080
+ if (trackedWorker && stateChangeHandler) {
3081
+ trackedWorker.removeEventListener("statechange", stateChangeHandler);
2499
3082
  }
2500
- };
2501
- reg.addEventListener("updatefound", updateFoundHandler);
2502
- }).catch((err) => {
2503
- if (disposed) return;
2504
- setError(err instanceof Error ? err : new Error(String(err)));
3083
+ trackedWorker = newWorker;
3084
+ stateChangeHandler = () => {
3085
+ if (unregistered) return;
3086
+ if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
3087
+ setIsUpdateAvailable(true);
3088
+ }
3089
+ };
3090
+ newWorker.addEventListener("statechange", stateChangeHandler);
3091
+ }
3092
+ };
3093
+ reg.addEventListener("updatefound", updateFoundHandler);
3094
+ }
3095
+ function activate(reg) {
3096
+ setRegistration(reg);
3097
+ setIsReady(true);
3098
+ attachListeners(reg);
3099
+ }
3100
+ function recoverFailedUnregister() {
3101
+ unregisterRequested = false;
3102
+ if (!registration() && arrivedRegistration) {
3103
+ activate(arrivedRegistration);
3104
+ arrivedRegistration = null;
3105
+ }
3106
+ }
3107
+ function adopt(reg) {
3108
+ if (unregisterRequested) {
3109
+ arrivedRegistration = reg;
3110
+ return null;
3111
+ }
3112
+ activate(reg);
3113
+ return reg;
3114
+ }
3115
+ if (supported) {
3116
+ registrationPromise = navigator.serviceWorker.register(scriptUrl, options).then(adopt).catch((err) => {
3117
+ if (!unregistered) {
3118
+ setError(err instanceof Error ? err : new Error(String(err)));
3119
+ }
3120
+ return null;
2505
3121
  });
2506
3122
  }
2507
3123
  async function update() {
@@ -2510,19 +3126,43 @@ function serviceWorker(scriptUrl, options) {
2510
3126
  await reg.update();
2511
3127
  }
2512
3128
  }
2513
- async function unregister() {
2514
- disposed = true;
2515
- detachListeners();
2516
- const reg = registration();
2517
- if (reg) {
2518
- const result = await reg.unregister();
2519
- if (result) {
2520
- setRegistration(null);
2521
- setIsReady(false);
2522
- }
2523
- return result;
3129
+ async function performUnregister() {
3130
+ unregisterRequested = true;
3131
+ if (registrationPromise) await registrationPromise.catch(() => null);
3132
+ const reg = registration() ?? arrivedRegistration;
3133
+ if (!reg) {
3134
+ unregisterRequested = false;
3135
+ return false;
2524
3136
  }
2525
- return false;
3137
+ let result;
3138
+ try {
3139
+ result = await reg.unregister();
3140
+ } catch (err) {
3141
+ recoverFailedUnregister();
3142
+ throw err;
3143
+ }
3144
+ if (result) {
3145
+ detachListeners();
3146
+ setRegistration(null);
3147
+ setIsReady(false);
3148
+ setIsUpdateAvailable(false);
3149
+ arrivedRegistration = null;
3150
+ unregistered = true;
3151
+ } else {
3152
+ recoverFailedUnregister();
3153
+ }
3154
+ return result;
3155
+ }
3156
+ async function unregister() {
3157
+ if (!supported || unregistered) return false;
3158
+ if (pendingUnregister) return pendingUnregister;
3159
+ const request = performUnregister();
3160
+ pendingUnregister = request;
3161
+ void request.catch(() => {
3162
+ }).then(() => {
3163
+ if (pendingUnregister === request) pendingUnregister = null;
3164
+ });
3165
+ return request;
2526
3166
  }
2527
3167
  return { registration, isReady, isUpdateAvailable, error, update, unregister };
2528
3168
  }
@@ -2750,22 +3390,34 @@ var instanceCache = globalSingleton(
2750
3390
  /* @__PURE__ */ Symbol.for("sibujs.wasm.instanceCache.v1"),
2751
3391
  () => /* @__PURE__ */ new Map()
2752
3392
  );
3393
+ var inFlightCache = globalSingleton(
3394
+ /* @__PURE__ */ Symbol.for("sibujs.wasm.inFlightCache.v1"),
3395
+ () => /* @__PURE__ */ new Map()
3396
+ );
3397
+ var cacheState = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.wasm.cacheState.v1"), () => ({ generation: 0 }));
3398
+ function isCurrentGeneration(generation) {
3399
+ return generation === cacheState.generation;
3400
+ }
2753
3401
  function wasm(source2, config = {}) {
2754
3402
  const [instance, setInstance] = signal(null);
2755
3403
  const [loading, setLoading] = signal(true);
2756
3404
  const [error, setError] = signal(null);
2757
3405
  const cacheKey = config.cacheKey || (typeof source2 === "string" ? source2 : void 0);
3406
+ let runId = 0;
2758
3407
  async function load() {
3408
+ const myRun = ++runId;
2759
3409
  setLoading(true);
2760
3410
  setError(null);
2761
3411
  setInstance(null);
2762
3412
  try {
2763
- const wasmInstance = await loadWasmModule(source2, config.imports, cacheKey);
3413
+ const wasmInstance = await loadWasmModuleWithOptions(source2, { ...config, cacheKey });
3414
+ if (myRun !== runId) return;
2764
3415
  setInstance(wasmInstance.exports);
2765
3416
  } catch (err) {
3417
+ if (myRun !== runId) return;
2766
3418
  setError(err instanceof Error ? err : new Error(String(err)));
2767
3419
  } finally {
2768
- setLoading(false);
3420
+ if (myRun === runId) setLoading(false);
2769
3421
  }
2770
3422
  }
2771
3423
  load();
@@ -2778,9 +3430,9 @@ function wasm(source2, config = {}) {
2778
3430
  };
2779
3431
  }
2780
3432
  async function loadWasmModule(source2, imports, cacheKey) {
2781
- const isOptionsBag = !!(imports && ("allowedOrigins" in imports || "unsafelyAllowAnyOrigin" in imports));
2782
- const opts = isOptionsBag ? imports : { imports, cacheKey };
2783
- const wasmImports = opts.imports;
3433
+ return loadWasmModuleWithOptions(source2, { imports, cacheKey });
3434
+ }
3435
+ async function loadWasmModuleWithOptions(source2, opts = {}) {
2784
3436
  const key = opts.cacheKey || (typeof source2 === "string" ? source2 : void 0);
2785
3437
  if (typeof source2 === "string") {
2786
3438
  const allowed = opts.allowedOrigins ?? [];
@@ -2800,44 +3452,50 @@ async function loadWasmModule(source2, imports, cacheKey) {
2800
3452
  );
2801
3453
  }
2802
3454
  }
2803
- if (key) {
2804
- const cachedInstance = instanceCache.get(key);
2805
- if (cachedInstance) {
2806
- return cachedInstance;
2807
- }
2808
- }
2809
- let module2;
3455
+ const generation = cacheState.generation;
3456
+ if (!key) return instantiateWasm(source2, opts.imports, void 0, generation);
3457
+ const cachedInstance = instanceCache.get(key);
3458
+ if (cachedInstance) return cachedInstance;
3459
+ const inFlight = inFlightCache.get(key);
3460
+ if (inFlight) return inFlight;
3461
+ const pending = instantiateWasm(source2, opts.imports, key, generation).then((instance) => {
3462
+ if (isCurrentGeneration(generation)) instanceCache.set(key, instance);
3463
+ return instance;
3464
+ });
3465
+ inFlightCache.set(key, pending);
3466
+ void pending.catch(() => {
3467
+ }).then(() => {
3468
+ if (inFlightCache.get(key) === pending) inFlightCache.delete(key);
3469
+ });
3470
+ return pending;
3471
+ }
3472
+ async function instantiateWasm(source2, wasmImports, key, generation) {
2810
3473
  const cachedModule = key ? moduleCache2.get(key) : void 0;
2811
3474
  if (cachedModule) {
2812
- module2 = cachedModule;
3475
+ return WebAssembly.instantiate(cachedModule, wasmImports || {});
3476
+ }
3477
+ let bytes;
3478
+ if (typeof source2 === "string") {
3479
+ if (typeof WebAssembly.instantiateStreaming === "function") {
3480
+ const response2 = fetch(source2);
3481
+ const result = await WebAssembly.instantiateStreaming(response2, wasmImports || {});
3482
+ if (key && isCurrentGeneration(generation)) moduleCache2.set(key, result.module);
3483
+ return result.instance;
3484
+ }
3485
+ const response = await fetch(source2);
3486
+ bytes = await response.arrayBuffer();
3487
+ } else if (source2 instanceof Uint8Array) {
3488
+ bytes = source2.buffer.slice(source2.byteOffset, source2.byteOffset + source2.byteLength);
2813
3489
  } else {
2814
- let bytes;
2815
- if (typeof source2 === "string") {
2816
- if (typeof WebAssembly.instantiateStreaming === "function") {
2817
- const response2 = fetch(source2);
2818
- const result = await WebAssembly.instantiateStreaming(response2, wasmImports || {});
2819
- if (key) {
2820
- moduleCache2.set(key, result.module);
2821
- instanceCache.set(key, result.instance);
2822
- }
2823
- return result.instance;
2824
- }
2825
- const response = await fetch(source2);
2826
- bytes = await response.arrayBuffer();
2827
- } else if (source2 instanceof Uint8Array) {
2828
- bytes = source2.buffer.slice(source2.byteOffset, source2.byteOffset + source2.byteLength);
2829
- } else {
2830
- bytes = source2;
2831
- }
2832
- module2 = await WebAssembly.compile(bytes);
2833
- if (key) moduleCache2.set(key, module2);
3490
+ bytes = source2;
2834
3491
  }
2835
- const instance = await WebAssembly.instantiate(module2, wasmImports || {});
2836
- if (key) instanceCache.set(key, instance);
2837
- return instance;
3492
+ const module2 = await WebAssembly.compile(bytes);
3493
+ if (key && isCurrentGeneration(generation)) moduleCache2.set(key, module2);
3494
+ return WebAssembly.instantiate(module2, wasmImports || {});
2838
3495
  }
2839
3496
  async function preloadWasm(url, options = {}) {
2840
3497
  if (moduleCache2.has(url)) return;
3498
+ const generation = cacheState.generation;
2841
3499
  const allowed = options.allowedOrigins ?? [];
2842
3500
  if (allowed.length > 0) {
2843
3501
  let parsed;
@@ -2862,7 +3520,7 @@ async function preloadWasm(url, options = {}) {
2862
3520
  const bytes = await response.arrayBuffer();
2863
3521
  module2 = await WebAssembly.compile(bytes);
2864
3522
  }
2865
- moduleCache2.set(url, module2);
3523
+ if (isCurrentGeneration(generation)) moduleCache2.set(url, module2);
2866
3524
  }
2867
3525
  function createWasmBridge(instance) {
2868
3526
  const exports2 = instance.exports;
@@ -2902,8 +3560,10 @@ function createWasmBridge(instance) {
2902
3560
  };
2903
3561
  }
2904
3562
  function clearWasmCache() {
3563
+ cacheState.generation++;
2905
3564
  moduleCache2.clear();
2906
3565
  instanceCache.clear();
3566
+ inFlightCache.clear();
2907
3567
  }
2908
3568
  function isWasmCached(key) {
2909
3569
  return moduleCache2.has(key);
@@ -2933,6 +3593,7 @@ function isWasmCached(key) {
2933
3593
  island,
2934
3594
  loadRemoteModule,
2935
3595
  loadWasmModule,
3596
+ loadWasmModuleWithOptions,
2936
3597
  preloadWasm,
2937
3598
  renderToDocument,
2938
3599
  renderToReadableStream,