1 | import {
|
2 | $mobx,
|
3 | Atom,
|
4 | EMPTY_ARRAY,
|
5 | IAtom,
|
6 | IEnhancer,
|
7 | IInterceptable,
|
8 | IInterceptor,
|
9 | IListenable,
|
10 | Lambda,
|
11 | addHiddenFinalProp,
|
12 | checkIfStateModificationsAreAllowed,
|
13 | createInstanceofPredicate,
|
14 | getNextId,
|
15 | hasInterceptors,
|
16 | hasListeners,
|
17 | interceptChange,
|
18 | isObject,
|
19 | isSpyEnabled,
|
20 | notifyListeners,
|
21 | registerInterceptor,
|
22 | registerListener,
|
23 | spyReportEnd,
|
24 | spyReportStart,
|
25 | allowStateChangesStart,
|
26 | allowStateChangesEnd,
|
27 | assertProxies,
|
28 | reserveArrayBuffer,
|
29 | hasProp,
|
30 | die,
|
31 | globalState
|
32 | } from "../internal"
|
33 |
|
34 | const SPLICE = "splice"
|
35 | export const UPDATE = "update"
|
36 | export const MAX_SPLICE_SIZE = 10000
|
37 |
|
38 | export interface IObservableArray<T = any> extends Array<T> {
|
39 | spliceWithArray(index: number, deleteCount?: number, newItems?: T[]): T[]
|
40 | clear(): T[]
|
41 | replace(newItems: T[]): T[]
|
42 | remove(value: T): boolean
|
43 | toJSON(): T[]
|
44 | }
|
45 |
|
46 | interface IArrayBaseChange<T> {
|
47 | object: IObservableArray<T>
|
48 | observableKind: "array"
|
49 | debugObjectName: string
|
50 | index: number
|
51 | }
|
52 |
|
53 | export type IArrayDidChange<T = any> = IArrayUpdate<T> | IArraySplice<T>
|
54 |
|
55 | export interface IArrayUpdate<T = any> extends IArrayBaseChange<T> {
|
56 | type: "update"
|
57 | newValue: T
|
58 | oldValue: T
|
59 | }
|
60 |
|
61 | export interface IArraySplice<T = any> extends IArrayBaseChange<T> {
|
62 | type: "splice"
|
63 | added: T[]
|
64 | addedCount: number
|
65 | removed: T[]
|
66 | removedCount: number
|
67 | }
|
68 |
|
69 | export interface IArrayWillChange<T = any> {
|
70 | object: IObservableArray<T>
|
71 | index: number
|
72 | type: "update"
|
73 | newValue: T
|
74 | }
|
75 |
|
76 | export interface IArrayWillSplice<T = any> {
|
77 | object: IObservableArray<T>
|
78 | index: number
|
79 | type: "splice"
|
80 | added: T[]
|
81 | removedCount: number
|
82 | }
|
83 |
|
84 | const arrayTraps = {
|
85 | get(target, name) {
|
86 | const adm: ObservableArrayAdministration = target[$mobx]
|
87 | if (name === $mobx) return adm
|
88 | if (name === "length") return adm.getArrayLength_()
|
89 | if (typeof name === "string" && !isNaN(name as any)) {
|
90 | return adm.get_(parseInt(name))
|
91 | }
|
92 | if (hasProp(arrayExtensions, name)) {
|
93 | return arrayExtensions[name]
|
94 | }
|
95 | return target[name]
|
96 | },
|
97 | set(target, name, value): boolean {
|
98 | const adm: ObservableArrayAdministration = target[$mobx]
|
99 | if (name === "length") {
|
100 | adm.setArrayLength_(value)
|
101 | }
|
102 | if (typeof name === "symbol" || isNaN(name)) {
|
103 | target[name] = value
|
104 | } else {
|
105 |
|
106 | adm.set_(parseInt(name), value)
|
107 | }
|
108 | return true
|
109 | },
|
110 | preventExtensions() {
|
111 | die(15)
|
112 | }
|
113 | }
|
114 |
|
115 | export class ObservableArrayAdministration
|
116 | implements IInterceptable<IArrayWillChange<any> | IArrayWillSplice<any>>, IListenable {
|
117 | atom_: IAtom
|
118 | readonly values_: any[] = []
|
119 | interceptors_
|
120 | changeListeners_
|
121 | enhancer_: (newV: any, oldV: any | undefined) => any
|
122 | dehancer: any
|
123 | proxy_!: IObservableArray<any>
|
124 | lastKnownLength_ = 0
|
125 |
|
126 | constructor(
|
127 | name = __DEV__ ? "ObservableArray@" + getNextId() : "ObservableArray",
|
128 | enhancer: IEnhancer<any>,
|
129 | public owned_: boolean,
|
130 | public legacyMode_: boolean
|
131 | ) {
|
132 | this.atom_ = new Atom(name)
|
133 | this.enhancer_ = (newV, oldV) =>
|
134 | enhancer(newV, oldV, __DEV__ ? name + "[..]" : "ObservableArray[..]")
|
135 | }
|
136 |
|
137 | dehanceValue_(value: any): any {
|
138 | if (this.dehancer !== undefined) return this.dehancer(value)
|
139 | return value
|
140 | }
|
141 |
|
142 | dehanceValues_(values: any[]): any[] {
|
143 | if (this.dehancer !== undefined && values.length > 0)
|
144 | return values.map(this.dehancer) as any
|
145 | return values
|
146 | }
|
147 |
|
148 | intercept_(handler: IInterceptor<IArrayWillChange<any> | IArrayWillSplice<any>>): Lambda {
|
149 | return registerInterceptor<IArrayWillChange<any> | IArrayWillSplice<any>>(this, handler)
|
150 | }
|
151 |
|
152 | observe_(
|
153 | listener: (changeData: IArrayDidChange<any>) => void,
|
154 | fireImmediately = false
|
155 | ): Lambda {
|
156 | if (fireImmediately) {
|
157 | listener(<IArraySplice<any>>{
|
158 | observableKind: "array",
|
159 | object: this.proxy_ as any,
|
160 | debugObjectName: this.atom_.name_,
|
161 | type: "splice",
|
162 | index: 0,
|
163 | added: this.values_.slice(),
|
164 | addedCount: this.values_.length,
|
165 | removed: [],
|
166 | removedCount: 0
|
167 | })
|
168 | }
|
169 | return registerListener(this, listener)
|
170 | }
|
171 |
|
172 | getArrayLength_(): number {
|
173 | this.atom_.reportObserved()
|
174 | return this.values_.length
|
175 | }
|
176 |
|
177 | setArrayLength_(newLength: number) {
|
178 | if (typeof newLength !== "number" || isNaN(newLength) || newLength < 0)
|
179 | die("Out of range: " + newLength)
|
180 | let currentLength = this.values_.length
|
181 | if (newLength === currentLength) return
|
182 | else if (newLength > currentLength) {
|
183 | const newItems = new Array(newLength - currentLength)
|
184 | for (let i = 0; i < newLength - currentLength; i++) newItems[i] = undefined
|
185 | this.spliceWithArray_(currentLength, 0, newItems)
|
186 | } else this.spliceWithArray_(newLength, currentLength - newLength)
|
187 | }
|
188 |
|
189 | updateArrayLength_(oldLength: number, delta: number) {
|
190 | if (oldLength !== this.lastKnownLength_) die(16)
|
191 | this.lastKnownLength_ += delta
|
192 | if (this.legacyMode_ && delta > 0) reserveArrayBuffer(oldLength + delta + 1)
|
193 | }
|
194 |
|
195 | spliceWithArray_(index: number, deleteCount?: number, newItems?: any[]): any[] {
|
196 | checkIfStateModificationsAreAllowed(this.atom_)
|
197 | const length = this.values_.length
|
198 |
|
199 | if (index === undefined) index = 0
|
200 | else if (index > length) index = length
|
201 | else if (index < 0) index = Math.max(0, length + index)
|
202 |
|
203 | if (arguments.length === 1) deleteCount = length - index
|
204 | else if (deleteCount === undefined || deleteCount === null) deleteCount = 0
|
205 | else deleteCount = Math.max(0, Math.min(deleteCount, length - index))
|
206 |
|
207 | if (newItems === undefined) newItems = EMPTY_ARRAY
|
208 |
|
209 | if (hasInterceptors(this)) {
|
210 | const change = interceptChange<IArrayWillSplice<any>>(this as any, {
|
211 | object: this.proxy_ as any,
|
212 | type: SPLICE,
|
213 | index,
|
214 | removedCount: deleteCount,
|
215 | added: newItems
|
216 | })
|
217 | if (!change) return EMPTY_ARRAY
|
218 | deleteCount = change.removedCount
|
219 | newItems = change.added
|
220 | }
|
221 |
|
222 | newItems =
|
223 | newItems.length === 0 ? newItems : newItems.map(v => this.enhancer_(v, undefined))
|
224 | if (this.legacyMode_ || __DEV__) {
|
225 | const lengthDelta = newItems.length - deleteCount
|
226 | this.updateArrayLength_(length, lengthDelta)
|
227 | }
|
228 | const res = this.spliceItemsIntoValues_(index, deleteCount, newItems)
|
229 |
|
230 | if (deleteCount !== 0 || newItems.length !== 0)
|
231 | this.notifyArraySplice_(index, newItems, res)
|
232 | return this.dehanceValues_(res)
|
233 | }
|
234 |
|
235 | spliceItemsIntoValues_(index: number, deleteCount: number, newItems: any[]): any[] {
|
236 | if (newItems.length < MAX_SPLICE_SIZE) {
|
237 | return this.values_.splice(index, deleteCount, ...newItems)
|
238 | } else {
|
239 |
|
240 | const res = this.values_.slice(index, index + deleteCount)
|
241 |
|
242 | let oldItems = this.values_.slice(index + deleteCount)
|
243 |
|
244 | this.values_.length += newItems.length - deleteCount
|
245 | for (let i = 0; i < newItems.length; i++) this.values_[index + i] = newItems[i]
|
246 | for (let i = 0; i < oldItems.length; i++)
|
247 | this.values_[index + newItems.length + i] = oldItems[i]
|
248 | return res
|
249 | }
|
250 | }
|
251 |
|
252 | notifyArrayChildUpdate_(index: number, newValue: any, oldValue: any) {
|
253 | const notifySpy = !this.owned_ && isSpyEnabled()
|
254 | const notify = hasListeners(this)
|
255 | const change: IArrayDidChange | null =
|
256 | notify || notifySpy
|
257 | ? ({
|
258 | observableKind: "array",
|
259 | object: this.proxy_,
|
260 | type: UPDATE,
|
261 | debugObjectName: this.atom_.name_,
|
262 | index,
|
263 | newValue,
|
264 | oldValue
|
265 | } as const)
|
266 | : null
|
267 |
|
268 |
|
269 |
|
270 | if (__DEV__ && notifySpy) spyReportStart(change!)
|
271 | this.atom_.reportChanged()
|
272 | if (notify) notifyListeners(this, change)
|
273 | if (__DEV__ && notifySpy) spyReportEnd()
|
274 | }
|
275 |
|
276 | notifyArraySplice_(index: number, added: any[], removed: any[]) {
|
277 | const notifySpy = !this.owned_ && isSpyEnabled()
|
278 | const notify = hasListeners(this)
|
279 | const change: IArraySplice | null =
|
280 | notify || notifySpy
|
281 | ? ({
|
282 | observableKind: "array",
|
283 | object: this.proxy_,
|
284 | debugObjectName: this.atom_.name_,
|
285 | type: SPLICE,
|
286 | index,
|
287 | removed,
|
288 | added,
|
289 | removedCount: removed.length,
|
290 | addedCount: added.length
|
291 | } as const)
|
292 | : null
|
293 |
|
294 | if (__DEV__ && notifySpy) spyReportStart(change!)
|
295 | this.atom_.reportChanged()
|
296 |
|
297 | if (notify) notifyListeners(this, change)
|
298 | if (__DEV__ && notifySpy) spyReportEnd()
|
299 | }
|
300 |
|
301 | get_(index: number): any | undefined {
|
302 | if (index < this.values_.length) {
|
303 | this.atom_.reportObserved()
|
304 | return this.dehanceValue_(this.values_[index])
|
305 | }
|
306 | console.warn(
|
307 | __DEV__
|
308 | ? `[mobx] Out of bounds read: ${index}`
|
309 | : `[mobx.array] Attempt to read an array index (${index}) that is out of bounds (${this.values_.length}). Please check length first. Out of bound indices will not be tracked by MobX`
|
310 | )
|
311 | }
|
312 |
|
313 | set_(index: number, newValue: any) {
|
314 | const values = this.values_
|
315 | if (index < values.length) {
|
316 |
|
317 | checkIfStateModificationsAreAllowed(this.atom_)
|
318 | const oldValue = values[index]
|
319 | if (hasInterceptors(this)) {
|
320 | const change = interceptChange<IArrayWillChange<any>>(this as any, {
|
321 | type: UPDATE,
|
322 | object: this.proxy_ as any,
|
323 | index,
|
324 | newValue
|
325 | })
|
326 | if (!change) return
|
327 | newValue = change.newValue
|
328 | }
|
329 | newValue = this.enhancer_(newValue, oldValue)
|
330 | const changed = newValue !== oldValue
|
331 | if (changed) {
|
332 | values[index] = newValue
|
333 | this.notifyArrayChildUpdate_(index, newValue, oldValue)
|
334 | }
|
335 | } else if (index === values.length) {
|
336 |
|
337 | this.spliceWithArray_(index, 0, [newValue])
|
338 | } else {
|
339 |
|
340 | die(17, index, values.length)
|
341 | }
|
342 | }
|
343 | }
|
344 |
|
345 | export function createObservableArray<T>(
|
346 | initialValues: T[] | undefined,
|
347 | enhancer: IEnhancer<T>,
|
348 | name = __DEV__ ? "ObservableArray@" + getNextId() : "ObservableArray",
|
349 | owned = false
|
350 | ): IObservableArray<T> {
|
351 | assertProxies()
|
352 | const adm = new ObservableArrayAdministration(name, enhancer, owned, false)
|
353 | addHiddenFinalProp(adm.values_, $mobx, adm)
|
354 | const proxy = new Proxy(adm.values_, arrayTraps) as any
|
355 | adm.proxy_ = proxy
|
356 | if (initialValues && initialValues.length) {
|
357 | const prev = allowStateChangesStart(true)
|
358 | adm.spliceWithArray_(0, 0, initialValues)
|
359 | allowStateChangesEnd(prev)
|
360 | }
|
361 | return proxy
|
362 | }
|
363 |
|
364 |
|
365 | export var arrayExtensions = {
|
366 | clear(): any[] {
|
367 | return this.splice(0)
|
368 | },
|
369 |
|
370 | replace(newItems: any[]) {
|
371 | const adm: ObservableArrayAdministration = this[$mobx]
|
372 | return adm.spliceWithArray_(0, adm.values_.length, newItems)
|
373 | },
|
374 |
|
375 |
|
376 | toJSON(): any[] {
|
377 | return this.slice()
|
378 | },
|
379 |
|
380 | |
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 | splice(index: number, deleteCount?: number, ...newItems: any[]): any[] {
|
387 | const adm: ObservableArrayAdministration = this[$mobx]
|
388 | switch (arguments.length) {
|
389 | case 0:
|
390 | return []
|
391 | case 1:
|
392 | return adm.spliceWithArray_(index)
|
393 | case 2:
|
394 | return adm.spliceWithArray_(index, deleteCount)
|
395 | }
|
396 | return adm.spliceWithArray_(index, deleteCount, newItems)
|
397 | },
|
398 |
|
399 | spliceWithArray(index: number, deleteCount?: number, newItems?: any[]): any[] {
|
400 | return (this[$mobx] as ObservableArrayAdministration).spliceWithArray_(
|
401 | index,
|
402 | deleteCount,
|
403 | newItems
|
404 | )
|
405 | },
|
406 |
|
407 | push(...items: any[]): number {
|
408 | const adm: ObservableArrayAdministration = this[$mobx]
|
409 | adm.spliceWithArray_(adm.values_.length, 0, items)
|
410 | return adm.values_.length
|
411 | },
|
412 |
|
413 | pop() {
|
414 | return this.splice(Math.max(this[$mobx].values_.length - 1, 0), 1)[0]
|
415 | },
|
416 |
|
417 | shift() {
|
418 | return this.splice(0, 1)[0]
|
419 | },
|
420 |
|
421 | unshift(...items: any[]): number {
|
422 | const adm: ObservableArrayAdministration = this[$mobx]
|
423 | adm.spliceWithArray_(0, 0, items)
|
424 | return adm.values_.length
|
425 | },
|
426 |
|
427 | reverse(): any[] {
|
428 |
|
429 |
|
430 | if (globalState.trackingDerivation) {
|
431 | die(37, "reverse")
|
432 | }
|
433 | this.replace(this.slice().reverse())
|
434 | return this
|
435 | },
|
436 |
|
437 | sort(): any[] {
|
438 |
|
439 |
|
440 | if (globalState.trackingDerivation) {
|
441 | die(37, "sort")
|
442 | }
|
443 | const copy = this.slice()
|
444 | copy.sort.apply(copy, arguments)
|
445 | this.replace(copy)
|
446 | return this
|
447 | },
|
448 |
|
449 | remove(value: any): boolean {
|
450 | const adm: ObservableArrayAdministration = this[$mobx]
|
451 | const idx = adm.dehanceValues_(adm.values_).indexOf(value)
|
452 | if (idx > -1) {
|
453 | this.splice(idx, 1)
|
454 | return true
|
455 | }
|
456 | return false
|
457 | }
|
458 | }
|
459 |
|
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 | addArrayExtension("concat", simpleFunc)
|
466 | addArrayExtension("flat", simpleFunc)
|
467 | addArrayExtension("includes", simpleFunc)
|
468 | addArrayExtension("indexOf", simpleFunc)
|
469 | addArrayExtension("join", simpleFunc)
|
470 | addArrayExtension("lastIndexOf", simpleFunc)
|
471 | addArrayExtension("slice", simpleFunc)
|
472 | addArrayExtension("toString", simpleFunc)
|
473 | addArrayExtension("toLocaleString", simpleFunc)
|
474 |
|
475 | addArrayExtension("every", mapLikeFunc)
|
476 | addArrayExtension("filter", mapLikeFunc)
|
477 | addArrayExtension("find", mapLikeFunc)
|
478 | addArrayExtension("findIndex", mapLikeFunc)
|
479 | addArrayExtension("flatMap", mapLikeFunc)
|
480 | addArrayExtension("forEach", mapLikeFunc)
|
481 | addArrayExtension("map", mapLikeFunc)
|
482 | addArrayExtension("some", mapLikeFunc)
|
483 |
|
484 | addArrayExtension("reduce", reduceLikeFunc)
|
485 | addArrayExtension("reduceRight", reduceLikeFunc)
|
486 |
|
487 | function addArrayExtension(funcName, funcFactory) {
|
488 | if (typeof Array.prototype[funcName] === "function") {
|
489 | arrayExtensions[funcName] = funcFactory(funcName)
|
490 | }
|
491 | }
|
492 |
|
493 |
|
494 | function simpleFunc(funcName) {
|
495 | return function () {
|
496 | const adm: ObservableArrayAdministration = this[$mobx]
|
497 | adm.atom_.reportObserved()
|
498 | const dehancedValues = adm.dehanceValues_(adm.values_)
|
499 | return dehancedValues[funcName].apply(dehancedValues, arguments)
|
500 | }
|
501 | }
|
502 |
|
503 |
|
504 | function mapLikeFunc(funcName) {
|
505 | return function (callback, thisArg) {
|
506 | const adm: ObservableArrayAdministration = this[$mobx]
|
507 | adm.atom_.reportObserved()
|
508 | const dehancedValues = adm.dehanceValues_(adm.values_)
|
509 | return dehancedValues[funcName]((element, index) => {
|
510 | return callback.call(thisArg, element, index, this)
|
511 | })
|
512 | }
|
513 | }
|
514 |
|
515 |
|
516 | function reduceLikeFunc(funcName) {
|
517 | return function () {
|
518 | const adm: ObservableArrayAdministration = this[$mobx]
|
519 | adm.atom_.reportObserved()
|
520 | const dehancedValues = adm.dehanceValues_(adm.values_)
|
521 |
|
522 | const callback = arguments[0]
|
523 | arguments[0] = (accumulator, currentValue, index) => {
|
524 | return callback(accumulator, currentValue, index, this)
|
525 | }
|
526 | return dehancedValues[funcName].apply(dehancedValues, arguments)
|
527 | }
|
528 | }
|
529 |
|
530 | const isObservableArrayAdministration = createInstanceofPredicate(
|
531 | "ObservableArrayAdministration",
|
532 | ObservableArrayAdministration
|
533 | )
|
534 |
|
535 | export function isObservableArray(thing): thing is IObservableArray<any> {
|
536 | return isObject(thing) && isObservableArrayAdministration(thing[$mobx])
|
537 | }
|