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.
- package/README.md +2 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.esm.js +70 -0
- package/dist/browser.esm.js.map +1 -0
- package/dist/browser.js +161 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/{index-Cvxdw6Ax.js → index-BFYK02LG.js} +5377 -4059
- package/dist/chunks/index-BFYK02LG.js.map +1 -0
- package/dist/chunks/{index-qiWwozOc.esm.js → index-CNR6QRUl.esm.js} +5247 -3963
- package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.esm.js +45 -0
- package/dist/node.esm.js.map +1 -0
- package/dist/node.js +136 -0
- package/dist/node.js.map +1 -0
- package/docs/ai/api-reference.md +0 -2
- package/docs/ai/manual.md +14 -95
- package/docs/reactive/advanced.md +7 -111
- package/docs/reactive/collections.md +0 -125
- package/docs/reactive/core.md +27 -24
- package/docs/reactive/debugging.md +168 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +78 -0
- package/docs/reactive.md +8 -6
- package/docs/std-decorators.md +1 -0
- package/docs/zone.md +88 -0
- package/package.json +47 -65
- package/src/async/browser.ts +87 -0
- package/src/async/index.ts +8 -0
- package/src/async/node.ts +46 -0
- package/src/decorator.ts +15 -9
- package/src/destroyable.ts +4 -4
- package/src/index.ts +54 -0
- package/src/indexable.ts +42 -0
- package/src/mixins.ts +2 -2
- package/src/reactive/array.ts +149 -141
- package/src/reactive/buffer.ts +168 -0
- package/src/reactive/change.ts +3 -3
- package/src/reactive/debug.ts +1 -1
- package/src/reactive/deep-touch.ts +1 -1
- package/src/reactive/deep-watch.ts +1 -1
- package/src/reactive/effect-context.ts +15 -91
- package/src/reactive/effects.ts +138 -170
- package/src/reactive/index.ts +10 -13
- package/src/reactive/interface.ts +20 -33
- package/src/reactive/map.ts +48 -61
- package/src/reactive/memoize.ts +87 -31
- package/src/reactive/project.ts +43 -22
- package/src/reactive/proxy.ts +18 -43
- package/src/reactive/record.ts +3 -3
- package/src/reactive/register.ts +5 -7
- package/src/reactive/registry.ts +59 -0
- package/src/reactive/set.ts +42 -56
- package/src/reactive/tracking.ts +5 -62
- package/src/reactive/types.ts +79 -19
- package/src/std-decorators.ts +9 -9
- package/src/utils.ts +203 -19
- package/src/zone.ts +127 -0
- package/dist/chunks/_tslib-BgjropY9.js +0 -81
- package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js +0 -75
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
- package/dist/chunks/decorator-DLvrD0UF.js +0 -265
- package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
- package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
- package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
- package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
- package/dist/decorator.d.ts +0 -107
- package/dist/decorator.esm.js +0 -2
- package/dist/decorator.esm.js.map +0 -1
- package/dist/decorator.js +0 -11
- package/dist/decorator.js.map +0 -1
- package/dist/destroyable.d.ts +0 -90
- package/dist/destroyable.esm.js +0 -109
- package/dist/destroyable.esm.js.map +0 -1
- package/dist/destroyable.js +0 -116
- package/dist/destroyable.js.map +0 -1
- package/dist/eventful.d.ts +0 -20
- package/dist/eventful.esm.js +0 -66
- package/dist/eventful.esm.js.map +0 -1
- package/dist/eventful.js +0 -68
- package/dist/eventful.js.map +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/index.esm.js +0 -8
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -95
- package/dist/index.js.map +0 -1
- package/dist/indexable.d.ts +0 -243
- package/dist/indexable.esm.js +0 -285
- package/dist/indexable.esm.js.map +0 -1
- package/dist/indexable.js +0 -291
- package/dist/indexable.js.map +0 -1
- package/dist/promiseChain.d.ts +0 -21
- package/dist/promiseChain.esm.js +0 -78
- package/dist/promiseChain.esm.js.map +0 -1
- package/dist/promiseChain.js +0 -80
- package/dist/promiseChain.js.map +0 -1
- package/dist/reactive.d.ts +0 -885
- package/dist/reactive.esm.js +0 -5
- package/dist/reactive.esm.js.map +0 -1
- package/dist/reactive.js +0 -59
- package/dist/reactive.js.map +0 -1
- package/dist/std-decorators.d.ts +0 -52
- package/dist/std-decorators.esm.js +0 -196
- package/dist/std-decorators.esm.js.map +0 -1
- package/dist/std-decorators.js +0 -204
- package/dist/std-decorators.js.map +0 -1
- package/src/reactive/mapped.ts +0 -129
- package/src/reactive/zone.ts +0 -208
package/src/reactive/array.ts
CHANGED
|
@@ -1,72 +1,125 @@
|
|
|
1
|
-
import {
|
|
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
|
|
75
|
+
const actualIndex = index < 0 ? this.length + index : index
|
|
23
76
|
dependant(this, actualIndex)
|
|
24
|
-
if (actualIndex < 0 || actualIndex >= this
|
|
25
|
-
return reactive(this[
|
|
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
|
|
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
|
|
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
|
|
43
|
-
: this
|
|
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
|
|
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
|
|
107
|
+
return makeReactiveEntriesIterator(this.entries())
|
|
55
108
|
}
|
|
56
109
|
|
|
57
110
|
keys() {
|
|
58
111
|
dependant(this, 'length')
|
|
59
|
-
return this
|
|
112
|
+
return this.keys()
|
|
60
113
|
}
|
|
61
114
|
|
|
62
115
|
values() {
|
|
63
116
|
dependant(this)
|
|
64
|
-
return makeReactiveIterator(this
|
|
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[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
184
|
+
const index = this.indexOf(predicateOrElement, fromIndex)
|
|
131
185
|
if (index === -1) return undefined
|
|
132
|
-
return reactive(this[
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
193
|
-
: this
|
|
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
|
|
205
|
-
: (this
|
|
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
|
|
269
|
+
for (const i of range(start || 0, end || this.length - 1)) dependant(this, i)
|
|
211
270
|
return start === undefined
|
|
212
|
-
? this
|
|
271
|
+
? this.slice()
|
|
213
272
|
: end === undefined
|
|
214
|
-
? this
|
|
215
|
-
: this
|
|
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
|
|
279
|
+
return reactive(this.concat(...items))
|
|
221
280
|
}
|
|
222
281
|
|
|
223
282
|
join(separator?: string): string {
|
|
224
283
|
dependant(this)
|
|
225
|
-
return this
|
|
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
|
|
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
|
|
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
|
|
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
|
|
317
|
+
const oldLength = this.length
|
|
306
318
|
try {
|
|
307
|
-
return this
|
|
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
|
|
330
|
+
if (this.length === 0) return undefined
|
|
319
331
|
try {
|
|
320
|
-
return reactive(this
|
|
332
|
+
return reactive(this.pop())
|
|
321
333
|
} finally {
|
|
322
|
-
touched(this, { type: 'bunch', method: 'pop' }, index(this
|
|
334
|
+
touched(this, { type: 'bunch', method: 'pop' }, index(this.length))
|
|
323
335
|
}
|
|
324
336
|
}
|
|
325
337
|
|
|
326
338
|
shift() {
|
|
327
|
-
if (this
|
|
339
|
+
if (this.length === 0) return undefined
|
|
328
340
|
try {
|
|
329
|
-
return reactive(this
|
|
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
|
|
349
|
+
return this.unshift(...items)
|
|
342
350
|
} finally {
|
|
343
351
|
touched(
|
|
344
352
|
this,
|
|
345
353
|
{ type: 'bunch', method: 'unshift' },
|
|
346
|
-
range(0, this
|
|
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
|
|
360
|
+
const oldLength = this.length
|
|
353
361
|
if (deleteCount === undefined) deleteCount = oldLength - start
|
|
354
362
|
try {
|
|
355
|
-
if (deleteCount === undefined) return reactive(this
|
|
356
|
-
return reactive(this
|
|
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
|
-
|
|
366
|
-
|
|
373
|
+
length: true,
|
|
374
|
+
})
|
|
367
375
|
)
|
|
368
376
|
}
|
|
369
377
|
}
|
|
370
378
|
|
|
371
379
|
reverse() {
|
|
372
380
|
try {
|
|
373
|
-
return this
|
|
381
|
+
return this.reverse()
|
|
374
382
|
} finally {
|
|
375
|
-
touched(this, { type: 'bunch', method: 'reverse' }, range(0, this
|
|
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
|
|
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
|
|
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
|
|
391
|
-
if (end === undefined) return this
|
|
392
|
-
return this
|
|
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
|
|
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
|
|
401
|
-
return this
|
|
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
|
|
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
|
+
}
|