From 6e2dc746d2bb60fce26d21c0b2d245f44cfe0074 Mon Sep 17 00:00:00 2001 From: hanako-eo Date: Tue, 5 Mar 2024 21:55:50 +0100 Subject: [PATCH] Start refactor of reactor with Node --- package.json | 2 +- src/reactor.ts | 144 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/reactor.ts diff --git a/package.json b/package.json index effab89..b2b535d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "coverage": "vitest --coverage", "dev": "tsc --watch", "build": "tsc", - "prepack": "npm run build" + "prepack": "rm -rf lib && npm run build" }, "repository": { "type": "git", diff --git a/src/reactor.ts b/src/reactor.ts new file mode 100644 index 0000000..c845cf6 --- /dev/null +++ b/src/reactor.ts @@ -0,0 +1,144 @@ +import { Reactive, Reactor, ReadOnlyReactor } from "../types/app" +import { isDefined, recordReactor } from "./helpers" + +type SubscribtionCallback = (previous: T, current: T) => void +type ComputeCallback = (value: T) => U +type Subscribtion = SubscribtionCallback | [SubscribtionCallback, string | symbol] + +const SUBSCRIBTIONS_SYMBOL = Symbol("subscribtions") +const PROPERTIES_SYMBOL = Symbol("properties") +const REACTABLE_SYMBOL = Symbol("reactable") +const NOOP = () => { } + +export function reactor(value: T, freeze: boolean, once: boolean): Reactive { + const node = new Node(value) + + return reactorFromNode(node, freeze, once) +} + +export function reactorFromNode(node: Node, freeze: boolean, once: boolean): Reactive { + const proxy = new Proxy(NOOP, { + apply(_, __, argArray: [ComputeCallback | T] | []) { + if (argArray.length === 0) { + recordReactor.push(proxy) + return node.get() + } + + if (freeze) throw new TypeError("Cannot assign to read only reactor") + if (once) freeze = true + + return node.set(argArray[0]) + }, + get(_: typeof NOOP, p: string | symbol) { + // if (p in properties) return properties[p] + if (p in node && p !== "_value") return node[p] + + const reactor = native.call, [K], Reactive | undefined>(node, p as K) + if (!isDefined(reactor)) return reactor + + return reactor + }, + set(_: typeof NOOP, p: string | symbol, newValue: T[K]) { + const descriptor = Object.getOwnPropertyDescriptor(node.get(), p) + const freezed = freeze || typeof node.get() !== "object" || !(descriptor?.writable ?? descriptor?.set) + if (freezed) throw new TypeError("Cannot assign to read only reactor") + + const reactor = native.call, [K], Reactive | undefined>(node, p as K) + if (!isDefined(reactor)) return false + + return true + }, + }) as Reactive + + return proxy +} + +class Node { + [x: PropertyKey]: any + + [REACTABLE_SYMBOL] = true; + [PROPERTIES_SYMBOL]: Record> = {}; + [SUBSCRIBTIONS_SYMBOL]: Array> = [] + constructor(public _value: T) { } + + get() { + return this._value + } + + set(newValue: any) { + const value = newValue instanceof Function ? newValue(this._value) : newValue; + + const oldValue = value + this._value = value + notify(this, oldValue) + } + + subscribe(subscribtion: SubscribtionCallback, id?: string | symbol) { + if (id) this[SUBSCRIBTIONS_SYMBOL].push([subscribtion, id]) + else this[SUBSCRIBTIONS_SYMBOL].push(subscribtion) + return () => { + this[SUBSCRIBTIONS_SYMBOL] = this[SUBSCRIBTIONS_SYMBOL].filter((callback) => (Array.isArray(callback) ? callback[0] : callback) !== subscribtion) + } + } + + copy(): Reactor { + return reactorFromNode(new Node(this._value), false, false) as Reactor + } + + freeze(): ReadOnlyReactor { + return reactorFromNode(new Node(this._value), true, false) + } + + reader(): ReadOnlyReactor { + const node = new Node(this._value) + + this.subscribe((_, value) => { + node.set(value) + }) + + return reactorFromNode(node, true, false) + } + + compute(callback: ComputeCallback): ReadOnlyReactor { + const node = new Node(callback(this._value)) + + this.subscribe((_, value) => { + node.set(callback(value)) + }) + + return reactorFromNode(node, true, false) + } +} + +function isReactable(arg: any): arg is Reactive { + return !!arg?.[REACTABLE_SYMBOL] +} + +function notify(node: Node, oldValue: T) { + node[SUBSCRIBTIONS_SYMBOL].forEach((handle) => { + if (Array.isArray(handle)) handle[0](oldValue, node._value) + else handle(oldValue, node._value) + }) +} + + +function native(this: Node, property: K): Reactive | undefined { + if (!!(this._value)) throw new TypeError(`Cannot read properties of ${this._value} (reading '${property.toString()}')`) + + const descriptor = Object.getOwnPropertyDescriptor(this._value, property) + if (descriptor === undefined) return undefined + + if (isReactable(descriptor.value)) return descriptor.value as Reactive + + const freezed = !(descriptor.writable ?? descriptor.set) + const node = new Node(descriptor.value) + + this.subscribe((_, value) => { + node.set(value[property]) + }) + node.subscribe((_, value) => { + + }) + + return reactorFromNode(node, freezed, false) +}