Skip to content

Commit 0651205

Browse files
DavertMikclaude
andcommitted
refactor(config): track per-hook ran flag, replace runHooksFrom indices
The old API exposed two coupled primitives — hooksCount() / runHooksFrom(N) — that pushed positional bookkeeping onto the caller. Container.create snapshotted the count before plugin boot and replayed everything past that index after. Brittle: any future change that reorders or removes hooks would silently re-run the wrong ones. Replace with a per-hook { fn, ran } record and a single runPendingHooks(cfg) that fires anything not yet applied to the current config. Caller no longer deals with indices: if (Config.runPendingHooks(config)) { // re-feed helper config… } Behavior is unchanged for the existing flow: - create() rebuilds config and re-runs every hook (marks all ran). - A hook registered after create() is pending until runPendingHooks() picks it up. - Hooks registered while runPendingHooks() is running are picked up in the same pass (loop re-checks length each iteration). Internal API only — no other call sites in the repo or test suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 24ba912 commit 0651205

2 files changed

Lines changed: 33 additions & 21 deletions

File tree

lib/config.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const defaultConfig = {
3232
],
3333
}
3434

35+
/** @type {Array<{ fn: (cfg: object) => void, ran: boolean }>} */
3536
let hooks = []
3637
let config = {}
3738

@@ -49,7 +50,12 @@ class Config {
4950
*/
5051
static create(newConfig) {
5152
config = deepMerge(deepClone(defaultConfig), newConfig)
52-
hooks.forEach(f => f(config))
53+
// Re-apply every hook against the freshly built config and mark them ran;
54+
// hooks added later (e.g. from plugin boot) stay pending until runPendingHooks.
55+
for (let i = 0; i < hooks.length; i++) {
56+
hooks[i].fn(config)
57+
hooks[i].ran = true
58+
}
5359
return config
5460
}
5561

@@ -121,25 +127,31 @@ class Config {
121127
}
122128

123129
static addHook(fn) {
124-
hooks.push(fn)
125-
}
126-
127-
/**
128-
* Number of registered config hooks. Useful for snapshotting before a phase
129-
* (e.g. plugin loading) and re-running only the hooks added during it.
130-
* @return {number}
131-
*/
132-
static hooksCount() {
133-
return hooks.length
130+
hooks.push({ fn, ran: false })
134131
}
135132

136133
/**
137-
* Run hooks in `[fromIndex, end)` against the given config object, mutating it.
138-
* @param {number} fromIndex
139-
* @param {Object<string, *>} cfg
134+
* Run every hook that hasn't been applied to the current config yet.
135+
* Hooks added after `Config.create()` (e.g. by a plugin's boot code) stay
136+
* pending until this is called; once it runs, they're marked applied so
137+
* subsequent calls are no-ops.
138+
*
139+
* Hooks registered while pending hooks are running are picked up too —
140+
* the loop re-checks length on each iteration.
141+
*
142+
* @param {Object<string, *>} [cfg] target config (defaults to the live singleton)
143+
* @return {boolean} true if any hook ran
140144
*/
141-
static runHooksFrom(fromIndex, cfg) {
142-
for (let i = fromIndex; i < hooks.length; i++) hooks[i](cfg)
145+
static runPendingHooks(cfg = config) {
146+
let ran = false
147+
for (let i = 0; i < hooks.length; i++) {
148+
const h = hooks[i]
149+
if (h.ran) continue
150+
h.fn(cfg)
151+
h.ran = true
152+
ran = true
153+
}
154+
return ran
143155
}
144156

145157
/**

lib/container.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ class Container {
7777
container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
7878
container.proxySupportConfig = config.include || {}
7979
container.proxySupport = createSupportObjects(container.proxySupportConfig)
80-
const hooksBeforePlugins = Config.hooksCount()
8180
container.plugins = await createPlugins(config.plugins || {}, opts)
8281
container.result = new Result()
8382

@@ -123,10 +122,11 @@ class Container {
123122
// Wait for all async helpers to finish loading and populate the actor
124123
await asyncHelperPromise
125124

126-
// If plugins registered any Config hooks during their boot, run them now
127-
// and re-apply the (possibly mutated) helper config to already-instantiated helpers.
128-
if (Config.hooksCount() > hooksBeforePlugins) {
129-
Config.runHooksFrom(hooksBeforePlugins, config)
125+
// Plugins may have registered Config hooks during their boot (e.g. the
126+
// browser plugin pushing `setBrowserConfig` overrides). Run anything that
127+
// hasn't been applied yet and re-feed the mutated helper config to the
128+
// already-instantiated helpers.
129+
if (Config.runPendingHooks(config)) {
130130
for (const name of Object.keys(container.helpers)) {
131131
const helper = container.helpers[name]
132132
if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {

0 commit comments

Comments
 (0)