fix(hooks): use globalThis singleton for handler registry to survive bundle splitting#32292
Conversation
Greptile SummaryThis PR fixes a silent hook-firing regression introduced by bundle splitting: because the bundler could emit Key observations:
Confidence Score: 4/5
Last reviewed commit: 3e8bd73 |
src/hooks/internal-hooks.ts
Outdated
| if (!_g.__openclaw_internal_hook_handlers__) { | ||
| _g.__openclaw_internal_hook_handlers__ = new Map<string, InternalHookHandler[]>(); | ||
| } | ||
| const handlers = _g.__openclaw_internal_hook_handlers__; |
There was a problem hiding this comment.
Missing non-null assertion on handlers assignment
After the if initialization guard, TypeScript may still infer handlers as Map<string, InternalHookHandler[]> | undefined because control-flow narrowing does not generally apply to mutable object properties (TypeScript cannot guarantee the property wasn't modified between the if block and this line). Adding a ! non-null assertion makes the intent explicit and guards against type errors if strictNullChecks is tightened in the future.
| const handlers = _g.__openclaw_internal_hook_handlers__; | |
| const handlers = _g.__openclaw_internal_hook_handlers__!; |
Alternatively, the three lines can be collapsed with the nullish assignment operator for clarity:
_g.__openclaw_internal_hook_handlers__ ??= new Map<string, InternalHookHandler[]>();
const handlers = _g.__openclaw_internal_hook_handlers__;Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/internal-hooks.ts
Line: 219
Comment:
**Missing non-null assertion on `handlers` assignment**
After the `if` initialization guard, TypeScript may still infer `handlers` as `Map<string, InternalHookHandler[]> | undefined` because control-flow narrowing does not generally apply to mutable object properties (TypeScript cannot guarantee the property wasn't modified between the `if` block and this line). Adding a `!` non-null assertion makes the intent explicit and guards against type errors if `strictNullChecks` is tightened in the future.
```suggestion
const handlers = _g.__openclaw_internal_hook_handlers__!;
```
Alternatively, the three lines can be collapsed with the nullish assignment operator for clarity:
```ts
_g.__openclaw_internal_hook_handlers__ ??= new Map<string, InternalHookHandler[]>();
const handlers = _g.__openclaw_internal_hook_handlers__;
```
How can I resolve this? If you propose a fix, please make it concise.…bundle splitting Without this fix, the bundler can emit multiple copies of internal-hooks into separate chunks. registerInternalHook writes to one Map instance while triggerInternalHook reads from another — resulting in hooks that silently fire with zero handlers regardless of how many were registered. Reproduce: load a hook via hooks.external.entries (loader reads one chunk), then send a message:transcribed event (get-reply imports a different chunk). The handler list is empty; the hook never runs. Fix: use globalThis.__openclaw_internal_hook_handlers__ as a shared singleton. All module copies check for and reuse the same Map, ensuring registrations are always visible to triggers.
Addresses greptile review: collapses the if-guard + assignment into a single ??= expression so TypeScript can narrow the type without a non-null assertion.
ee5ea80 to
8fee445
Compare
|
Landed via temp rebase onto main.
Thanks @Drickon! |
Problem
After PR #9859 landed, hooks registered via
registerInternalHookwere silently never firing in production. The bundler emitsinternal-hooksinto multiple chunks — the loader registers handlers into one chunk's Map, whiletriggerInternalHookinget-reply.tsreads from a different chunk's Map. The handler list is empty; hooks run zero times.Reproduce: enable any external hook via
hooks.external.entriesthat listens onmessage:transcribed. Send a voice message. The handler is registered at startup but never invoked.Fix
Use
globalThis.__openclaw_internal_hook_handlers__as a shared singleton. All module copies check for and reuse the same Map on first access, so registrations from one chunk are always visible to triggers in another.Tests
All 29 existing
internal-hooks.test.tstests pass.pnpm checkclean.Note: a unit test cannot reproduce the bundle-split scenario (tests run in a single module context). The fix was validated end-to-end by confirming external hooks fire after the change.
Follows #9859.