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.
123 lines
3.8 KiB
JavaScript
123 lines
3.8 KiB
JavaScript
// scripts/internal/lifecycle.js
|
|
//
|
|
// HOOK_CONTRACT.md §4: lifecycle management.
|
|
//
|
|
// install(): register Foundry hooks for every raw hook name in the
|
|
// registry. Idempotent — calling twice is a no-op.
|
|
// uninstall(): remove every Foundry hook the library registered.
|
|
// Idempotent.
|
|
//
|
|
// evaluateAdaptersAtReady(): called from the library's ready hook.
|
|
// Reads game.system and game.version, evaluates adapters.
|
|
|
|
import { allRawHookNames, getEntryForRawName } from "./registered-hooks.js";
|
|
import { buildEnvelope, dispatchEnvelope, dispatchEnvelopes } from "./envelope.js";
|
|
import {
|
|
evaluateAtReady,
|
|
listActiveAdapters,
|
|
listFailedAdapters,
|
|
reset as resetAdapters,
|
|
} from "./adapter-registry.js";
|
|
import { unsubscribeAll } from "./subscribers.js";
|
|
|
|
const MODULE_ID = "foundry-hooks-lib";
|
|
|
|
// Track which Foundry hooks we've registered and the listener fn, so
|
|
// uninstall can remove them.
|
|
const _registered = new Map(); // rawHookName -> listener fn
|
|
|
|
export function install() {
|
|
if (_registered.size > 0) return; // idempotent
|
|
if (typeof Hooks === "undefined" || !Hooks) {
|
|
throw new Error(
|
|
`[${MODULE_ID}] install() called before Foundry hooks are available. ` +
|
|
`Ensure install() runs inside the library's init hook.`
|
|
);
|
|
}
|
|
for (const rawName of allRawHookNames()) {
|
|
const entry = getEntryForRawName(rawName);
|
|
if (!entry) continue;
|
|
// The wrapper: builds envelopes and dispatches them.
|
|
const listener = (...args) => {
|
|
// Synthetic hooks (combatInactive) are emitted from updateCombat;
|
|
// skip the wrapper for the raw hook that drives synthesis.
|
|
// buildEnvelope returns the primary + any synthesized.
|
|
const result = buildEnvelope(rawName, args);
|
|
if (!result) return;
|
|
// Dispatch primary.
|
|
dispatchEnvelope(result.envelope, entry.mode);
|
|
// Dispatch synthesized envelopes (e.g. combatInactive).
|
|
if (result.synthesized && result.synthesized.length > 0) {
|
|
dispatchEnvelopes(
|
|
result.synthesized.map((s) => ({
|
|
ts: result.envelope.ts,
|
|
hook: s.hook,
|
|
args: s.args,
|
|
}))
|
|
);
|
|
}
|
|
};
|
|
Hooks.on(rawName, listener);
|
|
_registered.set(rawName, listener);
|
|
}
|
|
}
|
|
|
|
export function uninstall() {
|
|
if (typeof Hooks === "undefined" || !Hooks) {
|
|
// Best-effort: clear local state even if Foundry isn't around.
|
|
_registered.clear();
|
|
unsubscribeAll();
|
|
resetAdapters();
|
|
return;
|
|
}
|
|
for (const [rawName, listener] of _registered) {
|
|
try {
|
|
Hooks.off(rawName, listener);
|
|
} catch (e) {
|
|
console.warn(`[${MODULE_ID}] uninstall: Hooks.off(${rawName}) threw:`, e);
|
|
}
|
|
}
|
|
_registered.clear();
|
|
unsubscribeAll();
|
|
resetAdapters();
|
|
}
|
|
|
|
/**
|
|
* Evaluate adapters at the library's ready hook.
|
|
*
|
|
* Reads game.system and game.version. Calls adapter-registry's
|
|
* evaluateAtReady. Logs a single summary line at info level.
|
|
*
|
|
* Safe to call when game is undefined (returns silently).
|
|
*/
|
|
export function evaluateAdaptersAtReady() {
|
|
if (typeof game === "undefined" || !game) return;
|
|
const systemId = game.system?.id;
|
|
const systemVersion = game.system?.version;
|
|
const foundryVersion = game.version;
|
|
if (!systemId) {
|
|
console.warn(
|
|
`[${MODULE_ID}] ready: game.system.id is undefined; skipping adapter evaluation`
|
|
);
|
|
return;
|
|
}
|
|
evaluateAtReady(
|
|
{ id: systemId, version: systemVersion ?? "0.0.0" },
|
|
foundryVersion ?? "0.0.0"
|
|
);
|
|
const active = listActiveAdapters();
|
|
const failed = listFailedAdapters();
|
|
console.log(
|
|
`[${MODULE_ID}] ready: ${active.length} adapter(s) active, ${failed.length} failed`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* List the raw hook names this library has registered a Foundry
|
|
* listener for. For introspection (tests).
|
|
*/
|
|
export function listInstalledRawHooks() {
|
|
return [..._registered.keys()];
|
|
}
|
|
|
|
export { MODULE_ID }; |