lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
262 lines (238 loc) • 7.04 kB
text/typescript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const TOMBSTONE = null;
const GEN_MAP_SIZE_THRESHOLD = 1000;
/**
* @internal
*
* Create a copy of the given Map, returning either a fresh Map or a clone
* of a copy-on-write GenMap depending on the source type and size.
*
* - If the source is already a GenMap, returns `map.clone()` (O(1)).
* - If the source is a plain Map below the threshold, returns
* `new Map(map)` to avoid the GenMap overhead on small docs.
* - Otherwise wraps a fresh GenMap around the source.
*/
export function cloneMap<K, V>(
map: Map<K, V>,
minGenMapSize: number = GEN_MAP_SIZE_THRESHOLD,
): Map<K, V> {
if (map instanceof GenMap) {
return map.clone();
}
if (map.size < minGenMapSize) {
return new Map(map);
}
return new GenMap<K, V>().init(new Map(map), undefined, map.size);
}
/**
* @internal
*
* A copy-on-write Map suitable for cloning large collections cheaply.
*
* Before being written to, a GenMap shares its `_old` and `_nursery` Maps
* with the GenMap it was cloned from. On first write it either compacts
* (folds `_nursery` into a new `_old`) or shallow-copies `_nursery`,
* isolating subsequent writes from sibling clones.
*
* `_old` is the immutable snapshot from the most recent compaction;
* `_nursery` holds writes since the last compaction (deletions stored as
* `TOMBSTONE`). `_mutable` tracks whether `_nursery` may be written to
* directly or must first be cloned.
*
* Implements the full `Map<K, V>` interface; methods not documented
* individually behave as their native `Map` counterparts.
*/
export class GenMap<K, V> implements Map<K, V> {
_mutable: boolean = false;
_old: undefined | ReadonlyMap<K, V> = undefined;
_nursery: undefined | Map<K, typeof TOMBSTONE | V> = undefined;
_size: number = 0;
/**
* Returns a new GenMap that initially shares `_old` and `_nursery`
* with this one. Marks both as not-mutable so the next write on either
* side triggers a copy-on-write of the nursery before mutating.
*/
clone(): GenMap<K, V> {
this._mutable = false;
return new GenMap<K, V>().init(this._old, this._nursery, this._size);
}
init(
old: undefined | ReadonlyMap<K, V>,
nursery: undefined | Map<K, typeof TOMBSTONE | V>,
size: number,
): this {
this._old = old;
this._nursery = nursery;
this._size = size;
return this;
}
get size(): number {
return this._size;
}
has(key: K): boolean {
return this.get(key) !== undefined;
}
/**
* Returns the raw value for `key`, including TOMBSTONE for keys deleted
* since the last compaction. Used internally to distinguish "missing"
* from "deleted" without doing a second lookup.
*/
getWithTombstone(key: K): undefined | typeof TOMBSTONE | V {
const v = this._nursery && this._nursery.get(key);
if (v !== undefined) {
return v;
}
return this._old && this._old.get(key);
}
get(key: K): undefined | V {
const v = this.getWithTombstone(key);
return v === TOMBSTONE ? undefined : v;
}
shouldCompact(): boolean {
return this._nursery !== undefined && this._nursery.size * 2 > this._size;
}
/**
* Returns the nursery for in-place writes. If this GenMap is currently
* sharing its nursery with an ancestor clone, this either compacts (if
* the nursery has grown large enough) or makes a shallow copy.
*/
getNursery(): Map<K, typeof TOMBSTONE | V> {
if (!this._mutable || !this._nursery) {
this.compact();
this._nursery = new Map(this._nursery);
this._mutable = true;
}
return this._nursery;
}
/**
* Fold the nursery into a new `_old` snapshot when it has grown large
* enough that lookup overhead outweighs the savings from sharing.
* Triggered automatically from `getNursery` once `_nursery.size * 2 >
* _size`; can be forced via `compact(true)`.
*/
compact(force: boolean = false): this {
if (
this._nursery &&
this._nursery.size > 0 &&
(force || this.shouldCompact())
) {
const compact = new Map(this._old);
for (const [k, v] of this._nursery) {
if (v !== TOMBSTONE) {
compact.set(k, v as V);
} else {
compact.delete(k);
}
}
this._old = compact;
this._nursery = undefined;
}
this._mutable = false;
return this;
}
set(key: K, value: V): this {
const v = this.getWithTombstone(key);
if (v === value) {
return this;
}
const nursery = this.getNursery();
if (v === TOMBSTONE || v === undefined) {
this._size++;
if (v === TOMBSTONE) {
// Match native Map semantics where `delete(k); set(k, v)`
// re-inserts the key at the end of iteration order.
nursery.delete(key);
}
}
nursery.set(key, value);
return this;
}
delete(key: K): boolean {
const deleted = this.has(key);
if (deleted) {
this.getNursery().set(key, TOMBSTONE);
this._size--;
}
return deleted;
}
getOrInsert(key: K, defaultValue: V): V {
const existing = this.get(key);
if (existing !== undefined) {
return existing;
}
this.set(key, defaultValue);
return defaultValue;
}
getOrInsertComputed(key: K, computer: (k: K) => V): V {
const existing = this.get(key);
if (existing !== undefined) {
return existing;
}
const value = computer(key);
this.set(key, value);
return value;
}
clear(): void {
this._mutable = false;
this._old = undefined;
this._nursery = undefined;
this._size = 0;
}
*keys(): MapIterator<K> {
for (const pair of this.entries()) {
yield pair[0];
}
}
*values(): MapIterator<V> {
for (const pair of this.entries()) {
yield pair[1];
}
}
*entries(): MapIterator<[K, V]> {
const nursery = this._nursery;
const old = this._old;
if (old) {
for (const pair of old) {
const k = pair[0];
const v = nursery ? nursery.get(k) : undefined;
if (v === TOMBSTONE) {
continue;
} else if (v !== undefined) {
(pair as [K, V])[1] = v as V;
}
yield pair as [K, V];
}
}
if (nursery) {
for (const pair of nursery) {
if (pair[1] !== TOMBSTONE && !(old && old.has(pair[0]))) {
yield pair as [K, V];
}
}
}
}
forEach(
callbackfn: (value: V, key: K, map: Map<K, V>) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
thisArg?: any,
): void {
if (thisArg !== undefined) {
callbackfn = callbackfn.bind(thisArg);
}
for (const [k, v] of this.entries()) {
callbackfn(v, k, this);
}
}
get [Symbol.toStringTag](): string {
return 'GenMap';
}
[Symbol.iterator](): MapIterator<[K, V]> {
return this.entries();
}
}