sibujs 1.4.0 → 2.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 (190) hide show
  1. package/README.md +105 -119
  2. package/dist/browser.cjs +288 -80
  3. package/dist/browser.d.cts +19 -9
  4. package/dist/browser.d.ts +19 -9
  5. package/dist/browser.js +6 -6
  6. package/dist/build.cjs +1019 -313
  7. package/dist/build.d.cts +1 -1
  8. package/dist/build.d.ts +1 -1
  9. package/dist/build.js +15 -13
  10. package/dist/cdn.global.js +17 -16
  11. package/dist/chunk-2RA7SHDA.js +65 -0
  12. package/dist/chunk-2UPRY23K.js +80 -0
  13. package/dist/chunk-3JHCYHWN.js +125 -0
  14. package/dist/{chunk-ZWKZCBO6.js → chunk-3LR7GLWQ.js} +154 -33
  15. package/dist/{chunk-3AIRKM3B.js → chunk-3NSGB5JN.js} +115 -34
  16. package/dist/{chunk-3ARAQO7B.js → chunk-52YJLLRO.js} +29 -6
  17. package/dist/chunk-54EDRCEF.js +93 -0
  18. package/dist/chunk-7JDB7I65.js +1327 -0
  19. package/dist/{chunk-WZSPOOER.js → chunk-CC65Y57T.js} +8 -5
  20. package/dist/{chunk-23VV7YD3.js → chunk-DFPFITST.js} +25 -30
  21. package/dist/{chunk-WR5D4EGH.js → chunk-GTBNNBJ6.js} +14 -2
  22. package/dist/chunk-HB24TBAF.js +121 -0
  23. package/dist/{chunk-CZUGLNJS.js → chunk-ITX6OO3F.js} +3 -3
  24. package/dist/{chunk-JAKHTMQU.js → chunk-JA6667UN.js} +206 -46
  25. package/dist/{chunk-77L6NL3X.js → chunk-JXMMDLBY.js} +306 -183
  26. package/dist/{chunk-3X2YG6YM.js → chunk-JYD2PWXH.js} +59 -28
  27. package/dist/{chunk-F3FA4F32.js → chunk-KLRMB5ZS.js} +135 -79
  28. package/dist/{chunk-5X6PP2UK.js → chunk-LMLD24FC.js} +2 -2
  29. package/dist/{chunk-M4NLBH4I.js → chunk-LYTCUZ7H.js} +3 -2
  30. package/dist/{chunk-TSOKIX5Z.js → chunk-MIUAXB7K.js} +126 -74
  31. package/dist/{chunk-QWZG56ET.js → chunk-ND2664SF.js} +558 -190
  32. package/dist/{chunk-JCI5M6U6.js → chunk-O2MNQFLP.js} +261 -79
  33. package/dist/{chunk-EWFVA3TJ.js → chunk-R73P76YZ.js} +1 -1
  34. package/dist/{chunk-2BYQDGN3.js → chunk-SAHNHTFC.js} +234 -63
  35. package/dist/chunk-UCS6AMJ7.js +79 -0
  36. package/dist/{chunk-ZD6OAMTH.js → chunk-VLPPXTYG.js} +90 -35
  37. package/dist/{chunk-OUZZEE4S.js → chunk-WOMYAHHI.js} +17 -11
  38. package/dist/{contracts-xo5ckdRP.d.cts → contracts-ey_Qh8ef.d.cts} +7 -8
  39. package/dist/{contracts-xo5ckdRP.d.ts → contracts-ey_Qh8ef.d.ts} +7 -8
  40. package/dist/{customElement-D2DJp_xn.d.cts → customElement-CPfIrbvg.d.cts} +18 -9
  41. package/dist/{customElement-D2DJp_xn.d.ts → customElement-CPfIrbvg.d.ts} +18 -9
  42. package/dist/data.cjs +452 -100
  43. package/dist/data.d.cts +20 -2
  44. package/dist/data.d.ts +20 -2
  45. package/dist/data.js +11 -9
  46. package/dist/devtools.cjs +535 -247
  47. package/dist/devtools.d.cts +1 -1
  48. package/dist/devtools.d.ts +1 -1
  49. package/dist/devtools.js +34 -30
  50. package/dist/ecosystem.cjs +499 -143
  51. package/dist/ecosystem.d.cts +13 -11
  52. package/dist/ecosystem.d.ts +13 -11
  53. package/dist/ecosystem.js +12 -11
  54. package/dist/extras.cjs +3639 -1629
  55. package/dist/extras.d.cts +11 -11
  56. package/dist/extras.d.ts +11 -11
  57. package/dist/extras.js +58 -45
  58. package/dist/index.cjs +1023 -313
  59. package/dist/index.d.cts +128 -55
  60. package/dist/index.d.ts +128 -55
  61. package/dist/index.js +28 -16
  62. package/dist/{introspect-BumjnBKr.d.cts → introspect-BWNjNw64.d.cts} +22 -2
  63. package/dist/{introspect-CZrlcaYy.d.ts → introspect-cY2pg9pW.d.ts} +22 -2
  64. package/dist/motion.cjs +90 -36
  65. package/dist/motion.d.cts +1 -1
  66. package/dist/motion.d.ts +1 -1
  67. package/dist/motion.js +4 -4
  68. package/dist/patterns.cjs +414 -81
  69. package/dist/patterns.d.cts +53 -20
  70. package/dist/patterns.d.ts +53 -20
  71. package/dist/patterns.js +7 -7
  72. package/dist/performance.cjs +364 -108
  73. package/dist/performance.d.cts +29 -17
  74. package/dist/performance.d.ts +29 -17
  75. package/dist/performance.js +13 -6
  76. package/dist/plugin-D30wlGW5.d.cts +71 -0
  77. package/dist/plugin-D30wlGW5.d.ts +71 -0
  78. package/dist/plugins.cjs +652 -271
  79. package/dist/plugins.d.cts +13 -6
  80. package/dist/plugins.d.ts +13 -6
  81. package/dist/plugins.js +116 -50
  82. package/dist/{ssr-Do_SiVoL.d.cts → ssr-CrVNy6Pa.d.cts} +9 -15
  83. package/dist/{ssr-Do_SiVoL.d.ts → ssr-CrVNy6Pa.d.ts} +9 -15
  84. package/dist/{ssr-4PBXAOO3.js → ssr-FXD2PPMC.js} +4 -3
  85. package/dist/ssr.cjs +648 -219
  86. package/dist/ssr.d.cts +27 -7
  87. package/dist/ssr.d.ts +27 -7
  88. package/dist/ssr.js +12 -11
  89. package/dist/{tagFactory-DaJ0YWX6.d.ts → tagFactory-S17H2qxu.d.cts} +9 -1
  90. package/dist/{tagFactory-DaJ0YWX6.d.cts → tagFactory-S17H2qxu.d.ts} +9 -1
  91. package/dist/testing.cjs +252 -63
  92. package/dist/testing.d.cts +17 -4
  93. package/dist/testing.d.ts +17 -4
  94. package/dist/testing.js +100 -44
  95. package/dist/ui.cjs +576 -168
  96. package/dist/ui.d.cts +13 -16
  97. package/dist/ui.d.ts +13 -16
  98. package/dist/ui.js +20 -17
  99. package/dist/widgets.cjs +1001 -93
  100. package/dist/widgets.d.cts +104 -2
  101. package/dist/widgets.d.ts +104 -2
  102. package/dist/widgets.js +9 -7
  103. package/package.json +8 -2
  104. package/dist/chunk-32DY64NT.js +0 -282
  105. package/dist/chunk-3CRQALYP.js +0 -877
  106. package/dist/chunk-4EI4AG32.js +0 -482
  107. package/dist/chunk-4MYMUBRS.js +0 -21
  108. package/dist/chunk-6HLLIF3K.js +0 -398
  109. package/dist/chunk-6LSNVCS2.js +0 -937
  110. package/dist/chunk-6SA3QQES.js +0 -61
  111. package/dist/chunk-7BF6TK55.js +0 -1097
  112. package/dist/chunk-7TQKR4PP.js +0 -294
  113. package/dist/chunk-7V26P53V.js +0 -712
  114. package/dist/chunk-AZ3ISID5.js +0 -298
  115. package/dist/chunk-B7SWRFUT.js +0 -332
  116. package/dist/chunk-BGN5ZMP4.js +0 -26
  117. package/dist/chunk-BTU3TJDS.js +0 -365
  118. package/dist/chunk-BW3WT46K.js +0 -937
  119. package/dist/chunk-C6KFWOFV.js +0 -616
  120. package/dist/chunk-CHF5OHIA.js +0 -61
  121. package/dist/chunk-CHJ27IGK.js +0 -26
  122. package/dist/chunk-CMBFNA7L.js +0 -27
  123. package/dist/chunk-DAHRH4ON.js +0 -331
  124. package/dist/chunk-DKOHBI74.js +0 -924
  125. package/dist/chunk-DTCOOBMX.js +0 -725
  126. package/dist/chunk-EBGIRKQY.js +0 -616
  127. package/dist/chunk-EUZND3CB.js +0 -27
  128. package/dist/chunk-EVCZO745.js +0 -365
  129. package/dist/chunk-FGOEVHY3.js +0 -60
  130. package/dist/chunk-G3BOQPVO.js +0 -365
  131. package/dist/chunk-GCOK2LC3.js +0 -282
  132. package/dist/chunk-HGMJFBC7.js +0 -654
  133. package/dist/chunk-K5ZUMYVS.js +0 -89
  134. package/dist/chunk-KQPDEVVS.js +0 -398
  135. package/dist/chunk-L6JRBDNS.js +0 -60
  136. package/dist/chunk-LA6KQEDU.js +0 -712
  137. package/dist/chunk-MDVXJWFN.js +0 -304
  138. package/dist/chunk-MEZVEBPN.js +0 -2008
  139. package/dist/chunk-MK4ERFYL.js +0 -2249
  140. package/dist/chunk-MLKGABMK.js +0 -9
  141. package/dist/chunk-MQ5GOYPH.js +0 -2249
  142. package/dist/chunk-N6IZB6KJ.js +0 -567
  143. package/dist/chunk-NEKUBFPT.js +0 -60
  144. package/dist/chunk-NHUC2QWH.js +0 -282
  145. package/dist/chunk-NMRUZALC.js +0 -1097
  146. package/dist/chunk-NYVAC6P5.js +0 -37
  147. package/dist/chunk-OF7UZIVB.js +0 -725
  148. package/dist/chunk-P6W3STU4.js +0 -2249
  149. package/dist/chunk-PBHF5WKN.js +0 -616
  150. package/dist/chunk-PTQJDMRT.js +0 -146
  151. package/dist/chunk-PZEGYCF5.js +0 -61
  152. package/dist/chunk-QBMDLBU2.js +0 -975
  153. package/dist/chunk-RQGQSLQK.js +0 -725
  154. package/dist/chunk-SDLZDHKP.js +0 -107
  155. package/dist/chunk-TNQWPPE6.js +0 -37
  156. package/dist/chunk-UHNL42EF.js +0 -2730
  157. package/dist/chunk-UNXCEF6S.js +0 -21
  158. package/dist/chunk-V2XTI523.js +0 -347
  159. package/dist/chunk-VAU366PN.js +0 -2241
  160. package/dist/chunk-VMVDTCXB.js +0 -712
  161. package/dist/chunk-VRW3FULF.js +0 -725
  162. package/dist/chunk-WADYRCO2.js +0 -304
  163. package/dist/chunk-WILQZRO4.js +0 -282
  164. package/dist/chunk-WUHJISPP.js +0 -298
  165. package/dist/chunk-XYU6TZOW.js +0 -182
  166. package/dist/chunk-Y6GP4QGG.js +0 -276
  167. package/dist/chunk-YECR7UIA.js +0 -347
  168. package/dist/chunk-YUTWTI4B.js +0 -654
  169. package/dist/chunk-Z65KYU7I.js +0 -26
  170. package/dist/chunk-Z6POF5YC.js +0 -975
  171. package/dist/chunk-ZBJP6WFL.js +0 -482
  172. package/dist/contracts-DDrwxvJ-.d.cts +0 -245
  173. package/dist/contracts-DDrwxvJ-.d.ts +0 -245
  174. package/dist/contracts-DOrhwbke.d.cts +0 -245
  175. package/dist/contracts-DOrhwbke.d.ts +0 -245
  176. package/dist/customElement-BKQfbSZQ.d.cts +0 -262
  177. package/dist/customElement-BKQfbSZQ.d.ts +0 -262
  178. package/dist/customElement-yz8uyk-0.d.cts +0 -308
  179. package/dist/customElement-yz8uyk-0.d.ts +0 -308
  180. package/dist/introspect-Cb0zgpi2.d.cts +0 -477
  181. package/dist/introspect-Y2xNXGSf.d.ts +0 -477
  182. package/dist/plugin-Bek4RhJY.d.cts +0 -43
  183. package/dist/plugin-Bek4RhJY.d.ts +0 -43
  184. package/dist/ssr-3RXHP5ES.js +0 -38
  185. package/dist/ssr-6GIMY5MX.js +0 -38
  186. package/dist/ssr-BA6sxxUd.d.cts +0 -135
  187. package/dist/ssr-BA6sxxUd.d.ts +0 -135
  188. package/dist/ssr-WKUPVSSK.js +0 -36
  189. package/dist/tagFactory-Dl8QCLga.d.cts +0 -23
  190. package/dist/tagFactory-Dl8QCLga.d.ts +0 -23
package/dist/browser.cjs CHANGED
@@ -66,12 +66,12 @@ function isDev() {
66
66
  var _isDev = isDev();
67
67
  function devAssert(condition, message) {
68
68
  if (_isDev && !condition) {
69
- throw new Error(`[Sibu] ${message}`);
69
+ throw new Error(`[SibuJS] ${message}`);
70
70
  }
71
71
  }
72
72
  function devWarn(message) {
73
73
  if (_isDev) {
74
- console.warn(`[Sibu] ${message}`);
74
+ console.warn(`[SibuJS] ${message}`);
75
75
  }
76
76
  }
77
77
 
@@ -81,11 +81,11 @@ var subscriberStack = new Array(32);
81
81
  var stackCapacity = 32;
82
82
  var stackTop = -1;
83
83
  var currentSubscriber = null;
84
- var signalSubscribers = /* @__PURE__ */ new WeakMap();
85
84
  var SUBS = "__s";
86
85
  var notifyDepth = 0;
87
86
  var pendingQueue = [];
88
87
  var pendingSet = /* @__PURE__ */ new Set();
88
+ var propagateStack = [];
89
89
  function safeInvoke(sub) {
90
90
  try {
91
91
  sub();
@@ -131,7 +131,6 @@ function recordDependency(signal2) {
131
131
  let subs = signal2[SUBS];
132
132
  if (!subs) {
133
133
  subs = /* @__PURE__ */ new Set();
134
- signalSubscribers.set(signal2, subs);
135
134
  signal2[SUBS] = subs;
136
135
  }
137
136
  subs.add(currentSubscriber);
@@ -153,57 +152,71 @@ function queueSignalNotification(signal2) {
153
152
  }
154
153
  }
155
154
  }
155
+ var maxDrainIterations = 1e5;
156
156
  function drainNotificationQueue() {
157
157
  if (notifyDepth > 0) return;
158
158
  notifyDepth++;
159
159
  try {
160
160
  let i = 0;
161
161
  while (i < pendingQueue.length) {
162
+ if (i >= maxDrainIterations) {
163
+ if (typeof console !== "undefined") {
164
+ console.error(
165
+ `[SibuJS] Notification queue exceeded ${maxDrainIterations} iterations \u2014 likely an effect that writes to a signal it reads. Breaking to prevent infinite loop.`
166
+ );
167
+ }
168
+ break;
169
+ }
162
170
  safeInvoke(pendingQueue[i]);
163
171
  i++;
164
172
  }
165
173
  } finally {
166
- pendingQueue.length = 0;
167
- pendingSet.clear();
168
174
  notifyDepth--;
175
+ if (notifyDepth === 0) {
176
+ pendingQueue.length = 0;
177
+ pendingSet.clear();
178
+ }
169
179
  }
170
180
  }
171
181
  function propagateDirty(sub) {
172
182
  sub();
173
- let sig = sub._sig;
174
- while (sig) {
183
+ const rootSig = sub._sig;
184
+ if (!rootSig) return;
185
+ const stack = propagateStack;
186
+ const baseLen = stack.length;
187
+ stack.push(rootSig);
188
+ while (stack.length > baseLen) {
189
+ const sig = stack.pop();
175
190
  const first = sig.__f;
176
191
  if (first) {
177
192
  if (first._c) {
178
193
  const nSig = first._sig;
179
- nSig._d = true;
180
- sig = nSig;
181
- continue;
182
- }
183
- if (!pendingSet.has(first)) {
194
+ if (!nSig._d) {
195
+ nSig._d = true;
196
+ stack.push(nSig);
197
+ }
198
+ } else if (!pendingSet.has(first)) {
184
199
  pendingSet.add(first);
185
200
  pendingQueue.push(first);
186
201
  }
187
- break;
202
+ continue;
188
203
  }
189
204
  const subs = sig[SUBS];
190
- if (!subs) break;
191
- let nextSig;
205
+ if (!subs) continue;
192
206
  for (const s of subs) {
193
207
  if (s._c) {
194
- s();
195
208
  const nSig = s._sig;
196
- if (nSig && !nextSig) {
197
- nextSig = nSig;
198
- } else if (nSig) {
199
- propagateDirty(s);
209
+ if (nSig && !nSig._d) {
210
+ nSig._d = true;
211
+ stack.push(nSig);
212
+ } else if (!nSig) {
213
+ s();
200
214
  }
201
215
  } else if (!pendingSet.has(s)) {
202
216
  pendingSet.add(s);
203
217
  pendingQueue.push(s);
204
218
  }
205
219
  }
206
- sig = nextSig;
207
220
  }
208
221
  }
209
222
  function notifySubscribers(signal2) {
@@ -227,13 +240,23 @@ function notifySubscribers(signal2) {
227
240
  }
228
241
  let i = 0;
229
242
  while (i < pendingQueue.length) {
243
+ if (i >= maxDrainIterations) {
244
+ if (typeof console !== "undefined") {
245
+ console.error(
246
+ `[SibuJS] Notification queue exceeded ${maxDrainIterations} iterations \u2014 likely an effect that writes to a signal it reads. Breaking to prevent infinite loop.`
247
+ );
248
+ }
249
+ break;
250
+ }
230
251
  safeInvoke(pendingQueue[i]);
231
252
  i++;
232
253
  }
233
254
  } finally {
234
- pendingQueue.length = 0;
235
- pendingSet.clear();
236
255
  notifyDepth--;
256
+ if (notifyDepth === 0) {
257
+ pendingQueue.length = 0;
258
+ pendingSet.clear();
259
+ }
237
260
  }
238
261
  return;
239
262
  }
@@ -253,30 +276,48 @@ function notifySubscribers(signal2) {
253
276
  notifyDepth++;
254
277
  try {
255
278
  let directCount = 0;
279
+ let hasComputedSub = false;
256
280
  for (const sub of subs) {
281
+ if (sub._c) hasComputedSub = true;
257
282
  pendingQueue[directCount++] = sub;
258
283
  }
259
- for (let i2 = 0; i2 < directCount; i2++) {
260
- if (pendingQueue[i2]._c) {
261
- propagateDirty(pendingQueue[i2]);
284
+ if (!hasComputedSub) {
285
+ for (let i2 = 0; i2 < directCount; i2++) {
286
+ safeInvoke(pendingQueue[i2]);
262
287
  }
263
- }
264
- for (let i2 = 0; i2 < directCount; i2++) {
265
- if (!pendingQueue[i2]._c) {
266
- if (!pendingSet.has(pendingQueue[i2])) {
267
- safeInvoke(pendingQueue[i2]);
288
+ } else {
289
+ for (let i2 = 0; i2 < directCount; i2++) {
290
+ if (pendingQueue[i2]._c) {
291
+ propagateDirty(pendingQueue[i2]);
292
+ }
293
+ }
294
+ for (let i2 = 0; i2 < directCount; i2++) {
295
+ const sub = pendingQueue[i2];
296
+ if (!sub._c && !pendingSet.has(sub)) {
297
+ pendingSet.add(sub);
298
+ safeInvoke(sub);
268
299
  }
269
300
  }
270
301
  }
271
302
  let i = directCount;
272
303
  while (i < pendingQueue.length) {
304
+ if (i - directCount >= maxDrainIterations) {
305
+ if (typeof console !== "undefined") {
306
+ console.error(
307
+ `[SibuJS] Notification queue exceeded ${maxDrainIterations} iterations \u2014 likely an effect that writes to a signal it reads. Breaking to prevent infinite loop.`
308
+ );
309
+ }
310
+ break;
311
+ }
273
312
  safeInvoke(pendingQueue[i]);
274
313
  i++;
275
314
  }
276
315
  } finally {
277
- pendingQueue.length = 0;
278
- pendingSet.clear();
279
316
  notifyDepth--;
317
+ if (notifyDepth === 0) {
318
+ pendingQueue.length = 0;
319
+ pendingSet.clear();
320
+ }
280
321
  }
281
322
  }
282
323
  function cleanup(subscriber) {
@@ -287,7 +328,9 @@ function cleanup(subscriber) {
287
328
  if (subs) {
288
329
  subs.delete(subscriber);
289
330
  if (singleDep.__f === subscriber) {
290
- singleDep.__f = void 0;
331
+ singleDep.__f = subs.size === 1 ? subs.values().next().value : void 0;
332
+ } else if (subs.size === 1 && singleDep.__f === void 0) {
333
+ singleDep.__f = subs.values().next().value;
291
334
  }
292
335
  }
293
336
  sub._dep = void 0;
@@ -300,7 +343,9 @@ function cleanup(subscriber) {
300
343
  if (subs) {
301
344
  subs.delete(subscriber);
302
345
  if (signal2.__f === subscriber) {
303
- signal2.__f = void 0;
346
+ signal2.__f = subs.size === 1 ? subs.values().next().value : void 0;
347
+ } else if (subs.size === 1 && signal2.__f === void 0) {
348
+ signal2.__f = subs.values().next().value;
304
349
  }
305
350
  }
306
351
  }
@@ -327,10 +372,13 @@ function enqueueBatchedSignal(signal2) {
327
372
  return true;
328
373
  }
329
374
  function flushBatch() {
330
- for (const signal2 of pendingSignals) {
331
- queueSignalNotification(signal2);
375
+ try {
376
+ for (const signal2 of pendingSignals) {
377
+ queueSignalNotification(signal2);
378
+ }
379
+ } finally {
380
+ pendingSignals.clear();
332
381
  }
333
- pendingSignals.clear();
334
382
  drainNotificationQueue();
335
383
  }
336
384
 
@@ -392,9 +440,28 @@ function media(query) {
392
440
  }
393
441
 
394
442
  // src/core/ssr-context.ts
395
- var ssrMode = false;
443
+ var als = null;
444
+ try {
445
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
446
+ const req = Function("return typeof require==='function'?require:null")();
447
+ if (req) {
448
+ const mod = req("node:async_hooks");
449
+ als = new mod.AsyncLocalStorage();
450
+ }
451
+ }
452
+ } catch {
453
+ als = null;
454
+ }
455
+ var fallbackStore = { ssr: false, suspenseIdCounter: 0 };
456
+ function getSSRStore() {
457
+ if (als) {
458
+ const s = als.getStore();
459
+ if (s) return s;
460
+ }
461
+ return fallbackStore;
462
+ }
396
463
  function isSSR() {
397
- return ssrMode;
464
+ return getSSRStore().ssr;
398
465
  }
399
466
 
400
467
  // src/core/signals/effect.ts
@@ -404,26 +471,86 @@ function effect(effectFn, options) {
404
471
  if (isSSR()) return () => {
405
472
  };
406
473
  const onError = options?.onError;
474
+ let userCleanups = [];
475
+ const onCleanup = (fn) => {
476
+ userCleanups.push(fn);
477
+ };
478
+ const runUserCleanups = () => {
479
+ if (userCleanups.length === 0) return;
480
+ const list = userCleanups;
481
+ userCleanups = [];
482
+ for (let i = list.length - 1; i >= 0; i--) {
483
+ try {
484
+ list[i]();
485
+ } catch (err) {
486
+ if (typeof console !== "undefined") {
487
+ console.warn("[SibuJS effect] onCleanup threw:", err);
488
+ }
489
+ }
490
+ }
491
+ };
492
+ const invokeBody = () => effectFn(onCleanup);
407
493
  const wrappedFn = onError ? () => {
408
494
  try {
409
- effectFn();
495
+ invokeBody();
410
496
  } catch (err) {
411
497
  onError(err);
412
498
  }
413
- } : effectFn;
499
+ } : invokeBody;
414
500
  let cleanupHandle = () => {
415
501
  };
502
+ let running = false;
416
503
  const subscriber = () => {
417
- cleanupHandle();
418
- cleanupHandle = track(wrappedFn, subscriber);
504
+ if (running) {
505
+ if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
506
+ console.warn(
507
+ "[SibuJS] effect re-entered itself while running \u2014 the triggering update will be ignored. Wrap mutual writes in `batch()` or split the effect to avoid this."
508
+ );
509
+ }
510
+ return;
511
+ }
512
+ running = true;
513
+ try {
514
+ runUserCleanups();
515
+ cleanupHandle();
516
+ cleanupHandle = track(wrappedFn, subscriber);
517
+ } finally {
518
+ running = false;
519
+ }
419
520
  };
420
- cleanupHandle = track(wrappedFn, subscriber);
521
+ running = true;
522
+ try {
523
+ cleanupHandle = track(wrappedFn, subscriber);
524
+ } finally {
525
+ running = false;
526
+ }
421
527
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
422
528
  if (hook) hook.emit("effect:create", { effectFn });
529
+ let disposed = false;
423
530
  return () => {
531
+ if (disposed) return;
532
+ disposed = true;
424
533
  const h = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
425
- if (h) h.emit("effect:destroy", { effectFn });
426
- cleanupHandle();
534
+ if (h) {
535
+ try {
536
+ h.emit("effect:destroy", { effectFn });
537
+ } catch {
538
+ }
539
+ }
540
+ try {
541
+ runUserCleanups();
542
+ } catch (err) {
543
+ if (typeof console !== "undefined") {
544
+ console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
545
+ }
546
+ }
547
+ try {
548
+ cleanupHandle();
549
+ } catch (err) {
550
+ if (typeof console !== "undefined") {
551
+ console.warn("[SibuJS effect] dispose threw:", err);
552
+ }
553
+ }
427
554
  };
428
555
  }
429
556
 
@@ -474,6 +601,8 @@ function scroll(target) {
474
601
  const [y, setY] = signal(0);
475
602
  const [isScrolling, setIsScrolling] = signal(false);
476
603
  let scrollTimer = null;
604
+ let currentTarget = null;
605
+ let effectCleanup = null;
477
606
  if (typeof window === "undefined") {
478
607
  return { x, y, isScrolling, dispose: () => {
479
608
  } };
@@ -496,11 +625,26 @@ function scroll(target) {
496
625
  scrollTimer = null;
497
626
  }, 150);
498
627
  };
499
- const scrollTarget = target ? target() : null;
500
- const eventTarget = scrollTarget || window;
501
- eventTarget.addEventListener("scroll", handler, { passive: true });
628
+ function attachListener(eventTarget) {
629
+ if (currentTarget === eventTarget) return;
630
+ if (currentTarget) currentTarget.removeEventListener("scroll", handler);
631
+ currentTarget = eventTarget;
632
+ currentTarget.addEventListener("scroll", handler, { passive: true });
633
+ }
634
+ if (target) {
635
+ effectCleanup = effect(() => {
636
+ const el = target();
637
+ attachListener(el || window);
638
+ });
639
+ } else {
640
+ attachListener(window);
641
+ }
502
642
  function dispose() {
503
- eventTarget.removeEventListener("scroll", handler);
643
+ effectCleanup?.();
644
+ if (currentTarget) {
645
+ currentTarget.removeEventListener("scroll", handler);
646
+ currentTarget = null;
647
+ }
504
648
  if (scrollTimer !== null) {
505
649
  clearTimeout(scrollTimer);
506
650
  scrollTimer = null;
@@ -784,7 +928,10 @@ function dropZone(element, options) {
784
928
  const raw = e.dataTransfer.getData("application/json");
785
929
  if (raw) {
786
930
  try {
787
- transferData = JSON.parse(raw);
931
+ transferData = JSON.parse(
932
+ raw,
933
+ (k, v) => k === "__proto__" || k === "constructor" || k === "prototype" ? void 0 : v
934
+ );
788
935
  } catch {
789
936
  transferData = raw;
790
937
  }
@@ -1019,31 +1166,44 @@ function urlState() {
1019
1166
  }
1020
1167
  };
1021
1168
  }
1022
- const [params, setParamsSignal] = signal(new URLSearchParams(window.location.search));
1023
- const [hash, setHashSignal] = signal(window.location.hash);
1024
- const syncFromLocation = () => {
1025
- setParamsSignal(new URLSearchParams(window.location.search));
1026
- setHashSignal(window.location.hash);
1027
- };
1028
- const onPopState = () => syncFromLocation();
1029
- window.addEventListener("popstate", onPopState);
1169
+ let lastSearch = window.location.search;
1170
+ let lastHash = window.location.hash;
1171
+ const [params, setParamsSignal] = signal(new URLSearchParams(lastSearch));
1172
+ const [hash, setHashSignal] = signal(lastHash);
1173
+ function syncFromLocation() {
1174
+ const currentSearch = window.location.search;
1175
+ const currentHash = window.location.hash;
1176
+ if (currentSearch !== lastSearch) {
1177
+ lastSearch = currentSearch;
1178
+ setParamsSignal(new URLSearchParams(currentSearch));
1179
+ }
1180
+ if (currentHash !== lastHash) {
1181
+ lastHash = currentHash;
1182
+ setHashSignal(currentHash);
1183
+ }
1184
+ }
1185
+ window.addEventListener("popstate", syncFromLocation);
1186
+ window.addEventListener("hashchange", syncFromLocation);
1030
1187
  function setParams(next, opts = {}) {
1031
1188
  const p = next instanceof URLSearchParams ? next : new URLSearchParams(next);
1032
1189
  const query = p.toString();
1033
1190
  const newUrl = `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`;
1034
1191
  if (opts.replace) window.history.replaceState(null, "", newUrl);
1035
1192
  else window.history.pushState(null, "", newUrl);
1193
+ lastSearch = window.location.search;
1036
1194
  setParamsSignal(new URLSearchParams(p));
1037
1195
  }
1038
1196
  function setHash(next, opts = {}) {
1039
- const normalized = next.startsWith("#") ? next : next ? `#${next}` : "";
1197
+ const normalized = next && next !== "#" ? next.startsWith("#") ? next : `#${next}` : "";
1040
1198
  const newUrl = `${window.location.pathname}${window.location.search}${normalized}`;
1041
1199
  if (opts.replace) window.history.replaceState(null, "", newUrl);
1042
1200
  else window.history.pushState(null, "", newUrl);
1201
+ lastHash = normalized;
1043
1202
  setHashSignal(normalized);
1044
1203
  }
1045
1204
  function dispose() {
1046
- window.removeEventListener("popstate", onPopState);
1205
+ window.removeEventListener("popstate", syncFromLocation);
1206
+ window.removeEventListener("hashchange", syncFromLocation);
1047
1207
  }
1048
1208
  return { params, hash, setParams, setHash, dispose };
1049
1209
  }
@@ -1151,13 +1311,21 @@ function wakeLock() {
1151
1311
  }
1152
1312
  const onVisibility = () => {
1153
1313
  if (sentinel?.released && !document.hidden) {
1154
- void request();
1314
+ request().catch((err) => {
1315
+ if (typeof console !== "undefined") {
1316
+ console.warn("[SibuJS wakeLock] re-acquire failed:", err);
1317
+ }
1318
+ });
1155
1319
  }
1156
1320
  };
1157
1321
  document.addEventListener("visibilitychange", onVisibility);
1158
1322
  function dispose() {
1159
1323
  document.removeEventListener("visibilitychange", onVisibility);
1160
- void release();
1324
+ release().catch((err) => {
1325
+ if (typeof console !== "undefined") {
1326
+ console.warn("[SibuJS wakeLock] release failed:", err);
1327
+ }
1328
+ });
1161
1329
  }
1162
1330
  return { active, request, release, dispose };
1163
1331
  }
@@ -1353,11 +1521,21 @@ function speech() {
1353
1521
  };
1354
1522
  }
1355
1523
  const synth = window.speechSynthesis;
1356
- const interval = setInterval(() => {
1357
- setSpeaking(synth.speaking);
1358
- setPaused(synth.paused);
1359
- }, 200);
1524
+ let interval = null;
1525
+ function startPolling() {
1526
+ if (interval !== null) return;
1527
+ interval = setInterval(() => {
1528
+ setSpeaking(synth.speaking);
1529
+ setPaused(synth.paused);
1530
+ if (!synth.speaking && !synth.paused) {
1531
+ clearInterval(interval);
1532
+ interval = null;
1533
+ }
1534
+ }, 200);
1535
+ }
1536
+ let disposed = false;
1360
1537
  function speak(text, options = {}) {
1538
+ if (disposed) return;
1361
1539
  const u = new SpeechSynthesisUtterance(text);
1362
1540
  if (options.lang) u.lang = options.lang;
1363
1541
  if (options.rate != null) u.rate = options.rate;
@@ -1368,19 +1546,41 @@ function speech() {
1368
1546
  const match = voices.find((v) => v.name === options.voice);
1369
1547
  if (match) u.voice = match;
1370
1548
  }
1371
- u.addEventListener("start", () => setSpeaking(true));
1372
- u.addEventListener("end", () => {
1373
- setSpeaking(false);
1374
- setPaused(false);
1375
- });
1376
- u.addEventListener("error", () => {
1377
- setSpeaking(false);
1378
- setPaused(false);
1379
- });
1549
+ u.addEventListener(
1550
+ "start",
1551
+ () => {
1552
+ if (!disposed) setSpeaking(true);
1553
+ },
1554
+ { once: true }
1555
+ );
1556
+ u.addEventListener(
1557
+ "end",
1558
+ () => {
1559
+ if (disposed) return;
1560
+ setSpeaking(false);
1561
+ setPaused(false);
1562
+ },
1563
+ { once: true }
1564
+ );
1565
+ u.addEventListener(
1566
+ "error",
1567
+ () => {
1568
+ if (disposed) return;
1569
+ setSpeaking(false);
1570
+ setPaused(false);
1571
+ },
1572
+ { once: true }
1573
+ );
1380
1574
  synth.speak(u);
1575
+ setSpeaking(true);
1576
+ startPolling();
1381
1577
  }
1382
1578
  function dispose() {
1383
- clearInterval(interval);
1579
+ disposed = true;
1580
+ if (interval !== null) {
1581
+ clearInterval(interval);
1582
+ interval = null;
1583
+ }
1384
1584
  synth.cancel();
1385
1585
  }
1386
1586
  return {
@@ -1609,13 +1809,21 @@ function imageLoader(src) {
1609
1809
  };
1610
1810
  img.src = url;
1611
1811
  }
1812
+ let srcEffectTeardown = null;
1612
1813
  if (typeof src === "function") {
1613
- start(src());
1814
+ srcEffectTeardown = effect(() => {
1815
+ const url = src();
1816
+ start(url);
1817
+ });
1614
1818
  } else {
1615
1819
  start(src);
1616
1820
  }
1617
1821
  function dispose() {
1618
1822
  disposed = true;
1823
+ if (srcEffectTeardown) {
1824
+ srcEffectTeardown();
1825
+ srcEffectTeardown = null;
1826
+ }
1619
1827
  if (current) {
1620
1828
  current.onload = null;
1621
1829
  current.onerror = null;
@@ -31,6 +31,9 @@ declare function resize(target: ElementTarget$1): {
31
31
  * Returns reactive x/y scroll positions and an isScrolling indicator
32
32
  * that resets after 150ms of inactivity.
33
33
  *
34
+ * If a reactive target getter is provided, the listener re-attaches
35
+ * whenever the target element changes (same pattern as resize/dragDrop).
36
+ *
34
37
  * @param target Optional reactive getter for the scroll target element.
35
38
  * If omitted or returns null, tracks window scroll.
36
39
  * @returns Object with reactive x, y, isScrolling getters and a dispose function
@@ -336,7 +339,9 @@ declare function windowSize(): {
336
339
  * to sync a handful of UI state bits with the URL (filters, tabs, modals)
337
340
  * without a full router setup.
338
341
  *
339
- * Listens to `popstate` so browser back/forward updates the signals.
342
+ * Listens to both `popstate` (back/forward) and `hashchange` (anchor clicks,
343
+ * direct `location.hash` assignments) so the signals stay in sync regardless
344
+ * of how the URL was changed.
340
345
  *
341
346
  * @example
342
347
  * ```ts
@@ -365,6 +370,11 @@ declare function urlState(): {
365
370
  };
366
371
 
367
372
  /**
373
+ * Note on trust model: `BroadcastChannel` only delivers messages between
374
+ * same-origin browsing contexts (tabs, iframes, workers). We therefore treat
375
+ * incoming payloads as same-origin-trusted and pass them through unmodified.
376
+ * Do not use this transport for cross-origin messaging.
377
+ *
368
378
  * broadcast wraps the BroadcastChannel API as a reactive signal.
369
379
  * Unlike the `storage` event (which only fires for localStorage writes and
370
380
  * sends only the serialized value), a `BroadcastChannel` can send arbitrary
@@ -406,10 +416,10 @@ declare function broadcast<T = unknown>(channelName: string): {
406
416
  * @example
407
417
  * ```ts
408
418
  * const fs = fullscreen();
409
- * button({
410
- * nodes: () => (fs.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"),
411
- * on: { click: () => fs.toggle(videoEl) },
412
- * });
419
+ * button(
420
+ * { on: { click: () => fs.toggle(videoEl) } },
421
+ * () => (fs.isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"),
422
+ * );
413
423
  * ```
414
424
  */
415
425
  declare function fullscreen(): {
@@ -594,10 +604,10 @@ interface SpeakOptions {
594
604
  * @example
595
605
  * ```ts
596
606
  * const tts = speech();
597
- * button({
598
- * nodes: "Read it to me",
599
- * on: { click: () => tts.speak("Hello, world!", { rate: 1.1 }) },
600
- * });
607
+ * button(
608
+ * { on: { click: () => tts.speak("Hello, world!", { rate: 1.1 }) } },
609
+ * "Read it to me",
610
+ * );
601
611
  * ```
602
612
  */
603
613
  declare function speech(): {