Service Lifecycle

Services follow a simple three-phase lifecycle: init, event handling, and dispose.

Lifecycle Flow

register(name, module)


  init()           ← called once, set up state/timers


  onEvent(...)     ← called for every overlay event while active


  dispose()        ← widget unloads or overlay disconnects

init()

Called once immediately after service.register(). Use it to set up initial state, start timers, and load persisted data:
{
  async init() {
    this.scores = (await SBCanvas.store.get('scores')) || {};
    this.pollId = SBCanvas.setInterval(() => this.refresh(), 30000);
  }
}
init() can be async. The service is considered active immediately regardless of whether init’s promise has resolved.

onEvent(event, data)

Called for every event that passes through the overlay event bus while the service is active:
{
  onEvent(event, data) {
    if (event === 'activity:tip') {
      this.handleTip(data);
    }
    if (event === 'activity:subscribe') {
      this.handleSub(data);
    }
    if (event === 'chat:message') {
      this.handleChat(data);
    }
  }
}
The event parameter is the full event name (e.g. 'activity:tip', 'session:update', 'chat:message').

dispose()

Called when the service is being shut down. Clean up timers, save state, release resources:
{
  dispose() {
    SBCanvas.clearTimer(this.pollId);
    SBCanvas.store.set('scores', this.scores);
  }
}

When dispose() is called

TriggerWhat happens
service.dispose(name) calledThat service is disposed
service.disposeAll() calledAll services are disposed
Widget unloadsAll services for that widget are disposed
Overlay disconnectsAll services are disposed
Overlay reconnectsServices are re-initialized (init called again)

Survival Rules

EventService survives?
Widget field change (setField)Yes
Widget layout updateYes
Widget unloadNo — disposed
Overlay disconnectNo — disposed
Overlay reconnectRe-initialized (new init)

Example: Chat Command Service

SBCanvas.service.register('commands', {
  init() {
    this.cooldowns = {};
  },

  onEvent(event, data) {
    if (event !== 'chat:message') return;
    if (!data.text.startsWith('!')) return;

    const cmd = data.text.split(' ')[0].slice(1);
    if (this.cooldowns[cmd] && Date.now() - this.cooldowns[cmd] < 10000) return;

    this.cooldowns[cmd] = Date.now();

    if (cmd === 'confetti') {
      SBCanvas.confetti({ count: 100 });
    }
    if (cmd === 'shake') {
      SBCanvas.shake(document.getElementById('overlay'), 5);
    }
  },

  dispose() {}
});