Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 24 additions & 9 deletions src/store/base.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
export type CCObjectStoreEvents<T> = {
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<T>,
> extends EventEmitter<Events> {
export abstract class CCObjectStoreBase<
K extends CCObjectType,
Events extends CCObjectStoreEvents<CCObjectByType[K]>,
>
extends EventEmitter<Events>
implements CCObjectStore<K>
{
protected readonly store: CCStore;

protected readonly objects: Map<Id, T> = new Map();
protected readonly objects: Map<CCObjectId<K>, 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<CCObjectByType[K]>;

constructor(store: CCStore) {
super();
Expand All @@ -31,15 +46,15 @@ export abstract class CCBaseStore<
* @param id id of object
* @returns object of `id`
*/
get(id: Id): T | undefined {
get(id: CCObjectId<K>): CCObjectByType[K] | undefined {
return this.objects.get(id);
}

/**
* Get array of objects
* @returns array of objects
*/
getMany(): T[] {
getMany(): CCObjectByType[K][] {
return [...this.objects.values()];
}
}
15 changes: 8 additions & 7 deletions src/store/component.ts
Original file line number Diff line number Diff line change
@@ -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<string, "CCComponentId">;
export type CCComponentId = CCObjectId<"components">;

export type CCComponent = {
readonly id: CCComponentId;
Expand All @@ -14,7 +14,7 @@ export type CCComponent = {
name: string;
};

export type CCComponentStoreEvents = CCBaseStoreEvents<CCComponent> & {
export type CCComponentStoreEvents = CCObjectStoreEvents<CCComponent> & {
didUpdate(component: CCComponent): void;
};
export const ccComponentStoreChangeEventTypes: (keyof CCComponentStoreEvents)[] =
Expand All @@ -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);
Expand Down
18 changes: 11 additions & 7 deletions src/store/componentPin.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,7 +16,7 @@ export type CCComponentPin = {
name: string;
};

export type CCComponentPinId = Tagged<string, "CCPinId">;
export type CCComponentPinId = CCObjectId<"componentPins">;
export type CCComponentPinType = "input" | "output";
export const ccPinTypes: CCComponentPinType[] = ["input", "output"];

Expand Down Expand Up @@ -44,7 +44,7 @@ export type CCComponentPinBitWidthStatus =
| { isFixed: false; fixMode: "automatic" | "nodeDependent" }
| { isFixed: true; bitWidth: number };

export type CCComponentPinStoreEvents = CCBaseStoreEvents<CCComponentPin> & {
export type CCComponentPinStoreEvents = CCObjectStoreEvents<CCComponentPin> & {
didUpdate(pin: CCComponentPin): void;
};
export const ccComponentPinStoreChangeEventTypes: (keyof CCComponentPinStoreEvents)[] =
Expand All @@ -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);
Expand Down
19 changes: 12 additions & 7 deletions src/store/connection.ts
Original file line number Diff line number Diff line change
@@ -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<string, "CCConnectionId">;
export type CCConnectionId = CCObjectId<"connections">;

export type CCConnectionEndpoint = {
readonly nodeId: CCNodeId;
Expand All @@ -21,18 +21,23 @@ export type CCConnection = {
bentPortion: number;
};

export type CCConnectionStoreEvents = CCBaseStoreEvents<CCConnection>;
export type CCConnectionStoreEvents = CCObjectStoreEvents<CCConnection>;
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);
Expand Down
12 changes: 11 additions & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<K> };

/**
* Store of components, nodes, pins, and connections
*/
export default class CCStore {
export default class CCStore implements CCObjectStores {
components: CCComponentStore;

nodes: CCNodeStore;
Expand Down Expand Up @@ -108,6 +111,13 @@ export default class CCStore {
};
}

getObjectStore<K extends CCObjectType>(
this: CCObjectStores,
type: K,
): CCObjectStore<K> {
return this[type];
}

mount() {
this.components.mount();
this.nodes.mount();
Expand Down
19 changes: 10 additions & 9 deletions src/store/node.ts
Original file line number Diff line number Diff line change
@@ -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<string, "CCNodeId">;
export type CCNodeId = CCObjectId<"nodes">;

export type CCNode<
Spec extends CCIntrinsicComponentSpec = CCIntrinsicComponentSpec,
Expand All @@ -19,7 +19,7 @@ export type CCNode<
config: Spec["config"];
};

export type CCNodeStoreEvents = CCBaseStoreEvents<CCNode> & {
export type CCNodeStoreEvents = CCObjectStoreEvents<CCNode> & {
didUpdate(node: CCNode): void;
/**
* Emitted in addition to `didUpdate` when the config of a node changed, so that
Expand All @@ -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 };
Expand Down
18 changes: 11 additions & 7 deletions src/store/nodePin.ts
Original file line number Diff line number Diff line change
@@ -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<string, "CCNodePinId">;
export type CCNodePinId = CCObjectId<"nodePins">;

export type CCNodePin = {
id: CCNodePinId;
Expand All @@ -18,7 +18,7 @@ export type CCNodePin = {
manualBitWidth: number | null;
};

export type CCNodePinStoreEvents = CCBaseStoreEvents<CCNodePin> & {
export type CCNodePinStoreEvents = CCObjectStoreEvents<CCNodePin> & {
didUpdate(pin: CCNodePin): void;
};
export const ccNodePinStoreChangeEventTypes: (keyof CCNodePinStoreEvents)[] = [
Expand All @@ -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<CCNodePinId> = new Set();

#bitWidthsByComponent: Map<CCComponentId, ComponentBitWidths> = new Map();
Expand Down
52 changes: 31 additions & 21 deletions src/store/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<K extends CCObjectType>(
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.
Expand Down
Loading
Loading