mutts 1.0.4 → 1.0.5

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{index-GRBSx0mB.js → index-Cvxdw6Ax.js} +164 -12
  3. package/dist/chunks/index-Cvxdw6Ax.js.map +1 -0
  4. package/dist/chunks/{index-79Kk8D6e.esm.js → index-qiWwozOc.esm.js} +163 -13
  5. package/dist/chunks/index-qiWwozOc.esm.js.map +1 -0
  6. package/dist/destroyable.esm.js.map +1 -1
  7. package/dist/destroyable.js.map +1 -1
  8. package/dist/index.esm.js +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/mutts.umd.js +1 -1
  12. package/dist/mutts.umd.js.map +1 -1
  13. package/dist/mutts.umd.min.js +1 -1
  14. package/dist/mutts.umd.min.js.map +1 -1
  15. package/dist/reactive.d.ts +29 -1
  16. package/dist/reactive.esm.js +1 -1
  17. package/dist/reactive.js +3 -1
  18. package/dist/reactive.js.map +1 -1
  19. package/dist/std-decorators.esm.js.map +1 -1
  20. package/dist/std-decorators.js.map +1 -1
  21. package/docs/reactive/core.md +16 -16
  22. package/docs/reactive.md +7 -0
  23. package/package.json +1 -1
  24. package/src/destroyable.ts +2 -2
  25. package/src/reactive/array.ts +3 -5
  26. package/src/reactive/change.ts +6 -2
  27. package/src/reactive/effects.ts +70 -1
  28. package/src/reactive/index.ts +2 -1
  29. package/src/reactive/interface.ts +1 -1
  30. package/src/reactive/map.ts +6 -6
  31. package/src/reactive/mapped.ts +2 -3
  32. package/src/reactive/project.ts +103 -6
  33. package/src/reactive/set.ts +6 -6
  34. package/src/reactive/types.ts +22 -0
  35. package/src/reactive/zone.ts +1 -1
  36. package/src/std-decorators.ts +1 -1
  37. package/dist/chunks/index-79Kk8D6e.esm.js.map +0 -1
  38. package/dist/chunks/index-GRBSx0mB.js.map +0 -1
  39. /package/{src/reactive/project.project.md → docs/reactive/project.md} +0 -0
@@ -1,9 +1,29 @@
1
1
  import { ReflectGet, ReflectSet } from '../utils'
2
+ import { setEffectName } from './debug'
3
+ import { getActiveEffect } from './effect-context'
2
4
  import { effect, untracked } from './effects'
3
5
  import { cleanedBy, cleanup } from './interface'
4
6
  import { reactive } from './proxy'
5
7
  import { Register } from './register'
6
- import { type ScopedCallback } from './types'
8
+ import { type ProjectionContext, projectionInfo, type ScopedCallback } from './types'
9
+
10
+ /**
11
+ * Maps projection effects (item effects) to their projection context
12
+ */
13
+ export const effectProjectionMetadata = new WeakMap<ScopedCallback, ProjectionContext>()
14
+
15
+ /**
16
+ * Returns the projection context of the currently running effect, if any.
17
+ */
18
+ export function getActiveProjection(): ProjectionContext | undefined {
19
+ const active = getActiveEffect()
20
+ return active ? effectProjectionMetadata.get(active) : undefined
21
+ }
22
+
23
+ /* TODO
24
+ It seems to work and I feel like it's correct but I couldn't validate theoretically that `ascend`
25
+ is the correct way to deal with nested effects.
26
+ */
7
27
 
8
28
  type ProjectOldValue<Target> = Target extends readonly (infer Item)[]
9
29
  ? Item
@@ -50,8 +70,17 @@ function defineAccessValue<Access extends { get(): unknown; set(value: unknown):
50
70
  function makeCleanup<Result extends object>(
51
71
  target: Result,
52
72
  effectMap: Map<unknown, ScopedCallback>,
53
- onDispose: () => void
73
+ onDispose: () => void,
74
+ metadata?: any
54
75
  ): ProjectResult<Result> {
76
+ if (metadata) {
77
+ Object.defineProperty(target, projectionInfo, {
78
+ value: metadata,
79
+ writable: false,
80
+ enumerable: false,
81
+ configurable: true,
82
+ })
83
+ }
55
84
  return cleanedBy(target, () => {
56
85
  onDispose()
57
86
  for (const stop of effectMap.values()) stop?.()
@@ -80,6 +109,9 @@ function projectArray<SourceValue, ResultValue>(
80
109
  }
81
110
  }
82
111
 
112
+ const parent = getActiveProjection()
113
+ const depth = parent ? parent.depth + 1 : 0
114
+
83
115
  const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
84
116
  const length = observedSource.length
85
117
  normalizeTargetLength(length)
@@ -102,13 +134,27 @@ function projectArray<SourceValue, ResultValue>(
102
134
  const produced = apply(accessBase, target)
103
135
  target[index] = produced
104
136
  })
137
+ setEffectName(stop, `project[${depth}]:${index}`)
138
+ effectProjectionMetadata.set(stop, {
139
+ source: observedSource,
140
+ key: index,
141
+ target,
142
+ depth,
143
+ parent,
144
+ })
105
145
  indexEffects.set(i, stop)
106
146
  })
107
147
  }
108
148
  for (const index of existing) if (index >= length) disposeIndex(index)
109
149
  })
110
150
 
111
- return makeCleanup(target, indexEffects, () => cleanupLength())
151
+ return makeCleanup(target, indexEffects, () => cleanupLength(), {
152
+ source: observedSource,
153
+ target,
154
+ apply,
155
+ depth,
156
+ parent,
157
+ } as ProjectionContext)
112
158
  }
113
159
 
114
160
  function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
@@ -135,6 +181,9 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
135
181
  }
136
182
  }
137
183
 
184
+ const parent = getActiveProjection()
185
+ const depth = parent ? parent.depth + 1 : 0
186
+
138
187
  const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
139
188
  const keys = new Set<Key>()
140
189
  for (const key of observedSource.mapKeys()) keys.add(key)
@@ -158,6 +207,14 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
158
207
  const produced = apply(accessBase, target)
159
208
  target.set(key, produced)
160
209
  })
210
+ setEffectName(stop, `project[${depth}]:${String(key)}`)
211
+ effectProjectionMetadata.set(stop, {
212
+ source: observedSource,
213
+ key,
214
+ target,
215
+ depth,
216
+ parent,
217
+ })
161
218
  keyEffects.set(key, stop)
162
219
  })
163
220
  }
@@ -165,7 +222,13 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
165
222
  for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
166
223
  })
167
224
 
168
- return makeCleanup(target, keyEffects, () => cleanupKeys())
225
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
226
+ source: observedSource,
227
+ target,
228
+ apply,
229
+ depth,
230
+ parent,
231
+ } as ProjectionContext)
169
232
  }
170
233
 
171
234
  function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
@@ -191,6 +254,9 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
191
254
  }
192
255
  }
193
256
 
257
+ const parent = getActiveProjection()
258
+ const depth = parent ? parent.depth + 1 : 0
259
+
194
260
  const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
195
261
  const keys = new Set<PropertyKey>()
196
262
  for (const key in observedSource) keys.add(key)
@@ -222,6 +288,14 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
222
288
  const produced = apply(accessBase, target)
223
289
  ;(target as any)[sourceKey] = produced
224
290
  })
291
+ setEffectName(stop, `project[${depth}]:${String(key)}`)
292
+ effectProjectionMetadata.set(stop, {
293
+ source: observedSource,
294
+ key,
295
+ target,
296
+ depth,
297
+ parent,
298
+ })
225
299
  keyEffects.set(key, stop)
226
300
  })
227
301
  }
@@ -229,7 +303,13 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
229
303
  for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
230
304
  })
231
305
 
232
- return makeCleanup(target, keyEffects, () => cleanupKeys())
306
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
307
+ source: observedSource,
308
+ target,
309
+ apply,
310
+ depth,
311
+ parent,
312
+ } as ProjectionContext)
233
313
  }
234
314
 
235
315
  function projectMap<Key, Value, ResultValue>(
@@ -250,6 +330,9 @@ function projectMap<Key, Value, ResultValue>(
250
330
  }
251
331
  }
252
332
 
333
+ const parent = getActiveProjection()
334
+ const depth = parent ? parent.depth + 1 : 0
335
+
253
336
  const cleanupKeys = effect(function projectMapEffect({ ascend }) {
254
337
  const keys = new Set<Key>()
255
338
  for (const key of observedSource.keys()) keys.add(key)
@@ -273,6 +356,14 @@ function projectMap<Key, Value, ResultValue>(
273
356
  const produced = apply(accessBase, target)
274
357
  target.set(key, produced)
275
358
  })
359
+ setEffectName(stop, `project[${depth}]:${String(key)}`)
360
+ effectProjectionMetadata.set(stop, {
361
+ source: observedSource,
362
+ key,
363
+ target,
364
+ depth,
365
+ parent,
366
+ })
276
367
  keyEffects.set(key, stop)
277
368
  })
278
369
  }
@@ -280,7 +371,13 @@ function projectMap<Key, Value, ResultValue>(
280
371
  for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
281
372
  })
282
373
 
283
- return makeCleanup(target, keyEffects, () => cleanupKeys())
374
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
375
+ source: observedSource,
376
+ target,
377
+ apply,
378
+ depth,
379
+ parent,
380
+ } as ProjectionContext)
284
381
  }
285
382
 
286
383
  type ProjectOverload = {
@@ -11,14 +11,14 @@ const native = Symbol('native')
11
11
  * Only tracks individual value operations, no size tracking (WeakSet limitation)
12
12
  */
13
13
  export class ReactiveWeakSet<T extends object> {
14
- declare readonly [native]: WeakSet<T>
15
- declare readonly content: symbol
14
+ readonly [native]!: WeakSet<T>
15
+ readonly content!: symbol
16
16
 
17
17
  constructor(original: WeakSet<T>) {
18
18
  Object.defineProperties(this, {
19
19
  [native]: { value: original },
20
20
  [prototypeForwarding]: { value: original },
21
- content: { value: Symbol('content') },
21
+ content: { value: Symbol('WeakSetContent') },
22
22
  [Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
23
23
  })
24
24
  }
@@ -52,13 +52,13 @@ export class ReactiveWeakSet<T extends object> {
52
52
  * Tracks size changes, individual value operations, and collection-wide operations
53
53
  */
54
54
  export class ReactiveSet<T> {
55
- declare readonly [native]: Set<T>
56
- declare readonly content: symbol
55
+ readonly [native]!: Set<T>
56
+ readonly content!: symbol
57
57
  constructor(original: Set<T>) {
58
58
  Object.defineProperties(this, {
59
59
  [native]: { value: original },
60
60
  [prototypeForwarding]: { value: original },
61
- content: { value: Symbol('content') },
61
+ content: { value: Symbol('SetContent') },
62
62
  [Symbol.toStringTag]: { value: 'ReactiveSet' },
63
63
  })
64
64
  }
@@ -141,6 +141,22 @@ export const prototypeForwarding: unique symbol = Symbol('prototype-forwarding')
141
141
  */
142
142
  export const allProps = Symbol('all-props')
143
143
 
144
+ /**
145
+ * Symbol for accessing projection information on reactive objects
146
+ */
147
+ export const projectionInfo = Symbol('projection-info')
148
+
149
+ /**
150
+ * Context for a running projection item effect
151
+ */
152
+ export interface ProjectionContext {
153
+ source: any
154
+ key?: any
155
+ target: any
156
+ depth: number
157
+ parent?: ProjectionContext
158
+ }
159
+
144
160
  // Symbol to mark functions with their root function
145
161
  const rootFunction = Symbol('root-function')
146
162
 
@@ -250,6 +266,12 @@ export const options = {
250
266
  * @default 100
251
267
  */
252
268
  maxEffectChain: 100,
269
+ /**
270
+ * Maximum number of times an effect can be triggered by the same cause in a single batch
271
+ * Used to detect aggressive re-computation or infinite loops
272
+ * @default 10
273
+ */
274
+ maxTriggerPerBatch: 10,
253
275
  /**
254
276
  * Debug purpose: maximum effect reaction (like call stack max depth)
255
277
  * Used to prevent infinite loops
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { captureEffectStack, withEffectStack } from './effect-context'
18
- import { options, ScopedCallback } from './types'
18
+ import { options, type ScopedCallback } from './types'
19
19
 
20
20
  let zoneHooked = false
21
21
 
@@ -1,4 +1,4 @@
1
- import { decorator, GenericClassDecorator } from './decorator'
1
+ import { decorator, type GenericClassDecorator } from './decorator'
2
2
 
3
3
  // In order to avoid async re-entrance, we could use zone.js or something like that.
4
4
  const syncCalculating: { object: object; prop: PropertyKey }[] = []