Lab
Command
type Levels = Record<string, number>;
type Change = { levels: Levels; inverse: Command };
// A request the controller runs. It checks itself when it runs: it throws when
// the request is invalid and returns null when there is nothing to change.
interface Command {
label: string;
execute(levels: Levels): Change | null;
}
// The level a light has now, from 0 (off) to 100.
function current(levels: Levels, light: string): number {
const level = levels[light];
if (level === undefined) throw new Error('unknown light: ' + light);
return level;
}
function setLevel(light: string, level: number): Command {
return {
label: 'Set',
execute(levels) {
const before = current(levels, light);
if (level < 0 || level > 100) throw new Error('level must be from 0 to 100: ' + level);
if (level === before) return null;
return { levels: { ...levels, [light]: level }, inverse: setLevel(light, before) };
},
};
}
// One notch of the dimmer wheel: dims a light by step, never below 0.
// Draft: Undo brightens the light back by the same step.
function dimBy(light: string, step: number): Command {
return {
label: 'Dim',
execute(levels) {
const level = Math.max(0, current(levels, light) - step);
return { levels: { ...levels, [light]: level }, inverse: brightenBy(light, step) };
},
};
}
function brightenBy(light: string, step: number): Command {
return {
label: 'Brighten',
execute(levels) {
const level = Math.min(100, current(levels, light) + step);
return { levels: { ...levels, [light]: level }, inverse: dimBy(light, step) };
},
};
}
type Entry = { forward: Command; inverse: Command };
// The controller runs every request (wall panel, phone app, schedule), one at
// a time, and keeps the home's history.
class Controller {
levels: Levels;
past: Entry[] = [];
future: Entry[] = [];
constructor(levels: Levels) {
this.levels = { ...levels };
}
execute(command: Command): boolean {
const change = command.execute(this.levels);
if (!change) return false;
this.levels = change.levels;
this.past.push({ forward: command, inverse: change.inverse });
this.future = [];
return true;
}
undo(): boolean {
const entry = this.past.at(-1);
const change = entry?.inverse.execute(this.levels);
if (!entry || !change) return false;
this.levels = change.levels;
this.future.push(this.past.pop()!);
return true;
}
redo(): boolean {
const entry = this.future.at(-1);
const change = entry?.forward.execute(this.levels);
if (!entry || !change) return false;
this.levels = change.levels;
this.past.push(this.future.pop()!);
return true;
}
}