Plugin lifecycle
A plugin’s factory runs once at load with a PluginContext. It may return optional hooks that let it participate in unload and hot-reload:
import { plugin } from '@s2script/sdk/plugin';
export default plugin((ctx) => {
// first load: ctx.previous is undefined
// hot-reload: ctx.previous is the value the old instance's state() returned
const prev = ctx.previous as { reloads: number } | undefined;
let reloads = (prev?.reloads ?? 0) + 1;
return {
onUnload() {
// best-effort cleanup (the ledger is the real teardown authority)
},
state() {
return { reloads }; // captured, serialized, and revived as the next ctx.previous
}
};
}); The factory may be async — the load waits for it to settle before the plugin is considered live.
Hot reload handoff
On a same-id file-watch Reload, the old instance’s state() return is serialized (JSON + EntityRef revival) and handed to the new instance as ctx.previous.
- Primitives, strings, arrays, and nested objects round-trip.
EntityRefvalues revive live and liveness-gated in the new context (isValid() === falseif the entity died in the gap).- Carry 64-bit values as decimal strings —
bigintcannot beJSON.stringify‘d and would drop the whole handoff.
Vanished (delete the .s2sp) clears any pending handoff. A later re-add is a fresh load with ctx.previous === undefined.
Map changes
Plugins persist across changelevel — the factory does not re-run per map. Subscribe to ctx.server.onMapStart for map-aware work:
export default plugin((ctx) => {
ctx.server.onMapStart((mapName) => {
console.log('map start', mapName);
});
}); Teardown
The host ledger owns every persistent resource (commands, event subs, DB handles, websockets, imported interfaces). Unload walks the ledger in reverse-dependency order, so cleanup does not depend on the plugin’s own onUnload running correctly — onUnload is for best-effort work, not for releasing framework resources.
See Authoring and the server module.