refactor(event-bus): extract to shared module
The EventBus was duplicated inside shapeseq/ but is used by other playground code too. Move the canonical implementation to js/event-bus.js and turn the shapeseq copy into a re-export shim.
This commit is contained in:
parent
452754f448
commit
3f4ffcd4f9
2 changed files with 119 additions and 117 deletions
117
playground/js/event-bus.js
Normal file
117
playground/js/event-bus.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// Namespaced Event Bus
|
||||
// Lightweight pub/sub with seq.*/ml.*/ui.* namespaces and wildcard support.
|
||||
|
||||
// ── Event name constants ────────────────────────────────────────────
|
||||
|
||||
export const SEQ = Object.freeze({
|
||||
STEP: 'seq.step',
|
||||
NOTE_ON: 'seq.noteOn',
|
||||
NOTE_OFF: 'seq.noteOff',
|
||||
PARAM_CHANGE: 'seq.paramChange',
|
||||
LOOP_START: 'seq.loopStart',
|
||||
});
|
||||
|
||||
export const ML = Object.freeze({
|
||||
TRAINED: 'ml.trained',
|
||||
FROZEN: 'ml.frozen',
|
||||
UNFROZEN: 'ml.unfrozen',
|
||||
DELTA_UPDATE: 'ml.deltaUpdate',
|
||||
});
|
||||
|
||||
export const UI = Object.freeze({
|
||||
PARAM_SELECT: 'ui.paramSelect',
|
||||
CHAIN_EDIT: 'ui.chainEdit',
|
||||
PRESET_LOAD: 'ui.presetLoad',
|
||||
FREEZE_TOGGLE: 'ui.freezeToggle',
|
||||
});
|
||||
|
||||
// ── Bus implementation ──────────────────────────────────────────────
|
||||
|
||||
export class EventBus {
|
||||
/** @param {AudioContext} [audioCtx] - optional, used for seq.* timestamps */
|
||||
constructor(audioCtx) {
|
||||
this._listeners = new Map(); // event -> Set<callback>
|
||||
this._wildcards = new Map(); // namespace prefix (e.g. "seq") -> Set<callback>
|
||||
this._audioCtx = audioCtx ?? null;
|
||||
}
|
||||
|
||||
/** Provide or replace the AudioContext used for seq.* timestamps. */
|
||||
setAudioContext(ctx) {
|
||||
this._audioCtx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event or a wildcard namespace.
|
||||
* on('seq.step', cb) — exact match
|
||||
* on('seq.*', cb) — all events in the seq namespace
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (event.endsWith('.*')) {
|
||||
const ns = event.slice(0, -2); // "seq.*" -> "seq"
|
||||
if (!this._wildcards.has(ns)) this._wildcards.set(ns, new Set());
|
||||
this._wildcards.get(ns).add(callback);
|
||||
} else {
|
||||
if (!this._listeners.has(event)) this._listeners.set(event, new Set());
|
||||
this._listeners.get(event).add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** Unsubscribe. Mirrors the on() signature. */
|
||||
off(event, callback) {
|
||||
if (event.endsWith('.*')) {
|
||||
const ns = event.slice(0, -2);
|
||||
const set = this._wildcards.get(ns);
|
||||
if (set) {
|
||||
set.delete(callback);
|
||||
if (set.size === 0) this._wildcards.delete(ns);
|
||||
}
|
||||
} else {
|
||||
const set = this._listeners.get(event);
|
||||
if (set) {
|
||||
set.delete(callback);
|
||||
if (set.size === 0) this._listeners.delete(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event.
|
||||
* A `timestamp` field is added automatically:
|
||||
* - seq.* events use AudioContext.currentTime (seconds)
|
||||
* - all others use performance.now() (milliseconds)
|
||||
*/
|
||||
emit(event, data = {}) {
|
||||
const ns = event.split('.')[0];
|
||||
const stamped = Object.assign({ timestamp: this._stamp(ns) }, data);
|
||||
|
||||
// Exact listeners
|
||||
const exact = this._listeners.get(event);
|
||||
if (exact) {
|
||||
for (const cb of exact) cb(stamped, event);
|
||||
}
|
||||
|
||||
// Wildcard listeners for this namespace
|
||||
const wild = this._wildcards.get(ns);
|
||||
if (wild) {
|
||||
for (const cb of wild) cb(stamped, event);
|
||||
}
|
||||
}
|
||||
|
||||
// ── private ──
|
||||
|
||||
_stamp(ns) {
|
||||
if (ns === 'seq' && this._audioCtx) return this._audioCtx.currentTime;
|
||||
return performance.now();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Singleton convenience ───────────────────────────────────────────
|
||||
|
||||
let _default = null;
|
||||
|
||||
/** Return (and lazily create) the shared default bus. */
|
||||
export function getDefaultBus(audioCtx) {
|
||||
if (!_default) _default = new EventBus(audioCtx);
|
||||
else if (audioCtx) _default.setAudioContext(audioCtx);
|
||||
return _default;
|
||||
}
|
||||
|
|
@ -1,117 +1,2 @@
|
|||
// ShapeSeq Namespaced Event Bus
|
||||
// Lightweight pub/sub with seq.*/ml.*/ui.* namespaces and wildcard support.
|
||||
|
||||
// ── Event name constants ────────────────────────────────────────────
|
||||
|
||||
export const SEQ = Object.freeze({
|
||||
STEP: 'seq.step',
|
||||
NOTE_ON: 'seq.noteOn',
|
||||
NOTE_OFF: 'seq.noteOff',
|
||||
PARAM_CHANGE: 'seq.paramChange',
|
||||
LOOP_START: 'seq.loopStart',
|
||||
});
|
||||
|
||||
export const ML = Object.freeze({
|
||||
TRAINED: 'ml.trained',
|
||||
FROZEN: 'ml.frozen',
|
||||
UNFROZEN: 'ml.unfrozen',
|
||||
DELTA_UPDATE: 'ml.deltaUpdate',
|
||||
});
|
||||
|
||||
export const UI = Object.freeze({
|
||||
PARAM_SELECT: 'ui.paramSelect',
|
||||
CHAIN_EDIT: 'ui.chainEdit',
|
||||
PRESET_LOAD: 'ui.presetLoad',
|
||||
FREEZE_TOGGLE: 'ui.freezeToggle',
|
||||
});
|
||||
|
||||
// ── Bus implementation ──────────────────────────────────────────────
|
||||
|
||||
export class EventBus {
|
||||
/** @param {AudioContext} [audioCtx] - optional, used for seq.* timestamps */
|
||||
constructor(audioCtx) {
|
||||
this._listeners = new Map(); // event -> Set<callback>
|
||||
this._wildcards = new Map(); // namespace prefix (e.g. "seq") -> Set<callback>
|
||||
this._audioCtx = audioCtx ?? null;
|
||||
}
|
||||
|
||||
/** Provide or replace the AudioContext used for seq.* timestamps. */
|
||||
setAudioContext(ctx) {
|
||||
this._audioCtx = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event or a wildcard namespace.
|
||||
* on('seq.step', cb) — exact match
|
||||
* on('seq.*', cb) — all events in the seq namespace
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (event.endsWith('.*')) {
|
||||
const ns = event.slice(0, -2); // "seq.*" -> "seq"
|
||||
if (!this._wildcards.has(ns)) this._wildcards.set(ns, new Set());
|
||||
this._wildcards.get(ns).add(callback);
|
||||
} else {
|
||||
if (!this._listeners.has(event)) this._listeners.set(event, new Set());
|
||||
this._listeners.get(event).add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/** Unsubscribe. Mirrors the on() signature. */
|
||||
off(event, callback) {
|
||||
if (event.endsWith('.*')) {
|
||||
const ns = event.slice(0, -2);
|
||||
const set = this._wildcards.get(ns);
|
||||
if (set) {
|
||||
set.delete(callback);
|
||||
if (set.size === 0) this._wildcards.delete(ns);
|
||||
}
|
||||
} else {
|
||||
const set = this._listeners.get(event);
|
||||
if (set) {
|
||||
set.delete(callback);
|
||||
if (set.size === 0) this._listeners.delete(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event.
|
||||
* A `timestamp` field is added automatically:
|
||||
* - seq.* events use AudioContext.currentTime (seconds)
|
||||
* - all others use performance.now() (milliseconds)
|
||||
*/
|
||||
emit(event, data = {}) {
|
||||
const ns = event.split('.')[0];
|
||||
const stamped = Object.assign({ timestamp: this._stamp(ns) }, data);
|
||||
|
||||
// Exact listeners
|
||||
const exact = this._listeners.get(event);
|
||||
if (exact) {
|
||||
for (const cb of exact) cb(stamped, event);
|
||||
}
|
||||
|
||||
// Wildcard listeners for this namespace
|
||||
const wild = this._wildcards.get(ns);
|
||||
if (wild) {
|
||||
for (const cb of wild) cb(stamped, event);
|
||||
}
|
||||
}
|
||||
|
||||
// ── private ──
|
||||
|
||||
_stamp(ns) {
|
||||
if (ns === 'seq' && this._audioCtx) return this._audioCtx.currentTime;
|
||||
return performance.now();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Singleton convenience ───────────────────────────────────────────
|
||||
|
||||
let _default = null;
|
||||
|
||||
/** Return (and lazily create) the shared default bus. */
|
||||
export function getDefaultBus(audioCtx) {
|
||||
if (!_default) _default = new EventBus(audioCtx);
|
||||
else if (audioCtx) _default.setAudioContext(audioCtx);
|
||||
return _default;
|
||||
}
|
||||
// playground/js/shapeseq/event-bus.js — re-exports from shared module
|
||||
export { EventBus, SEQ, ML, UI, getDefaultBus } from '../event-bus.js';
|
||||
|
|
|
|||
Loading…
Reference in a new issue