Files
hooks-lib/scripts/internal/lifecycle.js
Kaysser Kayyali d038eb8c67 v0.3.0: rename module id hax-hooks-lib -> foundry-hooks-lib
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.
2026-06-20 16:53:37 -04:00

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 };