diff options
Diffstat (limited to 'src/data/patches.js')
-rw-r--r-- | src/data/patches.js | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/src/data/patches.js b/src/data/patches.js new file mode 100644 index 0000000..feeaf39 --- /dev/null +++ b/src/data/patches.js @@ -0,0 +1,395 @@ +// --> Patch + +export class Patch { + 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; + } + + 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); + } + } +} + +// --> PatchManager + +export class PatchManager extends Patch { + managedPatches = []; + managedInputs = {}; + + #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 inputName 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); + + return true; + } + + 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[identifier]; + } + + 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) { + // 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(); + +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()); |