mutts 1.0.6 → 1.0.7

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 (110) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.esm.js +70 -0
  4. package/dist/browser.esm.js.map +1 -0
  5. package/dist/browser.js +161 -0
  6. package/dist/browser.js.map +1 -0
  7. package/dist/chunks/{index-CDCOjzTy.js → index-BFYK02LG.js} +5760 -4338
  8. package/dist/chunks/index-BFYK02LG.js.map +1 -0
  9. package/dist/chunks/{index-DiP0RXoZ.esm.js → index-CNR6QRUl.esm.js} +5440 -4054
  10. package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
  11. package/dist/devtools/panel.js.map +1 -1
  12. package/dist/mutts.umd.js +1 -1
  13. package/dist/mutts.umd.js.map +1 -1
  14. package/dist/mutts.umd.min.js +1 -1
  15. package/dist/mutts.umd.min.js.map +1 -1
  16. package/dist/node.d.ts +2 -0
  17. package/dist/node.esm.js +45 -0
  18. package/dist/node.esm.js.map +1 -0
  19. package/dist/node.js +136 -0
  20. package/dist/node.js.map +1 -0
  21. package/docs/ai/api-reference.md +0 -2
  22. package/docs/reactive/advanced.md +2 -5
  23. package/docs/reactive/collections.md +0 -125
  24. package/docs/reactive/core.md +27 -24
  25. package/docs/reactive/debugging.md +12 -2
  26. package/docs/reactive/project.md +1 -1
  27. package/docs/reactive/scan.md +78 -0
  28. package/docs/reactive.md +2 -1
  29. package/docs/std-decorators.md +1 -0
  30. package/docs/zone.md +88 -0
  31. package/package.json +42 -10
  32. package/src/async/browser.ts +87 -0
  33. package/src/async/index.ts +8 -0
  34. package/src/async/node.ts +46 -0
  35. package/src/decorator.ts +5 -1
  36. package/src/destroyable.ts +1 -1
  37. package/src/index.ts +22 -14
  38. package/src/indexable.ts +42 -0
  39. package/src/mixins.ts +2 -2
  40. package/src/reactive/array.ts +149 -141
  41. package/src/reactive/buffer.ts +168 -0
  42. package/src/reactive/change.ts +2 -2
  43. package/src/reactive/effect-context.ts +15 -91
  44. package/src/reactive/effects.ts +119 -179
  45. package/src/reactive/index.ts +10 -13
  46. package/src/reactive/interface.ts +19 -33
  47. package/src/reactive/map.ts +48 -61
  48. package/src/reactive/memoize.ts +19 -9
  49. package/src/reactive/project.ts +43 -22
  50. package/src/reactive/proxy.ts +16 -41
  51. package/src/reactive/record.ts +3 -3
  52. package/src/reactive/register.ts +5 -7
  53. package/src/reactive/registry.ts +9 -17
  54. package/src/reactive/set.ts +42 -56
  55. package/src/reactive/tracking.ts +1 -29
  56. package/src/reactive/types.ts +46 -23
  57. package/src/utils.ts +80 -37
  58. package/src/zone.ts +127 -0
  59. package/dist/chunks/_tslib-BgjropY9.js +0 -81
  60. package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
  61. package/dist/chunks/_tslib-MCKDzsSq.esm.js +0 -75
  62. package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +0 -1
  63. package/dist/chunks/decorator-BGILvPtN.esm.js +0 -627
  64. package/dist/chunks/decorator-BGILvPtN.esm.js.map +0 -1
  65. package/dist/chunks/decorator-BQ2eBTCj.js +0 -651
  66. package/dist/chunks/decorator-BQ2eBTCj.js.map +0 -1
  67. package/dist/chunks/index-CDCOjzTy.js.map +0 -1
  68. package/dist/chunks/index-DiP0RXoZ.esm.js.map +0 -1
  69. package/dist/decorator.d.ts +0 -107
  70. package/dist/decorator.esm.js +0 -2
  71. package/dist/decorator.esm.js.map +0 -1
  72. package/dist/decorator.js +0 -11
  73. package/dist/decorator.js.map +0 -1
  74. package/dist/destroyable.d.ts +0 -90
  75. package/dist/destroyable.esm.js +0 -109
  76. package/dist/destroyable.esm.js.map +0 -1
  77. package/dist/destroyable.js +0 -116
  78. package/dist/destroyable.js.map +0 -1
  79. package/dist/eventful.d.ts +0 -20
  80. package/dist/eventful.esm.js +0 -66
  81. package/dist/eventful.esm.js.map +0 -1
  82. package/dist/eventful.js +0 -68
  83. package/dist/eventful.js.map +0 -1
  84. package/dist/index.d.ts +0 -19
  85. package/dist/index.esm.js +0 -53
  86. package/dist/index.esm.js.map +0 -1
  87. package/dist/index.js +0 -139
  88. package/dist/index.js.map +0 -1
  89. package/dist/indexable.d.ts +0 -243
  90. package/dist/indexable.esm.js +0 -285
  91. package/dist/indexable.esm.js.map +0 -1
  92. package/dist/indexable.js +0 -291
  93. package/dist/indexable.js.map +0 -1
  94. package/dist/promiseChain.d.ts +0 -21
  95. package/dist/promiseChain.esm.js +0 -78
  96. package/dist/promiseChain.esm.js.map +0 -1
  97. package/dist/promiseChain.js +0 -80
  98. package/dist/promiseChain.js.map +0 -1
  99. package/dist/reactive.d.ts +0 -910
  100. package/dist/reactive.esm.js +0 -5
  101. package/dist/reactive.esm.js.map +0 -1
  102. package/dist/reactive.js +0 -59
  103. package/dist/reactive.js.map +0 -1
  104. package/dist/std-decorators.d.ts +0 -52
  105. package/dist/std-decorators.esm.js +0 -196
  106. package/dist/std-decorators.esm.js.map +0 -1
  107. package/dist/std-decorators.js +0 -204
  108. package/dist/std-decorators.js.map +0 -1
  109. package/src/reactive/mapped.ts +0 -129
  110. package/src/reactive/zone.ts +0 -208
package/docs/zone.md ADDED
@@ -0,0 +1,88 @@
1
+ # Zones (`mutts/zone`)
2
+
3
+ Zones provide a high-performance **context management system** that follows the execution flow, ensuring your variables "stay put" even across asynchronous boundaries like `Promises`, `setTimeout`, or `queueMicrotask`.
4
+
5
+ ## Basic Usage
6
+
7
+ A `Zone` represents a piece of storage that is scoped to the current execution block.
8
+
9
+ ```typescript
10
+ import { Zone } from 'mutts/zone';
11
+
12
+ const myZone = new Zone<string>();
13
+
14
+ myZone.with("context-value", () => {
15
+ // Inside this function, the zone is active
16
+ console.log(myZone.active); // "context-value"
17
+ });
18
+
19
+ console.log(myZone.active); // undefined
20
+ ```
21
+
22
+ ## Async Propagation
23
+
24
+ By default, zones are lost when an async operation yields control (e.g., after `await`). To fix this, `mutts` provides `configureAsyncZone()`.
25
+
26
+ ```typescript
27
+ import { configureAsyncZone, asyncZone, Zone } from 'mutts/zone';
28
+
29
+ const requestId = new Zone<string>();
30
+
31
+ // 1. Tell the global aggregator to track this zone
32
+ asyncZone.add(requestId);
33
+
34
+ // 2. Patch global async primitives (once per app)
35
+ configureAsyncZone();
36
+
37
+ // 3. Usage
38
+ requestId.with("req-123", async () => {
39
+ await somePromise();
40
+ // Context is automatically preserved across await!
41
+ console.log(requestId.active); // "req-123"
42
+ });
43
+ ```
44
+
45
+ ## Core API
46
+
47
+ ### `AZone<T>` (Abstract)
48
+ The base class for all zone implementations.
49
+ - `active: T | undefined`: The current value in the zone.
50
+ - `with<R>(value: T, fn: () => R): R`: Executes `fn` with `value` set as active.
51
+ - `root<R>(fn: () => R): R`: Executes `fn` with the zone cleared (undefined).
52
+ - `zoned: FunctionWrapper`: A getter that returns a function which, when called, restores the zone to its **current** state.
53
+
54
+ ### `Zone<T>`
55
+ Simple stack-based storage.
56
+
57
+ ### `ZoneHistory<T>`
58
+ A zone wrapper that maintains a `history` of previously active values in the current stack.
59
+ - Useful for **Cycle Detection**.
60
+ - Prevents re-entering the same value if already in the history.
61
+ - `present: AZone<T>`: Access the current value without the history overhead.
62
+
63
+ ### `ZoneAggregator`
64
+ Combines multiple zones into one.
65
+ - Entering an aggregator (with `.with()`) enters all its member zones.
66
+ - `asyncZone` is a global aggregator used for the async patches.
67
+
68
+ ## Manual Context Bridging
69
+
70
+ If you are using an API that `mutts` doesn't automatically patch, you can use the `.zoned` capture mechanism:
71
+
72
+ ```typescript
73
+ const wrap = myZone.zoned; // Snapshot the current context
74
+
75
+ // Pass the wrapper to an unmanaged callback
76
+ externalLib.on('event', () => {
77
+ wrap(() => {
78
+ console.log("Context is back:", myZone.active);
79
+ });
80
+ });
81
+ ```
82
+
83
+ ## Integration with Reactivity
84
+
85
+ The `mutts` reactivity system uses zones internally to track the `activeEffect`.
86
+ - Every `effect()` execution runs inside the `effectHistory` zone.
87
+ - Circular dependency detection is powered by `ZoneHistory`.
88
+ - Async effects survive `await` because `effectHistory` is a member of the global `asyncZone`.
package/package.json CHANGED
@@ -1,19 +1,51 @@
1
1
  {
2
2
  "name": "mutts",
3
3
  "description": "Modern UTility TS: A collection of TypeScript utilities",
4
- "version": "1.0.6",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
7
- "types": "src/index.ts",
4
+ "version": "1.0.7",
5
+ "main": "dist/browser.js",
6
+ "module": "dist/browser.esm.js",
7
+ "types": "dist/browser.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "source": "./src/index.ts",
12
- "import": "./dist/index.esm.js",
13
- "require": "./dist/index.js",
14
- "script": "./dist/mutts.umd.min.js"
10
+ "node": {
11
+ "types": "./dist/node.d.ts",
12
+ "import": "./dist/node.esm.js",
13
+ "require": "./dist/node.js"
14
+ },
15
+ "default": {
16
+ "types": "./dist/browser.d.ts",
17
+ "import": "./dist/browser.esm.js",
18
+ "require": "./dist/browser.js"
19
+ }
15
20
  },
16
- "./src/*": "./src/*"
21
+ "./browser": {
22
+ "types": "./dist/browser.d.ts",
23
+ "import": "./dist/browser.esm.js",
24
+ "require": "./dist/browser.js"
25
+ },
26
+ "./node": {
27
+ "types": "./dist/node.d.ts",
28
+ "import": "./dist/node.esm.js",
29
+ "require": "./dist/node.js"
30
+ },
31
+ "./src": {
32
+ "node": {
33
+ "types": "./src/async/node.ts",
34
+ "import": "./src/async/node.ts"
35
+ },
36
+ "default": {
37
+ "types": "./src/async/browser.ts",
38
+ "import": "./src/async/browser.ts"
39
+ }
40
+ },
41
+ "./src/browser": {
42
+ "types": "./src/async/browser.ts",
43
+ "import": "./src/async/browser.ts"
44
+ },
45
+ "./src/node": {
46
+ "types": "./src/async/node.ts",
47
+ "import": "./src/async/node.ts"
48
+ }
17
49
  },
18
50
  "files": [
19
51
  "dist",
@@ -0,0 +1,87 @@
1
+ import { Hook, Restorer, asyncHooks } from '.'
2
+
3
+ const hooks = new Set<Hook>()
4
+
5
+ asyncHooks.addHook = function (hook: Hook) {
6
+ hooks.add(hook)
7
+ return () => {
8
+ hooks.delete(hook)
9
+ }
10
+ }
11
+
12
+ export * from '../index'
13
+
14
+ function wrap<Args extends any[], R>(fn: ((...args: Args) => R) | null | undefined) {
15
+ if (typeof fn !== 'function') return fn
16
+ const restorers = new Set<Restorer>()
17
+ for (const hook of hooks) restorers.add(hook())
18
+
19
+ return function (this: any, ...args: Args) {
20
+ const undoers = new Set<() => void>()
21
+ for (const restore of restorers) undoers.add(restore())
22
+ try {
23
+ return fn.apply(this, args)
24
+ } finally {
25
+ for (const undo of undoers) undo()
26
+ }
27
+ }
28
+ }
29
+
30
+ const originals = {
31
+ then: Promise.prototype.then,
32
+ catch: Promise.prototype.catch,
33
+ finally: Promise.prototype.finally,
34
+ setTimeout: globalThis.setTimeout,
35
+ setInterval: globalThis.setInterval,
36
+ setImmediate: globalThis.setImmediate,
37
+ requestAnimationFrame: globalThis.requestAnimationFrame,
38
+ queueMicrotask: globalThis.queueMicrotask,
39
+ }
40
+
41
+ Promise.prototype.then = function <T, R1, R2>(
42
+ this: Promise<T>,
43
+ onFulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null,
44
+ onRejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
45
+ ): Promise<R1 | R2> {
46
+ return originals.then.call(this, wrap(onFulfilled), wrap(onRejected))
47
+ }
48
+
49
+ Promise.prototype.catch = function <T>(
50
+ this: Promise<T>,
51
+ onRejected?: ((reason: any) => T | PromiseLike<T>) | null
52
+ ): Promise<T> {
53
+ return originals.catch.call(this, wrap(onRejected))
54
+ }
55
+
56
+ Promise.prototype.finally = function <T>(
57
+ this: Promise<T>,
58
+ onFinally?: (() => void) | null
59
+ ): Promise<T> {
60
+ return originals.finally.call(this, wrap(onFinally))
61
+ }
62
+
63
+ globalThis.setTimeout = ((callback: Function, ...args: any[]) => {
64
+ return originals.setTimeout.call(globalThis, wrap(callback as any), ...args)
65
+ }) as any
66
+
67
+ globalThis.setInterval = ((callback: Function, ...args: any[]) => {
68
+ return originals.setInterval.call(globalThis, wrap(callback as any), ...args)
69
+ }) as any
70
+
71
+ if (originals.setImmediate) {
72
+ globalThis.setImmediate = ((callback: Function, ...args: any[]) => {
73
+ return originals.setImmediate.call(globalThis, wrap(callback as any), ...args)
74
+ }) as any
75
+ }
76
+
77
+ if (originals.requestAnimationFrame) {
78
+ globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {
79
+ return originals.requestAnimationFrame.call(globalThis, wrap(callback))
80
+ }
81
+ }
82
+
83
+ if (originals.queueMicrotask) {
84
+ globalThis.queueMicrotask = (callback: VoidFunction): void => {
85
+ originals.queueMicrotask.call(globalThis, wrap(callback))
86
+ }
87
+ }
@@ -0,0 +1,8 @@
1
+ export type Restorer = () => () => void
2
+ export type Hook = () => Restorer
3
+
4
+ export const asyncHooks = {
5
+ addHook(_hook: Hook): () => void {
6
+ throw 'One must import the library from the server or the client side'
7
+ }
8
+ }
@@ -0,0 +1,46 @@
1
+ import { createHook } from 'node:async_hooks'
2
+ import { Hook, Restorer, asyncHooks } from '.'
3
+
4
+ const hooks = new Set<Hook>()
5
+ const restorersPerAsyncId = new Map<number, Set<Restorer>>()
6
+ const undoersPerAsyncId = new Map<number, Set<() => void>>()
7
+
8
+ createHook({
9
+ init(asyncId) {
10
+ const restorers = new Set<Restorer>()
11
+ for (const hook of hooks) {
12
+ restorers.add(hook())
13
+ }
14
+ restorersPerAsyncId.set(asyncId, restorers)
15
+ },
16
+ before(asyncId) {
17
+ const restorers = restorersPerAsyncId.get(asyncId)
18
+ if (restorers) {
19
+ const undoers = new Set<() => void>()
20
+ for (const restore of restorers) {
21
+ undoers.add(restore())
22
+ }
23
+ undoersPerAsyncId.set(asyncId, undoers)
24
+ }
25
+ },
26
+ after(asyncId) {
27
+ const undoers = undoersPerAsyncId.get(asyncId)
28
+ if (undoers) {
29
+ for (const undo of undoers) undo()
30
+ undoersPerAsyncId.delete(asyncId)
31
+ }
32
+ },
33
+ destroy(asyncId) {
34
+ restorersPerAsyncId.delete(asyncId)
35
+ undoersPerAsyncId.delete(asyncId)
36
+ }
37
+ }).enable()
38
+
39
+ asyncHooks.addHook = function (hook: Hook) {
40
+ hooks.add(hook)
41
+ return () => {
42
+ hooks.delete(hook)
43
+ }
44
+ }
45
+
46
+ export * from '../index'
package/src/decorator.ts CHANGED
@@ -62,7 +62,11 @@ type DDMethod<T> = (
62
62
  name: PropertyKey
63
63
  ) => ((this: T, ...args: any[]) => any) | void
64
64
 
65
- type DDGetter<T> = (original: (this: T) => any, target: any, name: PropertyKey) => ((this: T) => any) | void
65
+ type DDGetter<T> = (
66
+ original: (this: T) => any,
67
+ target: any,
68
+ name: PropertyKey
69
+ ) => ((this: T) => any) | void
66
70
 
67
71
  type DDSetter<T> = (
68
72
  original: (this: T, value: any) => void,
@@ -117,7 +117,7 @@ export function Destroyable<
117
117
  base = undefined
118
118
  }
119
119
  if (!base) {
120
- base = class { } as T
120
+ base = class {} as T
121
121
  }
122
122
 
123
123
  return class Destroyable extends (base as T) {
package/src/index.ts CHANGED
@@ -4,29 +4,37 @@ export * from './eventful'
4
4
  export * from './indexable'
5
5
  export * from './iterableWeak'
6
6
  export * from './mixins'
7
+ export * from './promiseChain'
7
8
  export * from './reactive'
8
9
  export * from './std-decorators'
9
10
  export * from './utils'
11
+ export * from './zone'
10
12
 
11
13
  import pkg from '../package.json'
14
+
12
15
  const { version } = pkg
13
16
 
14
17
  // Singleton verification
15
18
  const GLOBAL_MUTTS_KEY = '__MUTTS_INSTANCE__'
16
- const globalScope =
17
- (typeof globalThis !== 'undefined' ? globalThis :
18
- (typeof window !== 'undefined' ? window :
19
- (typeof global !== 'undefined' ? global : false))) as any
20
- if(globalScope) {
19
+ const globalScope = (
20
+ typeof globalThis !== 'undefined'
21
+ ? globalThis
22
+ : typeof window !== 'undefined'
23
+ ? window
24
+ : typeof global !== 'undefined'
25
+ ? global
26
+ : false
27
+ ) as any
28
+ if (globalScope) {
21
29
  // Detect the source of this instance safely across different environments
22
30
  let source = 'mutts/index'
31
+ const viteEval = eval
23
32
  try {
24
33
  // @ts-ignore
25
34
  if (typeof __filename !== 'undefined') source = __filename
26
- // @ts-ignore
27
35
  else {
28
36
  // Using eval to avoid SyntaxError in CJS environments where import.meta is not allowed
29
- const meta = eval('import.meta')
37
+ const meta = viteEval('import.meta')
30
38
  if (meta && meta.url) source = meta.url
31
39
  }
32
40
  } catch (e) {
@@ -36,18 +44,18 @@ if(globalScope) {
36
44
  const currentSourceInfo = {
37
45
  version,
38
46
  source,
39
- timestamp: Date.now()
47
+ timestamp: Date.now(),
40
48
  }
41
49
 
42
50
  if (globalScope[GLOBAL_MUTTS_KEY]) {
43
51
  const existing = globalScope[GLOBAL_MUTTS_KEY]
44
52
  throw new Error(
45
- `[Mutts] Multiple instances detected!\n` +
46
- `Existing instance: ${JSON.stringify(existing, null, 2)}\n` +
47
- `New instance: ${JSON.stringify(currentSourceInfo, null, 2)}\n` +
48
- `This usually happens when 'mutts' is both installed as a dependency and bundled, ` +
49
- `or when different versions are loaded. ` +
50
- `Please check your build configuration (aliases, externals) to ensure a single source of truth.`
53
+ `[Mutts] Multiple instances detected!\n` +
54
+ `Existing instance: ${JSON.stringify(existing, null, 2)}\n` +
55
+ `New instance: ${JSON.stringify(currentSourceInfo, null, 2)}\n` +
56
+ `This usually happens when 'mutts' is both installed as a dependency and bundled, ` +
57
+ `or when different versions are loaded. ` +
58
+ `Please check your build configuration (aliases, externals) to ensure a single source of truth.`
51
59
  )
52
60
  }
53
61
 
package/src/indexable.ts CHANGED
@@ -140,6 +140,48 @@ export function Indexable<Items, Base extends abstract new (...args: any[]) => a
140
140
  })
141
141
  return true
142
142
  },
143
+ has(target, prop) {
144
+ if (prop in target) return true
145
+ if (typeof prop === 'string') {
146
+ if (prop === 'length' && accessor.getLength) return true
147
+ const numProp = Number(prop)
148
+ if (!Number.isNaN(numProp)) return true
149
+ }
150
+ return false
151
+ },
152
+ ownKeys(target) {
153
+ const keys = Reflect.ownKeys(target)
154
+ if (accessor.getLength) {
155
+ keys.push('length')
156
+ const len = accessor.getLength.call(this as any)
157
+ for (let i = 0; i < len; i++) keys.push(String(i))
158
+ }
159
+ return keys
160
+ },
161
+ getOwnPropertyDescriptor(target, prop) {
162
+ if (prop in target) return Object.getOwnPropertyDescriptor(target, prop)
163
+ if (typeof prop === 'string') {
164
+ if (prop === 'length' && accessor.getLength) {
165
+ return {
166
+ enumerable: false,
167
+ configurable: true,
168
+ get: () => accessor.getLength!.call(this as any),
169
+ }
170
+ }
171
+ const numProp = Number(prop)
172
+ if (!Number.isNaN(numProp)) {
173
+ return {
174
+ enumerable: true,
175
+ configurable: true,
176
+ get: () => accessor.get!.call(this as any, numProp),
177
+ set: accessor.set
178
+ ? (v: any) => accessor.set!.call(this as any, numProp, v)
179
+ : undefined,
180
+ }
181
+ }
182
+ }
183
+ return undefined
184
+ },
143
185
  })
144
186
  )
145
187
  return Indexable
package/src/mixins.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isConstructor, ReflectGet } from './utils'
1
+ import { FoolProof, isConstructor } from './utils'
2
2
 
3
3
  /**
4
4
  * A mixin function that takes a base class and returns a new class with mixed-in functionality
@@ -85,7 +85,7 @@ export function mixin<MixinFn extends (base: any) => new (...args: any[]) => any
85
85
  const originalPrototype = baseClass.prototype
86
86
  const proxiedPrototype = new Proxy(originalPrototype, {
87
87
  get(target, prop, receiver) {
88
- const value = ReflectGet(target, prop, receiver)
88
+ const value = FoolProof.get(target, prop, receiver)
89
89
 
90
90
  // Only wrap methods that are likely to access private fields
91
91
  // Skip symbols and special properties that the reactive system needs