• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mobxjs / mobx / 6989803934

25 Nov 2023 03:01PM CUT coverage: 90.949% (-0.4%) from 91.391%
6989803934

Pull #3803

github

mweststrate
Removed old v4 compatibility tests
Pull Request #3803: Some cleanups of old stuff

1775 of 2214 branches covered (0.0%)

3296 of 3624 relevant lines covered (90.95%)

2879.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.08
/packages/mobx/src/types/observablearray.ts
1
import {
34✔
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
    assertProxies,
26
    reserveArrayBuffer,
27
    hasProp,
28
    die,
29
    globalState,
30
    initObservable
31
} from "../internal"
32

33
const SPLICE = "splice"
34✔
34
export const UPDATE = "update"
34✔
35
export const MAX_SPLICE_SIZE = 10000 // See e.g. https://github.com/mobxjs/mobx/issues/859
34✔
36

37
export interface IObservableArray<T = any> extends Array<T> {
38
    spliceWithArray(index: number, deleteCount?: number, newItems?: T[]): T[]
39
    clear(): T[]
40
    replace(newItems: T[]): T[]
41
    remove(value: T): boolean
42
    toJSON(): T[]
43
}
44

45
interface IArrayBaseChange<T> {
46
    object: IObservableArray<T>
47
    observableKind: "array"
48
    debugObjectName: string
49
    index: number
50
}
51

52
export type IArrayDidChange<T = any> = IArrayUpdate<T> | IArraySplice<T>
53

54
export interface IArrayUpdate<T = any> extends IArrayBaseChange<T> {
55
    type: "update"
56
    newValue: T
57
    oldValue: T
58
}
59

60
export interface IArraySplice<T = any> extends IArrayBaseChange<T> {
61
    type: "splice"
62
    added: T[]
63
    addedCount: number
64
    removed: T[]
65
    removedCount: number
66
}
67

68
export interface IArrayWillChange<T = any> {
69
    object: IObservableArray<T>
70
    index: number
71
    type: "update"
72
    newValue: T
73
}
74

75
export interface IArrayWillSplice<T = any> {
76
    object: IObservableArray<T>
77
    index: number
78
    type: "splice"
79
    added: T[]
80
    removedCount: number
81
}
82

83
const arrayTraps = {
34✔
84
    get(target, name) {
85
        const adm: ObservableArrayAdministration = target[$mobx]
97,466✔
86
        if (name === $mobx) {
97,466✔
87
            return adm
11,633✔
88
        }
89
        if (name === "length") {
85,833✔
90
            return adm.getArrayLength_()
3,812✔
91
        }
92
        if (typeof name === "string" && !isNaN(name as any)) {
82,021✔
93
            return adm.get_(parseInt(name))
70,247✔
94
        }
95
        if (hasProp(arrayExtensions, name)) {
11,774✔
96
            return arrayExtensions[name]
11,295✔
97
        }
98
        return target[name]
479✔
99
    },
100
    set(target, name, value): boolean {
101
        const adm: ObservableArrayAdministration = target[$mobx]
51✔
102
        if (name === "length") {
51✔
103
            adm.setArrayLength_(value)
12✔
104
        }
105
        if (typeof name === "symbol" || isNaN(name)) {
51✔
106
            target[name] = value
16✔
107
        } else {
108
            // numeric string
109
            adm.set_(parseInt(name), value)
35✔
110
        }
111
        return true
51✔
112
    },
113
    preventExtensions() {
114
        die(15)
1✔
115
    }
116
}
117

118
export class ObservableArrayAdministration
34✔
119
    implements IInterceptable<IArrayWillChange<any> | IArrayWillSplice<any>>, IListenable
120
{
121
    atom_: IAtom
237✔
122
    readonly values_: any[] = [] // this is the prop that gets proxied, so can't replace it!
237✔
123
    interceptors_
237✔
124
    changeListeners_
237✔
125
    enhancer_: (newV: any, oldV: any | undefined) => any
237✔
126
    dehancer: any
237✔
127
    proxy_!: IObservableArray<any>
237✔
128
    lastKnownLength_ = 0
237✔
129

130
    constructor(
131
        name = __DEV__ ? "ObservableArray@" + getNextId() : "ObservableArray",
×
132
        enhancer: IEnhancer<any>,
133
        public owned_: boolean,
237✔
134
        public legacyMode_: boolean
237✔
135
    ) {
136
        this.atom_ = new Atom(name)
237✔
137
        this.enhancer_ = (newV, oldV) =>
237✔
138
            enhancer(newV, oldV, __DEV__ ? name + "[..]" : "ObservableArray[..]")
85,325!
139
    }
140

141
    dehanceValue_(value: any): any {
142
        if (this.dehancer !== undefined) {
70,247✔
143
            return this.dehancer(value)
24✔
144
        }
145
        return value
70,223✔
146
    }
147

148
    dehanceValues_(values: any[]): any[] {
149
        if (this.dehancer !== undefined && values.length > 0) {
11,437✔
150
            return values.map(this.dehancer) as any
28✔
151
        }
152
        return values
11,409✔
153
    }
154

155
    intercept_(handler: IInterceptor<IArrayWillChange<any> | IArrayWillSplice<any>>): Lambda {
156
        return registerInterceptor<IArrayWillChange<any> | IArrayWillSplice<any>>(this, handler)
4✔
157
    }
158

159
    observe_(
160
        listener: (changeData: IArrayDidChange<any>) => void,
161
        fireImmediately = false
4✔
162
    ): Lambda {
163
        if (fireImmediately) {
5✔
164
            listener(<IArraySplice<any>>{
1✔
165
                observableKind: "array",
166
                object: this.proxy_ as any,
167
                debugObjectName: this.atom_.name_,
168
                type: "splice",
169
                index: 0,
170
                added: this.values_.slice(),
171
                addedCount: this.values_.length,
172
                removed: [],
173
                removedCount: 0
174
            })
175
        }
176
        return registerListener(this, listener)
5✔
177
    }
178

179
    getArrayLength_(): number {
180
        this.atom_.reportObserved()
3,812✔
181
        return this.values_.length
3,812✔
182
    }
183

184
    setArrayLength_(newLength: number) {
185
        if (typeof newLength !== "number" || isNaN(newLength) || newLength < 0) {
12!
186
            die("Out of range: " + newLength)
×
187
        }
188
        let currentLength = this.values_.length
12✔
189
        if (newLength === currentLength) {
12✔
190
            return
1✔
191
        } else if (newLength > currentLength) {
11✔
192
            const newItems = new Array(newLength - currentLength)
10✔
193
            for (let i = 0; i < newLength - currentLength; i++) {
10✔
194
                newItems[i] = undefined
16✔
195
            } // No Array.fill everywhere...
196
            this.spliceWithArray_(currentLength, 0, newItems)
10✔
197
        } else {
198
            this.spliceWithArray_(newLength, currentLength - newLength)
1✔
199
        }
200
    }
201

202
    updateArrayLength_(oldLength: number, delta: number) {
203
        if (oldLength !== this.lastKnownLength_) {
7,559!
204
            die(16)
×
205
        }
206
        this.lastKnownLength_ += delta
7,559✔
207
        if (this.legacyMode_ && delta > 0) {
7,559✔
208
            reserveArrayBuffer(oldLength + delta + 1)
2✔
209
        }
210
    }
211

212
    spliceWithArray_(index: number, deleteCount?: number, newItems?: any[]): any[] {
213
        checkIfStateModificationsAreAllowed(this.atom_)
7,560✔
214
        const length = this.values_.length
7,560✔
215

216
        if (index === undefined) {
7,560✔
217
            index = 0
360✔
218
        } else if (index > length) {
7,200✔
219
            index = length
842✔
220
        } else if (index < 0) {
6,358✔
221
            index = Math.max(0, length + index)
1,441✔
222
        }
223

224
        if (arguments.length === 1) {
7,560✔
225
            deleteCount = length - index
2✔
226
        } else if (deleteCount === undefined || deleteCount === null) {
7,558✔
227
            deleteCount = 0
360✔
228
        } else {
229
            deleteCount = Math.max(0, Math.min(deleteCount, length - index))
7,198✔
230
        }
231

232
        if (newItems === undefined) {
7,560✔
233
            newItems = EMPTY_ARRAY
641✔
234
        }
235

236
        if (hasInterceptors(this)) {
7,560✔
237
            const change = interceptChange<IArrayWillSplice<any>>(this as any, {
2✔
238
                object: this.proxy_ as any,
239
                type: SPLICE,
240
                index,
241
                removedCount: deleteCount,
242
                added: newItems
243
            })
244
            if (!change) {
2✔
245
                return EMPTY_ARRAY
1✔
246
            }
247
            deleteCount = change.removedCount
1✔
248
            newItems = change.added
1✔
249
        }
250

251
        newItems =
7,559✔
252
            newItems.length === 0 ? newItems : newItems.map(v => this.enhancer_(v, undefined))
85,297✔
253
        if (this.legacyMode_ || __DEV__) {
7,559✔
254
            const lengthDelta = newItems.length - deleteCount
7,559✔
255
            this.updateArrayLength_(length, lengthDelta) // checks if internal array wasn't modified
7,559✔
256
        }
257
        const res = this.spliceItemsIntoValues_(index, deleteCount, newItems)
7,559✔
258

259
        if (deleteCount !== 0 || newItems.length !== 0) {
7,559✔
260
            this.notifyArraySplice_(index, newItems, res)
7,001✔
261
        }
262
        return this.dehanceValues_(res)
7,559✔
263
    }
264

265
    spliceItemsIntoValues_(index: number, deleteCount: number, newItems: any[]): any[] {
266
        if (newItems.length < MAX_SPLICE_SIZE) {
7,559✔
267
            return this.values_.splice(index, deleteCount, ...newItems)
7,541✔
268
        } else {
269
            // The items removed by the splice
270
            const res = this.values_.slice(index, index + deleteCount)
18✔
271
            // The items that that should remain at the end of the array
272
            let oldItems = this.values_.slice(index + deleteCount)
18✔
273
            // New length is the previous length + addition count - deletion count
274
            this.values_.length += newItems.length - deleteCount
18✔
275
            for (let i = 0; i < newItems.length; i++) {
18✔
276
                this.values_[index + i] = newItems[i]
2,161,018✔
277
            }
278
            for (let i = 0; i < oldItems.length; i++) {
18✔
279
                this.values_[index + newItems.length + i] = oldItems[i]
1,010,009✔
280
            }
281
            return res
18✔
282
        }
283
    }
284

285
    notifyArrayChildUpdate_(index: number, newValue: any, oldValue: any) {
286
        const notifySpy = !this.owned_ && isSpyEnabled()
28✔
287
        const notify = hasListeners(this)
28✔
288
        const change: IArrayDidChange | null =
289
            notify || notifySpy
28✔
290
                ? ({
291
                      observableKind: "array",
292
                      object: this.proxy_,
293
                      type: UPDATE,
294
                      debugObjectName: this.atom_.name_,
295
                      index,
296
                      newValue,
297
                      oldValue
298
                  } as const)
299
                : null
300

301
        // The reason why this is on right hand side here (and not above), is this way the uglifier will drop it, but it won't
302
        // cause any runtime overhead in development mode without NODE_ENV set, unless spying is enabled
303
        if (__DEV__ && notifySpy) {
28✔
304
            spyReportStart(change!)
2✔
305
        }
306
        this.atom_.reportChanged()
28✔
307
        if (notify) {
28✔
308
            notifyListeners(this, change)
2✔
309
        }
310
        if (__DEV__ && notifySpy) {
28✔
311
            spyReportEnd()
2✔
312
        }
313
    }
314

315
    notifyArraySplice_(index: number, added: any[], removed: any[]) {
316
        const notifySpy = !this.owned_ && isSpyEnabled()
7,001✔
317
        const notify = hasListeners(this)
7,001✔
318
        const change: IArraySplice | null =
319
            notify || notifySpy
7,001✔
320
                ? ({
321
                      observableKind: "array",
322
                      object: this.proxy_,
323
                      debugObjectName: this.atom_.name_,
324
                      type: SPLICE,
325
                      index,
326
                      removed,
327
                      added,
328
                      removedCount: removed.length,
329
                      addedCount: added.length
330
                  } as const)
331
                : null
332

333
        if (__DEV__ && notifySpy) {
7,001✔
334
            spyReportStart(change!)
6✔
335
        }
336
        this.atom_.reportChanged()
7,001✔
337
        // conform: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/observe
338
        if (notify) {
7,001✔
339
            notifyListeners(this, change)
8✔
340
        }
341
        if (__DEV__ && notifySpy) {
7,001✔
342
            spyReportEnd()
6✔
343
        }
344
    }
345

346
    get_(index: number): any | undefined {
347
        if (this.legacyMode_ && index >= this.values_.length) {
70,247!
348
            console.warn(
×
349
                __DEV__
×
350
                    ? `[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`
351
                    : `[mobx] Out of bounds read: ${index}`
352
            )
353
            return undefined
×
354
        }
355
        this.atom_.reportObserved()
70,247✔
356
        return this.dehanceValue_(this.values_[index])
70,247✔
357
    }
358

359
    set_(index: number, newValue: any) {
360
        const values = this.values_
35✔
361
        if (this.legacyMode_ && index > values.length) {
35!
362
            // out of bounds
363
            die(17, index, values.length)
×
364
        }
365
        if (index < values.length) {
35✔
366
            // update at index in range
367
            checkIfStateModificationsAreAllowed(this.atom_)
28✔
368
            const oldValue = values[index]
28✔
369
            if (hasInterceptors(this)) {
28✔
370
                const change = interceptChange<IArrayWillChange<any>>(this as any, {
1✔
371
                    type: UPDATE,
372
                    object: this.proxy_ as any, // since "this" is the real array we need to pass its proxy
373
                    index,
374
                    newValue
375
                })
376
                if (!change) {
1!
377
                    return
×
378
                }
379
                newValue = change.newValue
1✔
380
            }
381
            newValue = this.enhancer_(newValue, oldValue)
28✔
382
            const changed = newValue !== oldValue
28✔
383
            if (changed) {
28✔
384
                values[index] = newValue
28✔
385
                this.notifyArrayChildUpdate_(index, newValue, oldValue)
28✔
386
            }
387
        } else {
388
            // For out of bound index, we don't create an actual sparse array,
389
            // but rather fill the holes with undefined (same as setArrayLength_).
390
            // This could be considered a bug.
391
            const newItems = new Array(index + 1 - values.length)
7✔
392
            for (let i = 0; i < newItems.length - 1; i++) {
7✔
393
                newItems[i] = undefined
1,001✔
394
            } // No Array.fill everywhere...
395
            newItems[newItems.length - 1] = newValue
7✔
396
            this.spliceWithArray_(values.length, 0, newItems)
7✔
397
        }
398
    }
399
}
400

401
export function createObservableArray<T>(
34✔
402
    initialValues: T[] | undefined,
403
    enhancer: IEnhancer<T>,
404
    name = __DEV__ ? "ObservableArray@" + getNextId() : "ObservableArray",
274!
405
    owned = false
233✔
406
): IObservableArray<T> {
407
    assertProxies()
233✔
408
    return initObservable(() => {
233✔
409
        const adm = new ObservableArrayAdministration(name, enhancer, owned, false)
233✔
410
        addHiddenFinalProp(adm.values_, $mobx, adm)
233✔
411
        const proxy = new Proxy(adm.values_, arrayTraps) as any
233✔
412
        adm.proxy_ = proxy
233✔
413
        if (initialValues && initialValues.length) {
233✔
414
            adm.spliceWithArray_(0, 0, initialValues)
163✔
415
        }
416
        return proxy
233✔
417
    })
418
}
419

420
// eslint-disable-next-line
421
export var arrayExtensions = {
34✔
422
    clear(): any[] {
423
        return this.splice(0)
1✔
424
    },
425

426
    replace(newItems: any[]) {
427
        const adm: ObservableArrayAdministration = this[$mobx]
3,617✔
428
        return adm.spliceWithArray_(0, adm.values_.length, newItems)
3,617✔
429
    },
430

431
    // Used by JSON.stringify
432
    toJSON(): any[] {
433
        return this.slice()
9✔
434
    },
435

436
    /*
437
     * functions that do alter the internal structure of the array, (based on lib.es6.d.ts)
438
     * since these functions alter the inner structure of the array, the have side effects.
439
     * Because the have side effects, they should not be used in computed function,
440
     * and for that reason the do not call dependencyState.notifyObserved
441
     */
442
    splice(index: number, deleteCount?: number, ...newItems: any[]): any[] {
443
        const adm: ObservableArrayAdministration = this[$mobx]
3,649✔
444
        switch (arguments.length) {
3,649!
445
            case 0:
446
                return []
×
447
            case 1:
448
                return adm.spliceWithArray_(index)
2✔
449
            case 2:
450
                return adm.spliceWithArray_(index, deleteCount)
638✔
451
        }
452
        return adm.spliceWithArray_(index, deleteCount, newItems)
3,009✔
453
    },
454

455
    spliceWithArray(index: number, deleteCount?: number, newItems?: any[]): any[] {
456
        return (this[$mobx] as ObservableArrayAdministration).spliceWithArray_(
4✔
457
            index,
458
            deleteCount,
459
            newItems
460
        )
461
    },
462

463
    push(...items: any[]): number {
464
        const adm: ObservableArrayAdministration = this[$mobx]
104✔
465
        adm.spliceWithArray_(adm.values_.length, 0, items)
104✔
466
        return adm.values_.length
104✔
467
    },
468

469
    pop() {
470
        return this.splice(Math.max(this[$mobx].values_.length - 1, 0), 1)[0]
10✔
471
    },
472

473
    shift() {
474
        return this.splice(0, 1)[0]
12✔
475
    },
476

477
    unshift(...items: any[]): number {
478
        const adm: ObservableArrayAdministration = this[$mobx]
5✔
479
        adm.spliceWithArray_(0, 0, items)
5✔
480
        return adm.values_.length
5✔
481
    },
482

483
    reverse(): any[] {
484
        // reverse by default mutates in place before returning the result
485
        // which makes it both a 'derivation' and a 'mutation'.
486
        if (globalState.trackingDerivation) {
3✔
487
            die(37, "reverse")
1✔
488
        }
489
        this.replace(this.slice().reverse())
2✔
490
        return this
2✔
491
    },
492

493
    sort(): any[] {
494
        // sort by default mutates in place before returning the result
495
        // which goes against all good practices. Let's not change the array in place!
496
        if (globalState.trackingDerivation) {
4✔
497
            die(37, "sort")
2✔
498
        }
499
        const copy = this.slice()
2✔
500
        copy.sort.apply(copy, arguments)
2✔
501
        this.replace(copy)
2✔
502
        return this
2✔
503
    },
504

505
    remove(value: any): boolean {
506
        const adm: ObservableArrayAdministration = this[$mobx]
6✔
507
        const idx = adm.dehanceValues_(adm.values_).indexOf(value)
6✔
508
        if (idx > -1) {
6✔
509
            this.splice(idx, 1)
4✔
510
            return true
4✔
511
        }
512
        return false
2✔
513
    }
514
}
515

516
/**
517
 * Wrap function from prototype
518
 * Without this, everything works as well, but this works
519
 * faster as everything works on unproxied values
520
 */
521
addArrayExtension("concat", simpleFunc)
34✔
522
addArrayExtension("flat", simpleFunc)
34✔
523
addArrayExtension("includes", simpleFunc)
34✔
524
addArrayExtension("indexOf", simpleFunc)
34✔
525
addArrayExtension("join", simpleFunc)
34✔
526
addArrayExtension("lastIndexOf", simpleFunc)
34✔
527
addArrayExtension("slice", simpleFunc)
34✔
528
addArrayExtension("toString", simpleFunc)
34✔
529
addArrayExtension("toLocaleString", simpleFunc)
34✔
530
// map
531
addArrayExtension("every", mapLikeFunc)
34✔
532
addArrayExtension("filter", mapLikeFunc)
34✔
533
addArrayExtension("find", mapLikeFunc)
34✔
534
addArrayExtension("findIndex", mapLikeFunc)
34✔
535
addArrayExtension("flatMap", mapLikeFunc)
34✔
536
addArrayExtension("forEach", mapLikeFunc)
34✔
537
addArrayExtension("map", mapLikeFunc)
34✔
538
addArrayExtension("some", mapLikeFunc)
34✔
539
// reduce
540
addArrayExtension("reduce", reduceLikeFunc)
34✔
541
addArrayExtension("reduceRight", reduceLikeFunc)
34✔
542

543
function addArrayExtension(funcName, funcFactory) {
544
    if (typeof Array.prototype[funcName] === "function") {
646✔
545
        arrayExtensions[funcName] = funcFactory(funcName)
646✔
546
    }
547
}
548

549
// Report and delegate to dehanced array
550
function simpleFunc(funcName) {
551
    return function () {
306✔
552
        const adm: ObservableArrayAdministration = this[$mobx]
3,732✔
553
        adm.atom_.reportObserved()
3,732✔
554
        const dehancedValues = adm.dehanceValues_(adm.values_)
3,732✔
555
        return dehancedValues[funcName].apply(dehancedValues, arguments)
3,732✔
556
    }
557
}
558

559
// Make sure callbacks recieve correct array arg #2326
560
function mapLikeFunc(funcName) {
561
    return function (callback, thisArg) {
272✔
562
        const adm: ObservableArrayAdministration = this[$mobx]
113✔
563
        adm.atom_.reportObserved()
113✔
564
        const dehancedValues = adm.dehanceValues_(adm.values_)
113✔
565
        return dehancedValues[funcName]((element, index) => {
113✔
566
            return callback.call(thisArg, element, index, this)
255✔
567
        })
568
    }
569
}
570

571
// Make sure callbacks recieve correct array arg #2326
572
function reduceLikeFunc(funcName) {
573
    return function () {
68✔
574
        const adm: ObservableArrayAdministration = this[$mobx]
27✔
575
        adm.atom_.reportObserved()
27✔
576
        const dehancedValues = adm.dehanceValues_(adm.values_)
27✔
577
        // #2432 - reduce behavior depends on arguments.length
578
        const callback = arguments[0]
27✔
579
        arguments[0] = (accumulator, currentValue, index) => {
27✔
580
            return callback(accumulator, currentValue, index, this)
70✔
581
        }
582
        return dehancedValues[funcName].apply(dehancedValues, arguments)
27✔
583
    }
584
}
585

586
const isObservableArrayAdministration = createInstanceofPredicate(
34✔
587
    "ObservableArrayAdministration",
588
    ObservableArrayAdministration
589
)
590

591
export function isObservableArray(thing): thing is IObservableArray<any> {
34✔
592
    return isObject(thing) && isObservableArrayAdministration(thing[$mobx])
1,006✔
593
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc