mutts 1.0.3 → 1.0.4

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.
@@ -470,6 +470,7 @@ declare function addBatchCleanup(cleanup: ScopedCallback): void;
470
470
  * ```
471
471
  */
472
472
  declare const defer: typeof addBatchCleanup;
473
+ declare function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'immediate'): any;
473
474
  /**
474
475
  * Decorator that makes methods atomic - batches all effects triggered within the method
475
476
  */
@@ -852,5 +853,5 @@ declare function isZoneEnabled(): boolean;
852
853
  */
853
854
  declare const profileInfo: any;
854
855
 
855
- export { ReactiveBase, ReactiveError, ReadOnlyError, Register, addBatchCleanup, atomic, biDi, buildReactivityGraph, cleanedBy, cleanup, deepWatch, defer, derived, effect, enableDevTools, getActiveEffect, getState, immutables, isDevtoolsEnabled, isNonReactive, isReactive, isZoneEnabled, mapped, memoize, organize, organized, profileInfo, project, reactive, options as reactiveOptions, reduced, register, registerEffectForDebug, registerNativeReactivity, registerObjectForDebug, root, setEffectName, setObjectName, setZoneEnabled, touched, touched1, trackEffect, unreactive, untracked, unwrap, watch };
856
+ export { ReactiveBase, ReactiveError, ReadOnlyError, Register, addBatchCleanup, atomic, batch, biDi, buildReactivityGraph, cleanedBy, cleanup, deepWatch, defer, derived, effect, enableDevTools, getActiveEffect, getState, immutables, isDevtoolsEnabled, isNonReactive, isReactive, isZoneEnabled, mapped, memoize, organize, organized, profileInfo, project, reactive, options as reactiveOptions, reduced, register, registerEffectForDebug, registerNativeReactivity, registerObjectForDebug, root, setEffectName, setObjectName, setZoneEnabled, touched, touched1, trackEffect, unreactive, untracked, unwrap, watch };
856
857
  export type { DependencyAccess, DependencyFunction, Evolution, Memoizable, ReactivityGraph, ScopedCallback };
@@ -1,4 +1,4 @@
1
- export { L as ReactiveBase, U as ReactiveError, R as ReadOnlyError, Q as Register, j as addBatchCleanup, k as atomic, l as biDi, c as buildReactivityGraph, x as cleanedBy, y as cleanup, h as deepWatch, n as defer, z as derived, o as effect, e as enableDevTools, q as getActiveEffect, g as getState, F as immutables, i as isDevtoolsEnabled, G as isNonReactive, K as isReactive, V as isZoneEnabled, C as mapped, E as memoize, O as organize, P as organized, p as profileInfo, J as project, M as reactive, T as reactiveOptions, D as reduced, S as register, r as registerEffectForDebug, H as registerNativeReactivity, d as registerObjectForDebug, u as root, s as setEffectName, f as setObjectName, W as setZoneEnabled, t as touched, b as touched1, v as trackEffect, A as unreactive, w as untracked, N as unwrap, B as watch } from './chunks/index-DzUDtFc7.esm.js';
1
+ export { M as ReactiveBase, V as ReactiveError, R as ReadOnlyError, S as Register, j as addBatchCleanup, k as atomic, l as batch, n as biDi, c as buildReactivityGraph, y as cleanedBy, z as cleanup, h as deepWatch, o as defer, A as derived, q as effect, e as enableDevTools, u as getActiveEffect, g as getState, G as immutables, i as isDevtoolsEnabled, H as isNonReactive, L as isReactive, W as isZoneEnabled, D as mapped, F as memoize, P as organize, Q as organized, p as profileInfo, K as project, N as reactive, U as reactiveOptions, E as reduced, T as register, r as registerEffectForDebug, J as registerNativeReactivity, d as registerObjectForDebug, v as root, s as setEffectName, f as setObjectName, X as setZoneEnabled, t as touched, b as touched1, w as trackEffect, B as unreactive, x as untracked, O as unwrap, C as watch } from './chunks/index-79Kk8D6e.esm.js';
2
2
  import './chunks/decorator-DqiszP7i.esm.js';
3
3
  import './indexable.esm.js';
4
4
  import './chunks/_tslib-Mzh1rNsX.esm.js';
package/dist/reactive.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var reactive = require('./chunks/index-HNVqPzjz.js');
3
+ var reactive = require('./chunks/index-GRBSx0mB.js');
4
4
  require('./chunks/decorator-DLvrD0UF.js');
5
5
  require('./indexable.js');
6
6
  require('./chunks/_tslib-BgjropY9.js');
@@ -13,6 +13,7 @@ exports.ReadOnlyError = reactive.ReadOnlyError;
13
13
  exports.Register = reactive.Register;
14
14
  exports.addBatchCleanup = reactive.addBatchCleanup;
15
15
  exports.atomic = reactive.atomic;
16
+ exports.batch = reactive.batch;
16
17
  exports.biDi = reactive.biDi;
17
18
  exports.buildReactivityGraph = reactive.buildReactivityGraph;
18
19
  exports.cleanedBy = reactive.cleanedBy;
@@ -1 +1 @@
1
- {"version":3,"file":"reactive.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"reactive.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mutts",
3
3
  "description": "Modern UTility TS: A collection of TypeScript utilities",
4
- "version": "1.0.3",
4
+ "version": "1.0.4",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
7
7
  "types": "dist/index.d.ts",
@@ -835,15 +835,18 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
835
835
  addToBatch(effect[i], caller, immediate === 'immediate')
836
836
  }
837
837
  if (immediate) {
838
+ const firstReturn: { value?: any } = {}
838
839
  // Execute immediately (before batch returns)
839
840
  for (let i = 0; i < effect.length; i++) {
840
841
  try {
841
- effect[i]()
842
+ const rv = effect[i]()
843
+ if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
842
844
  } finally {
843
845
  const root = getRoot(effect[i])
844
846
  batchQueue.all.delete(root)
845
847
  }
846
848
  }
849
+ return firstReturn.value
847
850
  }
848
851
  // Otherwise, effects will be picked up in next executeNext() call
849
852
  } else {
@@ -935,67 +938,76 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
935
938
  // Execute in dependency order
936
939
  const firstReturn: { value?: any } = {}
937
940
  try {
938
- while (batchQueue.all.size > 0) {
939
- if (effectuatedRoots.length > options.maxEffectChain) {
940
- const cycle = findCycleInChain(effectuatedRoots as any)
941
- const trace = formatRoots(effectuatedRoots as any)
942
- const message = cycle
943
- ? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
944
- : `Max effect chain reached (trace: ${trace})`
945
-
946
- const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : []
947
- const queued = queuedRoots.map((r) => r.name || '<anonymous>')
948
- const debugInfo = {
949
- code: ReactiveErrorCode.MaxDepthExceeded,
950
- effectuatedRoots,
951
- cycle,
952
- trace,
953
- maxEffectChain: options.maxEffectChain,
954
- queued: queued.slice(0, 50),
955
- queuedCount: queued.length,
956
- // Try to get causation for the last effect
957
- causalChain:
958
- effectuatedRoots.length > 0
959
- ? getTriggerChain(
960
- batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1])!
961
- )
962
- : [],
941
+ // Outer loop: continue while there are effects OR cleanups pending.
942
+ // This ensures effects triggered by cleanups are not lost.
943
+ while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
944
+ // Inner loop: execute all pending effects
945
+ while (batchQueue.all.size > 0) {
946
+ if (effectuatedRoots.length > options.maxEffectChain) {
947
+ const cycle = findCycleInChain(effectuatedRoots as any)
948
+ const trace = formatRoots(effectuatedRoots as any)
949
+ const message = cycle
950
+ ? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
951
+ : `Max effect chain reached (trace: ${trace})`
952
+
953
+ const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : []
954
+ const queued = queuedRoots.map((r) => r.name || '<anonymous>')
955
+ const debugInfo = {
956
+ code: ReactiveErrorCode.MaxDepthExceeded,
957
+ effectuatedRoots,
958
+ cycle,
959
+ trace,
960
+ maxEffectChain: options.maxEffectChain,
961
+ queued: queued.slice(0, 50),
962
+ queuedCount: queued.length,
963
+ // Try to get causation for the last effect
964
+ causalChain:
965
+ effectuatedRoots.length > 0
966
+ ? getTriggerChain(
967
+ batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1])!
968
+ )
969
+ : [],
970
+ }
971
+ switch (options.maxEffectReaction) {
972
+ case 'throw':
973
+ throw new ReactiveError(`[reactive] ${message}`, debugInfo)
974
+ case 'debug':
975
+ // biome-ignore lint/suspicious/noDebugger: This is the whole point here
976
+ debugger
977
+ throw new ReactiveError(`[reactive] ${message}`, debugInfo)
978
+ case 'warn':
979
+ options.warn(
980
+ `[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`
981
+ )
982
+ break
983
+ }
963
984
  }
964
- switch (options.maxEffectReaction) {
965
- case 'throw':
966
- throw new ReactiveError(`[reactive] ${message}`, debugInfo)
967
- case 'debug':
968
- // biome-ignore lint/suspicious/noDebugger: This is the whole point here
969
- debugger
970
- throw new ReactiveError(`[reactive] ${message}`, debugInfo)
971
- case 'warn':
972
- options.warn(
973
- `[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`
974
- )
975
- break
985
+ const rv = executeNext(effectuatedRoots)
986
+ // executeNext() returns null when batch is complete or cycle detected (throws error)
987
+ // But functions can legitimately return null, so we check batchQueue.all.size instead
988
+ if (batchQueue.all.size === 0) {
989
+ // Batch complete
990
+ break
976
991
  }
992
+ // If executeNext() returned null but batch is not empty, it means a cycle was detected
993
+ // and an error was thrown, so we won't reach here
994
+ if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
995
+ // Note: executeNext() already removed it from batchQueue, so we track by count
977
996
  }
978
- const rv = executeNext(effectuatedRoots)
979
- // executeNext() returns null when batch is complete or cycle detected (throws error)
980
- // But functions can legitimately return null, so we check batchQueue.all.size instead
981
- if (batchQueue.all.size === 0) {
982
- // Batch complete
983
- break
997
+ // Process cleanups. If they trigger new effects, the outer loop will catch them.
998
+ if (batchCleanups.size > 0) {
999
+ const cleanups = Array.from(batchCleanups)
1000
+ batchCleanups.clear()
1001
+ for (const cleanup of cleanups) cleanup()
984
1002
  }
985
- // If executeNext() returned null but batch is not empty, it means a cycle was detected
986
- // and an error was thrown, so we won't reach here
987
- if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
988
- // Note: executeNext() already removed it from batchQueue, so we track by count
989
1003
  }
990
- const cleanups = Array.from(batchCleanups)
991
- batchCleanups.clear()
992
- for (const cleanup of cleanups) cleanup()
993
1004
  return firstReturn.value
994
1005
  } finally {
995
1006
  batchQueue = undefined
996
1007
  options.endChain()
997
1008
  }
998
1009
  }
1010
+
999
1011
  }
1000
1012
  }
1001
1013
 
@@ -13,6 +13,7 @@ export { deepWatch } from './deep-watch'
13
13
  export {
14
14
  addBatchCleanup,
15
15
  atomic,
16
+ batch, // TODO: Batch is now exported for testing purposes, though it shouldn't be - modify the tests to go through `atomic`
16
17
  biDi,
17
18
  defer,
18
19
  effect,
@@ -79,7 +79,11 @@ const reactiveHandlers = {
79
79
  while (current && current !== Object.prototype) {
80
80
  dependant(current, prop)
81
81
  if (Object.hasOwn(current, prop)) break
82
- current = reactiveObject(Object.getPrototypeOf(current))
82
+ let next = reactiveObject(Object.getPrototypeOf(current))
83
+ if (next === current) {
84
+ next = reactiveObject(Object.getPrototypeOf(unwrap(current)))
85
+ }
86
+ current = next
83
87
  }
84
88
  }
85
89
  const value = ReflectGet(obj, prop, receiver)