sibujs 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/browser.cjs +324 -273
  2. package/dist/browser.js +4 -4
  3. package/dist/build.cjs +358 -328
  4. package/dist/build.js +10 -10
  5. package/dist/cdn.global.js +8 -8
  6. package/dist/{chunk-ZAQSMOED.js → chunk-2JQUV4Y3.js} +4 -4
  7. package/dist/{chunk-GWWURC5M.js → chunk-2KM2724A.js} +2 -2
  8. package/dist/{chunk-NASX6ST2.js → chunk-4YTVESDX.js} +1 -1
  9. package/dist/chunk-5WD7BYTZ.js +152 -0
  10. package/dist/{chunk-RDTDJCAB.js → chunk-6QZO7MMG.js} +48 -16
  11. package/dist/{chunk-DRUZZAK4.js → chunk-DF3GTP4Q.js} +7 -2
  12. package/dist/{chunk-AMK2TYNW.js → chunk-J63GPPCJ.js} +9 -9
  13. package/dist/{chunk-O6EFQ3KT.js → chunk-KH4OE6WY.js} +5 -5
  14. package/dist/{chunk-V6C4FADE.js → chunk-KZA7ANXP.js} +3 -3
  15. package/dist/chunk-L4DAT4WU.js +400 -0
  16. package/dist/{chunk-WANSMF2L.js → chunk-L52H775O.js} +4 -4
  17. package/dist/{chunk-45YP72ZQ.js → chunk-NEWH4O5U.js} +1 -1
  18. package/dist/{chunk-ON5MMR2J.js → chunk-RJIRT46U.js} +4 -4
  19. package/dist/{chunk-P2HSJDDN.js → chunk-STFTTMO2.js} +2 -2
  20. package/dist/{chunk-WIPZPFBQ.js → chunk-UKMXT5T6.js} +1 -1
  21. package/dist/{chunk-KGYT6UO6.js → chunk-V65KTDZW.js} +3 -3
  22. package/dist/{chunk-CWBVQML6.js → chunk-VSNLICTS.js} +1 -1
  23. package/dist/{chunk-3DZP6OIT.js → chunk-XDKP4T7G.js} +2 -2
  24. package/dist/{chunk-TH2ILCYW.js → chunk-XVYB3J6C.js} +27 -33
  25. package/dist/{chunk-OJ3P4ECI.js → chunk-YMOIAHWA.js} +1 -1
  26. package/dist/data.cjs +332 -298
  27. package/dist/data.js +6 -6
  28. package/dist/devtools.cjs +353 -296
  29. package/dist/devtools.d.cts +1 -1
  30. package/dist/devtools.d.ts +1 -1
  31. package/dist/devtools.js +4 -4
  32. package/dist/ecosystem.cjs +332 -298
  33. package/dist/ecosystem.js +7 -7
  34. package/dist/extras.cjs +372 -328
  35. package/dist/extras.d.cts +1 -1
  36. package/dist/extras.d.ts +1 -1
  37. package/dist/extras.js +19 -19
  38. package/dist/index.cjs +358 -328
  39. package/dist/index.d.cts +13 -22
  40. package/dist/index.d.ts +13 -22
  41. package/dist/index.js +10 -10
  42. package/dist/{introspect-DnIpHQQz.d.ts → introspect-BZWKvQUZ.d.ts} +2 -3
  43. package/dist/{introspect-2TOlQ7oa.d.cts → introspect-DsJlDD2T.d.cts} +2 -3
  44. package/dist/motion.cjs +147 -123
  45. package/dist/motion.js +3 -3
  46. package/dist/patterns.cjs +332 -298
  47. package/dist/patterns.js +5 -5
  48. package/dist/performance.cjs +315 -268
  49. package/dist/performance.js +4 -4
  50. package/dist/plugins.cjs +332 -266
  51. package/dist/plugins.js +6 -6
  52. package/dist/ssr.cjs +340 -270
  53. package/dist/ssr.js +7 -7
  54. package/dist/testing.cjs +167 -146
  55. package/dist/testing.js +2 -2
  56. package/dist/ui.cjs +324 -294
  57. package/dist/ui.js +6 -6
  58. package/dist/widgets.cjs +332 -298
  59. package/dist/widgets.js +6 -6
  60. package/package.json +1 -1
  61. package/dist/chunk-QO3WC6FS.js +0 -384
  62. package/dist/chunk-WZA53FXU.js +0 -149
package/dist/patterns.cjs CHANGED
@@ -61,24 +61,88 @@ function devWarn(message) {
61
61
 
62
62
  // src/reactivity/track.ts
63
63
  var _isDev2 = isDev();
64
- var STACK_INITIAL = 32;
65
- var STACK_SHRINK_THRESHOLD = 128;
66
- var subscriberStack = new Array(STACK_INITIAL);
67
- var stackCapacity = STACK_INITIAL;
68
- var stackTop = -1;
69
- var currentSubscriber = null;
70
- var SUBS = "__s";
71
- function syncFastPath(signal2, subs) {
72
- const size = subs.size;
73
- if (size === 0) {
74
- signal2.__f = void 0;
75
- delete signal2[SUBS];
76
- } else if (size === 1) {
77
- signal2.__f = subs.values().next().value;
78
- } else {
79
- signal2.__f = void 0;
64
+ var POOL_MAX = 4096;
65
+ var nodePool = [];
66
+ function createNode() {
67
+ return {
68
+ sig: null,
69
+ sub: null,
70
+ epoch: 0,
71
+ sigPrev: null,
72
+ sigNext: null,
73
+ subPrev: null,
74
+ subNext: null,
75
+ prevActive: null
76
+ };
77
+ }
78
+ function allocNode(sig, sub, epoch) {
79
+ const n = nodePool.pop();
80
+ if (n) {
81
+ n.sig = sig;
82
+ n.sub = sub;
83
+ n.epoch = epoch;
84
+ return n;
85
+ }
86
+ const fresh = createNode();
87
+ fresh.sig = sig;
88
+ fresh.sub = sub;
89
+ fresh.epoch = epoch;
90
+ return fresh;
91
+ }
92
+ function freeNode(node) {
93
+ node.sig = null;
94
+ node.sub = null;
95
+ node.sigPrev = null;
96
+ node.sigNext = null;
97
+ node.subPrev = null;
98
+ node.subNext = null;
99
+ node.prevActive = null;
100
+ if (nodePool.length < POOL_MAX) nodePool.push(node);
101
+ }
102
+ function linkSignal(sig, node) {
103
+ const oldHead = sig.subsHead ?? null;
104
+ node.sigPrev = null;
105
+ node.sigNext = oldHead;
106
+ if (oldHead) oldHead.sigPrev = node;
107
+ else sig.subsTail = node;
108
+ sig.subsHead = node;
109
+ sig.__sc = (sig.__sc ?? 0) + 1;
110
+ }
111
+ function unlinkSignal(node) {
112
+ const sig = node.sig;
113
+ if (!sig) return;
114
+ const prev = node.sigPrev;
115
+ const next = node.sigNext;
116
+ if (prev) prev.sigNext = next;
117
+ else sig.subsHead = next;
118
+ if (next) next.sigPrev = prev;
119
+ else sig.subsTail = prev;
120
+ sig.__sc = (sig.__sc ?? 1) - 1;
121
+ if (sig.__activeNode === node) sig.__activeNode = node.prevActive;
122
+ if (sig.__sc === 0) {
123
+ sig.subsHead = null;
124
+ sig.subsTail = null;
80
125
  }
81
126
  }
127
+ function linkSub(sub, node) {
128
+ const oldTail = sub.depsTail ?? null;
129
+ node.subPrev = oldTail;
130
+ node.subNext = null;
131
+ if (oldTail) oldTail.subNext = node;
132
+ else sub.depsHead = node;
133
+ sub.depsTail = node;
134
+ }
135
+ function unlinkSub(node) {
136
+ const sub = node.sub;
137
+ if (!sub) return;
138
+ const prev = node.subPrev;
139
+ const next = node.subNext;
140
+ if (prev) prev.subNext = next;
141
+ else sub.depsHead = next;
142
+ if (next) next.subPrev = prev;
143
+ else sub.depsTail = prev;
144
+ }
145
+ var currentSubscriber = null;
82
146
  var notifyDepth = 0;
83
147
  var pendingQueue = [];
84
148
  var pendingSet = /* @__PURE__ */ new Set();
@@ -98,110 +162,77 @@ function retrack(effectFn, subscriber) {
98
162
  const sub = subscriber;
99
163
  const epoch = ++subscriberEpochCounter;
100
164
  sub._epoch = epoch;
165
+ sub._structDirty = false;
166
+ for (let n = sub.depsHead ?? null; n !== null; n = n.subNext) {
167
+ const sig = n.sig;
168
+ n.prevActive = sig.__activeNode ?? null;
169
+ sig.__activeNode = n;
170
+ }
101
171
  try {
102
172
  effectFn();
103
173
  } finally {
104
174
  currentSubscriber = prev;
105
- pruneStaleDeps(sub, epoch);
106
- }
107
- }
108
- function pruneStaleDeps(sub, currentEpoch) {
109
- if (sub._dep !== void 0) {
110
- if (sub._depEpoch !== currentEpoch) {
111
- const sig = sub._dep;
112
- const subs = sig[SUBS];
113
- if (subs?.delete(sub)) syncFastPath(sig, subs);
114
- sub._dep = void 0;
115
- sub._depEpoch = void 0;
116
- }
117
- return;
118
- }
119
- const deps = sub._deps;
120
- if (!deps || deps.size === 0) return;
121
- let stales;
122
- for (const [signal2, epoch] of deps) {
123
- if (epoch !== currentEpoch) {
124
- (stales ?? (stales = [])).push(signal2);
175
+ let node = sub.depsHead ?? null;
176
+ while (node !== null) {
177
+ const next = node.subNext;
178
+ const sig = node.sig;
179
+ sig.__activeNode = node.prevActive;
180
+ node.prevActive = null;
181
+ if (node.epoch !== epoch) {
182
+ unlinkSub(node);
183
+ unlinkSignal(node);
184
+ freeNode(node);
185
+ }
186
+ node = next;
125
187
  }
126
188
  }
127
- if (!stales) return;
128
- for (const signal2 of stales) {
129
- deps.delete(signal2);
130
- const sig = signal2;
131
- const subs = sig[SUBS];
132
- if (subs?.delete(sub)) syncFastPath(sig, subs);
133
- }
134
189
  }
135
190
  function track(effectFn, subscriber) {
136
191
  if (!subscriber) subscriber = effectFn;
137
192
  cleanup(subscriber);
138
- ++stackTop;
139
- if (stackTop >= stackCapacity) {
140
- stackCapacity *= 2;
141
- subscriberStack.length = stackCapacity;
142
- }
143
- subscriberStack[stackTop] = subscriber;
193
+ const prev = currentSubscriber;
144
194
  currentSubscriber = subscriber;
145
195
  try {
146
196
  effectFn();
147
197
  } finally {
148
- stackTop--;
149
- currentSubscriber = stackTop >= 0 ? subscriberStack[stackTop] : null;
150
- if (stackTop < 0 && stackCapacity > STACK_SHRINK_THRESHOLD) {
151
- stackCapacity = Math.max(STACK_INITIAL, stackCapacity >>> 1);
152
- subscriberStack.length = stackCapacity;
198
+ currentSubscriber = prev;
199
+ const sub2 = subscriber;
200
+ for (let n = sub2.depsHead ?? null; n !== null; n = n.subNext) {
201
+ const sig = n.sig;
202
+ sig.__activeNode = n.prevActive;
203
+ n.prevActive = null;
153
204
  }
154
205
  }
155
- return () => cleanup(subscriber);
206
+ const sub = subscriber;
207
+ return sub._dispose ?? (sub._dispose = () => cleanup(subscriber));
156
208
  }
157
209
  function recordDependency(signal2) {
158
210
  if (!currentSubscriber) return;
159
211
  const sub = currentSubscriber;
160
- const epoch = sub._epoch;
161
- if (sub._dep === signal2) {
162
- sub._depEpoch = epoch;
163
- return;
164
- }
165
- const deps = sub._deps;
166
- if (deps) {
167
- deps.set(signal2, epoch);
168
- } else if (sub._dep !== void 0) {
169
- const map = /* @__PURE__ */ new Map();
170
- map.set(sub._dep, sub._depEpoch);
171
- map.set(signal2, epoch);
172
- sub._deps = map;
173
- sub._dep = void 0;
174
- sub._depEpoch = void 0;
175
- } else {
176
- sub._dep = signal2;
177
- sub._depEpoch = epoch;
178
- }
179
212
  const sig = signal2;
180
- let subs = sig[SUBS];
181
- if (!subs) {
182
- subs = /* @__PURE__ */ new Set();
183
- sig[SUBS] = subs;
184
- }
185
- const prevSize = subs.size;
186
- subs.add(currentSubscriber);
187
- if (subs.size !== prevSize) {
188
- if (subs.size === 1) {
189
- sig.__f = currentSubscriber;
190
- } else if (sig.__f !== void 0) {
191
- sig.__f = void 0;
192
- }
213
+ const epoch = sub._epoch ?? 0;
214
+ const active = sig.__activeNode ?? null;
215
+ if (active !== null && active.sub === sub) {
216
+ active.epoch = epoch;
217
+ return;
193
218
  }
219
+ const node = allocNode(signal2, sub, epoch);
220
+ node.prevActive = active;
221
+ sig.__activeNode = node;
222
+ linkSub(sub, node);
223
+ linkSignal(sig, node);
224
+ sub._structDirty = true;
194
225
  }
195
- function queueSignalNotification(signal2) {
196
- const subs = signal2[SUBS];
197
- if (!subs) return;
198
- for (const sub of subs) {
199
- if (sub._c) {
200
- propagateDirty(sub);
201
- } else if (!pendingSet.has(sub)) {
202
- pendingSet.add(sub);
203
- pendingQueue.push(sub);
204
- }
226
+ function cleanup(subscriber) {
227
+ const sub = subscriber;
228
+ let node = sub.depsHead ?? null;
229
+ sub.depsHead = null;
230
+ sub.depsTail = null;
231
+ while (node) {
232
+ const next = node.subNext;
233
+ unlinkSignal(node);
234
+ freeNode(node);
235
+ node = next;
205
236
  }
206
237
  }
207
238
  var maxSubscriberRepeats = 50;
@@ -214,7 +245,8 @@ function tickRepeat(sub) {
214
245
  s._runs = 1;
215
246
  return false;
216
247
  }
217
- return ++s._runs > maxSubscriberRepeats;
248
+ s._runs = (s._runs ?? 0) + 1;
249
+ return s._runs > maxSubscriberRepeats;
218
250
  }
219
251
  function cycleError(sub) {
220
252
  if (typeof console !== "undefined") {
@@ -270,93 +302,80 @@ function propagateDirty(sub) {
270
302
  stack.push(rootSig);
271
303
  while (stack.length > baseLen) {
272
304
  const sig = stack.pop();
273
- const first = sig.__f;
274
- if (first) {
275
- if (first._c) {
276
- const nSig = first._sig;
277
- if (!nSig._d) {
278
- nSig._d = true;
279
- stack.push(nSig);
305
+ let node = sig.subsHead ?? null;
306
+ while (node) {
307
+ const s = node.sub;
308
+ if (s) {
309
+ if (s._c) {
310
+ const nSig = s._sig;
311
+ if (nSig) {
312
+ if (!nSig._d) {
313
+ nSig._d = true;
314
+ stack.push(nSig);
315
+ }
316
+ } else {
317
+ s();
318
+ }
319
+ } else if (!pendingSet.has(s)) {
320
+ pendingSet.add(s);
321
+ pendingQueue.push(s);
280
322
  }
281
- } else if (!pendingSet.has(first)) {
282
- pendingSet.add(first);
283
- pendingQueue.push(first);
284
323
  }
285
- continue;
324
+ node = node.sigNext;
286
325
  }
287
- const subs = sig[SUBS];
288
- if (!subs) continue;
289
- for (const s of subs) {
326
+ }
327
+ }
328
+ function queueSignalNotification(signal2) {
329
+ const sig = signal2;
330
+ let node = sig.subsHead ?? null;
331
+ while (node) {
332
+ const s = node.sub;
333
+ if (s) {
290
334
  if (s._c) {
291
- const nSig = s._sig;
292
- if (nSig && !nSig._d) {
293
- nSig._d = true;
294
- stack.push(nSig);
295
- } else if (!nSig) {
296
- s();
297
- }
335
+ propagateDirty(s);
298
336
  } else if (!pendingSet.has(s)) {
299
337
  pendingSet.add(s);
300
338
  pendingQueue.push(s);
301
339
  }
302
340
  }
341
+ node = node.sigNext;
303
342
  }
304
343
  }
305
344
  function notifySubscribers(signal2) {
306
- const first = signal2.__f;
307
- if (first) {
308
- if (notifyDepth > 0) {
309
- if (first._c) {
310
- propagateDirty(first);
311
- } else if (!pendingSet.has(first)) {
312
- pendingSet.add(first);
313
- pendingQueue.push(first);
314
- }
315
- return;
316
- }
317
- notifyDepth++;
318
- drainEpoch++;
319
- try {
320
- if (first._c) {
321
- propagateDirty(first);
322
- } else if (tickRepeat(first)) {
323
- cycleError(first);
324
- } else {
325
- safeInvoke(first);
326
- }
327
- drainQueue();
328
- } finally {
329
- notifyDepth--;
330
- if (notifyDepth === 0) {
331
- pendingQueue.length = 0;
332
- pendingSet.clear();
333
- }
334
- }
335
- return;
336
- }
337
- const subs = signal2[SUBS];
338
- if (!subs || subs.size === 0) return;
345
+ const sig = signal2;
346
+ const head = sig.subsHead;
347
+ if (!head) return;
339
348
  if (notifyDepth > 0) {
340
- for (const sub of subs) {
341
- if (sub._c) {
342
- propagateDirty(sub);
343
- } else if (!pendingSet.has(sub)) {
344
- pendingSet.add(sub);
345
- pendingQueue.push(sub);
349
+ let node = head;
350
+ while (node) {
351
+ const s = node.sub;
352
+ if (s) {
353
+ if (s._c) {
354
+ propagateDirty(s);
355
+ } else if (!pendingSet.has(s)) {
356
+ pendingSet.add(s);
357
+ pendingQueue.push(s);
358
+ }
346
359
  }
360
+ node = node.sigNext;
347
361
  }
348
362
  return;
349
363
  }
350
364
  notifyDepth++;
351
365
  drainEpoch++;
352
366
  try {
353
- for (const sub of subs) {
354
- if (sub._c) {
355
- propagateDirty(sub);
356
- } else if (!pendingSet.has(sub)) {
357
- pendingSet.add(sub);
358
- pendingQueue.push(sub);
367
+ let node = head;
368
+ while (node) {
369
+ const s = node.sub;
370
+ if (s) {
371
+ if (s._c) {
372
+ propagateDirty(s);
373
+ } else if (!pendingSet.has(s)) {
374
+ pendingSet.add(s);
375
+ pendingQueue.push(s);
376
+ }
359
377
  }
378
+ node = node.sigNext;
360
379
  }
361
380
  drainQueue();
362
381
  } finally {
@@ -367,30 +386,6 @@ function notifySubscribers(signal2) {
367
386
  }
368
387
  }
369
388
  }
370
- function cleanup(subscriber) {
371
- const sub = subscriber;
372
- const singleDep = sub._dep;
373
- if (singleDep !== void 0) {
374
- const sig = singleDep;
375
- const subs = sig[SUBS];
376
- if (subs?.delete(subscriber)) {
377
- syncFastPath(sig, subs);
378
- }
379
- sub._dep = void 0;
380
- sub._depEpoch = void 0;
381
- return;
382
- }
383
- const deps = sub._deps;
384
- if (!deps || deps.size === 0) return;
385
- for (const signal2 of deps.keys()) {
386
- const sig = signal2;
387
- const subs = sig[SUBS];
388
- if (subs?.delete(subscriber)) {
389
- syncFastPath(sig, subs);
390
- }
391
- }
392
- deps.clear();
393
- }
394
389
 
395
390
  // src/reactivity/batch.ts
396
391
  var batchDepth = 0;
@@ -426,32 +421,64 @@ function flushBatch() {
426
421
  var _g = globalThis;
427
422
  var _isDev3 = isDev();
428
423
  function signal(initial, options) {
429
- const state = { value: initial };
424
+ const state = {
425
+ value: initial,
426
+ __v: 0,
427
+ __sc: 0,
428
+ subsHead: null,
429
+ subsTail: null,
430
+ __activeNode: null,
431
+ __name: void 0
432
+ };
430
433
  const debugName = _isDev3 ? options?.name : void 0;
431
434
  const equalsFn = options?.equals;
432
- if (debugName) {
433
- state.__name = debugName;
434
- }
435
+ if (debugName) state.__name = debugName;
435
436
  function get() {
436
437
  recordDependency(state);
437
438
  return state.value;
438
439
  }
439
440
  get.__signal = state;
440
441
  if (debugName) get.__name = debugName;
441
- function set(next) {
442
- const newValue = typeof next === "function" ? next(state.value) : next;
443
- if (equalsFn ? equalsFn(state.value, newValue) : Object.is(newValue, state.value)) return;
444
- if (_isDev3) {
445
- const oldValue = state.value;
442
+ let set;
443
+ if (equalsFn) {
444
+ set = (next) => {
445
+ const prev = state.value;
446
+ const newValue = typeof next === "function" ? next(prev) : next;
447
+ if (equalsFn(prev, newValue)) return;
448
+ state.value = newValue;
449
+ state.__v++;
450
+ if (_isDev3) {
451
+ const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
452
+ if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
453
+ }
454
+ if (!enqueueBatchedSignal(state)) {
455
+ notifySubscribers(state);
456
+ }
457
+ };
458
+ } else if (_isDev3) {
459
+ set = (next) => {
460
+ const prev = state.value;
461
+ const newValue = typeof next === "function" ? next(prev) : next;
462
+ if (Object.is(newValue, prev)) return;
446
463
  state.value = newValue;
464
+ state.__v++;
447
465
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
448
- if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue, newValue });
449
- } else {
466
+ if (hook) hook.emit("signal:update", { signal: state, name: debugName, oldValue: prev, newValue });
467
+ if (!enqueueBatchedSignal(state)) {
468
+ notifySubscribers(state);
469
+ }
470
+ };
471
+ } else {
472
+ set = (next) => {
473
+ const prev = state.value;
474
+ const newValue = typeof next === "function" ? next(prev) : next;
475
+ if (Object.is(newValue, prev)) return;
450
476
  state.value = newValue;
451
- }
452
- if (!enqueueBatchedSignal(state)) {
453
- notifySubscribers(state);
454
- }
477
+ state.__v++;
478
+ if (!enqueueBatchedSignal(state)) {
479
+ notifySubscribers(state);
480
+ }
481
+ };
455
482
  }
456
483
  if (_isDev3) {
457
484
  const hook = _g.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
@@ -549,120 +576,122 @@ function isSSR() {
549
576
 
550
577
  // src/core/signals/effect.ts
551
578
  var _g2 = globalThis;
579
+ var MAX_RERUNS = 100;
580
+ function flushUserCleanups(ctx) {
581
+ const list = ctx.userCleanups;
582
+ if (list.length === 0) return;
583
+ ctx.userCleanups = [];
584
+ for (let i = list.length - 1; i >= 0; i--) {
585
+ try {
586
+ list[i]();
587
+ } catch (err) {
588
+ if (typeof console !== "undefined") console.warn("[SibuJS effect] onCleanup threw:", err);
589
+ }
590
+ }
591
+ }
592
+ function drainReruns(ctx) {
593
+ let reruns = 1;
594
+ do {
595
+ ctx.rerunPending = false;
596
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
597
+ retrack(ctx.bodyFn, ctx.subscriber);
598
+ } while (ctx.rerunPending && ++reruns <= MAX_RERUNS);
599
+ if (ctx.rerunPending) {
600
+ ctx.rerunPending = false;
601
+ if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
602
+ console.error(
603
+ `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
604
+ );
605
+ }
606
+ }
607
+ }
608
+ function disposeEffect(ctx) {
609
+ if (ctx.disposed) return;
610
+ ctx.disposed = true;
611
+ const h = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
612
+ if (h) {
613
+ try {
614
+ h.emit("effect:destroy", { effectFn: ctx.fn });
615
+ } catch {
616
+ }
617
+ }
618
+ try {
619
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
620
+ } catch (err) {
621
+ if (typeof console !== "undefined") {
622
+ console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
623
+ }
624
+ }
625
+ try {
626
+ cleanup(ctx.subscriber);
627
+ } catch (err) {
628
+ if (typeof console !== "undefined") {
629
+ console.warn("[SibuJS effect] dispose threw:", err);
630
+ }
631
+ }
632
+ }
552
633
  function effect(effectFn, options) {
553
634
  devAssert(typeof effectFn === "function", "effect: argument must be a function.");
554
635
  if (isSSR()) return () => {
555
636
  };
556
- const onError = options?.onError;
557
- let userCleanups = [];
558
- const onCleanup = (fn) => {
559
- userCleanups.push(fn);
637
+ const ctx = {
638
+ fn: effectFn,
639
+ onError: options?.onError,
640
+ userCleanups: [],
641
+ running: false,
642
+ rerunPending: false,
643
+ disposed: false,
644
+ onCleanup: null,
645
+ subscriber: null,
646
+ bodyFn: null
560
647
  };
561
- const runUserCleanups = () => {
562
- if (userCleanups.length === 0) return;
563
- const list = userCleanups;
564
- userCleanups = [];
565
- for (let i = list.length - 1; i >= 0; i--) {
566
- try {
567
- list[i]();
568
- } catch (err) {
569
- if (typeof console !== "undefined") {
570
- console.warn("[SibuJS effect] onCleanup threw:", err);
571
- }
572
- }
573
- }
648
+ ctx.onCleanup = (fn) => {
649
+ ctx.userCleanups.push(fn);
574
650
  };
575
- const invokeBody = () => effectFn(onCleanup);
576
- const wrappedFn = onError ? () => {
651
+ const onErrorCaptured = ctx.onError;
652
+ ctx.bodyFn = onErrorCaptured ? () => {
577
653
  try {
578
- invokeBody();
654
+ ctx.fn(ctx.onCleanup);
579
655
  } catch (err) {
580
- onError(err);
656
+ onErrorCaptured(err);
581
657
  }
582
- } : invokeBody;
583
- let cleanupHandle = () => {
658
+ } : () => {
659
+ ctx.fn(ctx.onCleanup);
584
660
  };
585
- let running = false;
586
- let rerunPending = false;
587
- const MAX_RERUNS = 100;
588
- const subscriber = () => {
589
- if (running) {
590
- rerunPending = true;
661
+ const sub = (() => {
662
+ if (ctx.running) {
663
+ ctx.rerunPending = true;
591
664
  return;
592
665
  }
593
- running = true;
666
+ ctx.running = true;
594
667
  try {
595
- let reruns = 0;
596
- do {
597
- rerunPending = false;
598
- runUserCleanups();
599
- cleanupHandle();
600
- cleanupHandle = track(wrappedFn, subscriber);
601
- if (++reruns > MAX_RERUNS) {
602
- if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
603
- console.error(
604
- `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
605
- );
606
- }
607
- rerunPending = false;
608
- break;
609
- }
610
- } while (rerunPending);
668
+ ctx.rerunPending = false;
669
+ if (ctx.userCleanups.length > 0) flushUserCleanups(ctx);
670
+ retrack(ctx.bodyFn, sub);
671
+ if (ctx.rerunPending) drainReruns(ctx);
611
672
  } finally {
612
- running = false;
613
- rerunPending = false;
673
+ ctx.running = false;
674
+ ctx.rerunPending = false;
614
675
  }
615
- };
616
- running = true;
676
+ });
677
+ sub.depsHead = null;
678
+ sub.depsTail = null;
679
+ sub._epoch = 0;
680
+ sub._structDirty = false;
681
+ sub._runEpoch = 0;
682
+ sub._runs = 0;
683
+ ctx.subscriber = sub;
684
+ ctx.running = true;
617
685
  try {
618
- let reruns = 0;
619
- do {
620
- rerunPending = false;
621
- runUserCleanups();
622
- cleanupHandle();
623
- cleanupHandle = track(wrappedFn, subscriber);
624
- if (++reruns > MAX_RERUNS) {
625
- if (_g2.__SIBU_DEV_WARN__ !== false && typeof console !== "undefined") {
626
- console.error(
627
- `[SibuJS] effect re-requested itself ${MAX_RERUNS}+ times on initial run \u2014 likely a write-reads-self cycle. Breaking to prevent infinite loop.`
628
- );
629
- }
630
- rerunPending = false;
631
- break;
632
- }
633
- } while (rerunPending);
686
+ retrack(ctx.bodyFn, ctx.subscriber);
687
+ if (ctx.rerunPending) drainReruns(ctx);
634
688
  } finally {
635
- running = false;
636
- rerunPending = false;
689
+ ctx.running = false;
690
+ ctx.rerunPending = false;
637
691
  }
638
692
  const hook = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
639
693
  if (hook) hook.emit("effect:create", { effectFn });
640
- let disposed = false;
641
- return () => {
642
- if (disposed) return;
643
- disposed = true;
644
- const h = _g2.__SIBU_DEVTOOLS_GLOBAL_HOOK__;
645
- if (h) {
646
- try {
647
- h.emit("effect:destroy", { effectFn });
648
- } catch {
649
- }
650
- }
651
- try {
652
- runUserCleanups();
653
- } catch (err) {
654
- if (typeof console !== "undefined") {
655
- console.warn("[SibuJS effect] onCleanup threw during dispose:", err);
656
- }
657
- }
658
- try {
659
- cleanupHandle();
660
- } catch (err) {
661
- if (typeof console !== "undefined") {
662
- console.warn("[SibuJS effect] dispose threw:", err);
663
- }
664
- }
665
- };
694
+ return () => disposeEffect(ctx);
666
695
  }
667
696
 
668
697
  // src/patterns/persist.ts
@@ -895,6 +924,7 @@ function derived(getter, options) {
895
924
  const cs = {};
896
925
  cs._d = false;
897
926
  cs._g = getter;
927
+ cs.__v = 0;
898
928
  const markDirty = () => {
899
929
  if (cs._d) return;
900
930
  cs._d = true;
@@ -924,11 +954,14 @@ function derived(getter, options) {
924
954
  evaluating = true;
925
955
  let threw = true;
926
956
  try {
957
+ const prev = cs._v;
927
958
  retrack(() => {
928
- cs._v = getter();
959
+ const next = getter();
960
+ cs._v = equals && cs._v !== void 0 ? equals(cs._v, next) ? cs._v : next : next;
929
961
  cs._d = false;
930
962
  threw = false;
931
963
  }, markDirty);
964
+ if (!Object.is(prev, cs._v)) cs.__v++;
932
965
  } finally {
933
966
  evaluating = false;
934
967
  if (threw) cs._d = true;
@@ -948,6 +981,7 @@ function derived(getter, options) {
948
981
  cs._d = false;
949
982
  threw = false;
950
983
  }, markDirty);
984
+ if (!Object.is(oldValue, cs._v)) cs.__v++;
951
985
  } finally {
952
986
  evaluating = false;
953
987
  if (threw) cs._d = true;