@tldraw/store
Version: 
tldraw infinite canvas SDK (store).
177 lines (152 loc) • 4.32 kB
text/typescript
import { atom, Atom, transact, UNINITIALIZED } from '@tldraw/state'
import { assert } from '@tldraw/utils'
import { emptyMap, ImmutableMap } from './ImmutableMap'
/**
 * A drop-in replacement for Map that stores values in atoms and can be used in reactive contexts.
 * @public
 */
export class AtomMap<K, V> implements Map<K, V> {
	private atoms: Atom<ImmutableMap<K, Atom<V | UNINITIALIZED>>>
	constructor(
		private readonly name: string,
		entries?: Iterable<readonly [K, V]>
	) {
		let atoms = emptyMap<K, Atom<V>>()
		if (entries) {
			atoms = atoms.withMutations((atoms) => {
				for (const [k, v] of entries) {
					atoms.set(k, atom(`${name}:${String(k)}`, v))
				}
			})
		}
		this.atoms = atom(`${name}:atoms`, atoms)
	}
	/** @internal */
	getAtom(key: K): Atom<V | UNINITIALIZED> | undefined {
		const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
		if (!valueAtom) {
			// if the value is missing, we want to track whether it's in the present keys set
			this.atoms.get()
			return undefined
		}
		return valueAtom
	}
	get(key: K): V | undefined {
		const value = this.getAtom(key)?.get()
		assert(value !== UNINITIALIZED)
		return value
	}
	__unsafe__getWithoutCapture(key: K): V | undefined {
		const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
		if (!valueAtom) return undefined
		const value = valueAtom.__unsafe__getWithoutCapture()
		assert(value !== UNINITIALIZED)
		return value
	}
	has(key: K): boolean {
		const valueAtom = this.getAtom(key)
		if (!valueAtom) {
			return false
		}
		return valueAtom.get() !== UNINITIALIZED
	}
	__unsafe__hasWithoutCapture(key: K): boolean {
		const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
		if (!valueAtom) return false
		assert(valueAtom.__unsafe__getWithoutCapture() !== UNINITIALIZED)
		return true
	}
	set(key: K, value: V) {
		const existingAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
		if (existingAtom) {
			existingAtom.set(value)
		} else {
			this.atoms.update((atoms) => {
				return atoms.set(key, atom(`${this.name}:${String(key)}`, value))
			})
		}
		return this
	}
	update(key: K, updater: (value: V) => V) {
		const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
		if (!valueAtom) {
			throw new Error(`AtomMap: key ${key} not found`)
		}
		const value = valueAtom.__unsafe__getWithoutCapture()
		assert(value !== UNINITIALIZED)
		valueAtom.set(updater(value))
	}
	delete(key: K) {
		const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
		if (!valueAtom) {
			return false
		}
		transact(() => {
			valueAtom.set(UNINITIALIZED)
			this.atoms.update((atoms) => {
				return atoms.delete(key)
			})
		})
		return true
	}
	deleteMany(keys: Iterable<K>): [K, V][] {
		return transact(() => {
			const deleted: [K, V][] = []
			const newAtoms = this.atoms.get().withMutations((atoms) => {
				for (const key of keys) {
					const valueAtom = atoms.get(key)
					if (!valueAtom) continue
					const oldValue = valueAtom.get()
					assert(oldValue !== UNINITIALIZED)
					deleted.push([key, oldValue])
					atoms.delete(key)
					valueAtom.set(UNINITIALIZED)
				}
			})
			if (deleted.length) {
				this.atoms.set(newAtoms)
			}
			return deleted
		})
	}
	clear() {
		return transact(() => {
			for (const valueAtom of this.atoms.__unsafe__getWithoutCapture().values()) {
				valueAtom.set(UNINITIALIZED)
			}
			this.atoms.set(emptyMap())
		})
	}
	*entries(): Generator<[K, V], undefined, unknown> {
		for (const [key, valueAtom] of this.atoms.get()) {
			const value = valueAtom.get()
			assert(value !== UNINITIALIZED)
			yield [key, value]
		}
	}
	*keys(): Generator<K, undefined, unknown> {
		for (const key of this.atoms.get().keys()) {
			yield key
		}
	}
	*values(): Generator<V, undefined, unknown> {
		for (const valueAtom of this.atoms.get().values()) {
			const value = valueAtom.get()
			assert(value !== UNINITIALIZED)
			yield value
		}
	}
	// eslint-disable-next-line no-restricted-syntax
	get size() {
		return this.atoms.get().size
	}
	forEach(callbackfn: (value: V, key: K, map: AtomMap<K, V>) => void, thisArg?: any): void {
		for (const [key, value] of this.entries()) {
			callbackfn.call(thisArg, value, key, this)
		}
	}
	[Symbol.iterator]() {
		return this.entries()
	}
	[Symbol.toStringTag] = 'AtomMap'
}