From c9209b2ded62b5ba18b4d8522c67172e505f39a5 Mon Sep 17 00:00:00 2001 From: Diego Borges Date: Fri, 13 Sep 2024 09:12:05 -0300 Subject: [PATCH] Use scoping terminology in code to increase consistency --- packages/arbor-react/tests/matchers/index.ts | 2 +- .../{toBeTrackedNode.ts => toBeScopedNode.ts} | 8 +-- packages/arbor-react/tests/useArbor.test.ts | 8 +-- packages/arbor-react/tests/vitest.d.ts | 2 +- packages/arbor-store/src/handlers/default.ts | 11 ++++ packages/arbor-store/src/scoping/scope.ts | 40 ++++++------- packages/arbor-store/tests/matchers/index.ts | 4 +- .../tests/matchers/toBeScopedNode.ts | 15 +++++ .../{toBeTracking.ts => toBeScoping.ts} | 16 ++--- .../tests/matchers/toBeTrackedNode.ts | 15 ----- .../arbor-store/tests/scoping/array.test.ts | 2 +- .../arbor-store/tests/scoping/map.test.ts | 8 +-- .../arbor-store/tests/scoping/store.test.ts | 58 +++++++++---------- packages/arbor-store/tests/vitest.d.ts | 4 +- 14 files changed, 102 insertions(+), 91 deletions(-) rename packages/arbor-react/tests/matchers/{toBeTrackedNode.ts => toBeScopedNode.ts} (50%) create mode 100644 packages/arbor-store/tests/matchers/toBeScopedNode.ts rename packages/arbor-store/tests/matchers/{toBeTracking.ts => toBeScoping.ts} (53%) delete mode 100644 packages/arbor-store/tests/matchers/toBeTrackedNode.ts diff --git a/packages/arbor-react/tests/matchers/index.ts b/packages/arbor-react/tests/matchers/index.ts index eca36c3..f836aca 100644 --- a/packages/arbor-react/tests/matchers/index.ts +++ b/packages/arbor-react/tests/matchers/index.ts @@ -1 +1 @@ -import "./toBeTrackedNode" +import "./toBeScopedNode" diff --git a/packages/arbor-react/tests/matchers/toBeTrackedNode.ts b/packages/arbor-react/tests/matchers/toBeScopedNode.ts similarity index 50% rename from packages/arbor-react/tests/matchers/toBeTrackedNode.ts rename to packages/arbor-react/tests/matchers/toBeScopedNode.ts index 0f8b253..ec73ec3 100644 --- a/packages/arbor-react/tests/matchers/toBeTrackedNode.ts +++ b/packages/arbor-react/tests/matchers/toBeScopedNode.ts @@ -2,13 +2,13 @@ import { expect } from "vitest" expect.extend({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - toBeTrackedNode(received: any) { - const isTracked = received.$tracked === true + toBeScopedNode(received: any) { + const isScoped = received.$scoped === true return { - pass: isTracked, + pass: isScoped, actual: received, message: () => - `Received value is ${isTracked ? "" : "not"} a tracked Arbor node`, + `Received value is ${isScoped ? "" : "not"} a tracked Arbor node`, } }, }) diff --git a/packages/arbor-react/tests/useArbor.test.ts b/packages/arbor-react/tests/useArbor.test.ts index 73c944c..e4ea468 100644 --- a/packages/arbor-react/tests/useArbor.test.ts +++ b/packages/arbor-react/tests/useArbor.test.ts @@ -16,10 +16,10 @@ describe("useArbor", () => { const { result } = renderHook(() => useArbor(store)) - expect(result.current).toBeTrackedNode() - expect(result.current[0]).toBeTrackedNode() - expect(result.current[1]).toBeTrackedNode() - expect(result.current[2]).toBeTrackedNode() + expect(result.current).toBeScopedNode() + expect(result.current[0]).toBeScopedNode() + expect(result.current[1]).toBeScopedNode() + expect(result.current[2]).toBeScopedNode() }) it("returns the current state of the store", () => { diff --git a/packages/arbor-react/tests/vitest.d.ts b/packages/arbor-react/tests/vitest.d.ts index e6463b2..a5131ce 100644 --- a/packages/arbor-react/tests/vitest.d.ts +++ b/packages/arbor-react/tests/vitest.d.ts @@ -4,7 +4,7 @@ declare module "vitest" { interface Assertion { toBeDetached: () => T toBeArborNode: () => T - toBeTrackedNode: () => T + toBeScopedNode: () => T toBeProxiedExactlyOnce: () => T toBeNodeOf: (expected: unknown) => T toHaveNodeFor: (expected: unknown) => T diff --git a/packages/arbor-store/src/handlers/default.ts b/packages/arbor-store/src/handlers/default.ts index 1802ca9..b6dd328 100644 --- a/packages/arbor-store/src/handlers/default.ts +++ b/packages/arbor-store/src/handlers/default.ts @@ -40,6 +40,17 @@ export class DefaultHandler return true } + // TODO: expose seed value via a getter so user-defined data can have access to it. + // Example: + // get $seed() { return Seed.from(this) } + // + // User-defined classes could then: + // + // @proxiable + // class MyNode { + // $seed: number + // } + $getChildNode(link: Link): Node { return this[link] } diff --git a/packages/arbor-store/src/scoping/scope.ts b/packages/arbor-store/src/scoping/scope.ts index 25e24f1..aa157e4 100644 --- a/packages/arbor-store/src/scoping/scope.ts +++ b/packages/arbor-store/src/scoping/scope.ts @@ -4,16 +4,16 @@ import { Seed } from "../path" import { ArborNode, Link, MutationEvent, Node } from "../types" import { isGetter, recursivelyUnwrap } from "../utilities" -export type Tracked = T & { - $tracked?: boolean +export type Scoped = T & { + $scoped?: boolean } export class Scope { private readonly bindings = new WeakMap>() - private readonly cache = new WeakMap() - private tracking = new WeakMap>() + private readonly cache = new WeakMap() + private scopes = new WeakMap>() - getOrCache(node: ArborNode): Tracked { + getOrCache(node: ArborNode): Scoped { if (!this.cache.has(node)) { this.cache.set(node, this.wrap(node)) } @@ -21,19 +21,19 @@ export class Scope { return this.cache.get(node) } - isTracking(node: ArborNode, prop: string) { + toBeScoping(node: ArborNode, prop: string) { const seed = Seed.from(node) - const tracked = this.tracking.get(seed) + const scoped = this.scopes.get(seed) - if (!tracked) { + if (!scoped) { return false } - return tracked.has(prop) + return scoped.has(prop) } reset() { - this.tracking = new WeakMap>() + this.scopes = new WeakMap>() } // NOTE: Something to consider in the future as new node handler types @@ -84,28 +84,28 @@ export class Scope { const rootSeed = Seed.from(event.state) const targetSeed = event.mutationPath.seeds.at(-1) - const tracked = this.tracking.get(targetSeed || rootSeed) + const scoped = this.scopes.get(targetSeed || rootSeed) - // If the affected node is not being tracked, then no need to notify + // If the affected node is not being scoped, then no need to notify // any subscribers. - if (!tracked) { + if (!scoped) { return false } // Lastly, subscribers will be notified if any of the mutated props are - // being tracked. - return event.metadata.props.some((prop) => tracked.has(prop as string)) + // being scoped. + return event.metadata.props.some((prop) => scoped.has(prop as string)) } track(value: object, prop?: string) { const seed = Seed.from(value) - if (!this.tracking.has(seed)) { - this.tracking.set(seed, new Set()) + if (!this.scopes.has(seed)) { + this.scopes.set(seed, new Set()) } if (prop != null) { - this.tracking.get(seed).add(prop) + this.scopes.get(seed).add(prop) } } @@ -116,8 +116,8 @@ export class Scope { return new Proxy(node, { get(target: Node, prop, proxy) { - // TODO: Rename $tracked to Symbol.for("ArborScoped") - if (prop === "$tracked") { + // TODO: Rename $scoped to Symbol.for("ArborScoped") + if (prop === "$scoped") { return true } diff --git a/packages/arbor-store/tests/matchers/index.ts b/packages/arbor-store/tests/matchers/index.ts index fa56a99..34e0730 100644 --- a/packages/arbor-store/tests/matchers/index.ts +++ b/packages/arbor-store/tests/matchers/index.ts @@ -3,8 +3,8 @@ import "./toBeDetached" import "./toBeNodeOf" import "./toBeProxiedExactlyOnce" import "./toBeSeeded" -import "./toBeTrackedNode" -import "./toBeTracking" +import "./toBeScopedNode" +import "./toBeScoping" import "./toHaveLink" import "./toHaveLinkFor" import "./toHaveNodeFor" diff --git a/packages/arbor-store/tests/matchers/toBeScopedNode.ts b/packages/arbor-store/tests/matchers/toBeScopedNode.ts new file mode 100644 index 0000000..c4244c8 --- /dev/null +++ b/packages/arbor-store/tests/matchers/toBeScopedNode.ts @@ -0,0 +1,15 @@ +import { expect } from "vitest" + +import { Scoped } from "../../src/scoping/scope" + +expect.extend({ + toBeScopedNode(received) { + const isScoped = (received as Scoped)?.$scoped === true + return { + pass: isScoped, + actual: received, + message: () => + `Received value is ${isScoped ? "" : "not"} a scoped Arbor node`, + } + }, +}) diff --git a/packages/arbor-store/tests/matchers/toBeTracking.ts b/packages/arbor-store/tests/matchers/toBeScoping.ts similarity index 53% rename from packages/arbor-store/tests/matchers/toBeTracking.ts rename to packages/arbor-store/tests/matchers/toBeScoping.ts index 2c6c45d..657bb72 100644 --- a/packages/arbor-store/tests/matchers/toBeTracking.ts +++ b/packages/arbor-store/tests/matchers/toBeScoping.ts @@ -1,10 +1,10 @@ import { expect } from "vitest" import { ArborError, isNode, NotAnArborNodeError, ScopedStore } from "../../src" -import { Tracked } from "../../src/scoping/scope" +import { Scoped } from "../../src/scoping/scope" expect.extend({ - toBeTracking(scopedStore, node, prop) { + toBeScoping(scopedStore, node, prop) { if (!(scopedStore instanceof ScopedStore)) { throw new ArborError("received value is not an instance of ScopedStore") } @@ -13,17 +13,17 @@ expect.extend({ throw new NotAnArborNodeError() } - const isTrackedNode = (node as Tracked)?.$tracked === true - const isTrackingNodeProp = - isTrackedNode && scopedStore.scope.isTracking(node, prop) + const isScopedNode = (node as Scoped)?.$scoped === true + const toBeScopingNodeProp = + isScopedNode && scopedStore.scope.toBeScoping(node, prop) return { - pass: isTrackingNodeProp, + pass: toBeScopingNodeProp, actual: scopedStore, message: () => `ScopedStore is ${ - isTrackingNodeProp ? "" : "not" - } tracking prop ${prop} for node ${node}`, + toBeScopingNodeProp ? "" : "not" + } scoping updates to ${prop}`, } }, }) diff --git a/packages/arbor-store/tests/matchers/toBeTrackedNode.ts b/packages/arbor-store/tests/matchers/toBeTrackedNode.ts deleted file mode 100644 index e552634..0000000 --- a/packages/arbor-store/tests/matchers/toBeTrackedNode.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from "vitest" - -import { Tracked } from "../../src/scoping/scope" - -expect.extend({ - toBeTrackedNode(received) { - const isTracked = (received as Tracked)?.$tracked === true - return { - pass: isTracked, - actual: received, - message: () => - `Received value is ${isTracked ? "" : "not"} a tracked Arbor node`, - } - }, -}) diff --git a/packages/arbor-store/tests/scoping/array.test.ts b/packages/arbor-store/tests/scoping/array.test.ts index 00f2c2e..446c2a6 100644 --- a/packages/arbor-store/tests/scoping/array.test.ts +++ b/packages/arbor-store/tests/scoping/array.test.ts @@ -384,7 +384,7 @@ describe("Array", () => { delete scoped.state.todos[1] delete scoped.state.todos[1] - expect(scoped).not.toBeTracking(scoped.state.todos, 1) + expect(scoped).not.toBeScoping(scoped.state.todos, 1) expect(subscriber).toHaveBeenCalledTimes(2) }) diff --git a/packages/arbor-store/tests/scoping/map.test.ts b/packages/arbor-store/tests/scoping/map.test.ts index 5650d90..ee89d80 100644 --- a/packages/arbor-store/tests/scoping/map.test.ts +++ b/packages/arbor-store/tests/scoping/map.test.ts @@ -22,8 +22,8 @@ describe("map", () => { expect(unwrap(scopedNode1)).toBeNodeOf(alice) expect(unwrap(scopedNode2)).toBeNodeOf(bob) - expect(scopedNode1).toBeTrackedNode() - expect(scopedNode2).toBeTrackedNode() + expect(scopedNode1).toBeScopedNode() + expect(scopedNode2).toBeScopedNode() }) }) @@ -62,8 +62,8 @@ describe("map", () => { expect(unwrap(list[0][1])).toBeNodeOf(alice) expect(unwrap(list[1][1])).toBeNodeOf(bob) - expect(list[0][1]).toBeTrackedNode() - expect(list[1][1]).toBeTrackedNode() + expect(list[0][1]).toBeScopedNode() + expect(list[1][1]).toBeScopedNode() }) }) }) diff --git a/packages/arbor-store/tests/scoping/store.test.ts b/packages/arbor-store/tests/scoping/store.test.ts index e826bf2..25cf0d7 100644 --- a/packages/arbor-store/tests/scoping/store.test.ts +++ b/packages/arbor-store/tests/scoping/store.test.ts @@ -96,10 +96,10 @@ describe("path tracking", () => { const scopedStore1 = new ScopedStore(store) - expect(scopedStore1.state).toBeTrackedNode() - expect(scopedStore1.state.todos).toBeTrackedNode() - expect(scopedStore1.state.todos[0]).toBeTrackedNode() - expect(scopedStore1.state.todos[1]).toBeTrackedNode() + expect(scopedStore1.state).toBeScopedNode() + expect(scopedStore1.state.todos).toBeScopedNode() + expect(scopedStore1.state.todos[0]).toBeScopedNode() + expect(scopedStore1.state.todos[1]).toBeScopedNode() }) it("automatically unwraps tracked node when creating a derived tracking scope", () => { @@ -112,13 +112,13 @@ describe("path tracking", () => { const scopedStore1 = new ScopedStore(store) - expect(scopedStore1.state).toBeTrackedNode() + expect(scopedStore1.state).toBeScopedNode() expect(scopedStore1.state).not.toBe(store.state) expect(unwrap(scopedStore1.state)).toBe(store.state) const scopedStore2 = new ScopedStore(scopedStore1.state.todos[0]) - expect(scopedStore2.state).toBeTrackedNode() + expect(scopedStore2.state).toBeScopedNode() expect(scopedStore2.state).not.toBe(store.state.todos[0]) expect(scopedStore2.state).not.toBe(scopedStore1.state.todos[0]) expect(unwrap(scopedStore2.state)).toBe(store.state.todos[0]) @@ -176,7 +176,7 @@ describe("path tracking", () => { const activeTodo = scopedStore.state.todos.find((t) => t.active) - expect(activeTodo).toBeTrackedNode() + expect(activeTodo).toBeScopedNode() expect(activeTodo).toBe(scopedStore.state.todos[1]) }) @@ -265,10 +265,10 @@ describe("path tracking", () => { const tracked = new ScopedStore(store.state) store.state.filter // binds filter to the original store - const filterBoundToTrackedStore = tracked.state.filter - const activeUsers = filterBoundToTrackedStore((u) => u.active) + const filterBoundToScopedStore = tracked.state.filter + const activeUsers = filterBoundToScopedStore((u) => u.active) - expect(activeUsers[0]).toBeTrackedNode() + expect(activeUsers[0]).toBeScopedNode() }) it("preserves path tracking on nodes 'plucked' from the state tree", () => { @@ -285,7 +285,7 @@ describe("path tracking", () => { carol.active = false - expect(carol).toBeTrackedNode() + expect(carol).toBeScopedNode() expect(subscriber).toHaveBeenCalledTimes(1) }) @@ -299,7 +299,7 @@ describe("path tracking", () => { const tracked = new ScopedStore(store) const carol = tracked.state.users[0] - expect(carol).toBeTrackedNode() + expect(carol).toBeScopedNode() }) it("ensure node methods have stable memory reference across updates", () => { @@ -370,8 +370,8 @@ describe("path tracking", () => { store.state.untrackedProp store.state.trackedProp - expect(store.scope.isTracking(store.state, "trackedProp")).toBe(true) - expect(store.scope.isTracking(store.state, "untrackedProp")).toBe(false) + expect(store.scope.toBeScoping(store.state, "trackedProp")).toBe(true) + expect(store.scope.toBeScoping(store.state, "untrackedProp")).toBe(false) }) it("binds methods to the path tracking proxy", () => { @@ -501,14 +501,14 @@ describe("path tracking", () => { const scoped = new ScopedStore(new Arbor(new TodoApp())) const todo = scoped.state.activeTodos[0] - expect(scoped).toBeTracking(todo, "done") - expect(scoped).not.toBeTracking(todo, "text") + expect(scoped).toBeScoping(todo, "done") + expect(scoped).not.toBeScoping(todo, "text") // access the first todo in the result so the scoped store // start tracking changes to the todo's "text" prop. todo.text - expect(scoped).toBeTracking(todo, "text") + expect(scoped).toBeScoping(todo, "text") }) it("tracks changes to boolean fields accessed via a getter on a node", () => { @@ -533,11 +533,11 @@ describe("path tracking", () => { const node = scoped.state.node // warms up the cache - expect(scoped).not.toBeTracking(node, "flag") + expect(scoped).not.toBeScoping(node, "flag") node.flag // warms up the cache - expect(scoped).toBeTracking(node, "flag") + expect(scoped).toBeScoping(node, "flag") node.flag = true @@ -561,15 +561,15 @@ describe("path tracking", () => { const scoped = new ScopedStore(new Arbor(new App())) const node = scoped.state - expect(scoped).not.toBeTracking(node, "flag1") - expect(scoped).not.toBeTracking(node, "flag2") + expect(scoped).not.toBeScoping(node, "flag1") + expect(scoped).not.toBeScoping(node, "flag2") // warms up the cache // causes "scoped" to track "flag1" but not "flag2" scoped.state.check - expect(scoped).toBeTracking(node, "flag1") - expect(scoped).not.toBeTracking(node, "flag2") + expect(scoped).toBeScoping(node, "flag1") + expect(scoped).not.toBeScoping(node, "flag2") }) it("tracks eagarly accessed fields prior to short-circuit logic", () => { @@ -590,15 +590,15 @@ describe("path tracking", () => { const scoped = new ScopedStore(new Arbor(new App())) const node = scoped.state - expect(scoped).not.toBeTracking(node, "flag1") - expect(scoped).not.toBeTracking(node, "flag2") + expect(scoped).not.toBeScoping(node, "flag1") + expect(scoped).not.toBeScoping(node, "flag2") // warms up the cache // causes "scoped" to track "flag1" but not "flag2" scoped.state.check - expect(scoped).toBeTracking(node, "flag1") - expect(scoped).toBeTracking(node, "flag2") + expect(scoped).toBeScoping(node, "flag1") + expect(scoped).toBeScoping(node, "flag2") }) it("refreshes array node links successfully", () => { @@ -642,8 +642,8 @@ describe("path tracking", () => { store.state.flag2 = false - expect(scoped).toBeTracking(scoped.state, "flag1") - expect(scoped).not.toBeTracking(scoped.state, "flag2") + expect(scoped).toBeScoping(scoped.state, "flag1") + expect(scoped).not.toBeScoping(scoped.state, "flag2") expect(subscriber).not.toHaveBeenCalled() store.state.flag1 = false diff --git a/packages/arbor-store/tests/vitest.d.ts b/packages/arbor-store/tests/vitest.d.ts index b8ac386..eda6a34 100644 --- a/packages/arbor-store/tests/vitest.d.ts +++ b/packages/arbor-store/tests/vitest.d.ts @@ -6,8 +6,8 @@ declare module "vitest" { toBeSeeded: () => T toBeDetached: () => T toBeArborNode: () => T - toBeTrackedNode: () => T - toBeTracking: (node: ArborNode, prop: keyof D) => T + toBeScopedNode: () => T + toBeScoping: (node: ArborNode, prop: keyof D) => T toBeProxiedExactlyOnce: () => T toBeNodeOf: (expected: unknown) => T toHaveNodeFor: (expected: unknown) => T