mutts 1.0.5 → 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 (114) hide show
  1. package/README.md +2 -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-Cvxdw6Ax.js → index-BFYK02LG.js} +5377 -4059
  8. package/dist/chunks/index-BFYK02LG.js.map +1 -0
  9. package/dist/chunks/{index-qiWwozOc.esm.js → index-CNR6QRUl.esm.js} +5247 -3963
  10. package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
  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/node.d.ts +2 -0
  16. package/dist/node.esm.js +45 -0
  17. package/dist/node.esm.js.map +1 -0
  18. package/dist/node.js +136 -0
  19. package/dist/node.js.map +1 -0
  20. package/docs/ai/api-reference.md +0 -2
  21. package/docs/ai/manual.md +14 -95
  22. package/docs/reactive/advanced.md +7 -111
  23. package/docs/reactive/collections.md +0 -125
  24. package/docs/reactive/core.md +27 -24
  25. package/docs/reactive/debugging.md +168 -0
  26. package/docs/reactive/project.md +1 -1
  27. package/docs/reactive/scan.md +78 -0
  28. package/docs/reactive.md +8 -6
  29. package/docs/std-decorators.md +1 -0
  30. package/docs/zone.md +88 -0
  31. package/package.json +47 -65
  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 +15 -9
  36. package/src/destroyable.ts +4 -4
  37. package/src/index.ts +54 -0
  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 +3 -3
  43. package/src/reactive/debug.ts +1 -1
  44. package/src/reactive/deep-touch.ts +1 -1
  45. package/src/reactive/deep-watch.ts +1 -1
  46. package/src/reactive/effect-context.ts +15 -91
  47. package/src/reactive/effects.ts +138 -170
  48. package/src/reactive/index.ts +10 -13
  49. package/src/reactive/interface.ts +20 -33
  50. package/src/reactive/map.ts +48 -61
  51. package/src/reactive/memoize.ts +87 -31
  52. package/src/reactive/project.ts +43 -22
  53. package/src/reactive/proxy.ts +18 -43
  54. package/src/reactive/record.ts +3 -3
  55. package/src/reactive/register.ts +5 -7
  56. package/src/reactive/registry.ts +59 -0
  57. package/src/reactive/set.ts +42 -56
  58. package/src/reactive/tracking.ts +5 -62
  59. package/src/reactive/types.ts +79 -19
  60. package/src/std-decorators.ts +9 -9
  61. package/src/utils.ts +203 -19
  62. package/src/zone.ts +127 -0
  63. package/dist/chunks/_tslib-BgjropY9.js +0 -81
  64. package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
  65. package/dist/chunks/_tslib-Mzh1rNsX.esm.js +0 -75
  66. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  67. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  68. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  69. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  70. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  71. package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
  72. package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
  73. package/dist/decorator.d.ts +0 -107
  74. package/dist/decorator.esm.js +0 -2
  75. package/dist/decorator.esm.js.map +0 -1
  76. package/dist/decorator.js +0 -11
  77. package/dist/decorator.js.map +0 -1
  78. package/dist/destroyable.d.ts +0 -90
  79. package/dist/destroyable.esm.js +0 -109
  80. package/dist/destroyable.esm.js.map +0 -1
  81. package/dist/destroyable.js +0 -116
  82. package/dist/destroyable.js.map +0 -1
  83. package/dist/eventful.d.ts +0 -20
  84. package/dist/eventful.esm.js +0 -66
  85. package/dist/eventful.esm.js.map +0 -1
  86. package/dist/eventful.js +0 -68
  87. package/dist/eventful.js.map +0 -1
  88. package/dist/index.d.ts +0 -19
  89. package/dist/index.esm.js +0 -8
  90. package/dist/index.esm.js.map +0 -1
  91. package/dist/index.js +0 -95
  92. package/dist/index.js.map +0 -1
  93. package/dist/indexable.d.ts +0 -243
  94. package/dist/indexable.esm.js +0 -285
  95. package/dist/indexable.esm.js.map +0 -1
  96. package/dist/indexable.js +0 -291
  97. package/dist/indexable.js.map +0 -1
  98. package/dist/promiseChain.d.ts +0 -21
  99. package/dist/promiseChain.esm.js +0 -78
  100. package/dist/promiseChain.esm.js.map +0 -1
  101. package/dist/promiseChain.js +0 -80
  102. package/dist/promiseChain.js.map +0 -1
  103. package/dist/reactive.d.ts +0 -885
  104. package/dist/reactive.esm.js +0 -5
  105. package/dist/reactive.esm.js.map +0 -1
  106. package/dist/reactive.js +0 -59
  107. package/dist/reactive.js.map +0 -1
  108. package/dist/std-decorators.d.ts +0 -52
  109. package/dist/std-decorators.esm.js +0 -196
  110. package/dist/std-decorators.esm.js.map +0 -1
  111. package/dist/std-decorators.js +0 -204
  112. package/dist/std-decorators.js.map +0 -1
  113. package/src/reactive/mapped.ts +0 -129
  114. package/src/reactive/zone.ts +0 -208
@@ -1,72 +1,125 @@
1
- import { Indexable } from '../indexable'
1
+ import { FoolProof } from '../utils'
2
2
  import { touched } from './change'
3
3
  import { makeReactiveEntriesIterator, makeReactiveIterator } from './non-reactive'
4
4
  import { reactive } from './proxy'
5
5
  import { unwrap } from './proxy-state'
6
6
  import { dependant } from './tracking'
7
- import { prototypeForwarding } from './types'
8
-
9
- export const native = Symbol('native')
10
- const isArray = Array.isArray
11
- Array.isArray = ((value: any) =>
12
- isArray(value) ||
13
- (value &&
14
- typeof value === 'object' &&
15
- prototypeForwarding in value &&
16
- Array.isArray(value[prototypeForwarding]))) as any
17
- export class ReactiveBaseArray {
18
- readonly [native]!: any[]
19
7
 
8
+ function* index(i: number, { length = true } = {}): IterableIterator<number | 'length'> {
9
+ if (length) yield 'length'
10
+ yield i
11
+ }
12
+
13
+ function* range(
14
+ a: number,
15
+ b: number,
16
+ { length = false } = {}
17
+ ): IterableIterator<number | 'length'> {
18
+ const start = Math.min(a, b)
19
+ const end = Math.max(a, b)
20
+ if (length) yield 'length'
21
+ for (let i = start; i <= end; i++) yield i
22
+ }
23
+ export abstract class Indexer extends Array {
24
+ get(i: number): any {
25
+ dependant(this, i)
26
+ return reactive(this[i])
27
+ }
28
+ set(i: number, value: any) {
29
+ const added = i >= this.length
30
+ this[i] = value
31
+ touched(this, { type: 'set', prop: i }, index(i, { length: added }))
32
+ }
33
+ getLength() {
34
+ dependant(this, 'length')
35
+ return this.length
36
+ }
37
+ setLength(value: number) {
38
+ const oldLength = this.length
39
+ try {
40
+ this.length = value
41
+ } finally {
42
+ touched(this, { type: 'set', prop: 'length' }, range(oldLength, value, { length: true }))
43
+ }
44
+ }
45
+ }
46
+ const indexLess = { get: FoolProof.get, set: FoolProof.set }
47
+ Object.assign(FoolProof, {
48
+ get(obj: any, prop: any, receiver: any) {
49
+ if (obj instanceof Array && typeof prop === 'string') {
50
+ if (prop === 'length') return Indexer.prototype.getLength.call(obj)
51
+ const index = parseInt(prop)
52
+ if (!Number.isNaN(index)) return Indexer.prototype.get.call(obj, index)
53
+ }
54
+ return indexLess.get(obj, prop, receiver)
55
+ },
56
+ set(obj: any, prop: any, value: any, receiver: any) {
57
+ if (obj instanceof Array && typeof prop === 'string') {
58
+ if (prop === 'length') return Indexer.prototype.setLength.call(obj, value)
59
+ const index = parseInt(prop)
60
+ if (!Number.isNaN(index)) return Indexer.prototype.set.call(obj, index, value)
61
+ }
62
+ return indexLess.set(obj, prop, value, receiver)
63
+ },
64
+ })
65
+
66
+ export abstract class ReactiveArray extends Array {
67
+ toJSON() {
68
+ return this
69
+ }
70
+ get [Symbol.toStringTag]() {
71
+ return 'ReactiveArray'
72
+ }
20
73
  // Safe array access with negative indices
21
74
  at(index: number): any {
22
- const actualIndex = index < 0 ? this[native].length + index : index
75
+ const actualIndex = index < 0 ? this.length + index : index
23
76
  dependant(this, actualIndex)
24
- if (actualIndex < 0 || actualIndex >= this[native].length) return undefined
25
- return reactive(this[native][actualIndex])
77
+ if (actualIndex < 0 || actualIndex >= this.length) return undefined
78
+ return reactive(this[actualIndex])
26
79
  }
27
80
 
28
81
  // Immutable versions of mutator methods
29
82
  toReversed(): any[] {
30
83
  dependant(this)
31
- return reactive(this[native].toReversed())
84
+ return reactive(this.toReversed())
32
85
  }
33
86
 
34
87
  toSorted(compareFn?: (a: any, b: any) => number): any[] {
35
88
  dependant(this)
36
- return reactive(this[native].toSorted(compareFn))
89
+ return reactive(this.toSorted(compareFn))
37
90
  }
38
91
 
39
92
  toSpliced(start: number, deleteCount?: number, ...items: any[]): any[] {
40
93
  dependant(this)
41
94
  return deleteCount === undefined
42
- ? this[native].toSpliced(start)
43
- : this[native].toSpliced(start, deleteCount, ...items)
95
+ ? this.toSpliced(start)
96
+ : this.toSpliced(start, deleteCount, ...items)
44
97
  }
45
98
 
46
99
  with(index: number, value: any): any[] {
47
100
  dependant(this)
48
- return reactive(this[native].with(index, value))
101
+ return reactive(this.with(index, value))
49
102
  }
50
103
 
51
104
  // Iterator methods with reactivity tracking
52
105
  entries() {
53
106
  dependant(this)
54
- return makeReactiveEntriesIterator(this[native].entries())
107
+ return makeReactiveEntriesIterator(this.entries())
55
108
  }
56
109
 
57
110
  keys() {
58
111
  dependant(this, 'length')
59
- return this[native].keys()
112
+ return this.keys()
60
113
  }
61
114
 
62
115
  values() {
63
116
  dependant(this)
64
- return makeReactiveIterator(this[native].values())
117
+ return makeReactiveIterator(this.values())
65
118
  }
66
119
 
67
- [Symbol.iterator]() {
120
+ [Symbol.iterator](): ArrayIterator<any> {
68
121
  dependant(this)
69
- const nativeIterator = this[native][Symbol.iterator]()
122
+ const nativeIterator = this[Symbol.iterator]()
70
123
  return {
71
124
  next() {
72
125
  const result = nativeIterator.next()
@@ -75,37 +128,38 @@ export class ReactiveBaseArray {
75
128
  }
76
129
  return { value: reactive(result.value), done: false }
77
130
  },
78
- }
131
+ [Symbol.iterator]() {
132
+ return this
133
+ },
134
+ [Symbol.dispose]() {},
135
+ } as any
79
136
  }
80
137
 
81
138
  indexOf(searchElement: any, fromIndex?: number): number {
82
139
  dependant(this)
83
140
  const unwrappedSearch = unwrap(searchElement)
84
141
  // Check both wrapped and unwrapped versions since array may contain either
85
- const index = this[native].indexOf(unwrappedSearch, fromIndex)
142
+ const index = this.indexOf(unwrappedSearch, fromIndex)
86
143
  if (index !== -1) return index
87
144
  // If not found with unwrapped, try with wrapped (in case array contains wrapped version)
88
- return this[native].indexOf(searchElement, fromIndex)
145
+ return this.indexOf(searchElement, fromIndex)
89
146
  }
90
147
 
91
148
  lastIndexOf(searchElement: any, fromIndex?: number): number {
92
149
  dependant(this)
93
150
  const unwrappedSearch = unwrap(searchElement)
94
151
  // Check both wrapped and unwrapped versions since array may contain either
95
- const index = this[native].lastIndexOf(unwrappedSearch, fromIndex)
152
+ const index = this.lastIndexOf(unwrappedSearch, fromIndex)
96
153
  if (index !== -1) return index
97
154
  // If not found with unwrapped, try with wrapped (in case array contains wrapped version)
98
- return this[native].lastIndexOf(searchElement, fromIndex)
155
+ return this.lastIndexOf(searchElement, fromIndex)
99
156
  }
100
157
 
101
158
  includes(searchElement: any, fromIndex?: number): boolean {
102
159
  dependant(this)
103
160
  const unwrappedSearch = unwrap(searchElement)
104
161
  // Check both wrapped and unwrapped versions since array may contain either
105
- return (
106
- this[native].includes(unwrappedSearch, fromIndex) ||
107
- this[native].includes(searchElement, fromIndex)
108
- )
162
+ return this.includes(unwrappedSearch, fromIndex) || this.includes(searchElement, fromIndex)
109
163
  }
110
164
 
111
165
  find(predicate: (this: any, value: any, index: number, obj: any[]) => boolean, thisArg?: any): any
@@ -120,16 +174,16 @@ export class ReactiveBaseArray {
120
174
  obj: any[]
121
175
  ) => boolean
122
176
  return reactive(
123
- this[native].find(
177
+ this.find(
124
178
  (value, index, array) => predicate.call(thisArg, reactive(value), index, array),
125
179
  thisArg
126
180
  )
127
181
  )
128
182
  }
129
183
  const fromIndex = typeof thisArg === 'number' ? thisArg : undefined
130
- const index = this[native].indexOf(predicateOrElement, fromIndex)
184
+ const index = this.indexOf(predicateOrElement, fromIndex)
131
185
  if (index === -1) return undefined
132
- return reactive(this[native][index])
186
+ return reactive(this[index])
133
187
  }
134
188
 
135
189
  findIndex(
@@ -146,18 +200,18 @@ export class ReactiveBaseArray {
146
200
  index: number,
147
201
  obj: any[]
148
202
  ) => boolean
149
- return this[native].findIndex(
203
+ return this.findIndex(
150
204
  (value, index, array) => predicate.call(thisArg, reactive(value), index, array),
151
205
  thisArg
152
206
  )
153
207
  }
154
208
  const fromIndex = typeof thisArg === 'number' ? thisArg : undefined
155
- return this[native].indexOf(predicateOrElement, fromIndex)
209
+ return this.indexOf(predicateOrElement, fromIndex)
156
210
  }
157
211
 
158
- flat(): any[] {
212
+ flat(depth?: number): any[] {
159
213
  dependant(this)
160
- return reactive(this[native].flat())
214
+ return reactive(depth === undefined ? this.flat() : this.flat(depth))
161
215
  }
162
216
 
163
217
  flatMap(
@@ -165,20 +219,25 @@ export class ReactiveBaseArray {
165
219
  thisArg?: any
166
220
  ): any[] {
167
221
  dependant(this)
168
- return reactive(this[native].flatMap(callbackfn, thisArg))
222
+ return reactive(
223
+ this.flatMap(
224
+ (item, index, array) => callbackfn.call(thisArg, reactive(item), index, array),
225
+ thisArg
226
+ )
227
+ )
169
228
  }
170
229
 
171
230
  filter(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): any[] {
172
231
  dependant(this)
173
232
  return reactive(
174
- this[native].filter((item, index, array) => callbackfn(reactive(item), index, array), thisArg)
233
+ this.filter((item, index, array) => callbackfn(reactive(item), index, array), thisArg)
175
234
  )
176
235
  }
177
236
 
178
237
  map(callbackfn: (value: any, index: number, array: any[]) => any, thisArg?: any): any[] {
179
238
  dependant(this)
180
239
  return reactive(
181
- this[native].map((item, index, array) => callbackfn(reactive(item), index, array), thisArg)
240
+ this.map((item, index, array) => callbackfn(reactive(item), index, array), thisArg)
182
241
  )
183
242
  }
184
243
 
@@ -189,8 +248,8 @@ export class ReactiveBaseArray {
189
248
  dependant(this)
190
249
  const result =
191
250
  initialValue === undefined
192
- ? this[native].reduce(callbackfn as any)
193
- : this[native].reduce(callbackfn as any, initialValue)
251
+ ? this.reduce(callbackfn as any)
252
+ : this.reduce(callbackfn as any, initialValue)
194
253
  return reactive(result)
195
254
  }
196
255
 
@@ -201,110 +260,63 @@ export class ReactiveBaseArray {
201
260
  dependant(this)
202
261
  const result =
203
262
  initialValue !== undefined
204
- ? this[native].reduceRight(callbackfn as any, initialValue)
205
- : (this[native] as any).reduceRight(callbackfn as any)
263
+ ? this.reduceRight(callbackfn as any, initialValue)
264
+ : (this as any).reduceRight(callbackfn as any)
206
265
  return reactive(result)
207
266
  }
208
267
 
209
268
  slice(start?: number, end?: number): any[] {
210
- for (const i of range(start || 0, end || this[native].length - 1)) dependant(this, i)
269
+ for (const i of range(start || 0, end || this.length - 1)) dependant(this, i)
211
270
  return start === undefined
212
- ? this[native].slice()
271
+ ? this.slice()
213
272
  : end === undefined
214
- ? this[native].slice(start)
215
- : this[native].slice(start, end)
273
+ ? this.slice(start)
274
+ : this.slice(start, end)
216
275
  }
217
276
 
218
277
  concat(...items: any[]): any[] {
219
278
  dependant(this)
220
- return reactive(this[native].concat(...items))
279
+ return reactive(this.concat(...items))
221
280
  }
222
281
 
223
282
  join(separator?: string): string {
224
283
  dependant(this)
225
- return this[native].join(separator as any)
284
+ return this.join(separator as any)
226
285
  }
227
286
 
228
287
  forEach(callbackfn: (value: any, index: number, array: any[]) => void, thisArg?: any): void {
229
288
  dependant(this)
230
- this[native].forEach((value, index, array) => {
289
+ this.forEach((value, index, array) => {
231
290
  callbackfn.call(thisArg, reactive(value), index, array)
232
291
  })
233
292
  }
234
293
 
235
294
  // TODO: re-implement for fun dependencies? (eg - every only check the first ones until it find some),
236
295
  // no need to make it dependant on indexes after the found one
296
+ every<S>(
297
+ predicate: (value: any, index: number, array: any[]) => value is S,
298
+ thisArg?: any
299
+ ): this is S[]
300
+ every(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): boolean
237
301
  every(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): boolean {
238
302
  dependant(this)
239
- return this[native].every(
303
+ return this.every(
240
304
  (value, index, array) => callbackfn.call(thisArg, reactive(value), index, array),
241
305
  thisArg
242
306
  )
243
307
  }
244
-
245
308
  some(callbackfn: (value: any, index: number, array: any[]) => boolean, thisArg?: any): boolean {
246
309
  dependant(this)
247
- return this[native].some(
310
+ return this.some(
248
311
  (value, index, array) => callbackfn.call(thisArg, reactive(value), index, array),
249
312
  thisArg
250
313
  )
251
314
  }
252
- }
253
- function* index(i: number, { length = true } = {}): IterableIterator<number | 'length'> {
254
- if (length) yield 'length'
255
- yield i
256
- }
257
-
258
- function* range(
259
- a: number,
260
- b: number,
261
- { length = false } = {}
262
- ): IterableIterator<number | 'length'> {
263
- const start = Math.min(a, b)
264
- const end = Math.max(a, b)
265
- if (length) yield 'length'
266
- for (let i = start; i <= end; i++) yield i
267
- }
268
- /**
269
- * Reactive wrapper around JavaScript's Array class with full array method support
270
- * Tracks length changes, individual index operations, and collection-wide operations
271
- */
272
- export class ReactiveArray extends Indexable(ReactiveBaseArray, {
273
- get(i: number): any {
274
- dependant(this, i)
275
- return reactive(this[native][i])
276
- },
277
- set(i: number, value: any) {
278
- const added = i >= this[native].length
279
- this[native][i] = value
280
- touched(this, { type: 'set', prop: i }, index(i, { length: added }))
281
- },
282
- getLength() {
283
- dependant(this, 'length')
284
- return this[native].length
285
- },
286
- setLength(value: number) {
287
- const oldLength = this[native].length
288
- try {
289
- this[native].length = value
290
- } finally {
291
- touched(this, { type: 'set', prop: 'length' }, range(oldLength, value, { length: true }))
292
- }
293
- },
294
- }) {
295
- constructor(original: any[]) {
296
- super()
297
- Object.defineProperties(this, {
298
- // We have to make it double, as [native] must be `unique symbol` - impossible through import
299
- [native]: { value: original },
300
- [prototypeForwarding]: { value: original },
301
- })
302
- }
303
-
315
+ // Side-effectful
304
316
  push(...items: any[]) {
305
- const oldLength = this[native].length
317
+ const oldLength = this.length
306
318
  try {
307
- return this[native].push(...items)
319
+ return this.push(...items)
308
320
  } finally {
309
321
  touched(
310
322
  this,
@@ -315,45 +327,41 @@ export class ReactiveArray extends Indexable(ReactiveBaseArray, {
315
327
  }
316
328
 
317
329
  pop() {
318
- if (this[native].length === 0) return undefined
330
+ if (this.length === 0) return undefined
319
331
  try {
320
- return reactive(this[native].pop())
332
+ return reactive(this.pop())
321
333
  } finally {
322
- touched(this, { type: 'bunch', method: 'pop' }, index(this[native].length))
334
+ touched(this, { type: 'bunch', method: 'pop' }, index(this.length))
323
335
  }
324
336
  }
325
337
 
326
338
  shift() {
327
- if (this[native].length === 0) return undefined
339
+ if (this.length === 0) return undefined
328
340
  try {
329
- return reactive(this[native].shift())
341
+ return reactive(this.shift())
330
342
  } finally {
331
- touched(
332
- this,
333
- { type: 'bunch', method: 'shift' },
334
- range(0, this[native].length + 1, { length: true })
335
- )
343
+ touched(this, { type: 'bunch', method: 'shift' }, range(0, this.length + 1, { length: true }))
336
344
  }
337
345
  }
338
346
 
339
347
  unshift(...items: any[]) {
340
348
  try {
341
- return this[native].unshift(...items)
349
+ return this.unshift(...items)
342
350
  } finally {
343
351
  touched(
344
352
  this,
345
353
  { type: 'bunch', method: 'unshift' },
346
- range(0, this[native].length - items.length, { length: true })
354
+ range(0, this.length - items.length, { length: true })
347
355
  )
348
356
  }
349
357
  }
350
358
 
351
359
  splice(start: number, deleteCount?: number, ...items: any[]) {
352
- const oldLength = this[native].length
360
+ const oldLength = this.length
353
361
  if (deleteCount === undefined) deleteCount = oldLength - start
354
362
  try {
355
- if (deleteCount === undefined) return reactive(this[native].splice(start))
356
- return reactive(this[native].splice(start, deleteCount, ...items))
363
+ if (deleteCount === undefined) return reactive(this.splice(start))
364
+ return reactive(this.splice(start, deleteCount, ...items))
357
365
  } finally {
358
366
  touched(
359
367
  this,
@@ -362,49 +370,49 @@ export class ReactiveArray extends Indexable(ReactiveBaseArray, {
362
370
  deleteCount === items.length
363
371
  ? range(start, start + deleteCount)
364
372
  : range(start, oldLength + Math.max(items.length - deleteCount, 0), {
365
- length: true,
366
- })
373
+ length: true,
374
+ })
367
375
  )
368
376
  }
369
377
  }
370
378
 
371
379
  reverse() {
372
380
  try {
373
- return this[native].reverse()
381
+ return this.reverse()
374
382
  } finally {
375
- touched(this, { type: 'bunch', method: 'reverse' }, range(0, this[native].length - 1))
383
+ touched(this, { type: 'bunch', method: 'reverse' }, range(0, this.length - 1))
376
384
  }
377
385
  }
378
386
 
379
387
  sort(compareFn?: (a: any, b: any) => number) {
380
388
  compareFn = compareFn || ((a, b) => a.toString().localeCompare(b.toString()))
381
389
  try {
382
- return this[native].sort((a, b) => compareFn(reactive(a), reactive(b))) as any
390
+ return this.sort((a, b) => compareFn(reactive(a), reactive(b))) as any
383
391
  } finally {
384
- touched(this, { type: 'bunch', method: 'sort' }, range(0, this[native].length - 1))
392
+ touched(this, { type: 'bunch', method: 'sort' }, range(0, this.length - 1))
385
393
  }
386
394
  }
387
395
 
388
396
  fill(value: any, start?: number, end?: number) {
389
397
  try {
390
- if (start === undefined) return this[native].fill(value) as any
391
- if (end === undefined) return this[native].fill(value, start) as any
392
- return this[native].fill(value, start, end) as any
398
+ if (start === undefined) return this.fill(value) as any
399
+ if (end === undefined) return this.fill(value, start) as any
400
+ return this.fill(value, start, end) as any
393
401
  } finally {
394
- touched(this, { type: 'bunch', method: 'fill' }, range(0, this[native].length - 1))
402
+ touched(this, { type: 'bunch', method: 'fill' }, range(0, this.length - 1))
395
403
  }
396
404
  }
397
405
 
398
406
  copyWithin(target: number, start: number, end?: number) {
399
407
  try {
400
- if (end === undefined) return this[native].copyWithin(target, start) as any
401
- return this[native].copyWithin(target, start, end) as any
408
+ if (end === undefined) return this.copyWithin(target, start) as any
409
+ return this.copyWithin(target, start, end) as any
402
410
  } finally {
403
411
  touched(
404
412
  this,
405
413
  { type: 'bunch', method: 'copyWithin' },
406
414
  // TODO: calculate the range properly
407
- range(0, this[native].length - 1)
415
+ range(0, this.length - 1)
408
416
  )
409
417
  }
410
418
  // Touch all affected indices with a single allProps call
@@ -0,0 +1,168 @@
1
+ import { FoolProof } from '../utils'
2
+ import { effect, untracked } from './effects'
3
+ import { cleanedBy, cleanup } from './interface'
4
+ import { memoize } from './memoize'
5
+ import { reactive } from './proxy'
6
+ import type { ScopedCallback } from './types'
7
+
8
+ /**
9
+ * Result of a reactive scan, which is a reactive array of accumulated values
10
+ * with an attached cleanup function.
11
+ */
12
+ export type ScanResult<Output> = readonly Output[] & { [cleanup]: ScopedCallback }
13
+
14
+ /**
15
+ * Perform a reactive scan over an array of items.
16
+ *
17
+ * This implementation is highly optimized for performance and fine-grained reactivity:
18
+ * - **Incremental Updates**: Changes to an item only trigger re-computation from that
19
+ * point onwards in the result chain.
20
+ * - **Move Optimization**: If items are moved within the array, their accumulated
21
+ * values are reused as long as their predecessor remains the same.
22
+ * - **Duplicate Support**: Correctly handles multiple occurrences of the same object
23
+ * instance using an internal occurrence tracking mechanism.
24
+ * - **Memory Efficient**: Uses `WeakMap` for caching intermediates, which are
25
+ * automatically cleared when source items are garbage collected.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const source = reactive([{ val: 1 }, { val: 2 }, { val: 3 }])
30
+ * const sum = scan(source, (acc, item) => acc + item.val, 0)
31
+ *
32
+ * expect([...sum]).toEqual([1, 3, 6])
33
+ *
34
+ * // Modifying an item only re-computes subsequent sums
35
+ * source[1].val = 10
36
+ * expect([...sum]).toEqual([1, 11, 14])
37
+ * ```
38
+ *
39
+ * @param source The source array of objects (will be made reactive)
40
+ * @param callback The accumulator function called with (accumulator, currentItem)
41
+ * @param initialValue The starting value for the accumulation
42
+ * @returns A reactive array of accumulated values, with a [cleanup] property to stop the tracking
43
+ */
44
+ export function scan<Input extends object, Output>(
45
+ source: readonly Input[],
46
+ callback: (acc: Output, val: Input) => Output,
47
+ initialValue: Output
48
+ ): ScanResult<Output> {
49
+ const observedSource = reactive(source)
50
+ const result = reactive([] as Output[])
51
+
52
+ // Track effects for each index to dispose them when the array shrinks
53
+ const indexEffects = new Map<number, ScopedCallback>()
54
+ // Mapping from index to its current intermediate object
55
+ const indexToIntermediate = reactive([] as Intermediate[])
56
+ const intermediaries = new WeakMap<Input, Intermediate[]>()
57
+
58
+ class Intermediate {
59
+ public prev: Intermediate | undefined
60
+ constructor(public val: Input, prev: Intermediate | undefined) {
61
+ this.prev = prev
62
+ }
63
+
64
+ @memoize
65
+ get acc(): Output {
66
+ const prevAcc = this.prev ? this.prev.acc : initialValue
67
+ return callback(prevAcc, this.val)
68
+ }
69
+ }
70
+
71
+ function disposeIndex(index: number) {
72
+ const stop = indexEffects.get(index)
73
+ if (stop) {
74
+ stop()
75
+ indexEffects.delete(index)
76
+ untracked(() => {
77
+ Reflect.deleteProperty(indexToIntermediate as any, index)
78
+ Reflect.deleteProperty(result as any, index)
79
+ })
80
+ }
81
+ }
82
+
83
+ const mainEffect = effect(function scanMainEffect({ ascend }) {
84
+ const length = observedSource.length
85
+ const occurrenceCount = new Map<Input, number>()
86
+ let prev: Intermediate | undefined = undefined
87
+
88
+ for (let i = 0; i < length; i++) {
89
+ const val = FoolProof.get(observedSource as any, i, observedSource) as Input
90
+
91
+ if (!(val && (typeof val === 'object' || typeof val === 'function' || typeof val === 'symbol'))) {
92
+ throw new Error('scan: items must be objects (WeakKey) for intermediate caching')
93
+ }
94
+
95
+ const count = occurrenceCount.get(val) ?? 0
96
+ occurrenceCount.set(val, count + 1)
97
+
98
+ let list = intermediaries.get(val)
99
+ if (!list) {
100
+ list = []
101
+ intermediaries.set(val, list)
102
+ }
103
+
104
+ let intermediate = list[count]
105
+ if (!intermediate) {
106
+ intermediate = reactive(new Intermediate(val, prev))
107
+ list[count] = intermediate
108
+ } else {
109
+ // Update the link.
110
+ if (untracked(() => intermediate.prev) !== prev) {
111
+ intermediate.prev = prev
112
+ }
113
+ }
114
+
115
+ // Update the reactive mapping for this index
116
+ if (indexToIntermediate[i] !== intermediate) {
117
+ indexToIntermediate[i] = intermediate
118
+ }
119
+
120
+ // If we don't have an effect for this index yet, create one
121
+ if (!indexEffects.has(i)) {
122
+ ascend(() => {
123
+ const index = i
124
+ const stop = effect(function scanIndexSyncEffect() {
125
+ const inter = indexToIntermediate[index]
126
+ if (inter) {
127
+ const accValue = inter.acc
128
+ untracked(() => {
129
+ result[index] = accValue
130
+ })
131
+ }
132
+ })
133
+ indexEffects.set(index, stop)
134
+ })
135
+ }
136
+
137
+ prev = intermediate
138
+ }
139
+
140
+ // Cleanup trailing indices
141
+ for (const index of Array.from(indexEffects.keys())) {
142
+ if (index >= length) disposeIndex(index)
143
+ }
144
+
145
+ // Ensure result length matches source length
146
+ untracked(() => {
147
+ if (result.length !== length) {
148
+ FoolProof.set(result as any, 'length', length, result)
149
+ }
150
+ })
151
+ })
152
+
153
+ return cleanedBy(result, () => {
154
+ mainEffect()
155
+ for (const stop of indexEffects.values()) stop()
156
+ indexEffects.clear()
157
+ }) as ScanResult<Output>
158
+ }
159
+
160
+ export function resolve<Output>(cb: () => Output[]): Output[] & { [cleanup]: ScopedCallback } {
161
+ const result = reactive([] as Output[])
162
+ return cleanedBy(result, effect(() => {
163
+ const source = cb()
164
+ if (result.length !== source.length) result.length = source.length
165
+ for (let i = 0; i < source.length; i++)
166
+ if (result[i] !== source[i]) result[i] = source[i]
167
+ }))
168
+ }