150 lines
5.6 KiB
JavaScript
150 lines
5.6 KiB
JavaScript
import { createStorage } from './-private/util.ts.js';
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
// Unfortunately, TypeScript's ability to do inference *or* type-checking in a
|
|
// `Proxy`'s body is very limited, so we have to use a number of casts `as any`
|
|
// to make the internal accesses work. The type safety of these is guaranteed at
|
|
// the *call site* instead of within the body: you cannot do `Array.blah` in TS,
|
|
// and it will blow up in JS in exactly the same way, so it is safe to assume
|
|
// that properties within the getter have the correct type in TS.
|
|
|
|
const ARRAY_GETTER_METHODS = new Set([Symbol.iterator, "concat", "entries", "every", "filter", "find", "findIndex", "flat", "flatMap", "forEach", "includes", "indexOf", "join", "keys", "lastIndexOf", "map", "reduce", "reduceRight", "slice", "some", "values"]);
|
|
|
|
// For these methods, `Array` itself immediately gets the `.length` to return
|
|
// after invoking them.
|
|
const ARRAY_WRITE_THEN_READ_METHODS = new Set(["fill", "push", "unshift"]);
|
|
function convertToInt(prop) {
|
|
if (typeof prop === "symbol") return null;
|
|
const num = Number(prop);
|
|
if (isNaN(num)) return null;
|
|
return num % 1 === 0 ? num : null;
|
|
}
|
|
|
|
// This rule is correct in the general case, but it doesn't understand
|
|
// declaration merging, which is how we're using the interface here. This says
|
|
// `SignalArray` acts just like `Array<T>`, but also has the properties
|
|
// declared via the `class` declaration above -- but without the cost of a
|
|
// subclass, which is much slower than the proxied array behavior. That is: a
|
|
// `SignalArray` *is* an `Array`, just with a proxy in front of accessors and
|
|
// setters, rather than a subclass of an `Array` which would be de-optimized by
|
|
// the browsers.
|
|
//
|
|
|
|
class SignalArray {
|
|
/**
|
|
* Creates an array from an iterable object.
|
|
* @param iterable An iterable object to convert to an array.
|
|
*/
|
|
|
|
/**
|
|
* Creates an array from an iterable object.
|
|
* @param iterable An iterable object to convert to an array.
|
|
* @param mapfn A mapping function to call on every element of the array.
|
|
* @param thisArg Value of 'this' used to invoke the mapfn.
|
|
*/
|
|
|
|
static from(iterable, mapfn, thisArg) {
|
|
return mapfn ? new SignalArray(Array.from(iterable, mapfn, thisArg)) : new SignalArray(Array.from(iterable));
|
|
}
|
|
static of(...arr) {
|
|
return new SignalArray(arr);
|
|
}
|
|
constructor(arr = []) {
|
|
let clone = arr.slice();
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
let self = this;
|
|
let boundFns = new Map();
|
|
|
|
/**
|
|
Flag to track whether we have *just* intercepted a call to `.push()` or
|
|
`.unshift()`, since in those cases (and only those cases!) the `Array`
|
|
itself checks `.length` to return from the function call.
|
|
*/
|
|
let nativelyAccessingLengthFromPushOrUnshift = false;
|
|
return new Proxy(clone, {
|
|
get(target, prop /*, _receiver */) {
|
|
let index = convertToInt(prop);
|
|
if (index !== null) {
|
|
self.#readStorageFor(index);
|
|
self.#collection.get();
|
|
return target[index];
|
|
}
|
|
if (prop === "length") {
|
|
// If we are reading `.length`, it may be a normal user-triggered
|
|
// read, or it may be a read triggered by Array itself. In the latter
|
|
// case, it is because we have just done `.push()` or `.unshift()`; in
|
|
// that case it is safe not to mark this as a *read* operation, since
|
|
// calling `.push()` or `.unshift()` cannot otherwise be part of a
|
|
// "read" operation safely, and if done during an *existing* read
|
|
// (e.g. if the user has already checked `.length` *prior* to this),
|
|
// that will still trigger the mutation-after-consumption assertion.
|
|
if (nativelyAccessingLengthFromPushOrUnshift) {
|
|
nativelyAccessingLengthFromPushOrUnshift = false;
|
|
} else {
|
|
self.#collection.get();
|
|
}
|
|
return target[prop];
|
|
}
|
|
|
|
// Here, track that we are doing a `.push()` or `.unshift()` by setting
|
|
// the flag to `true` so that when the `.length` is read by `Array` (see
|
|
// immediately above), it knows not to dirty the collection.
|
|
if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) {
|
|
nativelyAccessingLengthFromPushOrUnshift = true;
|
|
}
|
|
if (ARRAY_GETTER_METHODS.has(prop)) {
|
|
let fn = boundFns.get(prop);
|
|
if (fn === undefined) {
|
|
fn = (...args) => {
|
|
self.#collection.get();
|
|
return target[prop](...args);
|
|
};
|
|
boundFns.set(prop, fn);
|
|
}
|
|
return fn;
|
|
}
|
|
return target[prop];
|
|
},
|
|
set(target, prop, value /*, _receiver */) {
|
|
target[prop] = value;
|
|
let index = convertToInt(prop);
|
|
if (index !== null) {
|
|
self.#dirtyStorageFor(index);
|
|
self.#collection.set(null);
|
|
} else if (prop === "length") {
|
|
self.#collection.set(null);
|
|
}
|
|
return true;
|
|
},
|
|
getPrototypeOf() {
|
|
return SignalArray.prototype;
|
|
}
|
|
});
|
|
}
|
|
#collection = createStorage();
|
|
#storages = new Map();
|
|
#readStorageFor(index) {
|
|
let storage = this.#storages.get(index);
|
|
if (storage === undefined) {
|
|
storage = createStorage();
|
|
this.#storages.set(index, storage);
|
|
}
|
|
storage.get();
|
|
}
|
|
#dirtyStorageFor(index) {
|
|
const storage = this.#storages.get(index);
|
|
if (storage) {
|
|
storage.set(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure instanceof works correctly
|
|
Object.setPrototypeOf(SignalArray.prototype, Array.prototype);
|
|
function signalArray(x) {
|
|
return new SignalArray(x);
|
|
}
|
|
|
|
export { SignalArray, signalArray };
|
|
//# sourceMappingURL=array.ts.js.map
|