Skip to content

perf: cold-start warm-up, O(1) protected-method gate, schema cache, debug-bar slimming#3210

Merged
bpamiri merged 4 commits into
developfrom
peter/perf-cold-start-serving-followups
Jun 15, 2026
Merged

perf: cold-start warm-up, O(1) protected-method gate, schema cache, debug-bar slimming#3210
bpamiri merged 4 commits into
developfrom
peter/perf-cold-start-serving-followups

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Profiling-driven optimizations. I profiled Wheels startup and page-serving with JDK21 JFR (attach-to-PID, 1ms sampler) + Apache Bench against the demo app (Lucee 7, SQLite).

What the profiling showed

Phase Measured Dominated by
Cold first request ~1.2–1.3s ~85% Lucee CFML→bytecode compilation (lucee.transformer.* + ASM)
Bootstrap logic (reload, templates compiled) ~20ms negligible
First ORM model touch ~0.4–0.5s compile + first-model column metadata
Warm serving (debug off) ~0.4ms / 2636 req/s HashMap churn, Global.cfc mixin layer
Dev debug bar +18KB/response inlined CSS/JS + panels

Two static-analysis "HIGH" candidates were empirically refuted (per-request $integrateComponents / directoryList / getMetaData are all 0% — cached at app start), so they are deliberately not touched.

Changes

  1. O(1) protected-method dispatch gate (controller/processing.cfc, Global.cfc, onapplicationstart.cfc) — replace the per-request ListFindNoCase over ~100–250 helper names with a StructKeyExists lookup against a companion application.wheels.protectedControllerMethodsLookup struct-as-set built once at app start. The comma-list is retained; case-insensitive matching unchanged.
  2. Column-metadata cache (databaseAdapters/Base.cfc, onapplicationstart.cfc) — memoize cfdbinfo type="columns" per datasource+table in application.wheels.cache.schema when cacheDatabaseSchema is on (default). Rebuilt on reload, so schema changes are still picked up on reload (same contract as the model/controller config caches). Mainly helps remote/wide-schema databases.
  3. Debug-bar slimming (events/onrequestend/debug.cfm + new vendor/wheels/public/assets/{css,js}/debugbar.*) — externalize the static CSS/JS to standalone files (eliminating the CFML ##-escaping footgun) and collapse inter-tag whitespace. Dev-only; production unaffected. Note: ## is resolved at compile time, so throughput is unchanged — this is a maintainability + footgun-removal + payload-trim change, not a throughput win.
  4. Cold-start warm-up (cli scaffold + docs) — new apps ship a /up liveness/warm-up endpoint (app/controllers/Up.cfc + route) that wheels deploy's proxy healthcheck already probes before traffic cutover, compiling the request path on a fresh node before the first real visitor. Production-config guide gains a warm-up recipe and inspectTemplate=never guidance.

Tests

  • Full core suite green locally: 4537 passed (Lucee 7 + SQLite).
  • New coverage: protectedMethodsSpec (lookup parity + case-insensitivity), schemaColumnCacheSpec (memoization, cached-path parity, off-switch).
  • Cross-engine (Adobe/BoxLang): the local Docker matrix could not start the Adobe engine here (a Docker Desktop macOS virtiofs file-mount bug — engine never booted, not a test failure). Relying on CI's full cross-engine matrix. The changes are engine-neutral (the cfinclude of .css/.js is the same pattern as the GUI's semantic.min.css).

No public API changes. Four changelog.d fragments included.

…ebug-bar slimming

Profiling-driven optimizations. JFR + ab profiling showed cold start is ~85%
Lucee CFML->bytecode compilation (bootstrap logic is only ~20ms) while warm
serving is already ~0.4ms; these target the cold path and a couple of clean
hot-path/dev wins, none changing public APIs.

- Dispatch gate (controller/processing.cfc): test the protected-helper set with
  an O(1) StructKeyExists lookup instead of an O(n) ListFindNoCase scan. A
  companion application.wheels.protectedControllerMethodsLookup struct-as-set is
  built once at app start alongside the retained comma-list; case-insensitive
  matching is unchanged.
- Column metadata (databaseAdapters/Base.cfc): memoize cfdbinfo "columns" per
  datasource+table in application.wheels.cache.schema when cacheDatabaseSchema
  is on. Rebuilt on reload, so schema changes are still picked up on reload.
- Dev debug bar (events/onrequestend/debug.cfm): externalize the static CSS/JS
  to vendor/wheels/public/assets/{css,js}/debugbar.* (eliminating the CFML
  ##-escaping footgun) and collapse inter-tag whitespace. Dev-only; production
  unaffected.
- Scaffold (cli templates): ship a /up liveness/warm-up endpoint that
  `wheels deploy`'s healthcheck probes before cutover, plus production-config
  warm-up recipe and inspectTemplate=never guidance.

Signed-off-by: Peter Amiri <petera@pai.com>
@github-actions github-actions Bot added docs enhancement javascript Pull requests that update javascript code labels Jun 15, 2026
Signed-off-by: Peter Amiri <petera@pai.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Profiling-driven perf PR: O(1) struct-as-set gate for the protected-method dispatch check, an app-scoped column-metadata cache, debug-bar asset externalization, and a scaffolded /up warm-up endpoint. The diff is well-scoped, well-tested (protectedMethodsSpec + new schemaColumnCacheSpec), and well-documented (4 valid changelog.d fragments + production-config guide). I verified the load-bearing assumptions and found no blocking correctness, cross-engine, or security issue. Verdict: comment — two non-blocking robustness/maintainability notes below.

Conventions

1. Schema cache: unlocked read isn't guarded the way the established cache pattern is (databaseAdapters/Base.cfc:431-435).

local.cacheKey = Hash(variables.dataSource & Chr(31) & arguments.tableName);
if (StructKeyExists(application.wheels.cache.schema, local.cacheKey)) {
    return application.wheels.cache.schema[local.cacheKey];
}

The write is wrapped in an exclusive lock, but the read is unlocked — so the lock provides no mutual exclusion against readers, and the write is idempotent anyway (the lock is nearly moot). The framework's own cache accessor $getFromCache() (Global.cfc:929-945) deliberately wraps its unlocked read in try { … } catch (any e) {} and Duplicate()s non-simple cached values before handing them out. This new path does neither.

Not a proven bug today: I confirmed no current caller mutates the returned query — Model.cfc:115 calls .filter(...) (returns a new query) and H2Model.cfc:167-181 builds a fresh QueryNew() rather than mutating its super.$getColumns() result. So the missing Duplicate() is latent, not active. But matching $getFromCache's defensive shape (try/catch on the read; Duplicate() on store-or-return) would make this robust against the cross-engine concurrent-struct-read footgun the existing code already guards against, and against a future caller that mutates the column query in place. Worth considering before merge; not blocking.

Cross-engine

No blocker. Spot-checks that passed: $protectedControllerMethodsLookup is correctly public + $-prefixed in Global.cfc (mixin invariant #7); application.wheels is the alias of application.$wheels set at onapplicationstart.cfc:447, after both protectedControllerMethodsLookup (:407) and cache.schema (:131) are populated, so the request-time reads in processing.cfc:138 and Base.cfc:433 resolve; the cache key preserves table-name case (no lCase), matching the verbatim-case contract noted in migrator/CLAUDE.md; cfsavecontent/ReReplace/cflock are all engine-neutral.

Nit — debug.cfm:478 whitespace-collapse regex runs over the embedded <script>/<style> too.

#ReReplace(local.wdbHtml, "(?m)>\s+<", "><", "all")#

Because the <cfinclude> of debugbar.css/debugbar.js happens inside the cfsavecontent, this >\s+< collapse also scans the inlined JS/CSS. It's safe as written — the bundled JS contains no > character (comparisons use < only) and the CSS uses no child combinator or < — so nothing matches inside those blocks. But it's a latent footgun: a future > arrow/child-combinator or a >-then-newline-then-< sequence in either asset could be silently mangled. Dev-only, so low stakes; flagging for awareness.

Tests

Good coverage and good hygiene. schemaColumnCacheSpec snapshots/restores cache.schema + cacheDatabaseSchema in beforeAll/afterAll so a mid-spec failure can't poison the shared suite, and covers memoization, cached-path parity, and the off-switch. protectedMethodsSpec adds list↔set parity, case-insensitivity, and a no-extra-entries (StructCount == ListLen) invariant. Both extend wheels.WheelsTest (BDD), not legacy RocketUnit.

Docs

changelog.d/ fragments use valid types (changed, added, performance×2) — no direct CHANGELOG.md edit. production-config.mdx items 10–11 document the warm-up recipe and inspectTemplate=never guidance. Complete.

Commits

Single commit / PR title perf: cold-start warm-up, O(1) protected-method gate, schema cache, debug-bar slimming — valid type, header well under 100 chars, not ALL-CAPS. Conforms to commitlint.config.js.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Profiling-driven perf PR: an O(1) struct-as-set gate for the protected-method dispatch check, an app-scoped column-metadata cache, debug-bar asset externalization, and a /up warm-up endpoint for wheels new. The protected-method gate, the debug-bar externalization, and the warm-up endpoint are all correct and well-tested. The column-metadata cache, however, stores raw query objects under application.wheels.cache.schema — a namespace owned by the global cache cull/count machinery, which assumes every entry is a {value, expiresAt} envelope and unconditionally dereferences .expiresAt. Once the global cache fills to maximumItemsToCache (5000) and a cull is due, the cull loop throws when it reaches a schema entry. Verdict: request changes on that one blocking correctness issue; the rest is solid.

Correctness

Blocking — schema cache poisons the global cache cull/count machinery. The new cache puts bare query objects directly into the shared application.wheels.cache struct:

  • vendor/wheels/events/onapplicationstart.cfcapplication.\$wheels.cache.schema = {}; registers schema as a category under .cache.
  • vendor/wheels/databaseAdapters/Base.cfc:451-455
    if (local.cacheSchema) {
        lock name="wheels.cache.schema" type="exclusive" timeout="10" {
            application.wheels.cache.schema[local.cacheKey] = local.rv;   // raw query, no {value, expiresAt} envelope
        }
    }

But every other producer in application.wheels.cache.* (page, partial, query, …) writes through \$addToCache(), which wraps the value as {value, expiresAt} (Global.cfc:914-922). The cull and count machinery relies on that invariant:

  • Global.cfc:963-964\$cacheCount() with no category sums StructCount across all categories, so schema entries inflate currentCount toward the 5000 cap.
  • Global.cfc:893-909 — when currentCount >= maximumItemsToCache and a cull is due, the cull iterates StructKeyArray(application.wheels.cache) (all categories, including schema) and for every key evaluates:
    && local.now > application.wheels.cache[local.cacheCategory][local.cacheKey].expiresAt
    There is no type guard. For a schema entry the value is a query object with no expiresAt column, so .expiresAt throws (column EXPIRESAT not found on Lucee/Adobe) — and even if an engine returned null, local.now > null is a cast error.

This is reachable in any app using page/partial/query caching (on by default per the production-config guide). maxItemsToDelete = Ceiling(5000 * 10/100) = 500, but the loop only deletes expired items; in steady state most entries have a future expiresAt, so the loop never hits 500 deletions and scans every category — including schema — before exiting, throwing inside \$addToCache() during render of cacheable content. StructKeyArray category order is non-deterministic on Lucee/Adobe, so the throw isn't reliably avoided even when few items are expired.

Fix: don't store raw values under application.wheels.cache.*. Either (a) move the schema cache to a sibling top-level key the cull doesn't walk, e.g. application.wheels.schemaColumnCache (set it in onapplicationstart next to — not under — cache), or (b) route writes/reads through \$addToCache/\$getFromCache with the standard envelope (less ideal here, since schema should persist for the app lifetime rather than expire). Option (a) matches the stated intent ("rebuilt on reload, same contract as the config caches") without colluding with the time-based cull.

Tests

  • protectedMethodsSpec.cfc — good coverage: list/set parity, case-insensitivity, and the helper round-trip. No gaps.
  • schemaColumnCacheSpec.cfc — exercises memoize / cached-path parity / off-switch, but never drives the cache to the maximumItemsToCache cull path, which is exactly why the blocking issue above slipped through. After the fix, consider a spec that seeds the schema cache alongside a near-full global cache and calls \$addToCache to assert the cull doesn't throw on schema entries.

Cross-engine

Verified clean. Spot-checked the two patterns most likely to break:

  • The debug-bar cfinclude of raw .css/.js (debug.cfm:88,478) is the same mechanism already shipping in vendor/wheels/public/layout/_header_simple.cfm:58 for semantic.min.css (which is full of # hex colors). Included templates don't inherit the caller's <cfoutput> expression-parsing mode, so the # literals are emitted verbatim — this genuinely removes the ##-escaping footgun as claimed.
  • to="up##index" in the route template resolves to the literal up#index controller#action form. Correct.

Conventions

Minor (non-blocking): debug.cfm:481 applies ReReplace(local.wdbHtml, "(?m)>\s+<", "><", "all") over the entire assembled bar, including dynamically-rendered panel content (params, SQL, request values). A logged value containing a literal >-whitespace-< sequence would have that whitespace collapsed in the display. It's dev-only and cosmetic, so fine to ship — just flagging that the collapse isn't limited to the static markup.

Docs / Commits

  • Four changelog.d/ fragments with valid types (added, changed, performance ×2) — correct fragment workflow, no direct [Unreleased] edit.
  • production-config.mdx warm-up + trusted-cache guidance reads well and is accurate.
  • Both commits conform to commitlint.config.js (perf: and docs(changelog):, subjects ≤ 100 chars, not ALL-CAPS).

github-actions Bot and others added 2 commits June 15, 2026 12:27
Manually triggered baseline refresh via
.github/workflows/refresh-visual-baselines.yml
on branch peter/perf-cold-start-serving-followups.

Run when an intentional content/layout change makes the visual-regression
check fail. The new PNG(s) under web/tests/visual-baselines/ are now the
expected rendering; re-run the failing visual-regression job to flip the
check green.
…space

Addresses the bot reviewer on #3210. The column-metadata cache stored raw
query objects under application.wheels.cache.schema, but every
application.wheels.cache.* category is owned by the time-based cull/count
machinery: $cacheCount() sums StructCount across all categories toward
maximumItemsToCache, and the cull dereferences `.expiresAt` on every entry
with no type guard (Global.cfc:904). A raw query has no expiresAt, so once the
global cache filled (page/partial/query caching, on by default in production)
and a cull was due, it would throw when it reached a schema entry.

- Move the cache to a sibling top-level key application.wheels.schemaColumnCache
  (set in onapplicationstart next to, not under, cache) so the cull/count never
  walk it. Matches the stated intent: persists for the app lifetime, rebuilt on
  reload.
- Mirror $getFromCache's defensive shape: wrap the read in try/catch and
  Duplicate() the cached query on store and on return, so a concurrent struct
  read can't surface a partial value and no caller can mutate the cached query
  in place (the first review's non-blocking note).
- Update schemaColumnCacheSpec and the changelog fragment for the new key.

Signed-off-by: Peter Amiri <petera@pai.com>

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Profiling-driven perf PR: O(1) struct-as-set gate for the protected-method dispatch check, an app-scoped column-metadata cache, debug-bar asset externalization, and a /up warm-up endpoint for wheels new. Re-review at head 55df686. The only new commit since the prior round (chore(web): refresh visual baseline(s)) refreshes web/tests/visual-baselines/*.png and does not touch the cache code. The blocking correctness issue from the previous round therefore still stands: the column-metadata cache stores raw query objects under application.wheels.cache.schema, a namespace the global cache cull/count machinery walks while unconditionally dereferencing .expiresAt. Verdict: request changes on that one blocking issue; everything else remains solid.

Correctness — BLOCKING (unchanged from prior round): schema cache poisons the global cache cull/count machinery. The new cache writes bare query objects into the shared application.wheels.cache struct with no {value, expiresAt} envelope:

  • vendor/wheels/events/onapplicationstart.cfc:131application.$wheels.cache.schema = {}; registers schema as a sibling category alongside page, partial, query, etc.
  • vendor/wheels/databaseAdapters/Base.cfc:451-453 stores the raw cfdbinfo query: application.wheels.cache.schema[local.cacheKey] = local.rv; inside lock name="wheels.cache.schema" type="exclusive". local.rv is the unwrapped query from $getColumnInfo() (Base.cfc:437/449).

Every other producer under application.wheels.cache.* writes through $addToCache(), which wraps the value as {value, expiresAt} (Global.cfc:914-922). The cull and count machinery relies on that invariant and has no type guard:

  • Global.cfc:961-967$cacheCount() with no category sums StructCount across all categories (for (local.key in application.wheels.cache)), so schema entries inflate currentCount toward the cap.
  • Global.cfc:893-906 — when currentCount >= maximumItemsToCache and a cull is due, the loop iterates StructKeyArray(application.wheels.cache) (line 893 — every category, schema included) and at line 904 evaluates local.now > application.wheels.cache[local.cacheCategory][local.cacheKey].expiresAt. For a schema entry the value is a query with no expiresAt column, so .expiresAt throws (column [EXPIRESAT] not found on Lucee/Adobe). No IsStruct/IsQuery guard precedes it.

Reachable with stock defaults (vendor/wheels/events/init/caching.cfm): cacheDatabaseSchema = true (schema entries exist after first model touch), maximumItemsToCache = 5000, cacheCullPercentage = 10, so maxItemsToDelete = Ceiling(5000 * 10/100) = 500. The cull deletes only expired items, so in steady state (most entries un-expired) the loop scans entire categories without reaching 500 deletions and walks schema before exiting — throwing inside $addToCache() mid-render of cacheable page/partial/query content. StructKeyArray category order is non-deterministic on Lucee/Adobe, so the throw is not reliably dodged even when few items are expired.

Fix (same as prior round): do not store raw values under application.wheels.cache.*. Preferred — move the schema cache to a sibling top-level key the cull never walks, e.g. set application.$wheels.schemaColumnCache = {} in onapplicationstart next to (not under) cache, and read/write/lock against that. This matches the stated "rebuilt on reload, app-lifetime" intent without colluding with the time-based cull. (Routing through $addToCache/$getFromCache is the alternative, but schema should persist for the app lifetime rather than expire, so the sibling-key approach fits better.)

Tests.

  • protectedMethodsSpec.cfc — solid: list/set parity, case-insensitivity, StructCount == ListLen no-extra-entries invariant. Extends wheels.WheelsTest (BDD). No gaps.
  • schemaColumnCacheSpec.cfc — exercises memoize / cached-path parity / off-switch and cleanly snapshots+restores cache.schema/cacheDatabaseSchema in beforeAll/afterAll. But it never drives the global cache to the maximumItemsToCache cull path — which is exactly why the blocking issue is invisible to the suite. After the fix, add a spec that seeds the schema cache alongside a near-full global cache and calls $addToCache to assert the cull does not throw on schema entries (a regression pin for this bug).

Cross-engine. Verified clean otherwise. $protectedControllerMethodsLookup is public + $-prefixed (mixin invariant #7); application.wheels aliases application.$wheels set after both lookups are populated, so the request-time reads resolve; the schema cache key preserves table-name case (no lCase), matching the verbatim-case contract in migrator/CLAUDE.md; the debug-bar cfinclude of raw .css/.js is the same mechanism already shipping for semantic.min.css in _header_simple.cfm, so the ##-escaping footgun is genuinely removed; to="up##index" resolves to the literal up#index form.

Minor (non-blocking): debug.cfm applies a whitespace-collapse ReReplace over the entire assembled bar, including the inlined JS/CSS and dynamically-rendered panel content. Safe as written (bundled assets contain no >, dev-only), but a future >-then-whitespace-then-< sequence in an asset or logged value could be silently mangled. Flagging for awareness.

Conventions / Docs / Commits.

  • Route addition .get(name="up", to="up##index") is correctly placed before .wildcard() — custom named route ahead of the wildcard, per the documented order.
  • Up.cfc index() is a clean read-only renderText("OK") warm-up action; comments accurately describe the deploy healthcheck probe.
  • Four changelog.d/ fragments with valid types (changed, added, performance x2) — correct fragment workflow, no direct [Unreleased] edit. production-config.mdx warm-up + inspectTemplate=never guidance is accurate.
  • Commits conform to commitlint.config.js (perf:, docs(changelog):, chore(web):; subjects <= 100 chars, not ALL-CAPS).

Once the schema cache is moved off the culled application.wheels.cache.* namespace (and ideally pinned by a cull-path spec), this is good to merge.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer

TL;DR — Profiling-driven perf PR: an O(1) struct-as-set protected-method dispatch gate, an app-scoped column-metadata cache, debug-bar asset externalization, and a /up warm-up endpoint for wheels new. Re-review at head 47d1e8c. The sole new commit since the last round — fix(model): move schema column cache out of the time-based cache namespaceresolves the one blocking correctness issue the previous two rounds flagged. With that fixed and no new blocking finding, verdict: approve. This --approve supersedes my still-active CHANGES_REQUESTED (at 55df686); the audit trail below is the evidence.

Correctness — previously-blocking issue RESOLVED

Prior rounds blocked on the schema cache storing raw query objects under application.wheels.cache.schema, a namespace the time-based cull/count machinery walks while unconditionally dereferencing .expiresAt (Global.cfc:904) — guaranteed to throw once the global cache filled and a cull was due. Commit 47d1e8c fixes this by structural separation:

  • vendor/wheels/events/onapplicationstart.cfc:118 — the cache is now a sibling top-level key: application.$wheels.schemaColumnCache = {};, set next to (not under) cache. The old application.$wheels.cache.schema = {}; line is removed (confirmed gone from the diff).
  • vendor/wheels/Global.cfc:893 — the cull iterates StructKeyArray(application.wheels.cache), which now contains only page / partial / query. vendor/wheels/Global.cfc:963-966$cacheCount() sums StructCount over the same set. Neither ever walks schemaColumnCache, so a raw query's missing .expiresAt can no longer be dereferenced. The bug is now impossible by construction, not merely avoided.
  • vendor/wheels/databaseAdapters/Base.cfc:438-439, 463-464 — read and write now target application.wheels.schemaColumnCache under lock name="wheels.schemaColumnCache". The read is alias-safe: application.wheels = application.$wheels is assigned at onapplicationstart.cfc:453, after the key is initialized at line 118.

I also confirmed local.rv is always defined when the store block runs (the showErrorInformation branch re-throws rather than falling through), so the Duplicate(local.rv) store can't hit an undefined variable.

Cross-engine

Clean. The fix commit only adds Duplicate() (universal on query objects), a try/catch (any e), and a renamed lock — no engine-specific idioms. The defensive read shape (try/catch + Duplicate) mirrors $getFromCache (Global.cfc:928-948). The non-fix portions remain as verified in prior rounds: $protectedControllerMethodsLookup is public + $-prefixed (mixin invariant #7); the debug-bar cfinclude of raw .css/.js matches the semantic.min.css mechanism in _header_simple.cfm, genuinely removing the ##-escaping footgun; to="up##index" resolves to the literal up#index form.

Tests

  • schemaColumnCacheSpec.cfc updated to snapshot/restore and assert against application.wheels.schemaColumnCache (lines 16-23, 37-61) — memoize / cached-path parity / off-switch all retargeted to the new key.
  • protectedMethodsSpec.cfc — unchanged, still solid (list/set parity, case-insensitivity, no-extra-entries invariant).
  • Minor (non-blocking): the suggested cull-path regression pin from prior rounds is now far less necessary — the schema cache no longer shares the culled namespace, so the cull cannot reach it. A pin would only guard against a future re-merge of the namespaces; nice-to-have, not required.

Docs / Commits

  • changelog.d/schema-column-cache.performance.md updated to name the new application.wheels.schemaColumnCache key — fragment workflow intact, no direct [Unreleased] edit.
  • fix(model): move schema column cache out of the time-based cache namespace conforms to commitlint.config.js (valid fix type, model scope, header <= 100 chars, not ALL-CAPS, DCO sign-off present). The commit body correctly explains the why (cull dereferences .expiresAt with no type guard).

Good to merge. Thanks for the thorough fix and the explanatory commit body.

@bpamiri bpamiri merged commit ef02be9 into develop Jun 15, 2026
15 of 19 checks passed
@bpamiri bpamiri deleted the peter/perf-cold-start-serving-followups branch June 15, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs enhancement javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant