User callout: 'Hax' is Kaysser's nickname. The module id should not use it. Rename the Foundry module id from 'hax-hooks-lib' to 'foundry-hooks-lib'. Gitea repo name stays as 'hooks-lib' (kept for the user-facing URL); the Gitea manifest URL is unchanged. **Scope of rename:** - module.json: id, title, version (0.2.0 -> 0.3.0), download URL - package.json: name - README.md, HOOK_CONTRACT.md, LICENSE: branding text - All 6 production JS files: MODULE_ID constant + comments - 4 active test files: console.log strings + test descriptions - Rename of release zips in git: hooks-lib-X.Y.Z.zip -> foundry-hooks-lib-X.Y.Z.zip (preserves the v0.1.0 and v0.2.0 zips as historical artifacts; the v0.3.0 zip is the new release artifact) - .gitignore: glob + un-ignore lines updated to match **Out of scope (deliberate):** - Gitea repo name 'kaykayyali/hooks-lib' stays. Per the user's direction, only the module id is renamed; the Gitea URL path is preserved for the existing 'url', 'manifest', 'download' fields. - scripts/_archive/v0.1.0/*: historical v0.1.0 code is left as-is. Those files tested 'hax-hooks-lib v0.1.0'; rewriting the history would be misleading. - tests/_archive_v0.1.0_*.mjs: same reason, left untouched. - .hermes/plans/* session-historian plans that reference 'Hax's Tools split': session artifact, not a release asset. **Verification:** 554/554 smoke assertions pass, 6/6 perf assertions pass, median 0.0004ms/fire (well under 0.1ms budget). No logic change; rename is string-only. **Consumer action required:** battle-focus and its-achievable both declare 'relationships.requires' pointing to 'hax-hooks-lib'. The next commits on those repos will update their relationships to 'foundry-hooks-lib' + bump their versions. Foundry instances with v0.2.0 of the old id installed will need to be reinstalled as v0.3.0 of the new id.
139 lines
4.1 KiB
JavaScript
139 lines
4.1 KiB
JavaScript
// scripts/internal/subscribers.js
|
|
//
|
|
// HOOK_CONTRACT.md §3: subscribe primitives + §3.5 error containment.
|
|
//
|
|
// subscribe(hookName, fn) — single hook, single handler
|
|
// subscribeMany(map) — atomic batch
|
|
// subscribeAll(fn) — every hook, one handler
|
|
// unsubscribeAll() — purge everything
|
|
//
|
|
// Error containment (§3.5): throwing consumer callbacks are caught,
|
|
// logged via console.error with the [foundry-hooks-lib] prefix and the
|
|
// hook name, and the dispatch chain continues to the next callback.
|
|
// Errors never propagate to Foundry's hook chain.
|
|
//
|
|
// Multiple subscriptions for the same hook stack in registration
|
|
// order.
|
|
|
|
import { REGISTERED_HOOKS } from "./registered-hooks.js";
|
|
|
|
const MODULE_ID = "foundry-hooks-lib";
|
|
|
|
// Per-envelope callback list. Order = registration order.
|
|
const _callbacks = new Map(); // hookName (envelope) -> [fn, fn, ...]
|
|
const _allCallbacks = []; // subscribeAll subscribers, in order
|
|
|
|
function assertRegistered(hookName) {
|
|
if (!REGISTERED_HOOKS.includes(hookName)) {
|
|
throw new TypeError(
|
|
`subscribe("${hookName}"): hook not in REGISTERED_HOOKS. ` +
|
|
`Available hooks: ${REGISTERED_HOOKS.length}. Use listActiveAdapters() + adapter factory to add system-specific derived events.`
|
|
);
|
|
}
|
|
}
|
|
|
|
export function subscribe(hookName, fn) {
|
|
if (typeof fn !== "function") {
|
|
throw new TypeError(`subscribe(hookName, fn): fn must be a function`);
|
|
}
|
|
assertRegistered(hookName);
|
|
if (!_callbacks.has(hookName)) {
|
|
_callbacks.set(hookName, []);
|
|
}
|
|
_callbacks.get(hookName).push(fn);
|
|
return () => {
|
|
const list = _callbacks.get(hookName);
|
|
if (!list) return;
|
|
const idx = list.indexOf(fn);
|
|
if (idx >= 0) list.splice(idx, 1);
|
|
};
|
|
}
|
|
|
|
export function subscribeMany(map) {
|
|
if (!map || typeof map !== "object") {
|
|
throw new TypeError(`subscribeMany(map): map must be an object`);
|
|
}
|
|
const entries = Object.entries(map);
|
|
// Atomicity: validate all names first. If any fails, throw WITHOUT
|
|
// registering any. (We don't register until all checks pass.)
|
|
for (const [name, fn] of entries) {
|
|
if (typeof fn !== "function") {
|
|
throw new TypeError(
|
|
`subscribeMany: handler for "${name}" must be a function`
|
|
);
|
|
}
|
|
assertRegistered(name);
|
|
}
|
|
// All validated — register all.
|
|
const unsubs = entries.map(([name, fn]) => subscribe(name, fn));
|
|
return () => {
|
|
for (const u of unsubs) u();
|
|
};
|
|
}
|
|
|
|
export function subscribeAll(fn) {
|
|
if (typeof fn !== "function") {
|
|
throw new TypeError(`subscribeAll(fn): fn must be a function`);
|
|
}
|
|
_allCallbacks.push(fn);
|
|
return () => {
|
|
const idx = _allCallbacks.indexOf(fn);
|
|
if (idx >= 0) _allCallbacks.splice(idx, 1);
|
|
};
|
|
}
|
|
|
|
export function unsubscribeAll() {
|
|
_callbacks.clear();
|
|
_allCallbacks.length = 0;
|
|
}
|
|
|
|
// Internal: dispatch an envelope to all matching callbacks + all
|
|
// subscribeAll subscribers. Called by the envelope dispatcher.
|
|
//
|
|
// For sync-mode hooks, this runs inline. For async-mode hooks, the
|
|
// envelope dispatcher wraps this in a microtask.
|
|
//
|
|
// Returns true if any callback threw (for diagnostics). Never re-throws.
|
|
export function dispatch(envelope) {
|
|
let threwAny = false;
|
|
// Targeted callbacks.
|
|
const list = _callbacks.get(envelope.hook);
|
|
if (list) {
|
|
for (const fn of list) {
|
|
try {
|
|
fn(envelope);
|
|
} catch (e) {
|
|
threwAny = true;
|
|
console.error(
|
|
`[${MODULE_ID}] consumer callback for "${envelope.hook}" threw:`,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Wildcard subscribers (subscribeAll). These also receive every
|
|
// envelope; consumers dispatch on envelope.hook themselves.
|
|
for (const fn of _allCallbacks) {
|
|
try {
|
|
fn(envelope);
|
|
} catch (e) {
|
|
threwAny = true;
|
|
console.error(
|
|
`[${MODULE_ID}] subscribeAll callback for "${envelope.hook}" threw:`,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
return threwAny;
|
|
}
|
|
|
|
// Internal: list envelope names that have at least one callback.
|
|
// Used by tests for verification.
|
|
export function listSubscribedHooks() {
|
|
return [..._callbacks.keys()];
|
|
}
|
|
|
|
export function hasCallbacksFor(envelopeName) {
|
|
const list = _callbacks.get(envelopeName);
|
|
return !!list && list.length > 0;
|
|
} |