« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/data/patches.js384
1 files changed, 369 insertions, 15 deletions
diff --git a/src/data/patches.js b/src/data/patches.js
index 3b9c8c3..3ed4fad 100644
--- a/src/data/patches.js
+++ b/src/data/patches.js
@@ -1,25 +1,379 @@
-export class PatchManager {
-    patches = [];
-}
+// --> Patch
 
 export class Patch {
-    static type = class {};
+    static INPUT_NONE = 0;
+    static INPUT_CONSTANT = 1;
+    static INPUT_DIRECT_CONNECTION = 2;
+    static INPUT_MANAGED_CONNECTION = 3;
+
+    static INPUT_UNAVAILABLE = 0;
+    static INPUT_AVAILABLE = 1;
+
+    static OUTPUT_UNAVAILABLE = 0;
+    static OUTPUT_AVAILABLE = 1;
+
+    static inputNames = []; inputNames = null;
+    static outputNames = []; outputNames = null;
+
+    manager = null;
+    inputs = Object.create(null);
+
+    constructor({
+        manager,
+
+        inputNames,
+        outputNames,
+
+        inputs,
+    } = {}) {
+        this.inputNames = inputNames ?? this.constructor.inputNames;
+        this.outputNames = outputNames ?? this.constructor.outputNames;
+
+        manager?.addManagedPatch(this);
+
+        if (inputs) {
+            Object.assign(this.inputs, inputs);
+        }
+
+        this.initializeInputs();
+    }
+
+    initializeInputs() {
+        for (const inputName of this.inputNames) {
+            if (!this.inputs[inputName]) {
+                this.inputs[inputName] = [Patch.INPUT_NONE];
+            }
+        }
+    }
+
+    computeInputs() {
+        const inputs = Object.create(null);
+
+        for (const inputName of this.inputNames) {
+            const input = this.inputs[inputName];
+            switch (input[0]) {
+                case Patch.INPUT_NONE:
+                    inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
+                    break;
+
+                case Patch.INPUT_CONSTANT:
+                    inputs[inputName] = [Patch.INPUT_AVAILABLE, input[1]];
+                    break;
+
+                case Patch.INPUT_DIRECT_CONNECTION: {
+                    const patch = input[1];
+                    const outputName = input[2];
+                    const output = patch.computeOutputs()[outputName];
+                    switch (output[0]) {
+                        case Patch.OUTPUT_UNAVAILABLE:
+                            inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
+                            break;
+                        case Patch.OUTPUT_AVAILABLE:
+                            inputs[inputName] = [Patch.INPUT_AVAILABLE, output[1]];
+                            break;
+                    }
+                    throw new Error('Unreachable');
+                }
+
+                case Patch.INPUT_MANAGED_CONNECTION: {
+                    if (!this.manager) {
+                        inputs[inputName] = [Patch.INPUT_UNAVAILABLE];
+                        break;
+                    }
 
-    static inputDescriptors = {};
-    static outputDescriptors = {};
+                    inputs[inputName] = this.manager.getManagedInput(input[1]);
+                    break;
+                }
+            }
+        }
+
+        return inputs;
+    }
+
+    computeOutputs() {
+        const inputs = this.computeInputs();
+        const outputs = Object.create(null);
+        console.log(`Compute: ${this.constructor.name}`);
+        this.compute(inputs, outputs);
+        return outputs;
+    }
+
+    compute(inputs, outputs) {
+        // No-op. Return all outputs as unavailable. This should be overridden
+        // in subclasses.
+
+        for (const outputName of this.constructor.outputNames) {
+            outputs[outputName] = [Patch.OUTPUT_UNAVAILABLE];
+        }
+    }
+
+    attachToManager(manager) {
+        manager.addManagedPatch(this);
+    }
+
+    detachFromManager() {
+        if (this.manager) {
+            this.manager.removeManagedPatch(this);
+        }
+    }
 }
 
-const patches = {};
+// --> PatchManager
 
-patches.common = {};
+export class PatchManager extends Patch {
+    managedPatches = [];
+    managedInputs = {};
 
-Object.assign(patches.common, {
-});
+    #externalInputPatch = null;
+    #externalOutputPatch = null;
+
+    constructor(...args) {
+        super(...args);
+
+        this.#externalInputPatch = new PatchManagerExternalInputPatch({manager: this});
+        this.#externalOutputPatch = new PatchManagerExternalOutputPatch({manager: this});
+    }
+
+    addManagedPatch(patch) {
+        if (patch.manager === this) {
+            return false;
+        }
+
+        patch.detachFromManager();
+        patch.manager = this;
+
+        if (patch.manager === this) {
+            this.managedPatches.push(patch);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    removeManagedPatch(patch) {
+        if (patch.manager !== this) {
+            return false;
+        }
+
+        patch.manager = null;
+
+        if (patch.manager === this) {
+            return false;
+        }
+
+        for (const inputNames of patch.inputNames) {
+            const input = patch.inputs[inputName];
+            if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
+                this.dropManagedInput(input[1]);
+                patch.inputs[inputName] = [Patch.INPUT_NONE];
+            }
+        }
+
+        this.managedPatches.splice(this.managedPatches.indexOf(patch), 1);
 
-patches.hsmusic = {};
+        return true;
+    }
 
-Object.assign(patches.hsmusic, {
-    Album: class extends Patch {},
-    Artist: class extends Patch {},
-    Track: class extends Patch {},
+    addManagedInput(patchWithInput, inputName, patchWithOutput, outputName) {
+        if (patchWithInput.manager !== this || patchWithOutput.manager !== this) {
+            throw new Error(`Input and output patches must belong to same manager (this)`);
+        }
+
+        const input = patchWithInput.inputs[inputName];
+        if (input[0] === Patch.INPUT_MANAGED_CONNECTION) {
+            this.managedInputs[input[1]] = [patchWithOutput, outputName, {}];
+        } else {
+            const key = this.getManagedConnectionIdentifier();
+            this.managedInputs[key] = [patchWithOutput, outputName, {}];
+            patchWithInput.inputs[inputName] = [Patch.INPUT_MANAGED_CONNECTION, key];
+        }
+
+        return true;
+    }
+
+    dropManagedInput(identifier) {
+        return delete this.managedInputs[key];
+    }
+
+    getManagedInput(identifier) {
+        const connection = this.managedInputs[identifier];
+        const patch = connection[0];
+        const outputName = connection[1];
+        const memory = connection[2];
+        return this.computeManagedInput(patch, outputName, memory);
+    }
+
+    computeManagedInput(patch, outputName, memory) {
+        // Override this function in subclasses to alter behavior of the "wire"
+        // used for connecting patches.
+
+        const output = patch.computeOutputs()[outputName];
+        switch (output[0]) {
+            case Patch.OUTPUT_UNAVAILABLE:
+                return [Patch.INPUT_UNAVAILABLE];
+            case Patch.OUTPUT_AVAILABLE:
+                return [Patch.INPUT_AVAILABLE, output[1]];
+        }
+    }
+
+    #managedConnectionIdentifier = 0;
+    getManagedConnectionIdentifier() {
+        return this.#managedConnectionIdentifier++;
+    }
+
+    addExternalInput(patchWithInput, patchInputName, managerInputName) {
+        return this.addManagedInput(patchWithInput, patchInputName, this.#externalInputPatch, managerInputName);
+    }
+
+    setExternalOutput(managerOutputName, patchWithOutput, patchOutputName) {
+        return this.addManagedInput(this.#externalOutputPatch, managerOutputName, patchWithOutput, patchOutputName);
+    }
+
+    compute(inputs, outputs) {
+        Object.assign(outputs, this.#externalOutputPatch.computeOutputs());
+    }
+}
+
+class PatchManagerExternalInputPatch extends Patch {
+    constructor({manager, ...rest}) {
+        super({
+            manager,
+            inputNames: manager.inputNames,
+            outputNames: manager.inputNames,
+            ...rest
+        });
+    }
+
+    computeInputs() {
+        return this.manager.computeInputs();
+    }
+
+    compute(inputs, outputs) {
+        for (const name of this.inputNames) {
+            const input = inputs[name];
+            switch (input[0]) {
+                case Patch.INPUT_UNAVAILABLE:
+                    outputs[name] = [Patch.OUTPUT_UNAVAILABLE];
+                    break;
+                case Patch.INPUT_AVAILABLE:
+                    outputs[name] = [Patch.INPUT_AVAILABLE, input[1]];
+                    break;
+            }
+        }
+    }
+}
+
+class PatchManagerExternalOutputPatch extends Patch {
+    constructor({manager, ...rest}) {
+        super({
+            manager,
+            inputNames: manager.outputNames,
+            outputNames: manager.outputNames,
+            ...rest
+        });
+    }
+
+    compute(inputs, outputs) {
+        for (const name of this.inputNames) {
+            const input = inputs[name];
+            switch (input[0]) {
+                case Patch.INPUT_UNAVAILABLE:
+                    outputs[name] = [Patch.OUTPUT_UNAVAILABLE];
+                    break;
+                case Patch.INPUT_AVAILABLE:
+                    outputs[name] = [Patch.INPUT_AVAILABLE, input[1]];
+                    break;
+            }
+        }
+    }
+}
+
+// --> demo
+
+const caches = Symbol();
+const common = Symbol();
+const hsmusic = Symbol();
+
+Patch[caches] = {
+    WireCachedPatchManager: class extends PatchManager {
+        // "Wire" caching for PatchManager: Remembers the last outputs to come
+        // from each patch. As long as the inputs for a patch do not change, its
+        // cached outputs are reused.
+
+        // TODO: This has a unique cache for each managed input. It should
+        // re-use a cache for the same patch and output name. How can we ensure
+        // the cache is dropped when the patch is removed, though? (Spoilers:
+        // probably just override removeManagedPatch)
+        computeManagedInput(patch, outputName, memory) {
+            let cache = true;
+
+            const { previousInputs } = memory;
+            const { inputs } = patch;
+            if (memory.previousInputs) {
+                for (const inputName of patch.inputNames) {
+                    // TODO: This doesn't account for connections whose values
+                    // have changed (analogous to bubbling cache invalidation).
+                    if (inputs[inputName] !== previousInputs[inputName]) {
+                        cache = false;
+                        break;
+                    }
+                }
+            } else {
+                cache = false;
+            }
+
+            if (cache) {
+                return memory.previousOutputs[outputName];
+            }
+
+            const outputs = patch.computeOutputs();
+            memory.previousOutputs = outputs;
+            memory.previousInputs = {...inputs};
+            return outputs[outputName];
+        }
+    },
+};
+
+Patch[common] = {
+    Stringify: class extends Patch {
+        static inputNames = ['value'];
+        static outputNames = ['value'];
+
+        compute(inputs, outputs) {
+            if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
+                outputs.value = [Patch.OUTPUT_AVAILABLE, inputs.value[1].toString()];
+            } else {
+                outputs.value = [Patch.OUTPUT_UNAVAILABLE];
+            }
+        }
+    },
+
+    Echo: class extends Patch {
+        static inputNames = ['value'];
+        static outputNames = ['value'];
+
+        compute(inputs, outputs) {
+            if (inputs.value[0] === Patch.INPUT_AVAILABLE) {
+                outputs.value = [Patch.OUTPUT_AVAILABLE, inputs.value[1]];
+            } else {
+                outputs.value = [Patch.OUTPUT_UNAVAILABLE];
+            }
+        }
+    },
+};
+
+const PM = new Patch[caches].WireCachedPatchManager({
+    inputNames: ['externalInput'],
+    outputNames: ['externalOutput'],
 });
+
+const P1 = new Patch[common].Stringify({manager: PM});
+const P2 = new Patch[common].Echo({manager: PM});
+
+PM.addExternalInput(P1, 'value', 'externalInput');
+PM.addManagedInput(P2, 'value', P1, 'value');
+PM.setExternalOutput('externalOutput', P2, 'value');
+
+PM.inputs.externalInput = [Patch.INPUT_CONSTANT, 123];
+console.log(PM.computeOutputs());
+console.log(PM.computeOutputs());