Files
hooks-lib/scripts/internal/envelope.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

80 lines
2.8 KiB
JavaScript

// scripts/internal/envelope.js
//
// HOOK_CONTRACT.md §2 (envelope shape) + §8 (sync vs async dispatch).
//
// buildEnvelope(rawHookName, args): produces a {ts, hook, args} object
// or null if the raw hook is not in our registered set.
//
// dispatchEnvelope(envelope): routes the envelope to subscribers via
// the subscribers module. For sync-mode hooks, this is inline. For
// async-mode hooks, this is deferred to a microtask.
//
// This is the only module that touches the envelope shape. All other
// modules consume envelopes through subscribers.subscribe.
import { getEntryForRawName, getEntryForEnvelope } from "./registered-hooks.js";
import { ARG_SHAPES, maybeSynthesize } from "./anti-corruption.js";
import { dispatch } from "./subscribers.js";
const MODULE_ID = "foundry-hooks-lib";
/**
* Build an envelope for a Foundry hook fire.
*
* Returns:
* - { envelope, synthesized?: true } for the primary envelope
* - or null if the raw hook is not registered
*
* For hooks that synthesize additional envelopes (combatInactive from
* updateCombat), the synthesized envelope(s) are also returned in the
* `synthesized` array on the result.
*/
export function buildEnvelope(rawHookName, args) {
const entry = getEntryForRawName(rawHookName);
if (!entry) return null;
const normalize = ARG_SHAPES[entry.envelope];
const normalizedArgs = normalize ? normalize(args ?? []) : (args ?? []);
const ts = Date.now();
const primary = { ts, hook: entry.envelope, args: normalizedArgs };
// Anti-corruption: hooks with synthesized envelopes may produce
// additional envelopes from a single Foundry fire.
const synth = maybeSynthesize(rawHookName, args);
return { envelope: primary, synthesized: synth };
}
/**
* Dispatch an envelope. Called by the Foundry hook wrappers
* registered in main.js.
*
* For sync-mode hooks, dispatches inline. For async-mode hooks,
* schedules dispatch on a microtask. The wrapper returns immediately
* in both cases (microtask dispatch returns synchronously to Foundry).
*
* Returns immediately in both modes — never awaited by callers.
*/
export function dispatchEnvelope(envelope, mode) {
if (mode === "sync") {
dispatch(envelope);
} else {
// Async dispatch via microtask. We schedule a single microtask
// per envelope; the dispatch itself runs synchronously inside
// the microtask.
Promise.resolve().then(() => dispatch(envelope));
}
}
/**
* Dispatch a batch of envelopes. Used when a single Foundry fire
* produces multiple envelopes (e.g. updateCombat produces both
* updateCombat and combatInactive). Each envelope is dispatched
* according to its own mode.
*/
export function dispatchEnvelopes(envelopes) {
for (const env of envelopes) {
const entry = getEntryForEnvelope(env.hook);
if (!entry) continue;
dispatchEnvelope(env, entry.mode);
}
}
export { MODULE_ID };