From 450e0a9d353bec37ef81885b081cbfa4f9aeb680 Mon Sep 17 00:00:00 2001 From: chelproc Date: Sat, 14 Nov 2026 13:38:16 +0900 Subject: [PATCH] refactor(store): declare the references between objects by object type --- README.md | 4 ++- src/store/base.ts | 33 ++++++++++++++++++------- src/store/component.ts | 15 +++++------ src/store/componentPin.ts | 18 ++++++++------ src/store/connection.ts | 19 ++++++++------ src/store/index.ts | 12 ++++++++- src/store/node.ts | 19 +++++++------- src/store/nodePin.ts | 18 ++++++++------ src/store/testUtils.ts | 52 +++++++++++++++++++++++---------------- src/store/types.ts | 44 +++++++++++++++++++++++++++++++++ 10 files changed, 165 insertions(+), 69 deletions(-) create mode 100644 src/store/types.ts diff --git a/README.md b/README.md index b7664cd..7abaf24 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ Every object in the store has a globally unique UUID stored in the id property, The store is divided into a root store and separate stores defined for each type of object, with actual data stored in these individual object stores. -Each object-specific store extends [`CCBaseStore`](/src/store/base.ts), which implements [EventEmitter](https://nodejs.org/docs/latest/api/events.html#class-eventemitter), allowing the renderer to monitor changes to the store through events. +Each object-specific store lists in `references` every field of its objects that holds the id of another object, along with the type of that object. Keep it complete and correct when adding such a field, since the type checker does not enforce it. + +Each object-specific store extends [`CCObjectStoreBase`](/src/store/base.ts), which implements [EventEmitter](https://nodejs.org/docs/latest/api/events.html#class-eventemitter), allowing the renderer to monitor changes to the store through events. Each object-specific store must implement the following four events, with the event object passing the object that triggered the event: - `didRegister`: Triggered when an object is registered in the store. diff --git a/src/store/base.ts b/src/store/base.ts index 7a98445..d7b41ba 100644 --- a/src/store/base.ts +++ b/src/store/base.ts @@ -1,22 +1,37 @@ import EventEmitter from "eventemitter3"; import type CCStore from "."; +import type { + CCObjectByType, + CCObjectId, + CCObjectStore, + CCObjectType, + CCReferences, +} from "./types"; /** Events emitted by every object-specific store */ -export type CCBaseStoreEvents = { +export type CCObjectStoreEvents = { didRegister(object: T): void; willUnregister(object: T): void; didUnregister(object: T): void; }; /** Base class of the object-specific stores (components, nodes, pins, and connections) */ -export abstract class CCBaseStore< - Id extends string, - T extends { readonly id: Id }, - Events extends CCBaseStoreEvents, -> extends EventEmitter { +export abstract class CCObjectStoreBase< + K extends CCObjectType, + Events extends CCObjectStoreEvents, + > + extends EventEmitter + implements CCObjectStore +{ protected readonly store: CCStore; - protected readonly objects: Map = new Map(); + protected readonly objects: Map, CCObjectByType[K]> = new Map(); + + /** + * Fields of the objects holding the id of another object, mapped to the object type of that id. + * Every such field must be listed with its correct object type. + */ + abstract readonly references: CCReferences; constructor(store: CCStore) { super(); @@ -31,7 +46,7 @@ export abstract class CCBaseStore< * @param id id of object * @returns object of `id` */ - get(id: Id): T | undefined { + get(id: CCObjectId): CCObjectByType[K] | undefined { return this.objects.get(id); } @@ -39,7 +54,7 @@ export abstract class CCBaseStore< * Get array of objects * @returns array of objects */ - getMany(): T[] { + getMany(): CCObjectByType[K][] { return [...this.objects.values()]; } } diff --git a/src/store/component.ts b/src/store/component.ts index 8f72a09..58ec852 100644 --- a/src/store/component.ts +++ b/src/store/component.ts @@ -1,11 +1,11 @@ import nullthrows from "nullthrows"; import invariant from "tiny-invariant"; -import type { Tagged } from "type-fest"; import type CCStore from "."; -import { CCBaseStore, type CCBaseStoreEvents } from "./base"; +import { CCObjectStoreBase, type CCObjectStoreEvents } from "./base"; import type { CCIntrinsicComponentType } from "./intrinsics/types"; +import type { CCObjectId } from "./types"; -export type CCComponentId = Tagged; +export type CCComponentId = CCObjectId<"components">; export type CCComponent = { readonly id: CCComponentId; @@ -14,7 +14,7 @@ export type CCComponent = { name: string; }; -export type CCComponentStoreEvents = CCBaseStoreEvents & { +export type CCComponentStoreEvents = CCObjectStoreEvents & { didUpdate(component: CCComponent): void; }; export const ccComponentStoreChangeEventTypes: (keyof CCComponentStoreEvents)[] = @@ -23,11 +23,12 @@ export const ccComponentStoreChangeEventTypes: (keyof CCComponentStoreEvents)[] /** * Store of components */ -export class CCComponentStore extends CCBaseStore< - CCComponentId, - CCComponent, +export class CCComponentStore extends CCObjectStoreBase< + "components", CCComponentStoreEvents > { + readonly references = {} as const; + import(components: CCComponent[]) { for (const component of components) { this.register(component); diff --git a/src/store/componentPin.ts b/src/store/componentPin.ts index d17c690..4845078 100644 --- a/src/store/componentPin.ts +++ b/src/store/componentPin.ts @@ -1,11 +1,11 @@ import nullthrows from "nullthrows"; import invariant from "tiny-invariant"; -import type { Tagged } from "type-fest"; -import { CCBaseStore, type CCBaseStoreEvents } from "./base"; +import { CCObjectStoreBase, type CCObjectStoreEvents } from "./base"; import type { CCComponentId } from "./component"; import { IntrinsicComponentDefinition } from "./intrinsics/base"; import { input, output } from "./intrinsics/definitions"; import type { CCNodePinId } from "./nodePin"; +import type { CCObjectId } from "./types"; export type CCComponentPin = { readonly id: CCComponentPinId; @@ -16,7 +16,7 @@ export type CCComponentPin = { name: string; }; -export type CCComponentPinId = Tagged; +export type CCComponentPinId = CCObjectId<"componentPins">; export type CCComponentPinType = "input" | "output"; export const ccPinTypes: CCComponentPinType[] = ["input", "output"]; @@ -44,7 +44,7 @@ export type CCComponentPinBitWidthStatus = | { isFixed: false; fixMode: "automatic" | "nodeDependent" } | { isFixed: true; bitWidth: number }; -export type CCComponentPinStoreEvents = CCBaseStoreEvents & { +export type CCComponentPinStoreEvents = CCObjectStoreEvents & { didUpdate(pin: CCComponentPin): void; }; export const ccComponentPinStoreChangeEventTypes: (keyof CCComponentPinStoreEvents)[] = @@ -53,11 +53,15 @@ export const ccComponentPinStoreChangeEventTypes: (keyof CCComponentPinStoreEven /** * Store of pins */ -export class CCComponentPinStore extends CCBaseStore< - CCComponentPinId, - CCComponentPin, +export class CCComponentPinStore extends CCObjectStoreBase< + "componentPins", CCComponentPinStoreEvents > { + readonly references = { + componentId: "components", + implementation: "nodePins", + } as const; + import(componentPins: CCComponentPin[]): void { for (const pin of componentPins) { this.objects.set(pin.id, pin); diff --git a/src/store/connection.ts b/src/store/connection.ts index 7af4615..2c65844 100644 --- a/src/store/connection.ts +++ b/src/store/connection.ts @@ -1,12 +1,12 @@ import nullthrows from "nullthrows"; -import type { Tagged } from "type-fest"; -import { CCBaseStore, type CCBaseStoreEvents } from "./base"; +import { CCObjectStoreBase, type CCObjectStoreEvents } from "./base"; import type { CCComponentId } from "./component"; import type { CCComponentPinId } from "./componentPin"; import type { CCNodeId } from "./node"; import type { CCNodePinId } from "./nodePin"; +import type { CCObjectId } from "./types"; -export type CCConnectionId = Tagged; +export type CCConnectionId = CCObjectId<"connections">; export type CCConnectionEndpoint = { readonly nodeId: CCNodeId; @@ -21,18 +21,23 @@ export type CCConnection = { bentPortion: number; }; -export type CCConnectionStoreEvents = CCBaseStoreEvents; +export type CCConnectionStoreEvents = CCObjectStoreEvents; export const ccConnectionStoreChangeEventTypes: (keyof CCConnectionStoreEvents)[] = ["didRegister", "didUnregister"]; /** * Store of connections */ -export class CCConnectionStore extends CCBaseStore< - CCConnectionId, - CCConnection, +export class CCConnectionStore extends CCObjectStoreBase< + "connections", CCConnectionStoreEvents > { + readonly references = { + from: "nodePins", + to: "nodePins", + parentComponentId: "components", + } as const; + import(connections: CCConnection[]): void { for (const connection of connections) { this.objects.set(connection.id, connection); diff --git a/src/store/index.ts b/src/store/index.ts index fb79db7..998a30b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -15,11 +15,14 @@ import * as intrinsics from "./intrinsics/definitions"; import { CCNodeStore } from "./node"; import { CCNodePinStore, ccNodePinStoreChangeEventTypes } from "./nodePin"; import TransactionManager from "./transaction"; +import type { CCObjectStore, CCObjectType } from "./types"; + +type CCObjectStores = { [K in CCObjectType]: CCObjectStore }; /** * Store of components, nodes, pins, and connections */ -export default class CCStore { +export default class CCStore implements CCObjectStores { components: CCComponentStore; nodes: CCNodeStore; @@ -108,6 +111,13 @@ export default class CCStore { }; } + getObjectStore( + this: CCObjectStores, + type: K, + ): CCObjectStore { + return this[type]; + } + mount() { this.components.mount(); this.nodes.mount(); diff --git a/src/store/node.ts b/src/store/node.ts index 905724a..17bccbe 100644 --- a/src/store/node.ts +++ b/src/store/node.ts @@ -1,13 +1,13 @@ import nullthrows from "nullthrows"; import invariant from "tiny-invariant"; -import type { Tagged } from "type-fest"; import type { Vector2 } from "../common/vector2"; -import { CCBaseStore, type CCBaseStoreEvents } from "./base"; +import { CCObjectStoreBase, type CCObjectStoreEvents } from "./base"; import type { CCComponentId } from "./component"; import { definitionByComponentId } from "./intrinsics/definitions"; import type { CCIntrinsicComponentSpec } from "./intrinsics/types"; +import type { CCObjectId } from "./types"; -export type CCNodeId = Tagged; +export type CCNodeId = CCObjectId<"nodes">; export type CCNode< Spec extends CCIntrinsicComponentSpec = CCIntrinsicComponentSpec, @@ -19,7 +19,7 @@ export type CCNode< config: Spec["config"]; }; -export type CCNodeStoreEvents = CCBaseStoreEvents & { +export type CCNodeStoreEvents = CCObjectStoreEvents & { didUpdate(node: CCNode): void; /** * Emitted in addition to `didUpdate` when the config of a node changed, so that @@ -36,11 +36,12 @@ export const ccNodeStoreChangeEventTypes: (keyof CCNodeStoreEvents)[] = [ /** * Store of nodes */ -export class CCNodeStore extends CCBaseStore< - CCNodeId, - CCNode, - CCNodeStoreEvents -> { +export class CCNodeStore extends CCObjectStoreBase<"nodes", CCNodeStoreEvents> { + readonly references = { + parentComponentId: "components", + componentId: "components", + } as const; + import(nodes: CCNode[]): void { for (const node of nodes) { node.position = { x: node.position.x, y: node.position.y }; diff --git a/src/store/nodePin.ts b/src/store/nodePin.ts index 32a0ecf..c03f205 100644 --- a/src/store/nodePin.ts +++ b/src/store/nodePin.ts @@ -1,14 +1,14 @@ import nullthrows from "nullthrows"; import invariant from "tiny-invariant"; -import type { Tagged } from "type-fest"; -import { CCBaseStore, type CCBaseStoreEvents } from "./base"; +import { CCObjectStoreBase, type CCObjectStoreEvents } from "./base"; import { analyzeComponentBitWidths, type ComponentBitWidths } from "./bitWidth"; import type { CCComponentId } from "./component"; import type { CCComponentPinId, CCNodePinBitWidthStatus } from "./componentPin"; import { IntrinsicComponentDefinition } from "./intrinsics/base"; import type { CCNodeId } from "./node"; +import type { CCObjectId } from "./types"; -export type CCNodePinId = Tagged; +export type CCNodePinId = CCObjectId<"nodePins">; export type CCNodePin = { id: CCNodePinId; @@ -18,7 +18,7 @@ export type CCNodePin = { manualBitWidth: number | null; }; -export type CCNodePinStoreEvents = CCBaseStoreEvents & { +export type CCNodePinStoreEvents = CCObjectStoreEvents & { didUpdate(pin: CCNodePin): void; }; export const ccNodePinStoreChangeEventTypes: (keyof CCNodePinStoreEvents)[] = [ @@ -27,11 +27,15 @@ export const ccNodePinStoreChangeEventTypes: (keyof CCNodePinStoreEvents)[] = [ "didUpdate", ]; -export class CCNodePinStore extends CCBaseStore< - CCNodePinId, - CCNodePin, +export class CCNodePinStore extends CCObjectStoreBase< + "nodePins", CCNodePinStoreEvents > { + readonly references = { + nodeId: "nodes", + componentPinId: "componentPins", + } as const; + #markedAsDeleted: Set = new Set(); #bitWidthsByComponent: Map = new Map(); diff --git a/src/store/testUtils.ts b/src/store/testUtils.ts index 7663c0c..ee51c75 100644 --- a/src/store/testUtils.ts +++ b/src/store/testUtils.ts @@ -14,35 +14,45 @@ import { input, output } from "./intrinsics/definitions"; import { type CCNode, CCNodeStore } from "./node"; import { type CCNodePinId, CCNodePinStore } from "./nodePin"; import type { SimulationValue } from "./simulation"; +import { + type CCObjectByType, + type CCObjectId, + type CCObjectType, + ccObjectTypes, +} from "./types"; export function expectConsistentStore(store: CCStore) { - const nodes = store.nodes.getMany(); - for (const node of nodes) { - expect(store.components.get(node.componentId)).toBeDefined(); - expect(store.components.get(node.parentComponentId)).toBeDefined(); - } - - const componentPins = store.componentPins.getMany(); - for (const componentPin of componentPins) { - expect(store.componentPins.get(componentPin.id)).toBeDefined(); - expect(store.components.get(componentPin.componentId)).toBeDefined(); + for (const type of ccObjectTypes) { + expectReferencesToExist(store, type); } - const nodePins = store.nodePins.getMany(); - for (const nodePin of nodePins) { - expect(store.nodes.get(nodePin.nodeId)).toBeDefined(); - expect(store.componentPins.get(nodePin.componentPinId)).toBeDefined(); - } - - const connections = store.connections.getMany(); - for (const connection of store.connections.getMany()) { - expect(store.nodePins.get(connection.from)).toBeDefined(); - expect(store.nodePins.get(connection.to)).toBeDefined(); - } // Ensure an input pin is only connected to one output pin + const connections = store.connections.getMany(); expect(connections).toEqual(uniqBy(connections, (c) => c.to)); } +/** Expect every object of `type` to refer only to objects in the store. */ +function expectReferencesToExist( + store: CCStore, + type: K, +) { + const objectStore = store.getObjectStore(type); + const references = Object.entries(objectStore.references) as [ + keyof CCObjectByType[K], + CCObjectType, + ][]; + for (const object of objectStore.getMany()) { + for (const [field, targetType] of references) { + const targetId = object[field] as CCObjectId | null; + if (targetId === null) continue; + expect( + store.getObjectStore(targetType).get(targetId), + `${type}.${String(field)} of ${object.id}`, + ).toBeDefined(); + } + } +} + /** * Builders for the circuits used in tests. They go through the same stores the editor * uses, so a circuit built with them behaves like one drawn by hand. diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 0000000..ce3140a --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,44 @@ +import type { Tagged } from "type-fest"; +import type { CCComponent } from "./component"; +import type { CCComponentPin } from "./componentPin"; +import type { CCConnection } from "./connection"; +import type { CCNode } from "./node"; +import type { CCNodePin } from "./nodePin"; + +export const ccObjectTypes = [ + "components", + "nodes", + "componentPins", + "nodePins", + "connections", +] as const; + +export type CCObjectType = (typeof ccObjectTypes)[number]; + +/** Id of an object of `K`, tagged with `K` so that ids of different object types do not mix */ +export type CCObjectId = + K extends CCObjectType ? Tagged : never; + +/** Base type for all objects, having an id of its type */ +export type CCObject = { + readonly id: CCObjectId; +}; + +export type CCObjectByType = { + components: CCComponent; + nodes: CCNode; + componentPins: CCComponentPin; + nodePins: CCNodePin; + connections: CCConnection; +}; + +export type CCReferences = Partial< + Record +>; + +/** Base interface for an object-specific store */ +export interface CCObjectStore { + readonly references: CCReferences; + get(id: CCObjectId): CCObjectByType[K] | undefined; + getMany(): CCObjectByType[K][]; +}