@derivesome/core
Advanced tools
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| import { describe, assert } from "vitest"; | ||
| import { MapProxy } from "./map-proxy"; | ||
| import { derived } from "./derived"; | ||
| type Person = { | ||
| firstname: string; | ||
| lastname: string; | ||
| age: number; | ||
| }; | ||
| describe("map-proxy", (it) => { | ||
| it("causes updates when set or deleted", () => { | ||
| const map = new MapProxy<string, Person>(); | ||
| const fooPerson = derived(() => map.get("foo") || null); | ||
| assert.strictEqual(fooPerson.peek(), null); | ||
| map.set("foo", { | ||
| firstname: "foo", | ||
| lastname: "bar", | ||
| age: 32, | ||
| }); | ||
| assert.deepEqual(fooPerson.peek(), { | ||
| firstname: "foo", | ||
| lastname: "bar", | ||
| age: 32, | ||
| }); | ||
| map.delete("foo"); | ||
| assert.strictEqual(fooPerson.peek(), null); | ||
| }); | ||
| it("causes updates on keys / values", () => { | ||
| const map = new MapProxy<string, Person>(); | ||
| const keys = derived(() => Array.from(map.keys())); | ||
| const values = derived(() => Array.from(map.values())); | ||
| assert.deepEqual(keys.peek(), []); | ||
| assert.deepEqual(values.peek(), []); | ||
| map.set("foo", { | ||
| firstname: "foo", | ||
| lastname: "bar", | ||
| age: 32, | ||
| }); | ||
| assert.deepEqual(keys.peek(), ["foo"]); | ||
| assert.deepEqual(values.peek(), [ | ||
| { | ||
| firstname: "foo", | ||
| lastname: "bar", | ||
| age: 32, | ||
| }, | ||
| ]); | ||
| map.delete("foo"); | ||
| assert.deepEqual(keys.peek(), []); | ||
| assert.deepEqual(values.peek(), []); | ||
| }); | ||
| }); |
Sorry, the diff of this file is not supported yet
| import { | ||
| compare, | ||
| SubscribeOptions, | ||
| Subscription, | ||
| VoidFunction, | ||
| } from "./common"; | ||
| import { Context } from "./context"; | ||
| import { Effect } from "./effect"; | ||
| import { Observable, OBSERVABLE } from "./observable"; | ||
| import { pubsub } from "./pubsub"; | ||
| export class MapProxy<K extends PropertyKey = PropertyKey, V extends any = any> | ||
| extends Map<K, V> | ||
| implements Observable<Record<K, V>> | ||
| { | ||
| [OBSERVABLE]: true = true; | ||
| ps = pubsub<Record<K, V>>(); | ||
| subscribers = this.ps.subscribers; | ||
| effects = this.ps.effects; | ||
| publish = this.ps.publish; | ||
| publishAsync = this.ps.publishAsync; | ||
| subscribe = this.ps.subscribe; | ||
| addEffect = this.ps.addEffect; | ||
| dispose = this.ps.dispose; | ||
| constructor(...args: ConstructorParameters<typeof Map<K, V>>) { | ||
| super(...args); | ||
| } | ||
| private getState(): Record<K, V> { | ||
| const state = { | ||
| ...Object.fromEntries(Array.from(super.entries())), | ||
| } as Record<K, V>; | ||
| return state; | ||
| } | ||
| addEffectDependency(fx: Effect) { | ||
| this.ps.addEffect(fx.run, fx.cleanup); | ||
| } | ||
| observe( | ||
| fn: Subscription<Record<K, V>>, | ||
| options: SubscribeOptions = {}, | ||
| ): VoidFunction { | ||
| if (options.immediate) { | ||
| fn(this.getState()); | ||
| } | ||
| return this.ps.subscribe(fn, options.cleanup); | ||
| } | ||
| set(key: K, value: V) { | ||
| const prev = super.get(key); | ||
| const next = value; | ||
| const result = super.set(key, value); | ||
| if (!compare(next, prev)) { | ||
| const state = { ...this.getState(), [key]: value } as Record<K, V>; | ||
| this.ps.publish(state); | ||
| } | ||
| return result; | ||
| } | ||
| get(key: K) { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.get(key); | ||
| } | ||
| delete(key: K) { | ||
| const didHave = super.has(key); | ||
| const result = super.delete(key); | ||
| if (didHave) { | ||
| this.ps.publish(this.getState()); | ||
| } | ||
| return result; | ||
| } | ||
| entries() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.entries(); | ||
| } | ||
| keys() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.keys(); | ||
| } | ||
| values() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.values(); | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| import { SubscribeOptions, Subscription, VoidFunction } from "./common"; | ||
| import { Context } from "./context"; | ||
| import { Effect } from "./effect"; | ||
| import { Observable, OBSERVABLE } from "./observable"; | ||
| import { pubsub } from "./pubsub"; | ||
| export class SetProxy<V = any> extends Set<V> implements Observable<Set<V>> { | ||
| [OBSERVABLE]: true = true; | ||
| ps = pubsub<Set<V>>(); | ||
| subscribers = this.ps.subscribers; | ||
| effects = this.ps.effects; | ||
| publish = this.ps.publish; | ||
| publishAsync = this.ps.publishAsync; | ||
| subscribe = this.ps.subscribe; | ||
| addEffect = this.ps.addEffect; | ||
| dispose = this.ps.dispose; | ||
| constructor(...args: ConstructorParameters<typeof Set<V>>) { | ||
| super(...args); | ||
| } | ||
| private getState(): Set<V> { | ||
| return new Set(super.values()); | ||
| } | ||
| addEffectDependency(fx: Effect) { | ||
| this.ps.addEffect(fx.run, fx.cleanup); | ||
| } | ||
| observe( | ||
| fn: Subscription<Set<V>>, | ||
| options: SubscribeOptions = {}, | ||
| ): VoidFunction { | ||
| if (options.immediate) { | ||
| fn(this.getState()); | ||
| } | ||
| return this.ps.subscribe(fn, options.cleanup); | ||
| } | ||
| get size() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.size; | ||
| } | ||
| add(value: V) { | ||
| const hadValue = super.has(value); | ||
| const result = super.add(value); | ||
| if (!hadValue) { | ||
| this.ps.publish(this.getState()); | ||
| } | ||
| return result; | ||
| } | ||
| delete(value: V) { | ||
| const didHave = super.has(value); | ||
| const result = super.delete(value); | ||
| if (didHave) { | ||
| this.ps.publish(this.getState()); | ||
| } | ||
| return result; | ||
| } | ||
| has(value: V) { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.has(value); | ||
| } | ||
| values() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.values(); | ||
| } | ||
| keys() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.keys(); | ||
| } | ||
| entries() { | ||
| const fx = Context.scope.current.effect; | ||
| if (fx) this.addEffectDependency(fx); | ||
| return super.entries(); | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet
| import { MapProxy } from "../map-proxy"; | ||
| import { SetProxy } from "../set-proxy"; | ||
| export const maybeProxy = <T>(x: T): T => { | ||
| if (x instanceof MapProxy) return x; | ||
| if (x instanceof Map) return new MapProxy(Array.from(x.entries())) as T; | ||
| if (x instanceof SetProxy) return x; | ||
| if (x instanceof Set) return new SetProxy(Array.from(x.values())) as T; | ||
| return x; | ||
| }; |
Sorry, the diff of this file is not supported yet
+1
-1
| { | ||
| "name": "@derivesome/core", | ||
| "version": "1.0.2", | ||
| "version": "1.0.3", | ||
| "description": "", | ||
@@ -5,0 +5,0 @@ "main": "./dist/cjs/index.js", |
+2
-0
@@ -7,2 +7,4 @@ export * from "./common"; | ||
| export * from "./derived"; | ||
| export * from "./map-proxy"; | ||
| export * from "./set-proxy"; | ||
| export * from "./utils"; |
| import { describe, assert } from "vitest"; | ||
| import { ref } from "./reference"; | ||
| import { Context } from "./context"; | ||
| import { derived } from "./derived"; | ||
@@ -32,2 +33,64 @@ describe("reference", (it) => { | ||
| }); | ||
| it("properly proxies Sets", () => { | ||
| const r = ref(new Set<number>()); | ||
| const size = derived(() => r.get().size); | ||
| const has1 = derived(() => r.get().has(1)); | ||
| assert.strictEqual(size.peek(), 0); | ||
| assert.strictEqual(has1.peek(), false); | ||
| r.peek().add(1); | ||
| assert.strictEqual(size.peek(), 1); | ||
| assert.strictEqual(has1.peek(), true); | ||
| r.peek().add(2); | ||
| assert.strictEqual(size.peek(), 2); | ||
| r.peek().add(1); | ||
| assert.strictEqual(size.peek(), 2); | ||
| r.peek().delete(1); | ||
| assert.strictEqual(size.peek(), 1); | ||
| assert.strictEqual(has1.peek(), false); | ||
| r.peek().delete(2); | ||
| assert.strictEqual(size.peek(), 0); | ||
| }); | ||
| it("properly proxies Maps", () => { | ||
| const r = ref(new Map<string, number>()); | ||
| const sum = derived(() => | ||
| Array.from(r.get().values()).reduce((a, b) => a + b, 0), | ||
| ); | ||
| assert.strictEqual(sum.peek(), 0); | ||
| r.peek().set("x", 1); | ||
| assert.strictEqual(sum.peek(), 1); | ||
| r.peek().set("x", 2); | ||
| assert.strictEqual(sum.peek(), 2); | ||
| r.peek().set("y", 1); | ||
| assert.strictEqual(sum.peek(), 3); | ||
| r.peek().delete("y"); | ||
| assert.strictEqual(sum.peek(), 2); | ||
| r.peek().delete("x"); | ||
| assert.strictEqual(sum.peek(), 0); | ||
| }); | ||
| }); |
+3
-2
@@ -9,2 +9,3 @@ import { has } from "./common"; | ||
| import { pubsub } from "./pubsub"; | ||
| import { maybeProxy } from "./utils/maybeProxy"; | ||
@@ -23,3 +24,3 @@ export const REFERENCE = Symbol("Reference"); | ||
| const ps = pubsub<T>(); | ||
| const state: Boxed<T> = { value }; | ||
| const state: Boxed<T> = { value: maybeProxy(value) }; | ||
| const obs = observable<T>(state, ps); | ||
@@ -39,3 +40,3 @@ | ||
| const prev = state.value; | ||
| const next = isFunction(fn) ? fn(state.value) : fn; | ||
| const next = maybeProxy(isFunction(fn) ? fn(state.value) : fn); | ||
| if (!compare(next, prev)) { | ||
@@ -42,0 +43,0 @@ state.value = next; |
@@ -1,3 +0,3 @@ | ||
| export const onNextTick = (fn: () => (void | Promise<void>)) => { | ||
| if (typeof queueMicrotask !== 'undefined') { | ||
| export const onNextTick = (fn: () => void | Promise<void>) => { | ||
| if (typeof queueMicrotask !== "undefined") { | ||
| queueMicrotask(fn); | ||
@@ -7,2 +7,2 @@ } else { | ||
| } | ||
| } | ||
| }; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1206669
269.64%254
6.72%3324
8.38%