@timber-js/app 0.2.0-alpha.166 → 0.2.0-alpha.168
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{actions-CDPfMp_I.js → actions-O_LsyCE4.js} +3 -3
- package/dist/_chunks/{actions-CDPfMp_I.js.map → actions-O_LsyCE4.js.map} +1 -1
- package/dist/_chunks/{cache-api-DygSeKCB.js → cache-api-B-lhk9p4.js} +107 -58
- package/dist/_chunks/cache-api-B-lhk9p4.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-EXGYPhI2.js → cli-schema-sync-B73L6pMq.js} +5 -3
- package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +1 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js +63 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +1 -0
- package/dist/_chunks/{logger-t3uxAmbX.js → logger-AWfuX-KJ.js} +2 -19
- package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
- package/dist/_chunks/{walkers-DBVzXuWc.js → walkers-CoOC8Hga.js} +2 -2
- package/dist/_chunks/{walkers-DBVzXuWc.js.map → walkers-CoOC8Hga.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +10 -3
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +12 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -461
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/cache-api.d.ts +33 -11
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/json-lossy-check.d.ts +11 -0
- package/dist/cache/json-lossy-check.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +42 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
- package/dist/cache/stores/memory.d.ts +1 -1
- package/dist/cache/stores/memory.d.ts.map +1 -1
- package/dist/cache/stores/redis.d.ts +1 -1
- package/dist/cache/stores/redis.d.ts.map +1 -1
- package/dist/cache/stores/vercel.d.ts +1 -1
- package/dist/cache/stores/vercel.d.ts.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
- package/dist/cdn/fastly-purge.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/child-segment-context.d.ts +2 -2
- package/dist/client/child-segment-context.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -0
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/internal.js +141 -116
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts +6 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -3
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-outlet.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -1
- package/dist/codec.js.map +1 -1
- package/dist/config-types.d.ts +16 -37
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/fonts/pipeline.d.ts +19 -0
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +378 -109
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +8 -8
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +27 -3
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-client.d.ts +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/body-limits.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +3 -1
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/index.js +20 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +28 -41
- package/dist/server/internal.js.map +1 -1
- package/dist/server/middleware-runner.d.ts +0 -17
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +6 -3
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +18 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +9 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +14 -14
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +44 -74
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +2 -2
- package/dist/shared/redirect-type.d.ts.map +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/image.d.ts +120 -120
- package/dist/shims/image.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +116 -4
- package/docs/api/34-api-config.mdx +0 -26
- package/docs/learn/09-caching.mdx +126 -10
- package/docs/learn/12-client-navigation.mdx +9 -1
- package/docs/learn/13-configuration.mdx +6 -8
- package/docs/learn/14-deploying.mdx +18 -18
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/docs/more/50-ai-agent-instructions.mdx +5 -5
- package/package.json +6 -5
- package/src/adapters/cloudflare-kv-cache.ts +27 -6
- package/src/adapters/cloudflare.ts +63 -25
- package/src/adapters/types.ts +2 -0
- package/src/cache/cache-api.ts +84 -84
- package/src/cache/index.ts +1 -1
- package/src/cache/json-lossy-check.ts +75 -0
- package/src/cache/redis-handler.ts +65 -14
- package/src/cache/timber-cache.ts +10 -2
- package/src/client/browser-entry/index.ts +2 -0
- package/src/client/browser-entry/post-hydration.ts +16 -9
- package/src/client/browser-entry/router-init.ts +2 -0
- package/src/client/history.ts +11 -0
- package/src/client/router.ts +224 -208
- package/src/codec.ts +3 -1
- package/src/config-types.ts +16 -37
- package/src/config-validation.ts +7 -5
- package/src/fonts/pipeline.ts +32 -0
- package/src/fonts/transform.ts +30 -24
- package/src/plugins/adapter-build.ts +132 -13
- package/src/plugins/cache.ts +45 -30
- package/src/plugins/entries.ts +40 -43
- package/src/plugins/fonts.ts +30 -0
- package/src/plugins/server-bundle.ts +7 -15
- package/src/routing/scanner.ts +7 -0
- package/src/server/action-client.ts +1 -1
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-boundary.ts +6 -3
- package/src/server/middleware-runner.ts +0 -44
- package/src/server/param-coercion.ts +1 -0
- package/src/server/pipeline-phases.ts +11 -13
- package/src/server/pipeline.ts +51 -3
- package/src/server/route-element-builder.ts +59 -9
- package/src/server/rsc-entry/action-middleware-runner.ts +14 -14
- package/src/server/rsc-entry/index.ts +30 -40
- package/src/server/rsc-entry/render-route.ts +6 -2
- package/src/server/rsc-entry/rsc-payload.ts +21 -4
- package/src/server/rsc-entry/rsc-stream.ts +8 -0
- package/src/server/rsc-entry/wrap-action-dispatch.ts +109 -372
- package/dist/_chunks/cache-api-DygSeKCB.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-EXGYPhI2.js.map +0 -1
- package/dist/_chunks/logger-t3uxAmbX.js.map +0 -1
- package/dist/_chunks/tree-match-D2l830j2.js +0 -102
- package/dist/_chunks/tree-match-D2l830j2.js.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cache-api-DygSeKCB.js","names":[],"sources":["../../src/cache/stable-stringify.ts","../../src/cache/singleflight.ts","../../src/cache/invalidation-epoch.ts","../../src/cache/timber-cache.ts","../../src/cache/handler-store.ts","../../src/cache/redis-handler.ts","../../src/cache/tag-aware-handler.ts","../../src/cache/index.ts","../../src/server/version-skew.ts","../../src/server/prebuilt/manifest-tag-index.ts","../../src/server/prebuilt/payload-source.ts","../../src/server/prebuilt/overlay.ts","../../src/cdn/purge-store.ts","../../src/cache/cache-api.ts"],"sourcesContent":["/**\n * Deterministic JSON serialization with sorted object keys.\n * Used for cache key generation — ensures { a: 1, b: 2 } and { b: 2, a: 1 }\n * produce the same string.\n *\n * Matches JSON.stringify semantics for toJSON: Date serializes to its ISO\n * string, URL to its href, and class instances with a custom toJSON use it.\n * Map and Set serialize their entries explicitly (insertion-order\n * independent). Values that would serialize lossily — functions, symbols,\n * and non-plain objects with no enumerable own state (RegExp, class\n * instances holding data behind getters or internal slots) — throw instead:\n * every such value would collapse to the same cache key, silently returning\n * one argument's cached data for another (TIM-1020).\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) return String(value);\n\n const type = typeof value;\n if (type === 'function' || type === 'symbol') {\n throw new TypeError(\n `stableStringify: cannot serialize a ${type} into a stable cache key — ` +\n 'all such values would collide. Provide an explicit `key` option to cache().'\n );\n }\n if (type !== 'object') return JSON.stringify(value);\n\n if (Array.isArray(value)) {\n return '[' + value.map((item) => stableStringify(item)).join(',') + ']';\n }\n\n // Honor toJSON like JSON.stringify does (Date → ISO string, URL → href,\n // custom class serialization). JSON.stringify does not re-invoke toJSON\n // on the value it returns, so a toJSON that returns `this` falls through\n // to plain own-property serialization instead of recursing forever.\n const withToJSON = value as { toJSON?: () => unknown };\n if (typeof withToJSON.toJSON === 'function') {\n const json = withToJSON.toJSON();\n if (json !== value) return stableStringify(json);\n }\n\n // Map/Set have no enumerable own properties — serialize entries explicitly.\n // Entries are sorted after serialization so insertion order doesn't change\n // the key. The Map(/Set( prefixes can't collide with object ('{'), array\n // ('['), or string ('\"') output.\n if (value instanceof Map) {\n const entries = [...value.entries()].map(\n ([k, v]) => stableStringify(k) + '=>' + stableStringify(v)\n );\n return 'Map(' + entries.sort().join(',') + ')';\n }\n if (value instanceof Set) {\n const members = [...value].map((member) => stableStringify(member));\n return 'Set(' + members.sort().join(',') + ')';\n }\n\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n const pairs: string[] = [];\n for (const key of keys) {\n if (obj[key] === undefined) continue;\n // A callable own toJSON only reaches this loop via the returns-`this`\n // fall-through above. It's the serialization protocol, not data —\n // JSON.stringify omits it too.\n if (key === 'toJSON' && typeof obj[key] === 'function') continue;\n pairs.push(JSON.stringify(key) + ':' + stableStringify(obj[key]));\n }\n\n // A non-plain object that serializes to '{}' carries its state behind\n // getters or internal slots (RegExp, sessions, ORM entities). Serializing\n // it would give every instance the same cache key.\n if (pairs.length === 0) {\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) {\n const name = (value as object).constructor?.name || 'Object';\n throw new TypeError(\n `stableStringify: ${name} instance has no serializable own state — ` +\n 'all instances would share one cache key. Provide an explicit `key` ' +\n 'option to cache(), or add a toJSON() method.'\n );\n }\n }\n\n return '{' + pairs.join(',') + '}';\n}\n","/**\n * Singleflight coalesces concurrent calls with the same key into a single\n * execution. All callers receive the same result (or error).\n *\n * Per-process, in-memory. Each process coalesces independently.\n *\n * An optional `timeoutMs` prevents hung `fn()` calls from permanently\n * blocking all future callers for that key. When set, `fn()` is raced\n * against a timeout — if the timeout fires first, the promise rejects\n * with `SingleflightTimeoutError`, `finally` cleans up the key, and\n * subsequent callers can retry. See TIM-518.\n */\n\nexport interface SingleflightOptions {\n /** Maximum time (ms) a coalesced call may run before being rejected. */\n timeoutMs?: number;\n}\n\nexport interface Singleflight {\n do<T>(key: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T>;\n}\n\n/**\n * Error thrown when a singleflight call exceeds `timeoutMs`.\n * Exported so callers can distinguish timeout from other errors.\n */\nexport class SingleflightTimeoutError extends Error {\n constructor(key: string, timeoutMs: number) {\n super(`Singleflight timeout: key \"${key}\" exceeded ${timeoutMs}ms`);\n this.name = 'SingleflightTimeoutError';\n }\n}\n\nexport function createSingleflight(opts?: SingleflightOptions): Singleflight {\n const inflight = new Map<string, Promise<unknown>>();\n const timeoutMs = opts?.timeoutMs;\n\n return {\n do<T>(key: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing) return existing as Promise<T>;\n\n const ac = new AbortController();\n let promise: Promise<T>;\n\n if (timeoutMs != null && timeoutMs > 0) {\n // Race fn() against a timeout to prevent hung calls from\n // permanently blocking the key. See TIM-518.\n // Abort the signal on timeout so the callback can skip\n // side effects (e.g. cache writes). See TIM-1092.\n promise = new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n ac.abort();\n reject(new SingleflightTimeoutError(key, timeoutMs));\n }, timeoutMs);\n try {\n fn(ac.signal).then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n }\n );\n } catch (err) {\n clearTimeout(timer);\n reject(err);\n }\n });\n } else {\n promise = fn(ac.signal);\n }\n\n const tracked = promise.finally(() => {\n inflight.delete(key);\n });\n\n inflight.set(key, tracked);\n return tracked as Promise<T>;\n },\n };\n}\n","/**\n * Per-process invalidation epoch tracking for timber.cache (TIM-1028).\n *\n * handler.set for a cache miss runs after `fn()` resolves. An explicit\n * `cache.invalidate({ tag })` / `revalidateTag` that lands while fn is in\n * flight would otherwise be silently undone — the just-invalidated value\n * re-stored as fresh for the full TTL, resurrecting pre-mutation data.\n *\n * Every invalidation bumps a monotonic epoch and records it against the\n * invalidated key/tag. Before storing a result, the cache wrapper checks\n * whether its key or any of its tags were invalidated after fn started and\n * skips the write if so. Skipping is always safe: the only cost is one\n * extra cache miss on the next call.\n *\n * Scope: per-process, like the singleflight map. An invalidation issued by\n * another instance against a shared handler (Redis/KV) during fn flight is\n * not observable here — closing that window fully would require store-level\n * versioning, which no CacheHandler backend offers.\n */\n\nlet epoch = 0;\n\n/**\n * Last-invalidated epoch per `key:<k>` / `tag:<t>` entry. Insertion order is\n * epoch order (entries are re-inserted on update), so the oldest entry is\n * always first — pruning evicts lowest epochs.\n */\nconst lastInvalidated = new Map<string, number>();\n\n/**\n * Bound on tracked entries so high-cardinality tag invalidation (e.g.\n * per-user tags) cannot grow this map without limit.\n */\nconst MAX_TRACKED_ENTRIES = 10_000;\n\n/**\n * Highest epoch evicted from the map. Any execution that started at or\n * before this epoch conservatively counts as invalidated — history was\n * truncated, so we can no longer prove it wasn't.\n */\nlet evictedEpochFloor = 0;\n\n/** The current epoch. Capture before running fn(), pass to wasInvalidatedSince. */\nexport function currentInvalidationEpoch(): number {\n return epoch;\n}\n\n/**\n * Record an invalidation. Call BEFORE handler.invalidate() so an execution\n * whose set-check races the invalidation errs toward skipping the write.\n */\nexport function recordInvalidation(opts: { key?: string; tag?: string }): void {\n epoch++;\n if (opts.key) track(`key:${opts.key}`);\n if (opts.tag) track(`tag:${opts.tag}`);\n}\n\nfunction track(entry: string): void {\n lastInvalidated.delete(entry);\n lastInvalidated.set(entry, epoch);\n while (lastInvalidated.size > MAX_TRACKED_ENTRIES) {\n const oldest = lastInvalidated.entries().next().value;\n if (oldest === undefined) break;\n evictedEpochFloor = Math.max(evictedEpochFloor, oldest[1]);\n lastInvalidated.delete(oldest[0]);\n }\n}\n\n/**\n * Whether the key or any tag was invalidated after `startEpoch` (the epoch\n * captured when fn started executing).\n */\nexport function wasInvalidatedSince(startEpoch: number, key: string, tags: string[]): boolean {\n return lastInvalidationEpochFor(key, tags) > startEpoch;\n}\n\n/**\n * The highest epoch at which the key or any of the tags was invalidated\n * (0 if never). Used to scope the singleflight key: callers arriving after\n * an invalidation must not coalesce onto a flight that started before it —\n * they would receive the pre-invalidation result the invalidation was\n * issued to discard.\n */\nexport function lastInvalidationEpochFor(key: string, tags: string[]): number {\n let last = evictedEpochFloor;\n const keyEpoch = lastInvalidated.get(`key:${key}`);\n if (keyEpoch !== undefined && keyEpoch > last) last = keyEpoch;\n for (const tag of tags) {\n const tagEpoch = lastInvalidated.get(`tag:${tag}`);\n if (tagEpoch !== undefined && tagEpoch > last) last = tagEpoch;\n }\n return last;\n}\n","import type { CacheHandler, CacheOptions } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport {\n currentInvalidationEpoch,\n lastInvalidationEpochFor,\n wasInvalidatedSince,\n} from './invalidation-epoch';\nimport { addSpanEventSync } from '../server/tracing.js';\nimport { logSwrRefetchFailed } from '../server/logger.js';\nimport { getWaitUntil } from '../server/waituntil-bridge.js';\nimport { fnv1aHash } from './fast-hash.js';\n\nlet defaultSingleflight = createSingleflight();\n\n/**\n * Per-key monotonic generation counter for CAS writes (TIM-1158).\n *\n * Unbounded, like the singleflight map — the key space is the same set of\n * cache keys the handler already tracks. Pruning was removed because it\n * desyncs from MemoryCacheHandler.generations: a pruned key restarts at 1\n * while the handler still holds the old higher generation, causing fresh\n * writes to be rejected as stale.\n */\nconst writeGenerations = new Map<string, number>();\nfunction nextGeneration(key: string): number {\n const gen = (writeGenerations.get(key) ?? 0) + 1;\n writeGenerations.set(key, gen);\n return gen;\n}\n\n/**\n * Set the timeout for the module-level default singleflight.\n * Called at framework boot with renderTimeoutMs from timber.config.ts so that\n * cache calls without a per-call timeoutMs still get deadlock protection.\n * See design/06-caching.md §\"Singleflight Timeout — Deadlock Risk\".\n */\nexport function setDefaultSingleflightTimeout(timeoutMs: number): void {\n defaultSingleflight = createSingleflight({ timeoutMs });\n}\n\n/**\n * Generate a cache key from function identity and serialized args.\n *\n * Uses FNV-1a (fast non-crypto hash) instead of SHA-256. Cache keys don't\n * need collision resistance — they need speed. The fnId prefix provides\n * namespace isolation; the hash covers the args.\n *\n * See TIM-370 for perf motivation.\n */\nfunction defaultKeyGenerator(fnId: string, args: unknown[]): string {\n const raw = fnId + ':' + stableStringify(args);\n return fnId + ':' + fnv1aHash(raw);\n}\n\n/**\n * Resolve tags from the options — supports static array or function form.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction resolveTags<Fn extends (...args: any[]) => any>(\n opts: CacheOptions<Fn>,\n args: Parameters<Fn>\n): string[] {\n if (!opts.tags) return [];\n if (Array.isArray(opts.tags)) return opts.tags;\n return opts.tags(...args);\n}\n\n/** Per-process count of byte-identical fn sources seen by deriveFallbackFnId. */\nconst fallbackIdOccurrences = new Map<string, number>();\nlet hasWarnedFallbackId = false;\n\n/**\n * Derive a fallback fnId for callsites the timber-cache-transform Vite plugin\n * could not statically detect (aliased bindings, user re-export wrappers).\n *\n * The base is fnv1a(fn.toString()) — deterministic across instances and cold\n * starts of the same build, so two *different* functions can never swap fnIds\n * by module-evaluation order (the TIM-997 poisoning scenario that the old\n * process-local counter allowed; see TIM-1054). A per-process occurrence\n * suffix disambiguates byte-identical duplicate sources. That residue is the\n * one remaining hazard: closures produced by the same factory share source\n * text but capture different values, and their occurrence order may differ\n * across instances — such callsites need an explicit `key` option.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction deriveFallbackFnId(fn: (...args: any[]) => any): string {\n const sourceHash = fnv1aHash(fn.toString());\n const occurrence = fallbackIdOccurrences.get(sourceHash) ?? 0;\n fallbackIdOccurrences.set(sourceHash, occurrence + 1);\n\n if (!hasWarnedFallbackId && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'test') {\n hasWarnedFallbackId = true;\n console.warn(\n '[timber] cache() callsite has no build-time stable ID and no explicit `key` option — ' +\n 'the call is not statically visible to the timber Vite plugin (e.g. an aliased ' +\n 'binding like `const c = cache` or a re-export wrapper module). Falling back to a ' +\n 'function-source hash for the cache key. With a shared CacheHandler (Redis/KV) this ' +\n 'is safe for distinct functions, but byte-identical closures from a factory can swap ' +\n 'cache entries across instances. Provide an explicit `key` option to make the cache ' +\n 'key contract stable and auditable.'\n );\n }\n\n return `timber-cache:fn:${sourceHash}:${occurrence}`;\n}\n\n/**\n * Creates a cached wrapper around an async function.\n *\n * - FNV-1a default keys with normalized JSON args\n * - Singleflight: concurrent misses → single execution AND single\n * handler.set — the write happens inside the coalesced callback (TIM-1028)\n * - Writes are skipped when the key/tags were invalidated while fn was in\n * flight, so explicit invalidation is never silently undone (TIM-1028)\n * - SWR: serve stale immediately, background refetch\n * - Tags as string[] or function of args\n * - The handler is resolved per invocation via `getHandler`, never captured\n * at wrapper-creation time — module-scope cache() wrappers created before\n * the framework's boot wiring calls setCacheHandler() still use the\n * configured handler once it arrives (TIM-1029)\n * - No ALS dependency for correctness — cache() works outside a request\n * context. The waitUntil ALS bridge is read optionally so the SWR\n * background refetch survives handler return on Workers/Lambda.\n *\n * Cache hits/misses are recorded as OTEL span events on the enclosing\n * span (not child spans). The DevSpanProcessor reads these for dev log output.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function createCache<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: CacheOptions<Fn>,\n getHandler: () => CacheHandler,\n stableId?: string\n): Fn {\n // With an explicit `key` option the fnId never reaches key generation, so\n // no fallback derivation (or warning) is needed in that case.\n const fnId = stableId ?? (opts.key ? undefined : deriveFallbackFnId(fn));\n\n // Per-call timeoutMs gets a dedicated singleflight instance. Otherwise\n // resolve defaultSingleflight per invocation (not captured at creation\n // time) so wrappers created before boot pick up the timeout once\n // setDefaultSingleflightTimeout is called (TIM-1032, same rationale as\n // getCacheHandler for TIM-1029).\n const perCallSf =\n opts.timeoutMs !== undefined ? createSingleflight({ timeoutMs: opts.timeoutMs }) : undefined;\n\n // Cast to Fn to preserve the original function's generic call signature.\n // Without this, generic type parameters (e.g. <T> in apiFetch<T>) are\n // erased and callers lose type safety on the return type.\n return (async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {\n const sf = perCallSf ?? defaultSingleflight;\n const key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId as string, args);\n\n const cacheStart = performance.now();\n const cached = await getHandler().get(key);\n\n if (cached && !cached.stale) {\n // Record as OTEL span event on enclosing span (not a child span).\n // Fire-and-forget — no microtask overhead on the cache hot path.\n addSpanEventSync('timber.cache.hit', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n });\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // From here fn() will execute (SWR refetch or miss). Scope the\n // singleflight key by the last invalidation epoch affecting this\n // key/tags: a caller arriving after an invalidation must not coalesce\n // onto a flight that started before it — it would receive the\n // pre-invalidation result the invalidation was issued to discard.\n // NUL separator: cannot collide with user-provided key strings.\n const tags = resolveTags(opts, args);\n const flightKey = `${key}\\u0000${lastInvalidationEpochFor(key, tags)}`;\n\n /**\n * Run fn and store the result — exactly once per coalesced execution,\n * inside the singleflight callback. The write is skipped when:\n * - the key/tags were invalidated after fn started (TIM-1028), or\n * - the singleflight timed out (signal aborted) — the timed-out\n * flight's write could overwrite a newer value from a subsequent\n * flight (TIM-1092), or\n * - the handler rejects the write because a newer generation has\n * already been stored (TIM-1158 CAS guard).\n */\n const executeAndStore = async (signal: AbortSignal): Promise<Awaited<ReturnType<Fn>>> => {\n const startEpoch = currentInvalidationEpoch();\n const generation = nextGeneration(key);\n const result = await fn(...args);\n if (!signal.aborted && !wasInvalidatedSince(startEpoch, key, tags)) {\n await getHandler().set(key, result, { ttl: opts.ttl, tags, generation });\n }\n return result;\n };\n\n if (cached && cached.stale && opts.staleWhileRevalidate) {\n // Record stale cache hit as OTEL span event (fire-and-forget).\n addSpanEventSync('timber.cache.hit', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n stale: true,\n });\n // Serve stale immediately, trigger background refetch\n const refetch = sf\n .do(`swr:${flightKey}`, async (signal) => {\n try {\n await executeAndStore(signal);\n } catch (err) {\n // Failed refetch — stale entry continues to be served.\n logSwrRefetchFailed({ cacheKey: key, error: err });\n }\n })\n .catch(() => {\n // Singleflight promise rejection handled — stale continues.\n });\n // Platforms that cancel work when the handler returns (Cloudflare\n // Workers, Lambda) would otherwise kill the refetch mid-flight —\n // stale data re-served forever. Register the (never-rejecting)\n // refetch with the request's waitUntil; undefined outside a request\n // context or on platforms without lifecycle extension (Node servers\n // keep floating promises alive, so behavior there is unchanged).\n getWaitUntil()?.(refetch);\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss (or stale without SWR) — execute with singleflight.\n // The handler.set lives inside the coalesced callback, so N concurrent\n // misses produce exactly one write (TIM-1028).\n const result = await sf.do(flightKey, executeAndStore);\n\n // Record cache miss as OTEL span event (fire-and-forget).\n addSpanEventSync('timber.cache.miss', {\n key,\n duration_ms: Math.round(performance.now() - cacheStart),\n });\n\n return result;\n }) as unknown as Fn;\n}\n","/**\n * Module-level cache handler singleton.\n *\n * Lazily initialized to MemoryCacheHandler on first access. The framework\n * replaces this at boot from timber.config.ts via setCacheHandler().\n *\n * This module avoids importing from ./index to prevent circular dependencies.\n */\n\n// Inline the interface to avoid circular import with index.ts\ninterface CacheHandlerLike {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\nlet handler: CacheHandlerLike | null = null;\n\nlet explicitlyConfigured = false;\n\n/** Replace the active cache handler. Called by the framework at boot. */\nexport function setCacheHandler(h: CacheHandlerLike): void {\n handler = h;\n explicitlyConfigured = true;\n}\n\n/** Whether the handler was explicitly configured (vs the default in-memory fallback). */\nexport function isCacheHandlerConfigured(): boolean {\n return explicitlyConfigured;\n}\n\n/**\n * Get the active cache handler. Creates a default MemoryCacheHandler on\n * first access if none has been set via setCacheHandler().\n */\nexport function getCacheHandler(): CacheHandlerLike {\n if (!handler) {\n // Inline a minimal LRU cache to avoid circular dep with index.ts.\n // In production, the framework always calls setCacheHandler() at boot.\n handler = createDefaultHandler();\n }\n return handler;\n}\n\nfunction createDefaultHandler(): CacheHandlerLike {\n const store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n const maxEntries = 1000;\n\n return {\n async get(key) {\n const entry = store.get(key);\n if (!entry) return null;\n store.delete(key);\n store.set(key, entry);\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n },\n async set(key, value, opts) {\n if (store.has(key)) store.delete(key);\n while (store.size >= maxEntries) {\n const oldest = store.keys().next().value;\n if (oldest !== undefined) store.delete(oldest);\n else break;\n }\n store.set(key, { value, expiresAt: Date.now() + opts.ttl * 1000, tags: opts.tags });\n },\n async invalidate(opts) {\n if (opts.key) store.delete(opts.key);\n if (opts.tag) {\n for (const [key, entry] of store) {\n if (entry.tags.includes(opts.tag)) store.delete(key);\n }\n }\n },\n };\n}\n","import type { CacheHandler } from './index';\n\n/**\n * Minimal Redis client contract for `RedisCacheHandler`.\n *\n * This is deliberately NOT the native shape of any client library. ioredis,\n * node-redis v4, and @upstash/redis all disagree on TTL argument shapes\n * (positional `'EX'` vs `{ EX }` vs `{ ex }`), method casing (`sadd` vs\n * `sAdd`), and array handling in `del` — a contract that pattern-matches one\n * client lets another silently drop the expiry (TIM-1030: node-redis ignored\n * the positional `'EX'` and wrote keys with no TTL). Instead, TTL semantics\n * are explicit in the signature and you adapt your client with a thin\n * wrapper, so a mismatch is a type error rather than silent unbounded\n * keyspace growth.\n *\n * @example ioredis\n * ```ts\n * import Redis from 'ioredis';\n * const redis = new Redis(process.env.REDIS_URL);\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, 'EX', ttl),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sadd(k, ...m),\n * srem: (k, ...m) => redis.srem(k, ...m),\n * smembers: (k) => redis.smembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n *\n * @example node-redis v4\n * ```ts\n * import { createClient } from 'redis';\n * const redis = createClient({ url: process.env.REDIS_URL });\n * await redis.connect();\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, { EX: ttl }),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sAdd(k, m),\n * srem: (k, ...m) => redis.sRem(k, m),\n * smembers: (k) => redis.sMembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n *\n * @example @upstash/redis\n * ```ts\n * import { Redis } from '@upstash/redis';\n * // automaticDeserialization must be off: the handler stores JSON strings\n * // and parses them itself — auto-parsing would hand back objects.\n * const redis = new Redis({ url, token, automaticDeserialization: false });\n * const client: RedisClient = {\n * get: (k) => redis.get(k),\n * set: (k, v, ttl) => redis.set(k, v, { ex: ttl }),\n * expireNX: (k, s) => redis.expire(k, s, 'NX'),\n * expireGT: (k, s) => redis.expire(k, s, 'GT'),\n * del: (k) => redis.del(k),\n * sadd: (k, ...m) => redis.sadd(k, m[0], ...m.slice(1)),\n * srem: (k, ...m) => redis.srem(k, m[0], ...m.slice(1)),\n * smembers: (k) => redis.smembers(k),\n * ttl: (k) => redis.ttl(k),\n * };\n * ```\n */\nexport interface RedisClient {\n /** `GET key` — the stored string, or null when absent. */\n get(key: string): Promise<string | null>;\n /**\n * `SET key value EX ttlSeconds` — store with an expiry. The TTL is a\n * required positional argument so no adapter can forget it.\n */\n set(key: string, value: string, ttlSeconds: number): Promise<unknown>;\n /**\n * `EXPIRE key seconds NX` — set the TTL only if the key has no existing\n * expiry (Redis 7.0+). Used to set the initial TTL on new tag sets.\n */\n expireNX(key: string, seconds: number): Promise<unknown>;\n /**\n * `EXPIRE key seconds GT` — update the TTL only if the new value is greater\n * than the current one (Redis 7.0+). Used to extend tag set TTLs without\n * allowing a short-TTL writer to shrink them.\n */\n expireGT(key: string, seconds: number): Promise<unknown>;\n /** `DEL key` — delete a single key. Multi-key deletes loop per key. */\n del(key: string): Promise<unknown>;\n /** `SADD key member [member ...]` — add members to a set. */\n sadd(key: string, ...members: string[]): Promise<unknown>;\n /** `SREM key member [member ...]` — remove members from a set. */\n srem(key: string, ...members: string[]): Promise<unknown>;\n /** `SMEMBERS key` — all members of a set. */\n smembers(key: string): Promise<string[]>;\n /**\n * `TTL key` — remaining time-to-live in seconds. Returns -2 if the key does\n * not exist, -1 if the key exists but has no expiry.\n */\n ttl(key: string): Promise<number>;\n}\n\nconst KEY_PREFIX = 'timber:cache:';\nconst TAG_PREFIX = 'timber:tag:';\n\n/**\n * Redis-backed CacheHandler for distributed caching.\n *\n * All instances sharing the same Redis see each other's cache entries and\n * invalidations. Tag-based invalidation uses Redis Sets to track which keys\n * belong to which tags.\n *\n * Bring your own Redis client wrapped to the {@link RedisClient} contract —\n * see the adapter examples on the interface for ioredis, node-redis v4, and\n * @upstash/redis.\n */\nexport class RedisCacheHandler implements CacheHandler {\n private client: RedisClient;\n private prefix: string;\n\n constructor(client: RedisClient, opts?: { prefix?: string }) {\n this.client = client;\n this.prefix = opts?.prefix ?? '';\n }\n\n private cacheKey(key: string): string {\n return `${this.prefix}${KEY_PREFIX}${key}`;\n }\n\n private tagKey(tag: string): string {\n return `${this.prefix}${TAG_PREFIX}${tag}`;\n }\n\n async get(key: string): Promise<{ value: unknown; stale: boolean } | null> {\n const raw = await this.client.get(this.cacheKey(key));\n if (raw === null) return null;\n\n const entry = JSON.parse(raw) as { value: unknown; expiresAt: number };\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void> {\n const ck = this.cacheKey(key);\n const expiresAt = Date.now() + opts.ttl * 1000;\n const payload = JSON.stringify({ value, expiresAt, tags: opts.tags });\n\n // Redis TTL with generous margin beyond the logical TTL to allow SWR reads\n // on stale entries. The logical staleness is determined by expiresAt.\n // We use 2x TTL + 60s as the Redis expiry so stale entries remain\n // available for SWR background refetches.\n const redisTtlSeconds = Math.max(opts.ttl * 2 + 60, 120);\n await this.client.set(ck, payload, redisTtlSeconds);\n\n // Track key membership in each tag set, and expire the set so it doesn't\n // outlive all its members. Only extend the TTL, never shorten — a short-TTL\n // entry must not shrink a tag set that already contains long-TTL entries,\n // otherwise invalidate({tag}) would miss the long-lived entries.\n for (const tag of opts.tags) {\n await this.client.sadd(this.tagKey(tag), key);\n // Two atomic EXPIRE calls cover both the initial-set and extend cases:\n // - NX: sets the TTL only if the key has no expiry (new tag set from SADD)\n // - GT: extends the TTL only if the new value is greater than current\n // Together they close the race where concurrent workers could shrink the\n // tag set lifetime (TIM-1093), while ensuring new tag sets always get an\n // initial expiry (without NX, GT is a no-op on non-volatile keys).\n await this.client.expireNX(this.tagKey(tag), redisTtlSeconds);\n await this.client.expireGT(this.tagKey(tag), redisTtlSeconds);\n }\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\n const raw = await this.client.get(this.cacheKey(opts.key));\n if (raw !== null) {\n const entry = JSON.parse(raw) as { tags?: string[] };\n if (entry.tags) {\n await Promise.all(entry.tags.map((tag) => this.client.srem(this.tagKey(tag), opts.key!)));\n }\n }\n await this.client.del(this.cacheKey(opts.key));\n }\n\n if (opts.tag) {\n const tk = this.tagKey(opts.tag);\n const keys = await this.client.smembers(tk);\n\n // Re-check each member before deleting — the tag set can contain stale\n // memberships from entries whose tags changed since they were added.\n // Mirrors TagAwareCacheHandler's eager strategy (tag-aware-handler.ts:183-198).\n await Promise.all(\n keys.map(async (k) => {\n const raw = await this.client.get(this.cacheKey(k));\n if (raw === null) {\n await this.client.srem(tk, k);\n return;\n }\n const entry = JSON.parse(raw) as { tags?: string[] };\n if (entry.tags?.includes(opts.tag!)) {\n await this.client.del(this.cacheKey(k));\n }\n await this.client.srem(tk, k);\n })\n );\n\n // Clean up the now-empty tag set so it doesn't consume memory\n const remaining = await this.client.smembers(tk);\n if (remaining.length === 0) {\n await this.client.del(tk);\n }\n }\n }\n}\n","/**\n * TagAwareCacheHandler — wraps any CacheStore and adds tag-based invalidation.\n *\n * Three strategies, selected by store capabilities (best available wins):\n *\n * 1. **Native** (store has setWithTags + invalidateTag): Delegates entirely.\n * The store owns tag indexing. Used for Vercel Runtime Cache, DO-backed\n * stores, or any platform with first-class tag support.\n *\n * 2. **Eager** (store has sadd/smembers/srem/sdel): Maintains an inverted\n * index via atomic set operations. Invalidation enumerates the set and\n * deletes entries. Used for Redis.\n *\n * 3. **Lazy** (bare get/set/del only): Version-stamps entries with tag\n * counters. Invalidation bumps a counter (O(1)). Reads validate stored\n * versions against current. Race-free, correct under eventual consistency.\n * Used for Cloudflare KV.\n *\n * Implements the CacheHandler interface consumed by timber-cache.ts.\n */\n\nimport type { CacheHandler } from './index';\nimport type { CacheStore } from './store';\n\nconst DATA_PREFIX = 'd:';\nconst TAG_SET_PREFIX = 'ts:';\nconst TAG_VERSION_PREFIX = 'tv:';\n\ntype Strategy = 'native' | 'eager' | 'lazy';\n\n/** Stored entry format for eager strategy. */\ninterface EagerEntry {\n value: unknown;\n expiresAt: number;\n tags: string[];\n}\n\n/** Stored entry format for lazy (version-stamp) strategy. */\ninterface LazyEntry {\n value: unknown;\n expiresAt: number;\n tags: string[];\n tagVersions: Record<string, number>;\n}\n\nfunction detectStrategy(store: CacheStore): Strategy {\n if (store.setWithTags && store.invalidateTag) return 'native';\n if (store.sadd && store.smembers && store.srem && store.sdel) return 'eager';\n return 'lazy';\n}\n\nexport class TagAwareCacheHandler implements CacheHandler {\n private store: CacheStore;\n private prefix: string;\n private strategy: Strategy;\n\n constructor(store: CacheStore, opts?: { prefix?: string }) {\n this.store = store;\n this.prefix = opts?.prefix ?? 'timber:';\n this.strategy = detectStrategy(store);\n }\n\n get consistency(): 'strong' | 'eventual' | 'local' {\n return this.store.consistency;\n }\n\n private dataKey(key: string): string {\n return `${this.prefix}${DATA_PREFIX}${key}`;\n }\n\n /** Namespace tags for native stores so prefix isolates shared stores. */\n private prefixTags(tags: string[]): string[] {\n return tags.map((t) => `${this.prefix}${t}`);\n }\n\n private tagSetKey(tag: string): string {\n return `${this.prefix}${TAG_SET_PREFIX}${tag}`;\n }\n\n private tagVersionKey(tag: string): string {\n return `${this.prefix}${TAG_VERSION_PREFIX}${tag}`;\n }\n\n async get(key: string): Promise<{ value: unknown; stale: boolean } | null> {\n const raw = await this.store.get(this.dataKey(key));\n if (raw === null) return null;\n\n if (this.strategy === 'native' || this.strategy === 'eager') {\n const entry = JSON.parse(raw) as EagerEntry;\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n // Lazy: validate tag versions before returning\n const entry = JSON.parse(raw) as LazyEntry;\n if (entry.tags.length > 0) {\n const currentVersions = await this.getTagVersions(entry.tags);\n for (const tag of entry.tags) {\n const stored = entry.tagVersions[tag] ?? 0;\n const current = currentVersions[tag] ?? 0;\n if (current > stored) return null;\n }\n }\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void> {\n const physicalTtl = Math.max(opts.ttl * 2 + 60, 120);\n\n if (this.strategy === 'native') {\n const entry: EagerEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n };\n const payload = JSON.stringify(entry);\n if (opts.tags.length > 0) {\n await this.store.setWithTags!(\n this.dataKey(key),\n payload,\n physicalTtl,\n this.prefixTags(opts.tags)\n );\n } else {\n await this.store.set(this.dataKey(key), payload, physicalTtl);\n }\n return;\n }\n\n if (this.strategy === 'eager') {\n const entry: EagerEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n };\n await this.store.set(this.dataKey(key), JSON.stringify(entry), physicalTtl);\n if (opts.tags.length > 0) {\n await Promise.all(\n opts.tags.map((tag) => this.store.sadd!(this.tagSetKey(tag), [key], physicalTtl))\n );\n }\n return;\n }\n\n // Lazy: snapshot current tag versions into the entry\n const tagVersions = opts.tags.length > 0 ? await this.getTagVersions(opts.tags) : {};\n const entry: LazyEntry = {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n tagVersions,\n };\n await this.store.set(this.dataKey(key), JSON.stringify(entry), physicalTtl);\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\n if (this.strategy === 'eager') {\n const raw = await this.store.get(this.dataKey(opts.key));\n if (raw !== null) {\n const entry = JSON.parse(raw) as EagerEntry;\n if (entry.tags.length > 0) {\n await Promise.all(\n entry.tags.map((tag) => this.store.srem!(this.tagSetKey(tag), [opts.key!]))\n );\n }\n }\n }\n await this.store.del(this.dataKey(opts.key));\n }\n\n if (opts.tag) {\n const tag = opts.tag;\n if (this.strategy === 'native') {\n await this.store.invalidateTag!(`${this.prefix}${tag}`);\n } else if (this.strategy === 'eager') {\n const members = await this.store.smembers!(this.tagSetKey(tag));\n // Verify each member still carries the tag before deleting — the set\n // can contain stale memberships from overwritten entries or LRU eviction.\n // Remove only the snapshot members (via srem, not sdel) so concurrent\n // writes that add new members between smembers() and cleanup are preserved.\n await Promise.all(\n members.map(async (k) => {\n const raw = await this.store.get(this.dataKey(k));\n if (raw === null) {\n await this.store.srem!(this.tagSetKey(tag), [k]);\n return;\n }\n const entry = JSON.parse(raw) as EagerEntry;\n if (entry.tags.includes(tag)) {\n await this.store.del(this.dataKey(k));\n await this.store.srem!(this.tagSetKey(tag), [k]);\n } else {\n await this.store.srem!(this.tagSetKey(tag), [k]);\n }\n })\n );\n } else {\n await this.bumpTagVersion(opts.tag);\n }\n }\n }\n\n // --- Lazy strategy helpers ---\n\n private async getTagVersions(tags: string[]): Promise<Record<string, number>> {\n const versions: Record<string, number> = {};\n await Promise.all(\n tags.map(async (tag) => {\n const raw = await this.store.get(this.tagVersionKey(tag));\n versions[tag] = raw !== null ? parseInt(raw, 10) : 0;\n })\n );\n return versions;\n }\n\n private async bumpTagVersion(tag: string): Promise<void> {\n const key = this.tagVersionKey(tag);\n const raw = await this.store.get(key);\n const stored = raw !== null ? parseInt(raw, 10) : 0;\n // max(stored, now) + 1: strictly advancing even within the same\n // millisecond (two bumps in 1ms produce N, N+1 not N, N). On\n // eventually-consistent stores the read can be stale, but Date.now()\n // provides a floor — regression only if BOTH the read is stale AND the\n // clock is behind, bounded by the store's propagation window (~60s).\n const version = Math.max(stored, Date.now()) + 1;\n // Version keys must outlive all data entries stamped with them. Data\n // entries use physical TTL of max(ttl*2+60, 120); 365 days exceeds any\n // practical cache lifetime.\n await this.store.set(key, String(version), 365 * 86400);\n }\n}\n","// @timber-js/app/cache — Caching primitives\n\nimport { estimateByteSize } from './sizeof.js';\n\nexport interface CacheHandler {\n get(key: string): Promise<{ value: unknown; stale: boolean } | null>;\n set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ): Promise<void>;\n invalidate(opts: { key?: string; tag?: string }): Promise<void>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface CacheOptions<Fn extends (...args: any[]) => any> {\n ttl: number;\n key?: (...args: Parameters<Fn>) => string;\n staleWhileRevalidate?: boolean;\n tags?: string[] | ((...args: Parameters<Fn>) => string[]);\n /** Timeout (ms) for singleflight-coalesced calls. Prevents hung fn() from\n * permanently blocking all future callers for the same cache key. See TIM-518. */\n timeoutMs?: number;\n}\n\nexport interface MemoryCacheHandlerOptions {\n /** Maximum number of entries. Oldest accessed entries are evicted first. Default: 1000. */\n maxEntries?: number;\n /**\n * @deprecated Use `maxEntries` instead. Will be removed in a future release.\n * Alias for `maxEntries` — maximum number of entries (not bytes).\n */\n maxSize?: number;\n /** Maximum total byte budget for all cached values. Oldest entries are evicted when exceeded. Default: no limit. */\n maxBytes?: number;\n /** Maximum byte size for a single cache entry. Entries exceeding this are silently dropped. Default: no limit. */\n maxEntryBytes?: number;\n}\n\nexport class MemoryCacheHandler implements CacheHandler {\n private store = new Map<\n string,\n { value: unknown; expiresAt: number; tags: string[]; byteSize: number }\n >();\n private generations = new Map<string, number>();\n private maxEntries: number;\n private maxBytes: number | undefined;\n private maxEntryBytes: number | undefined;\n private currentBytes = 0;\n private readonly _trackBytes: boolean;\n\n constructor(opts?: MemoryCacheHandlerOptions) {\n // maxEntries takes precedence over deprecated maxSize\n this.maxEntries = opts?.maxEntries ?? opts?.maxSize ?? 1000;\n this.maxBytes = opts?.maxBytes;\n this.maxEntryBytes = opts?.maxEntryBytes;\n this._trackBytes = this.maxBytes !== undefined || this.maxEntryBytes !== undefined;\n }\n\n async get(key: string) {\n const entry = this.store.get(key);\n if (!entry) return null;\n\n // Move to end of Map (most recently used) for LRU ordering\n this.store.delete(key);\n this.store.set(key, entry);\n\n const stale = Date.now() > entry.expiresAt;\n return { value: entry.value, stale };\n }\n\n async set(\n key: string,\n value: unknown,\n opts: { ttl: number; tags: string[]; generation?: number }\n ) {\n // CAS guard: skip write if a newer generation has already been written\n if (opts.generation !== undefined) {\n const current = this.generations.get(key) ?? 0;\n if (opts.generation < current) return;\n }\n\n const byteSize = this._trackBytes ? estimateByteSize(value) : 0;\n\n // Reject entries exceeding per-entry byte limit\n if (this.maxEntryBytes !== undefined && byteSize > this.maxEntryBytes) {\n return;\n }\n\n // If key already exists, delete first to refresh insertion order and reclaim bytes\n if (this.store.has(key)) {\n const existing = this.store.get(key)!;\n this.currentBytes -= existing.byteSize;\n this.store.delete(key);\n }\n\n // Evict oldest entries (front of Map) if at entry count capacity\n while (this.store.size >= this.maxEntries) {\n this.evictOldest();\n }\n\n // Evict oldest entries if byte budget would be exceeded\n if (this.maxBytes !== undefined) {\n while (this.currentBytes + byteSize > this.maxBytes && this.store.size > 0) {\n this.evictOldest();\n }\n // If the single entry exceeds the total byte budget, don't store it\n if (this.currentBytes + byteSize > this.maxBytes) {\n return;\n }\n }\n\n // Record generation only after all admission checks pass\n if (opts.generation !== undefined) {\n this.generations.set(key, opts.generation);\n }\n\n this.store.set(key, {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n byteSize,\n });\n this.currentBytes += byteSize;\n }\n\n async invalidate(opts: { key?: string; tag?: string }) {\n if (opts.key) {\n const entry = this.store.get(opts.key);\n if (entry) {\n this.currentBytes -= entry.byteSize;\n this.store.delete(opts.key);\n }\n this.generations.delete(opts.key);\n }\n if (opts.tag) {\n for (const [key, entry] of this.store) {\n if (entry.tags.includes(opts.tag)) {\n this.currentBytes -= entry.byteSize;\n this.store.delete(key);\n this.generations.delete(key);\n }\n }\n }\n }\n\n /** Number of entries currently in the cache. */\n get size(): number {\n return this.store.size;\n }\n\n /** Estimated total byte size of all cached values. Only tracked when maxBytes or maxEntryBytes is configured; returns 0 otherwise. */\n get bytes(): number {\n return this.currentBytes;\n }\n\n /** Evict the oldest entry (front of Map). */\n private evictOldest(): void {\n const oldest = this.store.keys().next().value;\n if (oldest !== undefined) {\n const entry = this.store.get(oldest)!;\n this.currentBytes -= entry.byteSize;\n this.store.delete(oldest);\n this.generations.delete(oldest);\n }\n }\n}\n\nexport { RedisCacheHandler } from './redis-handler';\nexport type { RedisClient } from './redis-handler';\nexport { cache } from './cache-api';\nexport { setCacheHandler, getCacheHandler } from './handler-store';\nexport { setDefaultSingleflightTimeout } from './timber-cache';\n// NOTE: registerCachedFunction (runtime for 'use cache' directive) removed.\n// Future feature pending design doc. See design/06-caching.md.\nexport { estimateByteSize } from './sizeof';\n\n// --- Cache Store layering (design/46) ---\nexport { TagAwareCacheHandler } from './tag-aware-handler';\nexport type { CacheStore } from './store';\n// stableStringify, createSingleflight, and SingleflightTimeoutError are\n// internal utilities used by the cache implementation. They are not\n// re-exported here — import directly from the source files if needed\n// within the package. See TIM-720.\n","/**\n * Version Skew Detection — graceful recovery when stale clients hit new deployments.\n *\n * When a new version of the app is deployed, clients with open tabs still have\n * the old JavaScript bundle. Without version skew handling, these stale clients\n * will experience:\n *\n * 1. Server action calls that crash (action IDs are content-hashed)\n * 2. Chunk load failures (old filenames gone from CDN)\n * 3. RSC payload mismatches (component references differ between builds)\n *\n * This module implements deployment ID comparison:\n * - A per-build deployment ID is generated at build time (see build-manifest.ts)\n * - The client sends it via `X-Timber-Deployment-Id` header on every RSC/action request\n * - The server compares it against the current build's ID\n * - On mismatch: signal the client to reload (not crash)\n *\n * The deployment ID is always-on in production. Dev mode skips the check\n * (HMR handles code updates without full reloads).\n *\n * See design/25-production-deployments.md, TIM-446\n */\n\n// ─── Constants ───────────────────────────────────────────────────\n\n/** Header sent by the client with every RSC/action request. */\nexport const DEPLOYMENT_ID_HEADER = 'X-Timber-Deployment-Id';\n\n/** Response header that signals the client to do a full page reload. */\nexport const RELOAD_HEADER = 'X-Timber-Reload';\n\n// ─── Deployment ID ───────────────────────────────────────────────\n\n/**\n * The current build's deployment ID. Set at startup from the manifest init\n * module (globalThis.__TIMBER_DEPLOYMENT_ID__). Null in dev mode.\n */\nlet currentDeploymentId: string | null = null;\n\n/**\n * Set the current deployment ID. Called once at server startup from the\n * manifest init module. In dev mode this is never called (deployment ID\n * checks are skipped).\n */\nexport function setDeploymentId(id: string): void {\n currentDeploymentId = id;\n}\n\n/**\n * Get the current deployment ID. Returns null in dev mode.\n */\nexport function getDeploymentId(): string | null {\n return currentDeploymentId;\n}\n\n// ─── Skew Detection ──────────────────────────────────────────────\n\n/** Result of a version skew check. */\nexport interface SkewCheckResult {\n /** Whether the client's deployment ID matches the server's. */\n ok: boolean;\n /** The client's deployment ID (null if header not sent — e.g., initial page load). */\n clientId: string | null;\n}\n\n/**\n * Check if a request's deployment ID matches the current build.\n *\n * Returns `{ ok: true }` when:\n * - Dev mode (no deployment ID set — HMR handles updates)\n * - No deployment ID header (initial page load, non-RSC request)\n * - Deployment IDs match\n *\n * Returns `{ ok: false }` when:\n * - Client sends a deployment ID that differs from the current build\n */\nexport function checkVersionSkew(req: Request): SkewCheckResult {\n // Dev mode — no deployment ID checks (HMR handles updates)\n if (!currentDeploymentId) {\n return { ok: true, clientId: null };\n }\n\n const clientId = req.headers.get(DEPLOYMENT_ID_HEADER);\n\n // No header — initial page load or non-RSC request. Always OK.\n if (!clientId) {\n return { ok: true, clientId: null };\n }\n\n // Compare deployment IDs\n if (clientId === currentDeploymentId) {\n return { ok: true, clientId };\n }\n\n return { ok: false, clientId };\n}\n\n/**\n * Apply version skew reload headers to a response.\n * Sets X-Timber-Reload: 1 to signal the client to do a full page reload.\n */\nexport function applyReloadHeaders(headers: Headers): void {\n headers.set(RELOAD_HEADER, '1');\n}\n","/**\n * Manifest tag index — maps tags to seed cache keys for tombstone writes.\n *\n * When `revalidateTag(tag)` fires, the framework must write tombstones for\n * every build-seed entry carrying that tag. The manifest records tags per\n * entry; this module inverts the index at startup so the lookup is O(1) per\n * tag instead of a full manifest scan per invalidation.\n *\n * The index is built lazily on first access and cached for the process\n * lifetime — the manifest is immutable within a deploy.\n *\n * See design/45-cache-lifetimes.md §ISR mechanics.\n */\n\nimport type { PrebuiltManifest } from './payload-source.js';\n\nexport interface SeedKeyInfo {\n componentId: string;\n cacheKey: string;\n}\n\n/**\n * Build the inverted tag → seed key index from the manifest.\n *\n * Returns a Map where each tag maps to the set of (componentId, cacheKey)\n * pairs whose manifest entries carry that tag. Only entries with non-empty\n * `tags` arrays participate.\n */\nexport function buildTagIndex(manifest: PrebuiltManifest): Map<string, SeedKeyInfo[]> {\n const index = new Map<string, SeedKeyInfo[]>();\n for (const [componentId, component] of Object.entries(manifest)) {\n if (!component || typeof component !== 'object' || !('entries' in component)) continue;\n for (const [cacheKey, entry] of Object.entries(component.entries)) {\n if (!entry.tags || entry.tags.length === 0) continue;\n for (const tag of entry.tags) {\n let list = index.get(tag);\n if (!list) {\n list = [];\n index.set(tag, list);\n }\n list.push({ componentId, cacheKey });\n }\n }\n }\n return index;\n}\n","/**\n * Prebuilt payload source — the adapter seam through which the runtime\n * reads build-time flight artifacts (`.timber/dist/prebuilt/`).\n *\n * The runtime never touches the filesystem directly: adapters register a\n * source at startup (Node: fs reads; Cloudflare: the assets binding —\n * design/44-render-at-build-time.md §Adapter Integration). Artifacts are\n * produced by the post-build capture pass (TIM-1119, prebuilt-builder.ts);\n * adapter registration of the source is TIM-1122. Until a source is\n * registered (adapter not wired yet, dev mode, no artifacts built), every\n * lookup misses and cache.component wrappers render live — the designed\n * dynamic-fallback behavior.\n *\n * Phase 3 (design/45 runtime tier) inserts the CacheHandler overlay lookup\n * in front of `lookupPrebuiltPayload` — overlay first, then artifact. The\n * single-lookup-function shape here is that seam.\n */\n\nimport { initTagIndex, resetTagIndex } from './overlay.js';\n\nexport interface PrebuiltManifestEntry {\n /** Segment params this entry was captured under. */\n params: Record<string, string | string[]>;\n /** Payload path relative to the prebuilt/ directory. */\n file: string;\n /** Payload size in bytes (build report / diagnostics). */\n bytes?: number;\n /** Runtime invalidation tags (design/45 ISR tier). */\n tags?: string[];\n /** Epoch ms when this entry was captured (seed freshness for ISR). */\n buildTimestamp?: number;\n}\n\nexport interface PrebuiltManifest {\n [componentId: string]: {\n entries: Record<string, PrebuiltManifestEntry>;\n /** When true, the component does not read segment params — the cache key uses `{}`. */\n paramIndependent?: boolean;\n };\n}\n\nexport interface PrebuiltPayloadSource {\n /** Load prebuilt/manifest.json. Null when no artifacts exist. */\n loadManifest(): Promise<PrebuiltManifest | null>;\n /** Read a `.flight` payload by manifest file path. Null when missing. */\n readPayload(file: string): Promise<Uint8Array | null>;\n}\n\nlet source: PrebuiltPayloadSource | null = null;\nlet manifestPromise: Promise<PrebuiltManifest | null> | null = null;\n\n/**\n * Ceiling on the memoized manifest load. Without it, a stalled\n * `loadManifest()` would leave `manifestPromise` pending forever and every\n * subsequent cache.component render would await the same hung promise —\n * the coalesced-promise deadlock class from design/13-security.md (A5).\n * On expiry the load memoizes as null (permanent dynamic fallback, same\n * policy as a failed load) and callers proceed immediately.\n */\nconst MANIFEST_LOAD_TIMEOUT_MS = 5_000;\n\n/**\n * Per-read ceiling on payload reads. The read is awaited inside a server\n * component render — a stalled adapter promise would otherwise hang the\n * RSC stream until the render timeout kills the whole page, when the\n * correct degradation is a miss → live render (codex review round 4 on\n * PR #835).\n */\nconst PAYLOAD_READ_TIMEOUT_MS = 5_000;\n\n/** Race a source promise against a timeout that resolves to `fallback`. */\nasync function withTimeout<T>(\n work: Promise<T>,\n timeoutMs: number,\n fallback: T,\n onTimeout: () => void\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n work,\n new Promise<T>((resolve) => {\n timer = setTimeout(() => {\n onTimeout();\n resolve(fallback);\n }, timeoutMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function loadManifestWithTimeout(\n src: PrebuiltPayloadSource\n): Promise<PrebuiltManifest | null> {\n let manifest: PrebuiltManifest | null;\n try {\n manifest = await withTimeout(src.loadManifest(), MANIFEST_LOAD_TIMEOUT_MS, null, () => {\n console.error(\n `[timber] prebuilt manifest load exceeded ${MANIFEST_LOAD_TIMEOUT_MS}ms — all cache.component lookups will render dynamically`\n );\n });\n } catch (error) {\n console.error(\n '[timber] failed to load prebuilt manifest — all cache.component lookups will miss:',\n error\n );\n return null;\n }\n if (manifest) {\n initTagIndex(manifest);\n }\n return manifest;\n}\n\n/**\n * Register the payload source. Called once at server startup by the\n * adapter/entry wiring (TIM-1122); tests inject fakes. Passing null\n * unregisters (and drops the memoized manifest).\n */\nexport function setPrebuiltPayloadSource(next: PrebuiltPayloadSource | null): void {\n source = next;\n manifestPromise = null;\n if (!next) resetTagIndex();\n}\n\n/**\n * Whether a source is registered — the gate for both marker emission and\n * splice-transform installation. False means zero prebuilt overhead.\n */\nexport function hasPrebuiltPayloadSource(): boolean {\n return source !== null;\n}\n\n/**\n * Ensure the manifest is loaded (and the tag index initialized). No-op\n * when no source is registered. Used by the tombstone path to avoid\n * skipping tombstones on cold processes where the first request is an\n * action rather than a page render.\n */\nexport async function ensureManifestLoaded(): Promise<void> {\n if (source === null) return;\n manifestPromise ??= loadManifestWithTimeout(source);\n await manifestPromise;\n}\n\n/**\n * Look up a manifest entry (metadata only, no payload read).\n * Used by the overlay to check seed existence and freshness.\n */\nexport async function isParamIndependent(componentId: string): Promise<boolean> {\n if (source === null) return false;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n return manifest?.[componentId]?.paramIndependent === true;\n}\n\nexport async function getManifestEntry(\n componentId: string,\n cacheKey: string\n): Promise<PrebuiltManifestEntry | null> {\n if (source === null) return null;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n return manifest?.[componentId]?.entries?.[cacheKey] ?? null;\n}\n\n/**\n * Look up a cached payload: manifest entry → payload bytes.\n * Null on any miss or read failure — callers fall back to live render.\n *\n * The manifest is loaded once and memoized (design/44 §manifest.json:\n * loaded at startup, held in memory; payloads read on demand). A failed\n * manifest load is memoized as null rather than retried per-request — a\n * broken artifact should degrade to dynamic rendering, not add a failing\n * read to every request.\n */\nexport async function lookupPrebuiltPayload(\n componentId: string,\n cacheKey: string\n): Promise<Uint8Array | null> {\n if (source === null) return null;\n manifestPromise ??= loadManifestWithTimeout(source);\n const manifest = await manifestPromise;\n const entry = manifest?.[componentId]?.entries?.[cacheKey];\n if (!entry) return null;\n try {\n return await withTimeout(source.readPayload(entry.file), PAYLOAD_READ_TIMEOUT_MS, null, () => {\n console.error(\n `[timber] prebuilt payload read for \"${entry.file}\" exceeded ${PAYLOAD_READ_TIMEOUT_MS}ms — rendering dynamically`\n );\n });\n } catch (error) {\n console.error(\n `[timber] failed to read prebuilt payload \"${entry.file}\" for ${componentId} — rendering dynamically:`,\n error\n );\n return null;\n }\n}\n","/**\n * Component cache overlay — runtime CacheHandler layer for ISR.\n *\n * Sits in front of the immutable build artifact (`.flight` files) and\n * stores replacement payloads and tombstones via the configured\n * CacheHandler. Overlay entries are per-deploy namespaced:\n * `timber:c:{deploymentId}:{componentId}:{hash}`\n *\n * The CacheHandler contract is untouched — tombstones and the tag index\n * live above the interface as value conventions. Redis, KV, and custom\n * handlers work unmodified.\n *\n * See design/45-cache-lifetimes.md §ISR mechanics.\n */\n\nimport { getCacheHandler, isCacheHandlerConfigured } from '../../cache/handler-store.js';\nimport { MemoryCacheHandler } from '../../cache/index.js';\nimport { getDeploymentId } from '../version-skew.js';\nimport { buildTagIndex, type SeedKeyInfo } from './manifest-tag-index.js';\nimport type { PrebuiltManifest } from './payload-source.js';\nimport { ensureManifestLoaded } from './payload-source.js';\n\n/** Default ceiling TTL for tag-only entries (24 hours), in seconds. */\nconst CEILING_TTL_SECONDS = 24 * 60 * 60;\n\n/** Deploy-long physical TTL for seed-backed overlay records (90 days). */\nconst DEPLOY_LONG_TTL_SECONDS = 90 * 24 * 60 * 60;\n\n// ─── Overlay record shapes (value conventions above CacheHandler) ────────\n\n/**\n * A cached component flight payload stored in the overlay.\n * The `_t` discriminant distinguishes this from a tombstone.\n */\nexport interface ComponentOverlayRecord {\n _t: 'co';\n /** Base64-encoded flight payload bytes. */\n p: string;\n /** Logical freshness deadline (Date.now() + ttl * 1000). */\n fu: number;\n /** Whether a build-artifact seed exists for this key. */\n s: boolean;\n}\n\n/**\n * A tombstone marking a build seed as invalidated.\n * Written by revalidateTag for seeds carrying the invalidated tag.\n */\nexport interface ComponentTombstoneRecord {\n _t: 'ct';\n}\n\ntype OverlayValue = ComponentOverlayRecord | ComponentTombstoneRecord;\n\nexport function isOverlayRecord(v: unknown): v is ComponentOverlayRecord {\n return v !== null && typeof v === 'object' && (v as Record<string, unknown>)._t === 'co';\n}\n\nexport function isTombstone(v: unknown): v is ComponentTombstoneRecord {\n return v !== null && typeof v === 'object' && (v as Record<string, unknown>)._t === 'ct';\n}\n\n// ─── Overlay key generation ──────────────────────────────────────────────\n\nexport function overlayKey(componentId: string, cacheKey: string): string {\n const deploymentId = getDeploymentId() ?? 'dev';\n return `timber:c:${deploymentId}:${componentId}:${cacheKey}`;\n}\n\n// ─── Overlay read ────────────────────────────────────────────────────────\n\nexport interface OverlayLookupResult {\n kind: 'fresh' | 'stale' | 'tombstone';\n /** The overlay value (null for tombstones). */\n record: ComponentOverlayRecord | null;\n /** Raw flight payload bytes (decoded from base64). Null for tombstones. */\n payload: Uint8Array | null;\n}\n\n/**\n * Look up an overlay entry in the cache handler.\n * Returns null on a clean miss (no overlay entry at all).\n */\nexport async function lookupOverlay(\n componentId: string,\n cacheKey: string\n): Promise<OverlayLookupResult | null> {\n const handler = getCacheHandler();\n const key = overlayKey(componentId, cacheKey);\n const result = await handler.get(key);\n if (!result) return null;\n\n const value = result.value as OverlayValue;\n if (isTombstone(value)) {\n return { kind: 'tombstone', record: null, payload: null };\n }\n if (isOverlayRecord(value)) {\n const logicallyStale = Date.now() > value.fu;\n return {\n kind: logicallyStale ? 'stale' : 'fresh',\n record: value,\n payload: base64ToBytes(value.p),\n };\n }\n // Unknown value shape — treat as miss (forward compatibility).\n return null;\n}\n\n// ─── Overlay write ───────────────────────────────────────────────────────\n\n/**\n * Resolve the effective TTL for a component's runtime lifetime.\n * Tag-only entries (no explicit ttl) get the ceiling TTL (design/45\n * §Tag-only entries carry a ceiling TTL).\n */\nexport function effectiveTtl(ttl: number | undefined): number {\n return ttl ?? CEILING_TTL_SECONDS;\n}\n\n/**\n * Store a component flight payload in the overlay.\n */\nexport async function storeOverlayEntry(\n componentId: string,\n cacheKey: string,\n payloadBytes: Uint8Array,\n opts: {\n ttl?: number;\n tags: string[];\n hasSeed: boolean;\n }\n): Promise<void> {\n const handler = getCacheHandler();\n const key = overlayKey(componentId, cacheKey);\n const logicalTtl = effectiveTtl(opts.ttl);\n const physicalTtl = opts.hasSeed ? DEPLOY_LONG_TTL_SECONDS : logicalTtl;\n\n const record: ComponentOverlayRecord = {\n _t: 'co',\n p: bytesToBase64(payloadBytes),\n fu: Date.now() + logicalTtl * 1000,\n s: opts.hasSeed,\n };\n\n await handler.set(key, record, { ttl: physicalTtl, tags: opts.tags });\n}\n\n// ─── Tombstone writes ────────────────────────────────────────────────────\n\nlet tagIndex: Map<string, SeedKeyInfo[]> | null = null;\n\n/**\n * Initialize the tag index from the prebuilt manifest. Called once when the\n * manifest is loaded. Without a manifest (no prebuilt components), this is\n * never called and revalidateTag skips the tombstone path entirely.\n */\nexport function initTagIndex(manifest: PrebuiltManifest): void {\n tagIndex = buildTagIndex(manifest);\n emitIsrDiagnosticIfNeeded(manifest);\n}\n\n/**\n * Get the tag index, if initialized.\n * @internal — exported for testing.\n */\nexport function getTagIndex(): Map<string, SeedKeyInfo[]> | null {\n return tagIndex;\n}\n\n/**\n * Reset the tag index (for tests).\n * @internal\n */\nexport function resetTagIndex(): void {\n tagIndex = null;\n}\n\n/**\n * Write tombstones for all seed entries carrying the given tag.\n * Called from cache.invalidate (cache-api.ts) AFTER the handler's own\n * invalidation clears overlay entries.\n *\n * The tombstone prevents the immutable seed from being served after\n * invalidation — without it, a handler.invalidate that clears the overlay\n * would expose the (now-stale) build artifact on the next request.\n *\n * Ensures the manifest is loaded first so cold-process actions that\n * invalidate before any page render still write tombstones.\n */\nexport async function writeTombstonesForTag(tag: string): Promise<void> {\n if (!tagIndex) {\n await ensureManifestLoaded();\n }\n if (!tagIndex) return;\n const seeds = tagIndex.get(tag);\n if (!seeds || seeds.length === 0) return;\n\n const handler = getCacheHandler();\n const tombstone: ComponentTombstoneRecord = { _t: 'ct' };\n\n await Promise.all(\n seeds.map((seed) => {\n const key = overlayKey(seed.componentId, seed.cacheKey);\n return handler.set(key, tombstone, {\n ttl: DEPLOY_LONG_TTL_SECONDS,\n tags: [tag],\n });\n })\n );\n}\n\n// ─── ISR build diagnostic ────────────────────────────────────────────────\n\nlet isrDiagnosticEmitted = false;\n\nfunction emitIsrDiagnosticIfNeeded(manifest: PrebuiltManifest): void {\n if (isrDiagnosticEmitted) return;\n\n const handler = getCacheHandler();\n const isMemoryBased = handler instanceof MemoryCacheHandler || !isCacheHandlerConfigured();\n if (!isMemoryBased) return;\n\n for (const [, component] of Object.entries(manifest)) {\n if (!component || typeof component !== 'object' || !('entries' in component)) continue;\n for (const [, entry] of Object.entries(component.entries)) {\n if (entry.tags && entry.tags.length > 0) {\n isrDiagnosticEmitted = true;\n console.warn(\n '[timber] tag revalidation of prerendered components requires a shared cache ' +\n 'handler in multi-instance deployments. The current MemoryCacheHandler is ' +\n 'per-process — revalidateTag on one instance will not purge the build seed ' +\n 'on others. Configure a shared handler (Redis, KV) for production ISR.'\n );\n return;\n }\n }\n }\n}\n\n/**\n * Reset the diagnostic flag (for tests).\n * @internal\n */\nexport function resetIsrDiagnostic(): void {\n isrDiagnosticEmitted = false;\n}\n\n// ─── Encoding helpers ────────────────────────────────────────────────────\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64');\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'));\n}\n","/**\n * CDN purge handler singleton — same pattern as cache/handler-store.ts.\n *\n * Set via timber.config.ts or setCdnPurgeHandler() at boot. When set,\n * cache.invalidate({ tag }) also triggers CDN purge for the invalidated\n * tags, so revalidateTag() composes with CDN caching out of the box.\n *\n * See design/25-production-deployments.md §Layer 2.\n */\n\nexport interface CdnPurgeHandler {\n /**\n * Purge the given cache tags from the CDN.\n * Called by cache.invalidate when a tag is invalidated.\n */\n purgeTags(tags: string[]): Promise<void>;\n}\n\nlet handler: CdnPurgeHandler | undefined;\n\n/** Install a CDN purge handler. Called by the framework at boot from timber.config.ts. */\nexport function setCdnPurgeHandler(h: CdnPurgeHandler): void {\n handler = h;\n}\n\n/** Get the current CDN purge handler, if configured. */\nexport function getCdnPurgeHandler(): CdnPurgeHandler | undefined {\n return handler;\n}\n\n/** @internal — reset for tests only. */\nexport function _resetCdnPurgeHandler(): void {\n handler = undefined;\n}\n","import type { CacheOptions } from './index';\nimport type { PrebuiltComponentOptions } from '../server/prebuilt-runtime';\nimport { createCache } from './timber-cache';\nimport { getCacheHandler } from './handler-store';\nimport { recordInvalidation } from './invalidation-epoch';\nimport { writeTombstonesForTag } from '../server/prebuilt/overlay.js';\nimport { getCdnPurgeHandler } from '../cdn/purge-store.js';\n\n/**\n * Public caching API: `cache(fn, opts)`.\n *\n * Wraps an async function with cross-request caching. Uses the configured\n * cache handler (defaults to MemoryCacheHandler, overridable via timber.config.ts).\n *\n * ```ts\n * import { cache } from '@timber-js/app/cache';\n *\n * const getUser = cache(\n * async (id: string) => db.users.findUnique({ where: { id } }),\n * { ttl: 60, tags: (id) => [`user:${id}`] }\n * );\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function cache<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: CacheOptions<Fn>,\n stableId?: string\n): Fn {\n // Pass the getter, not a resolved handler: the wrapper re-resolves it on\n // every call, so module-scope wrappers created before the framework's boot\n // wiring calls setCacheHandler() still pick up the configured handler\n // instead of permanently binding the in-memory fallback (TIM-1029).\n return createCache(fn, opts, getCacheHandler, stableId);\n}\n\n/**\n * Invalidate cache entries by tag or key.\n *\n * ```ts\n * cache.invalidate({ tag: 'products' });\n * cache.invalidate({ key: 'user:abc' });\n * ```\n */\ncache.invalidate = async function invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n // Record before the handler delete so a cached fn in flight skips its\n // post-resolve set instead of resurrecting the invalidated value (TIM-1028).\n recordInvalidation(opts);\n await getCacheHandler().invalidate(opts);\n // Write tombstones for build-seed entries carrying the tag (design/45\n // §ISR mechanics). Runs AFTER handler.invalidate so the tombstone\n // overwrites the cleared overlay slot. This covers ALL invalidation\n // paths — actions, webhooks, route handlers — not just executeAction.\n if (opts.tag) {\n await writeTombstonesForTag(opts.tag);\n }\n // Trigger CDN purge if a handler is configured (design/25 §Layer 2).\n // Purge is best-effort: a CDN API failure or timeout must not block\n // cache invalidation. 10s ceiling prevents a hung CDN API from stalling\n // the entire revalidateTag/action flow.\n if (opts.tag) {\n const purgeHandler = getCdnPurgeHandler();\n if (purgeHandler) {\n try {\n await Promise.race([\n purgeHandler.purgeTags([opts.tag]),\n new Promise<void>((_, reject) =>\n setTimeout(() => reject(new Error('CDN purge timed out (10s)')), 10_000)\n ),\n ]);\n } catch (err) {\n console.error('[timber] CDN purge failed for tag:', err);\n }\n }\n }\n};\n\nconst componentFallbackWarned = new WeakSet<object>();\n\n/**\n * Runtime fallback for `cache.component(...)` callsites the timber-prebuilt\n * transform does not rewrite — nested/computed/argument-position forms (see\n * plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`\n * declarations are transformed and eligible for prebuilding; everything else\n * lands here, warns once per component, and renders dynamically. Same\n * safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):\n * an untraceable callsite degrades in performance, never in correctness.\n *\n * The full public surface (types, docs, `timber` namespace) is TIM-1121;\n * design/45-cache-lifetimes.md specifies the options.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ncache.component = function component<C extends (props: any) => unknown>(\n componentFn: C,\n _options?: PrebuiltComponentOptions\n): C {\n if (!componentFallbackWarned.has(componentFn)) {\n componentFallbackWarned.add(componentFn);\n const name = componentFn.name || 'anonymous component';\n console.warn(\n `[timber] cache.component: \"${name}\" was not transformed at build time — ` +\n `rendering dynamically. Only module-scope declarations ` +\n `(const X = cache.component(...)) are prebuilt.`\n );\n }\n return componentFn;\n};\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-schema-sync-EXGYPhI2.js","names":[],"sources":["../../src/routing/scanner.ts","../../src/cli-schema-sync.ts"],"sourcesContent":["/**\n * Route discovery scanner.\n *\n * Pure function: (appDir, config) → RouteTree\n *\n * Scans the app/ directory and builds a segment tree recognizing all\n * timber.js file conventions. Does NOT handle request matching — this\n * is discovery only.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, extname, basename } from 'node:path';\nimport type {\n RouteTree,\n SegmentNode,\n SegmentType,\n RouteFile,\n ScannerConfig,\n InterceptionMarker,\n} from './types.js';\nimport { classifySegment } from './segment-classify.js';\nimport { DEFAULT_PAGE_EXTENSIONS } from './types.js';\nimport { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';\nimport { swallow } from '../server/logger.js';\nimport { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.js';\n\n/**\n * File convention names that use pageExtensions (can be .tsx, .ts, .jsx, .js, .mdx, etc.)\n */\nconst PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);\n\n/**\n * Legacy compat status-code files.\n * Maps legacy file name → HTTP status code for the fallback chain.\n * See design/10-error-handling.md §\"Fallback Chain\".\n */\nconst LEGACY_STATUS_FILES: Record<string, number> = {\n 'not-found': 404,\n 'forbidden': 403,\n 'unauthorized': 401,\n};\n\n/**\n * File convention names that are always .ts/.tsx (never .mdx etc.)\n */\nconst FIXED_CONVENTIONS = new Set(['middleware', 'access', 'route', 'params']);\n\n/**\n * Status-code file patterns:\n * - Exact 3-digit codes: 401.tsx, 429.tsx, 503.tsx\n * - Category catch-alls: 4xx.tsx, 5xx.tsx\n */\nconst STATUS_CODE_PATTERN = /^(\\d{3}|[45]xx)$/;\n\n/**\n * Scan the app/ directory and build the route tree.\n *\n * @param appDir - Absolute path to the app/ directory\n * @param config - Scanner configuration\n * @returns The complete route tree\n */\nexport function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTree {\n const pageExtensions = config.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;\n const extSet = new Set(pageExtensions);\n\n const tree: RouteTree = {\n root: createSegmentNode('', 'static', '/'),\n };\n\n // Check for proxy.ts at app root\n const proxyFile = findFixedFile(appDir, 'proxy');\n if (proxyFile) {\n tree.proxy = proxyFile;\n }\n\n // Check for global-error.{tsx,ts,jsx,js} at app root.\n // Tier 2 error page — renders standalone (no layouts) when no segment-level\n // error file is found. See design/10-error-handling.md §\"Tier 2\".\n const globalErrorFile = findPageExtFile(appDir, 'global-error', extSet);\n if (globalErrorFile) {\n tree.globalError = globalErrorFile;\n }\n\n // Scan the root directory's files\n scanSegmentFiles(appDir, tree.root, extSet);\n\n // Scan children recursively\n scanChildren(appDir, tree.root, extSet);\n\n // Validate: detect route group collisions (different groups producing pages at the same URL)\n validateRouteGroupCollisions(tree.root);\n\n // Validate: detect duplicate param names in nested dynamic segments\n // e.g., /[id]/items/[id] — same param name in ancestor and descendant\n validateDuplicateParamNames(tree.root);\n\n // Validate: detect same param name used with different bracket forms\n // e.g., [id] in one route and [...id] in another\n validateUniqueBracketForms(tree.root);\n\n return tree;\n}\n\n/**\n * Create an empty segment node.\n */\nfunction createSegmentNode(\n segmentName: string,\n segmentType: SegmentType,\n urlPath: string,\n paramName?: string,\n interceptionMarker?: InterceptionMarker,\n interceptedSegmentName?: string\n): SegmentNode {\n return {\n segmentName,\n segmentType,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName,\n children: [],\n slots: {},\n };\n}\n\n/**\n * Compute the URL path for a child segment given its parent's URL path.\n * Route groups, slots, and intercepting routes do NOT add URL depth.\n */\nfunction computeUrlPath(parentUrlPath: string, dirName: string, segmentType: SegmentType): string {\n // Groups, slots, and intercepting routes don't add to URL path\n if (segmentType === 'group' || segmentType === 'slot' || segmentType === 'intercepting') {\n return parentUrlPath;\n }\n\n const parentPath = parentUrlPath === '/' ? '' : parentUrlPath;\n return `${parentPath}/${dirName}`;\n}\n\n/**\n * Scan a directory for file conventions and populate the segment node.\n */\nfunction scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanSegmentFiles: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n // Skip directories — handled by scanChildren\n try {\n if (statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const ext = extname(entry).slice(1); // remove leading dot\n const name = basename(entry, `.${ext}`);\n\n // Page-extension conventions (page, layout, error, default, denied)\n if (PAGE_EXT_CONVENTIONS.has(name) && extSet.has(ext)) {\n const key = name as 'page' | 'layout' | 'error' | 'default' | 'denied';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Fixed conventions (middleware, access, route, params) — only\n // .ts/.tsx/.js/.jsx, never pageExtensions like .mdx. The regex is\n // anchored: mts/cts/mjs/cjs (and stray tail matches like `.foots`)\n // are NOT recognized. See design/07-routing.md §\"Page Extensions\".\n if (FIXED_CONVENTIONS.has(name) && /^[jt]sx?$/.test(ext)) {\n const key = name as 'middleware' | 'access' | 'route' | 'params';\n const existing = node[key];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node[key] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // JSON status-code files (401.json, 4xx.json, 503.json, 5xx.json)\n // Recognized regardless of pageExtensions — .json is a data format, not a page extension.\n if (STATUS_CODE_PATTERN.test(name) && ext === 'json') {\n if (!node.jsonStatusFiles) {\n node.jsonStatusFiles = {};\n }\n node.jsonStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Status-code files (401.tsx, 4xx.tsx, 503.tsx, 5xx.tsx)\n if (STATUS_CODE_PATTERN.test(name) && extSet.has(ext)) {\n if (!node.statusFiles) {\n node.statusFiles = {};\n }\n const existing = node.statusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node.statusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)\n if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {\n if (!node.legacyStatusFiles) {\n node.legacyStatusFiles = {};\n }\n const existing = node.legacyStatusFiles[name];\n if (existing) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n node.legacyStatusFiles[name] = { filePath: fullPath, extension: ext };\n continue;\n }\n\n // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)\n // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.\n // When both exist for the same base name, dynamic takes precedence.\n // Two files of the SAME kind (static+static or dynamic+dynamic) are a\n // build error — picking one by readdir order is platform-dependent.\n // See design/16-metadata.md §\"Metadata Routes\"\n const metaInfo = classifyMetadataRoute(entry);\n if (metaInfo) {\n if (!node.metadataRoutes) {\n node.metadataRoutes = {};\n }\n const existing = node.metadataRoutes[name];\n if (existing) {\n const existingIsDynamic = isDynamicMetadataExtension(name, existing.extension);\n const newIsDynamic = isDynamicMetadataExtension(name, ext);\n if (existingIsDynamic === newIsDynamic) {\n throwDuplicateConvention(name, existing.filePath, fullPath);\n }\n // Dynamic > static precedence: overwrite only when the new file is dynamic.\n if (newIsDynamic) {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n } else {\n node.metadataRoutes[name] = { filePath: fullPath, extension: ext };\n }\n }\n }\n\n // Validate: route.ts + page.* is a hard build error\n if (node.route && node.page) {\n throw new Error(\n `Build error: route.ts and page.* cannot coexist in the same segment.\\n` +\n ` route.ts: ${node.route.filePath}\\n` +\n ` page: ${node.page.filePath}\\n` +\n `A URL is either an API endpoint or a rendered page, not both.`\n );\n }\n}\n\n/**\n * Throw a build error for two files claiming the same convention slot in\n * one segment (e.g. page.tsx + page.jsx). Which file readdir returns first\n * is platform-dependent (APFS sorts entries, ext4 does not) — silently\n * picking one would let macOS dev and Linux CI serve different\n * implementations of the same route.\n */\nfunction throwDuplicateConvention(\n convention: string,\n firstPath: string,\n secondPath: string\n): never {\n throw new Error(\n `Build error: duplicate '${convention}' files in the same route segment.\\n` +\n ` File 1: ${firstPath}\\n` +\n ` File 2: ${secondPath}\\n` +\n `Each segment may define '${convention}' at most once. Remove one of the files.`\n );\n}\n\n/**\n * Recursively scan child directories and build the segment tree.\n */\nfunction scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<string>): void {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n swallow(err, `scanChildren: unreadable directory ${dirPath}`, { level: 'warn' });\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry);\n\n try {\n if (!statSync(fullPath).isDirectory()) continue;\n } catch {\n continue;\n }\n\n // Reject directories with encoded path delimiters or null bytes.\n // These can cause route collisions when decoded at the URL boundary.\n // See design/13-security.md §\"Encoded separators rejected\" and §\"Null bytes rejected\".\n if (ENCODED_SEPARATOR_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded path delimiter (%2F or %5C).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded separators in directory names cause route collisions when decoded. ` +\n `Rename the directory to remove the encoded delimiter.`\n );\n }\n if (NULL_BYTE_RE.test(entry)) {\n throw new Error(\n `Build error: directory name contains an encoded null byte (%00).\\n` +\n ` Directory: ${fullPath}\\n` +\n `Encoded null bytes in directory names are not allowed. ` +\n `Rename the directory to remove the null byte encoding.`\n );\n }\n\n const { type, paramName, interceptionMarker, interceptedSegmentName } = classifySegment(entry);\n\n // Skip private folders — underscore-prefixed dirs are excluded from routing\n if (type === 'private') continue;\n\n const urlPath = computeUrlPath(parentNode.urlPath, entry, type);\n const childNode = createSegmentNode(\n entry,\n type,\n urlPath,\n paramName,\n interceptionMarker,\n interceptedSegmentName\n );\n\n // Scan this segment's files\n scanSegmentFiles(fullPath, childNode, extSet);\n\n // Recurse into subdirectories\n scanChildren(fullPath, childNode, extSet);\n\n // Attach to parent: slots go into slots record, everything else is a child\n if (type === 'slot') {\n const slotName = entry.slice(1); // remove @\n parentNode.slots[slotName] = childNode;\n } else {\n parentNode.children.push(childNode);\n }\n }\n}\n\n/**\n * Validate that route groups don't produce conflicting pages/routes at the same URL path.\n *\n * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim\n * /login — the scanner must detect and reject this at build time.\n *\n * Each parallel slot is validated as its OWN collision space: a slot's pages\n * intentionally share URL paths with the parent tree (that's the point of\n * parallel routes), but two route groups INSIDE one slot claiming the same\n * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.\n */\nfunction validateRouteGroupCollisions(root: SegmentNode): void {\n // Map from urlPath → { filePath, source } for the first page/route seen at that path\n collectRoutableLeaves(root, new Map(), '');\n}\n\n/**\n * Walk the segment tree and collect all routable leaves (page or route files),\n * throwing on collision. Each slot recurses with a FRESH collision map —\n * slots are parallel routes that intentionally share URL paths with their\n * parent tree, but collisions within a single slot are real errors.\n */\nfunction collectRoutableLeaves(\n node: SegmentNode,\n seen: Map<string, { filePath: string; segmentPath: string }>,\n segmentPath: string\n): void {\n // Intercepting routes are resolved by the slot resolver, not URL matching,\n // and their computed urlPaths intentionally mirror other routes (e.g. two\n // interceptors in one slot both produce /gallery/[id]). Skip them entirely.\n if (node.segmentType === 'intercepting') return;\n\n const currentPath = segmentPath\n ? `${segmentPath}/${node.segmentName}`\n : node.segmentName || '(root)';\n\n const routableFile = node.page ?? node.route;\n if (routableFile) {\n const existing = seen.get(node.urlPath);\n if (existing) {\n throw new Error(\n `Build error: route collision — multiple route groups produce a page/route at the same URL path.\\n` +\n ` URL path: ${node.urlPath}\\n` +\n ` File 1: ${existing.filePath} (via ${existing.segmentPath})\\n` +\n ` File 2: ${routableFile.filePath} (via ${currentPath})\\n` +\n `Each URL path must map to exactly one page or route handler. ` +\n `Rename or move one of the conflicting files.`\n );\n }\n seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });\n }\n\n // Recurse into children\n for (const child of node.children) {\n collectRoutableLeaves(child, seen, currentPath);\n }\n\n // Recurse into slots — each slot gets a fresh map (its own collision space)\n for (const slotNode of Object.values(node.slots)) {\n collectRoutableLeaves(slotNode, new Map(), currentPath);\n }\n}\n\n/**\n * Validate that no route chain contains duplicate dynamic param names.\n *\n * Example violation:\n * app/[id]/items/[id]/page.tsx — 'id' appears twice in the ancestor chain.\n *\n * Route groups are transparent — params accumulate through them.\n * Slots are independent — duplicate detection does NOT cross slot boundaries.\n *\n * See design/07-routing.md §\"Duplicate Param Name Detection\"\n */\nfunction validateDuplicateParamNames(root: SegmentNode): void {\n walkForDuplicateParams(root, new Map());\n}\n\n/**\n * Recursively walk the segment tree, tracking seen param names → segment paths.\n * Throws on the first duplicate found.\n */\nfunction walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {\n // If this node introduces a param name, check for duplicates\n if (node.paramName) {\n const existing = seen.get(node.paramName);\n if (existing) {\n throw new Error(\n `[timber] Duplicate param name '${node.paramName}' in route chain.\\n` +\n ` First defined at: ${existing}\\n` +\n ` Duplicate at: ${node.urlPath || '/'}\\n` +\n ` Rename one of the segments to avoid ambiguity.`\n );\n }\n // Add to seen for descendants (use a new Map to avoid polluting siblings)\n seen = new Map(seen);\n seen.set(node.paramName, node.urlPath || '/');\n }\n\n // Recurse into children (they inherit the accumulated params)\n for (const child of node.children) {\n walkForDuplicateParams(child, seen);\n }\n\n // Slots are independent parallel routes — start fresh param tracking\n // (a slot's params don't conflict with the main route's params)\n for (const slotNode of Object.values(node.slots)) {\n walkForDuplicateParams(slotNode, new Map(seen));\n }\n}\n\n/**\n * Validate that each param name uses a single bracket form across the entire route tree.\n *\n * Different bracket forms produce different types:\n * [id] → string\n * [...id] → string[]\n * [[...id]] → string[] | undefined\n *\n * If the same param name appears with multiple forms, the no-argument\n * getSegmentParams() / useSegmentParams() return type becomes ambiguous.\n *\n * Same name + same form across different routes is fine and expected.\n */\nfunction validateUniqueBracketForms(root: SegmentNode): void {\n const forms = new Map<string, { segmentType: SegmentType; urlPath: string }>();\n collectBracketForms(root, forms);\n}\n\nfunction collectBracketForms(\n node: SegmentNode,\n forms: Map<string, { segmentType: SegmentType; urlPath: string }>\n): void {\n if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {\n const existing = forms.get(node.paramName);\n if (existing && existing.segmentType !== node.segmentType) {\n const bracketExamples: Record<string, string> = {\n 'dynamic': `[${node.paramName}]`,\n 'catch-all': `[...${node.paramName}]`,\n 'optional-catch-all': `[[...${node.paramName}]]`,\n };\n throw new Error(\n `[timber] Param name '${node.paramName}' uses multiple bracket forms.\\n` +\n ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\\n` +\n ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\\n` +\n ` Each param name must use a single bracket form across all routes.\\n` +\n ` Rename one of the segments to fix this (e.g., [${node.paramName}] → [${node.paramName}Id]).`\n );\n }\n if (!existing) {\n forms.set(node.paramName, {\n segmentType: node.segmentType,\n urlPath: node.urlPath || '/',\n });\n }\n }\n\n for (const child of node.children) {\n collectBracketForms(child, forms);\n }\n for (const slotNode of Object.values(node.slots)) {\n collectBracketForms(slotNode, forms);\n }\n}\n\n/**\n * Find a fixed-extension file (proxy.ts) in a directory.\n */\nfunction findFixedFile(dirPath: string, name: string): RouteFile | undefined {\n for (const ext of ['ts', 'tsx']) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n\n/**\n * Find a file using the configured page extensions (tsx, ts, jsx, js, mdx, etc.).\n * Used for app-root conventions like global-error that aren't per-segment.\n */\nfunction findPageExtFile(\n dirPath: string,\n name: string,\n extSet: Set<string>\n): RouteFile | undefined {\n for (const ext of extSet) {\n const fullPath = join(dirPath, `${name}.${ext}`);\n try {\n if (statSync(fullPath).isFile()) {\n return { filePath: fullPath, extension: ext };\n }\n } catch {\n // File doesn't exist\n }\n }\n return undefined;\n}\n","/**\n * `timber schema sync` — scan filesystem for dynamic segments, append\n * missing entries with `codec.string` defaults, and remove stale entries\n * whose segments no longer exist in the filesystem.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.js';\nimport type { SegmentNode } from './routing/types.js';\nimport { resolveAppDir } from './plugin-context.js';\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Reconstruct bracket key from segmentName (already has brackets)\n segments.add(node.segmentName);\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Replace comment bodies (and optionally string contents) with spaces,\n * preserving length and newlines so every index maps 1:1 to the original.\n * Lets the structural scanning below ignore `segmentParams` mentions and\n * braces inside comments/strings without a TS parser (fine pre-alpha,\n * see TIM-1066).\n *\n * With `maskStrings`, string contents are blanked too — except literal\n * 'segmentParams' strings, kept so a quoted property key stays findable.\n */\nfunction maskSource(content: string, maskStrings: boolean): string {\n const out = content.split('');\n const n = content.length;\n let i = 0;\n\n const blank = (from: number, to: number): void => {\n for (let j = from; j < to; j++) {\n if (content[j] !== '\\n') out[j] = ' ';\n }\n };\n\n while (i < n) {\n const ch = content[i];\n const next = content[i + 1];\n if (ch === '/' && next === '/') {\n const start = i;\n while (i < n && content[i] !== '\\n') i++;\n blank(start, i);\n } else if (ch === '/' && next === '*') {\n const start = i;\n i += 2;\n while (i < n && !(content[i] === '*' && content[i + 1] === '/')) i++;\n i = Math.min(i + 2, n);\n blank(start, i);\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n const start = i;\n i++;\n while (i < n && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n const end = i; // at closing quote (or n if unterminated)\n if (maskStrings && content.slice(start + 1, end) !== 'segmentParams') {\n blank(start + 1, end);\n }\n i++;\n } else {\n i++;\n }\n }\n return out.join('');\n}\n\n/**\n * Locate the `segmentParams: { ... }` block in a schema file.\n * Returns the indices of the opening and matching closing brace, or null\n * if no segmentParams property is found. Comments and strings are masked\n * first, so a JSDoc example mentioning `segmentParams: { ... }` above the\n * real defineSchema call is not mistaken for the block, and braces inside\n * comments/strings don't skew the depth count. The search is anchored to\n * the `defineSchema(` call when present, so helper objects with their own\n * segmentParams property earlier in the file are skipped.\n */\nexport function findSegmentParamsBlock(\n content: string\n): { openBrace: number; closeBrace: number } | null {\n const masked = maskSource(content, true);\n\n const defineCall = /\\bdefineSchema\\s*\\(/.exec(masked);\n const keyRe = /(['\"]?)segmentParams\\1\\s*:\\s*\\{/g;\n keyRe.lastIndex = defineCall ? defineCall.index + defineCall[0].length : 0;\n const keyMatch = keyRe.exec(masked);\n if (!keyMatch) return null;\n\n const openBrace = keyMatch.index + keyMatch[0].length - 1;\n let depth = 1;\n let i = openBrace + 1;\n while (i < masked.length && depth > 0) {\n if (masked[i] === '{') depth++;\n else if (masked[i] === '}') depth--;\n i++;\n }\n if (depth > 0) return null;\n // i now points to just after the closing brace\n return { openBrace, closeBrace: i - 1 };\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Scoped to the segmentParams: { ... } block — bracket keys elsewhere in\n * the file (unrelated objects, comments above defineSchema) are ignored.\n * Comments inside the block are masked, so a commented-out entry does not\n * count as registered.\n *\n * The key charset matches the route classifier (segment-classify.ts):\n * any character except `]`, so hyphenated params like '[my-param]' parse.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const block = findSegmentParamsBlock(content);\n if (!block) return [];\n\n const blockContent = maskSource(content, false).slice(block.openBrace, block.closeBrace);\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?[^\\]]+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(blockContent)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\nexport function defaultCodecForSegment(seg: string): string {\n if (seg.startsWith('[[...')) return 'codec.optionalCatchAll(codec.string)';\n if (seg.startsWith('[...')) return 'codec.stringArray';\n return 'codec.string';\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': ${defaultCodecForSegment(seg)},`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nconst CODEC_IMPORT = \"import { codec } from '@timber-js/app/codec';\";\n\nfunction hasCodecImport(content: string): boolean {\n return /import\\s+\\{[^}]*\\bcodec\\b[^}]*\\}\\s+from\\s+['\"]@timber-js\\/app\\/codec['\"]/.test(content);\n}\n\nfunction ensureCodecImport(content: string): string {\n if (hasCodecImport(content)) return content;\n\n // Insert after the last existing import statement\n const lines = content.split('\\n');\n let lastImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*import\\s/.test(lines[i])) lastImportIndex = i;\n }\n\n if (lastImportIndex === -1) {\n return CODEC_IMPORT + '\\n' + content;\n }\n\n lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);\n return lines.join('\\n');\n}\n\nfunction appendToSchema(content: string, newSegments: string[]): string {\n content = ensureCodecImport(content);\n\n const block = findSegmentParamsBlock(content);\n if (!block) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n const { openBrace, closeBrace } = block;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments\n .map((seg) => `${indent}'${seg}': ${defaultCodecForSegment(seg)},`)\n .join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\n/**\n * Remove entries from the segmentParams block whose keys match `staleSegments`.\n * Removes the entire entry including trailing comma/whitespace. Scoped to\n * the segmentParams block — identical keys in unrelated objects elsewhere\n * in the file are left untouched.\n */\nfunction removeFromSchema(content: string, staleSegments: string[]): string {\n const block = findSegmentParamsBlock(content);\n if (!block) return content;\n\n let blockContent = content.slice(block.openBrace, block.closeBrace);\n // Key matching and the entry walk run on a comment-masked copy (same\n // length, indices align) so commented-out entries are never removed.\n // Both copies are spliced in sync to keep indices valid.\n let blockMasked = maskSource(blockContent, false);\n\n for (const seg of staleSegments) {\n const escaped = seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const keyRe = new RegExp(`^[ \\t]*['\"]${escaped}['\"][ \\t]*:`, 'gm');\n let m: RegExpExecArray | null;\n while ((m = keyRe.exec(blockMasked)) !== null) {\n const start = m.index;\n let i = m.index + m[0].length;\n let depth = 0;\n let inString: string | null = null;\n\n while (i < blockMasked.length) {\n const ch = blockMasked[i];\n if (inString) {\n if (ch === '\\\\') {\n i++;\n } else if (ch === inString) {\n inString = null;\n }\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch;\n } else if (ch === '(' || ch === '{' || ch === '[') {\n depth++;\n } else if (ch === ')' || ch === '}' || ch === ']') {\n depth--;\n } else if (depth === 0 && ch === ',') {\n i++;\n break;\n } else if (depth === 0 && (ch === '\\n' || ch === '}')) {\n break;\n }\n i++;\n }\n\n // Consume trailing newline\n if (i < blockMasked.length && blockMasked[i] === '\\n') i++;\n\n blockContent = blockContent.slice(0, start) + blockContent.slice(i);\n blockMasked = blockMasked.slice(0, start) + blockMasked.slice(i);\n keyRe.lastIndex = start;\n }\n }\n\n return content.slice(0, block.openBrace) + blockContent + content.slice(block.closeBrace);\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Segments that were removed (no longer in filesystem). */\n removedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n const schemaPath = findSchemaFile(appDir);\n\n if (filesystemSegments.length === 0 && !schemaPath) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n removedSegments: [],\n created: true,\n };\n }\n\n // Read existing schema and diff\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n const stale = existingSegments.filter((seg) => !filesystemSegments.includes(seg));\n\n if (missing.length === 0 && stale.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n let updated = content;\n if (stale.length > 0) {\n updated = removeFromSchema(updated, stale);\n }\n if (missing.length > 0) {\n updated = appendToSchema(updated, missing);\n }\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n removedSegments: stale,\n created: false,\n };\n}\n"],"mappings":"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"}
|