@thomas-siegfried/tapout 0.0.1 → 0.0.2

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1205 -1205
  3. package/package.json +55 -55
  4. package/src/applyBindings.ts +372 -372
  5. package/src/arrayToDomMapping.ts +300 -300
  6. package/src/attributeInterpolationMarkup.ts +91 -91
  7. package/src/bindingContext.ts +207 -207
  8. package/src/bindingEvent.ts +198 -198
  9. package/src/bindingProvider.ts +260 -260
  10. package/src/bindings.ts +963 -963
  11. package/src/compareArrays.ts +122 -122
  12. package/src/componentBinding.ts +318 -318
  13. package/src/componentDecorator.ts +62 -62
  14. package/src/components.ts +367 -367
  15. package/src/computed.ts +439 -439
  16. package/src/configure.ts +52 -52
  17. package/src/controlFlowBindings.ts +206 -206
  18. package/src/core.ts +60 -60
  19. package/src/decorators.ts +407 -407
  20. package/src/dependencyDetection.ts +76 -76
  21. package/src/disposable.ts +36 -36
  22. package/src/domData.ts +42 -42
  23. package/src/domNodeDisposal.ts +94 -94
  24. package/src/effects.ts +29 -29
  25. package/src/event.ts +173 -173
  26. package/src/expressionRewriting.ts +219 -219
  27. package/src/extenders.ts +102 -102
  28. package/src/filters.ts +91 -91
  29. package/src/index.ts +150 -150
  30. package/src/interpolationMarkup.ts +130 -130
  31. package/src/memoization.ts +71 -71
  32. package/src/namespacedBindings.ts +132 -132
  33. package/src/observable.ts +48 -48
  34. package/src/observableArray.ts +397 -397
  35. package/src/options.ts +25 -25
  36. package/src/selectExtensions.ts +68 -68
  37. package/src/slotBinding.ts +84 -84
  38. package/src/subscribable.ts +266 -266
  39. package/src/tasks.ts +79 -79
  40. package/src/templateEngine.ts +108 -108
  41. package/src/templateRendering.ts +399 -399
  42. package/src/templateRewriting.ts +94 -94
  43. package/src/templateSources.ts +134 -134
  44. package/src/utils.ts +123 -123
  45. package/src/utilsDom.ts +87 -87
  46. package/src/virtualElements.ts +153 -153
  47. package/src/wireParams.ts +49 -49
@@ -1,397 +1,397 @@
1
- import { Observable, isObservable } from './observable.js';
2
- import { Subscription } from './subscribable.js';
3
- import { compareArrays, findMovesInArrayComparison, type ArrayChange, type CompareArraysOptions } from './compareArrays.js';
4
-
5
- const ARRAY_CHANGE_EVENT = 'arrayChange';
6
-
7
- export class ObservableArray<T> extends Observable<T[]> {
8
- private _trackingChanges = false;
9
- private _cachedDiff: ArrayChange<T>[] | null = null;
10
- private _pendingChanges = 0;
11
- private _previousContents: T[] | undefined;
12
- private _changeSubscription: Subscription<T[]> | null = null;
13
- private _spectateSubscription: Subscription<T[]> | null = null;
14
- compareArrayOptions: CompareArraysOptions = { sparse: true };
15
-
16
- constructor(initialValues?: T[]) {
17
- super(initialValues ?? []);
18
- }
19
-
20
- // --- Array change tracking (lifecycle hooks) ---
21
-
22
- protected override beforeSubscriptionAdd(event: string): void {
23
- if (event === ARRAY_CHANGE_EVENT) {
24
- this._trackChanges();
25
- }
26
- }
27
-
28
- protected override afterSubscriptionRemove(event: string): void {
29
- if (event === ARRAY_CHANGE_EVENT && !this.hasSubscriptionsForEvent(ARRAY_CHANGE_EVENT)) {
30
- this._changeSubscription?.dispose();
31
- this._spectateSubscription?.dispose();
32
- this._changeSubscription = null;
33
- this._spectateSubscription = null;
34
- this._trackingChanges = false;
35
- this._previousContents = undefined;
36
- }
37
- }
38
-
39
- private _trackChanges(): void {
40
- if (this._trackingChanges) {
41
- this._notifyArrayChanges();
42
- return;
43
- }
44
-
45
- this._trackingChanges = true;
46
-
47
- this._spectateSubscription = this.subscribe(() => {
48
- ++this._pendingChanges;
49
- }, 'spectate');
50
-
51
- this._previousContents = ([] as T[]).concat(this.peek() || []);
52
- this._cachedDiff = null;
53
-
54
- this._changeSubscription = this.subscribe(() => this._notifyArrayChanges());
55
- }
56
-
57
- private _notifyArrayChanges(): void {
58
- if (this._pendingChanges) {
59
- const currentContents = ([] as T[]).concat(this.peek() || []);
60
- let changes: ArrayChange<T>[] | undefined;
61
-
62
- if (this.hasSubscriptionsForEvent(ARRAY_CHANGE_EVENT)) {
63
- changes = this._getChanges(this._previousContents!, currentContents);
64
- }
65
-
66
- this._previousContents = currentContents;
67
- this._cachedDiff = null;
68
- this._pendingChanges = 0;
69
-
70
- if (changes && changes.length) {
71
- this.notifySubscribers(changes as unknown as T[], ARRAY_CHANGE_EVENT);
72
- }
73
- }
74
- }
75
-
76
- private _getChanges(previousContents: T[], currentContents: T[]): ArrayChange<T>[] {
77
- if (!this._cachedDiff || this._pendingChanges > 1) {
78
- this._cachedDiff = compareArrays(previousContents, currentContents, this.compareArrayOptions);
79
- }
80
- return this._cachedDiff;
81
- }
82
-
83
- /** @internal */
84
- cacheDiffForKnownOperation(rawArray: T[], operationName: string, args: IArguments | T[]): void {
85
- if (!this._trackingChanges || this._pendingChanges) {
86
- return;
87
- }
88
-
89
- const diff: ArrayChange<T>[] = [];
90
- const arrayLength = rawArray.length;
91
- const argsLength = args.length;
92
- let offset = 0;
93
-
94
- function pushDiff(status: 'added' | 'deleted', value: T, index: number): ArrayChange<T> {
95
- const entry: ArrayChange<T> = { status, value, index };
96
- diff.push(entry);
97
- return entry;
98
- }
99
-
100
- switch (operationName) {
101
- case 'push':
102
- offset = arrayLength;
103
- // falls through
104
- case 'unshift':
105
- for (let index = 0; index < argsLength; index++) {
106
- pushDiff('added', args[index] as T, offset + index);
107
- }
108
- break;
109
-
110
- case 'pop':
111
- offset = arrayLength - 1;
112
- // falls through
113
- case 'shift':
114
- if (arrayLength) {
115
- pushDiff('deleted', rawArray[offset], offset);
116
- }
117
- break;
118
-
119
- case 'splice': {
120
- const startIndex = Math.min(Math.max(0, (args[0] as number) < 0 ? arrayLength + (args[0] as number) : (args[0] as number)), arrayLength);
121
- const endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + ((args[1] as number) || 0), arrayLength);
122
- const endAddIndex = startIndex + argsLength - 2;
123
- const endIndex = Math.max(endDeleteIndex, endAddIndex);
124
- const additions: ArrayChange<T>[] = [];
125
- const deletions: ArrayChange<T>[] = [];
126
- for (let index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
127
- if (index < endDeleteIndex)
128
- deletions.push(pushDiff('deleted', rawArray[index], index));
129
- if (index < endAddIndex)
130
- additions.push(pushDiff('added', args[argsIndex] as T, index));
131
- }
132
- findMovesInArrayComparison(deletions, additions);
133
- break;
134
- }
135
-
136
- default:
137
- return;
138
- }
139
-
140
- this._cachedDiff = diff;
141
- }
142
-
143
- // --- Standard array mutators ---
144
-
145
- push(...items: T[]): number {
146
- const underlyingArray = this.peek();
147
- this.valueWillMutate();
148
- this.cacheDiffForKnownOperation(underlyingArray, 'push', items);
149
- const result = underlyingArray.push(...items);
150
- this.valueHasMutated();
151
- return result;
152
- }
153
-
154
- pop(): T | undefined {
155
- const underlyingArray = this.peek();
156
- this.valueWillMutate();
157
- this.cacheDiffForKnownOperation(underlyingArray, 'pop', [] as T[]);
158
- const result = underlyingArray.pop();
159
- this.valueHasMutated();
160
- return result;
161
- }
162
-
163
- shift(): T | undefined {
164
- const underlyingArray = this.peek();
165
- this.valueWillMutate();
166
- this.cacheDiffForKnownOperation(underlyingArray, 'shift', [] as T[]);
167
- const result = underlyingArray.shift();
168
- this.valueHasMutated();
169
- return result;
170
- }
171
-
172
- unshift(...items: T[]): number {
173
- const underlyingArray = this.peek();
174
- this.valueWillMutate();
175
- this.cacheDiffForKnownOperation(underlyingArray, 'unshift', items);
176
- const result = underlyingArray.unshift(...items);
177
- this.valueHasMutated();
178
- return result;
179
- }
180
-
181
- splice(start: number, deleteCount?: number, ...items: T[]): T[] {
182
- const underlyingArray = this.peek();
183
- this.valueWillMutate();
184
- const spliceArgs: unknown[] = [start];
185
- if (deleteCount !== undefined) spliceArgs.push(deleteCount);
186
- spliceArgs.push(...items);
187
- this.cacheDiffForKnownOperation(underlyingArray, 'splice', spliceArgs as T[]);
188
- const result = underlyingArray.splice(start, deleteCount ?? underlyingArray.length, ...items);
189
- this.valueHasMutated();
190
- return result;
191
- }
192
-
193
- sort(compareFunction?: (a: T, b: T) => number): this {
194
- const underlyingArray = this.peek();
195
- this.valueWillMutate();
196
- underlyingArray.sort(compareFunction);
197
- this.valueHasMutated();
198
- return this;
199
- }
200
-
201
- reverse(): this {
202
- const underlyingArray = this.peek();
203
- this.valueWillMutate();
204
- underlyingArray.reverse();
205
- this.valueHasMutated();
206
- return this;
207
- }
208
-
209
- // --- Custom mutators ---
210
-
211
- remove(valueOrPredicate: T | ((item: T) => boolean)): T[] {
212
- const underlyingArray = this.peek();
213
- const removedValues: T[] = [];
214
- const predicate = typeof valueOrPredicate === 'function' && !isObservable(valueOrPredicate)
215
- ? valueOrPredicate as (item: T) => boolean
216
- : (value: T) => value === valueOrPredicate;
217
-
218
- for (let i = 0; i < underlyingArray.length; i++) {
219
- const value = underlyingArray[i];
220
- if (predicate(value)) {
221
- if (removedValues.length === 0) {
222
- this.valueWillMutate();
223
- }
224
- if (underlyingArray[i] !== value) {
225
- throw new Error('Array modified during remove; cannot remove item');
226
- }
227
- removedValues.push(value);
228
- underlyingArray.splice(i, 1);
229
- i--;
230
- }
231
- }
232
-
233
- if (removedValues.length) {
234
- this.valueHasMutated();
235
- }
236
-
237
- return removedValues;
238
- }
239
-
240
- removeAll(arrayOfValues?: T[]): T[] {
241
- if (arrayOfValues === undefined) {
242
- const underlyingArray = this.peek();
243
- const allValues = underlyingArray.slice(0);
244
- this.valueWillMutate();
245
- underlyingArray.splice(0, underlyingArray.length);
246
- this.valueHasMutated();
247
- return allValues;
248
- }
249
-
250
- if (!arrayOfValues) return [];
251
-
252
- return this.remove((value) => arrayOfValues.indexOf(value) >= 0);
253
- }
254
-
255
- // TODO: destroy/destroyAll may be removed in favor of a mapping library approach.
256
- destroy(valueOrPredicate: T | ((item: T) => boolean)): void {
257
- const underlyingArray = this.peek();
258
- const predicate = typeof valueOrPredicate === 'function' && !isObservable(valueOrPredicate)
259
- ? valueOrPredicate as (item: T) => boolean
260
- : (value: T) => value === valueOrPredicate;
261
-
262
- this.valueWillMutate();
263
- for (let i = underlyingArray.length - 1; i >= 0; i--) {
264
- const value = underlyingArray[i];
265
- if (predicate(value)) {
266
- (value as Record<symbol, boolean>)[DESTROY] = true;
267
- }
268
- }
269
- this.valueHasMutated();
270
- }
271
-
272
- destroyAll(arrayOfValues?: T[]): void {
273
- if (arrayOfValues === undefined) {
274
- this.destroy(() => true);
275
- return;
276
- }
277
- if (!arrayOfValues) return;
278
- this.destroy((value) => arrayOfValues.indexOf(value) >= 0);
279
- }
280
-
281
- replace(oldItem: T, newItem: T): void {
282
- const index = this.indexOf(oldItem);
283
- if (index >= 0) {
284
- this.valueWillMutate();
285
- this.peek()[index] = newItem;
286
- this.valueHasMutated();
287
- }
288
- }
289
-
290
- // --- Readers ---
291
-
292
- get length(): number {
293
- return this.get().length;
294
- }
295
-
296
- indexOf(item: T): number {
297
- return this.get().indexOf(item);
298
- }
299
-
300
- slice(start?: number, end?: number): T[] {
301
- return this.get().slice(start, end);
302
- }
303
-
304
- sorted(compareFunction?: (a: T, b: T) => number): T[] {
305
- const copy = this.get().slice(0);
306
- return compareFunction ? copy.sort(compareFunction) : copy.sort();
307
- }
308
-
309
- reversed(): T[] {
310
- return this.get().slice(0).reverse();
311
- }
312
-
313
- map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: unknown): U[] {
314
- return this.get().map(callbackfn, thisArg);
315
- }
316
-
317
- filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): T[] {
318
- return this.get().filter(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
319
- }
320
-
321
- find(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): T | undefined {
322
- return this.get().find(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
323
- }
324
-
325
- findIndex(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): number {
326
- return this.get().findIndex(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
327
- }
328
-
329
- some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): boolean {
330
- return this.get().some(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
331
- }
332
-
333
- every(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): boolean {
334
- return this.get().every(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
335
- }
336
-
337
- forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: unknown): void {
338
- this.get().forEach(callbackfn, thisArg);
339
- }
340
-
341
- reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
342
- reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T;
343
- reduce<U>(callbackfn: (previousValue: U | T, currentValue: T, currentIndex: number, array: T[]) => U | T, ...args: unknown[]): U | T {
344
- const arr = this.get();
345
- if (args.length > 0) {
346
- return arr.reduce(callbackfn as (pv: U | T, cv: T, ci: number, a: T[]) => U | T, args[0] as U | T);
347
- }
348
- return arr.reduce(callbackfn as (pv: T, cv: T, ci: number, a: T[]) => T);
349
- }
350
-
351
- includes(searchElement: T, fromIndex?: number): boolean {
352
- return this.get().includes(searchElement, fromIndex);
353
- }
354
-
355
- at(index: number): T | undefined {
356
- return this.get().at(index);
357
- }
358
-
359
- join(separator?: string): string {
360
- return this.get().join(separator);
361
- }
362
-
363
- flat<D extends number = 1>(depth?: D): FlatArray<T[], D>[] {
364
- return this.get().flat(depth) as FlatArray<T[], D>[];
365
- }
366
-
367
- flatMap<U>(callback: (value: T, index: number, array: T[]) => U | readonly U[], thisArg?: unknown): U[] {
368
- return this.get().flatMap(callback, thisArg);
369
- }
370
-
371
- entries(): ArrayIterator<[number, T]> {
372
- return this.get().entries();
373
- }
374
-
375
- keys(): ArrayIterator<number> {
376
- return this.get().keys();
377
- }
378
-
379
- values(): ArrayIterator<T> {
380
- return this.get().values();
381
- }
382
-
383
- [Symbol.iterator](): ArrayIterator<T> {
384
- return this.get()[Symbol.iterator]();
385
- }
386
- }
387
-
388
- export function isObservableArray(value: unknown): value is ObservableArray<unknown> {
389
- return value instanceof ObservableArray;
390
- }
391
-
392
- // TODO: DESTROY/isDestroyed may be removed in favor of a mapping library approach.
393
- export const DESTROY: unique symbol = Symbol('destroy');
394
-
395
- export function isDestroyed(item: unknown): boolean {
396
- return item != null && (item as Record<symbol, unknown>)[DESTROY] === true;
397
- }
1
+ import { Observable, isObservable } from './observable.js';
2
+ import { Subscription } from './subscribable.js';
3
+ import { compareArrays, findMovesInArrayComparison, type ArrayChange, type CompareArraysOptions } from './compareArrays.js';
4
+
5
+ const ARRAY_CHANGE_EVENT = 'arrayChange';
6
+
7
+ export class ObservableArray<T> extends Observable<T[]> {
8
+ private _trackingChanges = false;
9
+ private _cachedDiff: ArrayChange<T>[] | null = null;
10
+ private _pendingChanges = 0;
11
+ private _previousContents: T[] | undefined;
12
+ private _changeSubscription: Subscription<T[]> | null = null;
13
+ private _spectateSubscription: Subscription<T[]> | null = null;
14
+ compareArrayOptions: CompareArraysOptions = { sparse: true };
15
+
16
+ constructor(initialValues?: T[]) {
17
+ super(initialValues ?? []);
18
+ }
19
+
20
+ // --- Array change tracking (lifecycle hooks) ---
21
+
22
+ protected override beforeSubscriptionAdd(event: string): void {
23
+ if (event === ARRAY_CHANGE_EVENT) {
24
+ this._trackChanges();
25
+ }
26
+ }
27
+
28
+ protected override afterSubscriptionRemove(event: string): void {
29
+ if (event === ARRAY_CHANGE_EVENT && !this.hasSubscriptionsForEvent(ARRAY_CHANGE_EVENT)) {
30
+ this._changeSubscription?.dispose();
31
+ this._spectateSubscription?.dispose();
32
+ this._changeSubscription = null;
33
+ this._spectateSubscription = null;
34
+ this._trackingChanges = false;
35
+ this._previousContents = undefined;
36
+ }
37
+ }
38
+
39
+ private _trackChanges(): void {
40
+ if (this._trackingChanges) {
41
+ this._notifyArrayChanges();
42
+ return;
43
+ }
44
+
45
+ this._trackingChanges = true;
46
+
47
+ this._spectateSubscription = this.subscribe(() => {
48
+ ++this._pendingChanges;
49
+ }, 'spectate');
50
+
51
+ this._previousContents = ([] as T[]).concat(this.peek() || []);
52
+ this._cachedDiff = null;
53
+
54
+ this._changeSubscription = this.subscribe(() => this._notifyArrayChanges());
55
+ }
56
+
57
+ private _notifyArrayChanges(): void {
58
+ if (this._pendingChanges) {
59
+ const currentContents = ([] as T[]).concat(this.peek() || []);
60
+ let changes: ArrayChange<T>[] | undefined;
61
+
62
+ if (this.hasSubscriptionsForEvent(ARRAY_CHANGE_EVENT)) {
63
+ changes = this._getChanges(this._previousContents!, currentContents);
64
+ }
65
+
66
+ this._previousContents = currentContents;
67
+ this._cachedDiff = null;
68
+ this._pendingChanges = 0;
69
+
70
+ if (changes && changes.length) {
71
+ this.notifySubscribers(changes as unknown as T[], ARRAY_CHANGE_EVENT);
72
+ }
73
+ }
74
+ }
75
+
76
+ private _getChanges(previousContents: T[], currentContents: T[]): ArrayChange<T>[] {
77
+ if (!this._cachedDiff || this._pendingChanges > 1) {
78
+ this._cachedDiff = compareArrays(previousContents, currentContents, this.compareArrayOptions);
79
+ }
80
+ return this._cachedDiff;
81
+ }
82
+
83
+ /** @internal */
84
+ cacheDiffForKnownOperation(rawArray: T[], operationName: string, args: IArguments | T[]): void {
85
+ if (!this._trackingChanges || this._pendingChanges) {
86
+ return;
87
+ }
88
+
89
+ const diff: ArrayChange<T>[] = [];
90
+ const arrayLength = rawArray.length;
91
+ const argsLength = args.length;
92
+ let offset = 0;
93
+
94
+ function pushDiff(status: 'added' | 'deleted', value: T, index: number): ArrayChange<T> {
95
+ const entry: ArrayChange<T> = { status, value, index };
96
+ diff.push(entry);
97
+ return entry;
98
+ }
99
+
100
+ switch (operationName) {
101
+ case 'push':
102
+ offset = arrayLength;
103
+ // falls through
104
+ case 'unshift':
105
+ for (let index = 0; index < argsLength; index++) {
106
+ pushDiff('added', args[index] as T, offset + index);
107
+ }
108
+ break;
109
+
110
+ case 'pop':
111
+ offset = arrayLength - 1;
112
+ // falls through
113
+ case 'shift':
114
+ if (arrayLength) {
115
+ pushDiff('deleted', rawArray[offset], offset);
116
+ }
117
+ break;
118
+
119
+ case 'splice': {
120
+ const startIndex = Math.min(Math.max(0, (args[0] as number) < 0 ? arrayLength + (args[0] as number) : (args[0] as number)), arrayLength);
121
+ const endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + ((args[1] as number) || 0), arrayLength);
122
+ const endAddIndex = startIndex + argsLength - 2;
123
+ const endIndex = Math.max(endDeleteIndex, endAddIndex);
124
+ const additions: ArrayChange<T>[] = [];
125
+ const deletions: ArrayChange<T>[] = [];
126
+ for (let index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
127
+ if (index < endDeleteIndex)
128
+ deletions.push(pushDiff('deleted', rawArray[index], index));
129
+ if (index < endAddIndex)
130
+ additions.push(pushDiff('added', args[argsIndex] as T, index));
131
+ }
132
+ findMovesInArrayComparison(deletions, additions);
133
+ break;
134
+ }
135
+
136
+ default:
137
+ return;
138
+ }
139
+
140
+ this._cachedDiff = diff;
141
+ }
142
+
143
+ // --- Standard array mutators ---
144
+
145
+ push(...items: T[]): number {
146
+ const underlyingArray = this.peek();
147
+ this.valueWillMutate();
148
+ this.cacheDiffForKnownOperation(underlyingArray, 'push', items);
149
+ const result = underlyingArray.push(...items);
150
+ this.valueHasMutated();
151
+ return result;
152
+ }
153
+
154
+ pop(): T | undefined {
155
+ const underlyingArray = this.peek();
156
+ this.valueWillMutate();
157
+ this.cacheDiffForKnownOperation(underlyingArray, 'pop', [] as T[]);
158
+ const result = underlyingArray.pop();
159
+ this.valueHasMutated();
160
+ return result;
161
+ }
162
+
163
+ shift(): T | undefined {
164
+ const underlyingArray = this.peek();
165
+ this.valueWillMutate();
166
+ this.cacheDiffForKnownOperation(underlyingArray, 'shift', [] as T[]);
167
+ const result = underlyingArray.shift();
168
+ this.valueHasMutated();
169
+ return result;
170
+ }
171
+
172
+ unshift(...items: T[]): number {
173
+ const underlyingArray = this.peek();
174
+ this.valueWillMutate();
175
+ this.cacheDiffForKnownOperation(underlyingArray, 'unshift', items);
176
+ const result = underlyingArray.unshift(...items);
177
+ this.valueHasMutated();
178
+ return result;
179
+ }
180
+
181
+ splice(start: number, deleteCount?: number, ...items: T[]): T[] {
182
+ const underlyingArray = this.peek();
183
+ this.valueWillMutate();
184
+ const spliceArgs: unknown[] = [start];
185
+ if (deleteCount !== undefined) spliceArgs.push(deleteCount);
186
+ spliceArgs.push(...items);
187
+ this.cacheDiffForKnownOperation(underlyingArray, 'splice', spliceArgs as T[]);
188
+ const result = underlyingArray.splice(start, deleteCount ?? underlyingArray.length, ...items);
189
+ this.valueHasMutated();
190
+ return result;
191
+ }
192
+
193
+ sort(compareFunction?: (a: T, b: T) => number): this {
194
+ const underlyingArray = this.peek();
195
+ this.valueWillMutate();
196
+ underlyingArray.sort(compareFunction);
197
+ this.valueHasMutated();
198
+ return this;
199
+ }
200
+
201
+ reverse(): this {
202
+ const underlyingArray = this.peek();
203
+ this.valueWillMutate();
204
+ underlyingArray.reverse();
205
+ this.valueHasMutated();
206
+ return this;
207
+ }
208
+
209
+ // --- Custom mutators ---
210
+
211
+ remove(valueOrPredicate: T | ((item: T) => boolean)): T[] {
212
+ const underlyingArray = this.peek();
213
+ const removedValues: T[] = [];
214
+ const predicate = typeof valueOrPredicate === 'function' && !isObservable(valueOrPredicate)
215
+ ? valueOrPredicate as (item: T) => boolean
216
+ : (value: T) => value === valueOrPredicate;
217
+
218
+ for (let i = 0; i < underlyingArray.length; i++) {
219
+ const value = underlyingArray[i];
220
+ if (predicate(value)) {
221
+ if (removedValues.length === 0) {
222
+ this.valueWillMutate();
223
+ }
224
+ if (underlyingArray[i] !== value) {
225
+ throw new Error('Array modified during remove; cannot remove item');
226
+ }
227
+ removedValues.push(value);
228
+ underlyingArray.splice(i, 1);
229
+ i--;
230
+ }
231
+ }
232
+
233
+ if (removedValues.length) {
234
+ this.valueHasMutated();
235
+ }
236
+
237
+ return removedValues;
238
+ }
239
+
240
+ removeAll(arrayOfValues?: T[]): T[] {
241
+ if (arrayOfValues === undefined) {
242
+ const underlyingArray = this.peek();
243
+ const allValues = underlyingArray.slice(0);
244
+ this.valueWillMutate();
245
+ underlyingArray.splice(0, underlyingArray.length);
246
+ this.valueHasMutated();
247
+ return allValues;
248
+ }
249
+
250
+ if (!arrayOfValues) return [];
251
+
252
+ return this.remove((value) => arrayOfValues.indexOf(value) >= 0);
253
+ }
254
+
255
+ // TODO: destroy/destroyAll may be removed in favor of a mapping library approach.
256
+ destroy(valueOrPredicate: T | ((item: T) => boolean)): void {
257
+ const underlyingArray = this.peek();
258
+ const predicate = typeof valueOrPredicate === 'function' && !isObservable(valueOrPredicate)
259
+ ? valueOrPredicate as (item: T) => boolean
260
+ : (value: T) => value === valueOrPredicate;
261
+
262
+ this.valueWillMutate();
263
+ for (let i = underlyingArray.length - 1; i >= 0; i--) {
264
+ const value = underlyingArray[i];
265
+ if (predicate(value)) {
266
+ (value as Record<symbol, boolean>)[DESTROY] = true;
267
+ }
268
+ }
269
+ this.valueHasMutated();
270
+ }
271
+
272
+ destroyAll(arrayOfValues?: T[]): void {
273
+ if (arrayOfValues === undefined) {
274
+ this.destroy(() => true);
275
+ return;
276
+ }
277
+ if (!arrayOfValues) return;
278
+ this.destroy((value) => arrayOfValues.indexOf(value) >= 0);
279
+ }
280
+
281
+ replace(oldItem: T, newItem: T): void {
282
+ const index = this.indexOf(oldItem);
283
+ if (index >= 0) {
284
+ this.valueWillMutate();
285
+ this.peek()[index] = newItem;
286
+ this.valueHasMutated();
287
+ }
288
+ }
289
+
290
+ // --- Readers ---
291
+
292
+ get length(): number {
293
+ return this.get().length;
294
+ }
295
+
296
+ indexOf(item: T): number {
297
+ return this.get().indexOf(item);
298
+ }
299
+
300
+ slice(start?: number, end?: number): T[] {
301
+ return this.get().slice(start, end);
302
+ }
303
+
304
+ sorted(compareFunction?: (a: T, b: T) => number): T[] {
305
+ const copy = this.get().slice(0);
306
+ return compareFunction ? copy.sort(compareFunction) : copy.sort();
307
+ }
308
+
309
+ reversed(): T[] {
310
+ return this.get().slice(0).reverse();
311
+ }
312
+
313
+ map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: unknown): U[] {
314
+ return this.get().map(callbackfn, thisArg);
315
+ }
316
+
317
+ filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): T[] {
318
+ return this.get().filter(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
319
+ }
320
+
321
+ find(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): T | undefined {
322
+ return this.get().find(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
323
+ }
324
+
325
+ findIndex(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): number {
326
+ return this.get().findIndex(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
327
+ }
328
+
329
+ some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): boolean {
330
+ return this.get().some(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
331
+ }
332
+
333
+ every(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: unknown): boolean {
334
+ return this.get().every(predicate as (value: T, index: number, array: T[]) => boolean, thisArg);
335
+ }
336
+
337
+ forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: unknown): void {
338
+ this.get().forEach(callbackfn, thisArg);
339
+ }
340
+
341
+ reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
342
+ reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T;
343
+ reduce<U>(callbackfn: (previousValue: U | T, currentValue: T, currentIndex: number, array: T[]) => U | T, ...args: unknown[]): U | T {
344
+ const arr = this.get();
345
+ if (args.length > 0) {
346
+ return arr.reduce(callbackfn as (pv: U | T, cv: T, ci: number, a: T[]) => U | T, args[0] as U | T);
347
+ }
348
+ return arr.reduce(callbackfn as (pv: T, cv: T, ci: number, a: T[]) => T);
349
+ }
350
+
351
+ includes(searchElement: T, fromIndex?: number): boolean {
352
+ return this.get().includes(searchElement, fromIndex);
353
+ }
354
+
355
+ at(index: number): T | undefined {
356
+ return this.get().at(index);
357
+ }
358
+
359
+ join(separator?: string): string {
360
+ return this.get().join(separator);
361
+ }
362
+
363
+ flat<D extends number = 1>(depth?: D): FlatArray<T[], D>[] {
364
+ return this.get().flat(depth) as FlatArray<T[], D>[];
365
+ }
366
+
367
+ flatMap<U>(callback: (value: T, index: number, array: T[]) => U | readonly U[], thisArg?: unknown): U[] {
368
+ return this.get().flatMap(callback, thisArg);
369
+ }
370
+
371
+ entries(): ArrayIterator<[number, T]> {
372
+ return this.get().entries();
373
+ }
374
+
375
+ keys(): ArrayIterator<number> {
376
+ return this.get().keys();
377
+ }
378
+
379
+ values(): ArrayIterator<T> {
380
+ return this.get().values();
381
+ }
382
+
383
+ [Symbol.iterator](): ArrayIterator<T> {
384
+ return this.get()[Symbol.iterator]();
385
+ }
386
+ }
387
+
388
+ export function isObservableArray(value: unknown): value is ObservableArray<unknown> {
389
+ return value instanceof ObservableArray;
390
+ }
391
+
392
+ // TODO: DESTROY/isDestroyed may be removed in favor of a mapping library approach.
393
+ export const DESTROY: unique symbol = Symbol('destroy');
394
+
395
+ export function isDestroyed(item: unknown): boolean {
396
+ return item != null && (item as Record<symbol, unknown>)[DESTROY] === true;
397
+ }