diff --git a/frontend/src/lib/components/graph/Graph.svelte b/frontend/src/lib/components/graph/Graph.svelte index 9a08949..3cf721a 100644 --- a/frontend/src/lib/components/graph/Graph.svelte +++ b/frontend/src/lib/components/graph/Graph.svelte @@ -524,7 +524,7 @@ $edges = $edges; }); - graph.history.save(); + graph.save(); } else if ($hoveredSocket && $activeSocket) { if ( typeof $hoveredSocket.index === "number" && @@ -547,7 +547,7 @@ $hoveredSocket.index, ); } - graph.history.save(); + graph.save(); } mouseDown = null; diff --git a/frontend/src/lib/components/graph/GraphView.svelte b/frontend/src/lib/components/graph/GraphView.svelte index ce51eb8..8d92531 100644 --- a/frontend/src/lib/components/graph/GraphView.svelte +++ b/frontend/src/lib/components/graph/GraphView.svelte @@ -5,7 +5,7 @@ import Node from "../Node.svelte"; import { getContext, onMount } from "svelte"; import type { Writable } from "svelte/store"; - import { activeSocket, colors } from "./stores"; + import { activeSocket } from "./stores"; export let nodes: Writable>; export let edges: Writable; diff --git a/frontend/src/lib/graph-manager.ts b/frontend/src/lib/graph-manager.ts index f2a1d10..396a0ea 100644 --- a/frontend/src/lib/graph-manager.ts +++ b/frontend/src/lib/graph-manager.ts @@ -2,8 +2,9 @@ import { writable, type Writable } from "svelte/store"; import { type Graph, type Node, type Edge, type Socket, type NodeRegistry, type RuntimeExecutor } from "./types"; import { HistoryManager } from "./history-manager"; import * as templates from "./graphs"; +import EventEmitter from "./helpers/EventEmitter"; -export class GraphManager { +export class GraphManager extends EventEmitter<{ "save": Graph }> { status: Writable<"loading" | "idle" | "error"> = writable("loading"); @@ -19,6 +20,7 @@ export class GraphManager { history: HistoryManager = new HistoryManager(this); constructor(private nodeRegistry: NodeRegistry, private runtime: RuntimeExecutor) { + super(); this.nodes.subscribe((nodes) => { this._nodes = nodes; }); @@ -187,6 +189,7 @@ export class GraphManager { } save() { + this.emit("save", this.serialize()); this.history.save(); } diff --git a/frontend/src/lib/helpers/EventEmitter.ts b/frontend/src/lib/helpers/EventEmitter.ts new file mode 100644 index 0000000..b37eeb5 --- /dev/null +++ b/frontend/src/lib/helpers/EventEmitter.ts @@ -0,0 +1,90 @@ + +import throttle from './throttle'; + +const debug = { amountEmitters: 0, amountCallbacks: 0, emitters: [] }; + + +type EventMap = Record; +type EventKey = string & keyof T; +type EventReceiver = (params: T, stuff?: Record) => unknown; + + +export default class EventEmitter { + index = 0; + public eventMap: T = {} as T; + constructor() { + this.index = debug.amountEmitters; + debug.amountEmitters++; + } + + private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {}; + private cbsOnce: { [key: string]: ((data?: unknown) => unknown)[] } = {}; + + /** + * Emit an event with optional data to all the listeners + * @param {string} event Name of the event to emit + * @param data Data to send along + */ + public emit(event: string, data?: unknown) { + if (event in this.cbs) { + this.cbs[event].forEach((c) => c(data)); + } + if (event in this.cbsOnce) { + this.cbsOnce[event].forEach((c) => c(data)); + delete this.cbsOnce[event]; + } + } + + public on>(event: K, cb: EventReceiver, throttleTimer = 0) { + if (throttleTimer > 0) cb = throttle(cb, throttleTimer); + const cbs = Object.assign(this.cbs, { + [event]: [...(this.cbs[event] || []), cb], + }); + this.cbs = cbs; + + debug.emitters[this.index] = { + name: this.constructor.name, + cbs: Object.fromEntries( + Object.keys(this.cbs).map((key) => [key, this.cbs[key].length]), + ), + }; + debug.amountCallbacks++; + + // console.log('New EventEmitter ', this.constructor.name); + return () => { + debug.amountCallbacks--; + cbs[event]?.splice(cbs[event].indexOf(cb), 1); + debug.emitters[this.index] = { + name: this.constructor.name, + cbs: Object.fromEntries( + Object.keys(this.cbs).map((key) => [key, this.cbs[key].length]), + ), + }; + }; + } + + /** + * Register a special listener which only gets called once + * @param {string} event Name of the event to listen to + * @param {function} cb Listener, gets called everytime the event is emitted + * @returns {function} Returns a function which removes the listener when called + */ + public once>(event: K, cb: EventReceiver): () => void { + this.cbsOnce[event] = [...(this.cbsOnce[event] || []), cb]; + return () => { + this.cbsOnce[event].splice(this.cbsOnce[event].indexOf(cb), 1); + }; + } + + public destroyEventEmitter() { + debug.amountEmitters--; + Object.keys(this.cbs).forEach((key) => { + debug.amountCallbacks -= this.cbs[key].length; + delete this.cbs[key]; + }); + Object.keys(this.cbsOnce).forEach((key) => delete this.cbsOnce[key]); + this.cbs = {}; + this.cbsOnce = {}; + delete debug.emitters[this.index]; + } +} diff --git a/frontend/src/lib/helpers.ts b/frontend/src/lib/helpers/index.ts similarity index 100% rename from frontend/src/lib/helpers.ts rename to frontend/src/lib/helpers/index.ts diff --git a/frontend/src/lib/helpers/throttle.ts b/frontend/src/lib/helpers/throttle.ts new file mode 100644 index 0000000..9dcb09c --- /dev/null +++ b/frontend/src/lib/helpers/throttle.ts @@ -0,0 +1,20 @@ +export default ( + fn: (...args: A) => R, + delay: number +): ((...args: A) => R) => { + let wait = false; + + return (...args: A) => { + if (wait) return undefined; + + const val = fn(...args); + + wait = true; + + setTimeout(() => { + wait = false; + }, delay); + + return val; + } +}; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 2ebb019..4c3eca8 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -9,7 +9,17 @@ const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry); const graphManager = new GraphManager(nodeRegistry, runtimeExecutor); - graphManager.load(graphManager.createTemplate("grid", 5, 5)); + + let graph = localStorage.getItem("graph"); + if (graph) { + graphManager.load(JSON.parse(graph)); + } else { + graphManager.load(graphManager.createTemplate("grid", 5, 5)); + } + + graphManager.on("save", (graph) => { + localStorage.setItem("graph", JSON.stringify(graph)); + }); let debug: undefined;