@timber-js/app 0.2.0-alpha.134 → 0.2.0-alpha.136

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.
Files changed (153) hide show
  1. package/dist/_chunks/{actions-B8Fhsosm.js → actions-CHFblsdT.js} +13 -9
  2. package/dist/_chunks/actions-CHFblsdT.js.map +1 -0
  3. package/dist/_chunks/{build-output-helper-K7oUCRqU.js → build-output-helper-CfBfOU9m.js} +1 -1
  4. package/dist/_chunks/{build-output-helper-K7oUCRqU.js.map → build-output-helper-CfBfOU9m.js.map} +1 -1
  5. package/dist/_chunks/{codec-registry-DqkeAydt.js → codec-registry-G9hD5LOq.js} +1 -1
  6. package/dist/_chunks/{codec-registry-DqkeAydt.js.map → codec-registry-G9hD5LOq.js.map} +1 -1
  7. package/dist/_chunks/{define-jeHDXZGq.js → define-DXWUJb-K.js} +3 -3
  8. package/dist/_chunks/{define-jeHDXZGq.js.map → define-DXWUJb-K.js.map} +1 -1
  9. package/dist/_chunks/{define-DMq-Yjp7.js → define-Dp-qu3ql.js} +3 -3
  10. package/dist/_chunks/{define-DMq-Yjp7.js.map → define-Dp-qu3ql.js.map} +1 -1
  11. package/dist/_chunks/fast-hash-DXZ7S2D5.js +36 -0
  12. package/dist/_chunks/fast-hash-DXZ7S2D5.js.map +1 -0
  13. package/dist/_chunks/{format-BjN9HLu9.js → format-CR6v1pWE.js} +1 -1
  14. package/dist/_chunks/{format-BjN9HLu9.js.map → format-CR6v1pWE.js.map} +1 -1
  15. package/dist/_chunks/{handler-store-bIAstrxQ.js → handler-store-aQMpqPe7.js} +1 -1
  16. package/dist/_chunks/{handler-store-bIAstrxQ.js.map → handler-store-aQMpqPe7.js.map} +1 -1
  17. package/dist/_chunks/{href-validation-BJmD5mZh.js → href-validation-2G-HZaQ8.js} +8 -8
  18. package/dist/_chunks/href-validation-2G-HZaQ8.js.map +1 -0
  19. package/dist/_chunks/{navigation-root-B5-RmsPi.js → navigation-root-BQqFQI-n.js} +3 -3
  20. package/dist/_chunks/{navigation-root-B5-RmsPi.js.map → navigation-root-BQqFQI-n.js.map} +1 -1
  21. package/dist/_chunks/{registry-BI9cgCt7.js → registry-Dp_hJL4s.js} +1 -1
  22. package/dist/_chunks/{registry-BI9cgCt7.js.map → registry-Dp_hJL4s.js.map} +1 -1
  23. package/dist/_chunks/{resolve-schema-DU2nbVOi.js → resolve-schema-D3CbcdCW.js} +2 -2
  24. package/dist/_chunks/{resolve-schema-DU2nbVOi.js.map → resolve-schema-D3CbcdCW.js.map} +1 -1
  25. package/dist/_chunks/{router-ref-B2-2kIIC.js → router-ref-B50j42zB.js} +2 -2
  26. package/dist/_chunks/{router-ref-B2-2kIIC.js.map → router-ref-B50j42zB.js.map} +1 -1
  27. package/dist/_chunks/{schema-bridge-o0SwFtXW.js → schema-bridge-Cuy8BaYu.js} +2 -2
  28. package/dist/_chunks/{schema-bridge-o0SwFtXW.js.map → schema-bridge-Cuy8BaYu.js.map} +1 -1
  29. package/dist/_chunks/{segment-context-Bg0pFbLT.js → segment-context-BnxoPSFQ.js} +1 -1
  30. package/dist/_chunks/{segment-context-Bg0pFbLT.js.map → segment-context-BnxoPSFQ.js.map} +1 -1
  31. package/dist/_chunks/{ssr-data-DVX8S7vj.js → ssr-data-DxXyoRrz.js} +1 -1
  32. package/dist/_chunks/{ssr-data-DVX8S7vj.js.map → ssr-data-DxXyoRrz.js.map} +1 -1
  33. package/dist/_chunks/{stale-reload-CCqb62Z3.js → stale-reload-DIIIVGh_.js} +1 -1
  34. package/dist/_chunks/{stale-reload-CCqb62Z3.js.map → stale-reload-DIIIVGh_.js.map} +1 -1
  35. package/dist/_chunks/{use-query-states-C9G6LP0F.js → use-query-states-D9SBlAfp.js} +2 -2
  36. package/dist/_chunks/{use-query-states-C9G6LP0F.js.map → use-query-states-D9SBlAfp.js.map} +1 -1
  37. package/dist/_chunks/{use-segment-params-CIeinIx2.js → use-segment-params-x2poKmk8.js} +2 -2
  38. package/dist/_chunks/use-segment-params-x2poKmk8.js.map +1 -0
  39. package/dist/_chunks/{validation-AfEFM5ze.js → validation-BoWM_qI1.js} +31 -2
  40. package/dist/_chunks/validation-BoWM_qI1.js.map +1 -0
  41. package/dist/_chunks/walkers-BbEu2_Pp.js.map +1 -1
  42. package/dist/adapters/cloudflare.js +1 -1
  43. package/dist/adapters/nitro.js +1 -1
  44. package/dist/cache/cache-api.d.ts +1 -1
  45. package/dist/cache/cache-api.d.ts.map +1 -1
  46. package/dist/cache/index.js +17 -39
  47. package/dist/cache/index.js.map +1 -1
  48. package/dist/cache/timber-cache.d.ts +1 -1
  49. package/dist/cache/timber-cache.d.ts.map +1 -1
  50. package/dist/client/error-boundary.js +2 -2
  51. package/dist/client/index.d.ts.map +1 -1
  52. package/dist/client/index.js +10 -10
  53. package/dist/client/index.js.map +1 -1
  54. package/dist/client/internal.js +8 -8
  55. package/dist/client/internal.js.map +1 -1
  56. package/dist/client/link.d.ts.map +1 -1
  57. package/dist/client/navigation-context.d.ts.map +1 -1
  58. package/dist/client/use-pathname.d.ts.map +1 -1
  59. package/dist/client/use-search-params.d.ts.map +1 -1
  60. package/dist/codec.js +1 -1
  61. package/dist/cookies/index.js +3 -3
  62. package/dist/cookies/validation.d.ts +11 -23
  63. package/dist/cookies/validation.d.ts.map +1 -1
  64. package/dist/dev-tools/error-page.d.ts.map +1 -1
  65. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +106 -3
  68. package/dist/index.js.map +1 -1
  69. package/dist/params/define-schema.d.ts.map +1 -1
  70. package/dist/params/index.js +2 -2
  71. package/dist/params/index.js.map +1 -1
  72. package/dist/plugins/cache.d.ts +31 -0
  73. package/dist/plugins/cache.d.ts.map +1 -0
  74. package/dist/routing/link-codegen.d.ts.map +1 -1
  75. package/dist/schema-bridge.d.ts.map +1 -1
  76. package/dist/search-params/index.js +3 -3
  77. package/dist/segment-params/index.js +1 -1
  78. package/dist/server/actions.d.ts.map +1 -1
  79. package/dist/server/body-limits.d.ts.map +1 -1
  80. package/dist/server/cookie-context.d.ts +2 -2
  81. package/dist/server/cookie-context.d.ts.map +1 -1
  82. package/dist/server/csrf.d.ts +3 -2
  83. package/dist/server/csrf.d.ts.map +1 -1
  84. package/dist/server/html-injector-core.d.ts.map +1 -1
  85. package/dist/server/html-injectors.d.ts.map +1 -1
  86. package/dist/server/index.js +17 -8
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/internal.js +50 -13
  89. package/dist/server/internal.js.map +1 -1
  90. package/dist/server/pipeline.d.ts.map +1 -1
  91. package/dist/server/primitives.d.ts.map +1 -1
  92. package/dist/server/route-element-builder.d.ts.map +1 -1
  93. package/dist/server/rsc-entry/api-handler.d.ts +11 -2
  94. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  95. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  96. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  97. package/dist/server/rsc-entry/revalidate-renderer.d.ts +108 -0
  98. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -0
  99. package/dist/server/rsc-entry/ssr-renderer.d.ts +15 -0
  100. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  101. package/dist/server/sitemap-handler.d.ts.map +1 -1
  102. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  103. package/dist/server/tree-builder.d.ts.map +1 -1
  104. package/dist/server/types.d.ts +22 -2
  105. package/dist/server/types.d.ts.map +1 -1
  106. package/dist/shared/href-validation.d.ts.map +1 -1
  107. package/package.json +7 -6
  108. package/src/cache/cache-api.ts +3 -2
  109. package/src/cache/timber-cache.ts +27 -4
  110. package/src/cli.ts +0 -0
  111. package/src/client/index.ts +1 -0
  112. package/src/client/link.tsx +8 -5
  113. package/src/client/navigation-context.ts +2 -0
  114. package/src/client/use-pathname.ts +1 -0
  115. package/src/client/use-search-params.ts +1 -0
  116. package/src/client/use-segment-params.ts +1 -0
  117. package/src/config-validation.ts +1 -1
  118. package/src/cookies/validation.ts +58 -0
  119. package/src/dev-tools/error-page.ts +1 -0
  120. package/src/fonts/virtual-modules.ts +2 -0
  121. package/src/index.ts +5 -1
  122. package/src/params/define-schema.ts +1 -0
  123. package/src/plugins/cache.ts +180 -0
  124. package/src/routing/link-codegen.ts +5 -3
  125. package/src/schema-bridge.ts +3 -2
  126. package/src/server/action-client.ts +31 -13
  127. package/src/server/actions.ts +20 -3
  128. package/src/server/body-limits.ts +10 -2
  129. package/src/server/cookie-context.ts +7 -2
  130. package/src/server/cookie-parsing.ts +1 -1
  131. package/src/server/csrf.ts +39 -6
  132. package/src/server/html-injector-core.ts +6 -5
  133. package/src/server/html-injectors.ts +8 -5
  134. package/src/server/pipeline.ts +19 -10
  135. package/src/server/primitives.ts +11 -1
  136. package/src/server/route-element-builder.ts +2 -0
  137. package/src/server/rsc-entry/api-handler.ts +15 -9
  138. package/src/server/rsc-entry/error-renderer.ts +4 -3
  139. package/src/server/rsc-entry/index.ts +16 -36
  140. package/src/server/rsc-entry/render-route.ts +1 -1
  141. package/src/server/rsc-entry/revalidate-renderer.ts +344 -0
  142. package/src/server/rsc-entry/ssr-renderer.ts +76 -0
  143. package/src/server/sitemap-handler.ts +24 -2
  144. package/src/server/slot-resolver.ts +8 -2
  145. package/src/server/ssr-wrappers.tsx +2 -0
  146. package/src/server/tree-builder.ts +2 -0
  147. package/src/server/types.ts +24 -4
  148. package/src/shared/href-validation.ts +12 -8
  149. package/LICENSE +0 -8
  150. package/dist/_chunks/actions-B8Fhsosm.js.map +0 -1
  151. package/dist/_chunks/href-validation-BJmD5mZh.js.map +0 -1
  152. package/dist/_chunks/use-segment-params-CIeinIx2.js.map +0 -1
  153. package/dist/_chunks/validation-AfEFM5ze.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { n as addSpanEventSync } from "../_chunks/tracing-DP5djRq5.js";
2
2
  import { d as logSwrRefetchFailed } from "../_chunks/logger-CMd9P1xQ.js";
3
- import { n as setCacheHandler, t as getCacheHandler } from "../_chunks/handler-store-bIAstrxQ.js";
3
+ import { t as fnv1aHash } from "../_chunks/fast-hash-DXZ7S2D5.js";
4
+ import { n as setCacheHandler, t as getCacheHandler } from "../_chunks/handler-store-aQMpqPe7.js";
4
5
  //#region src/cache/sizeof.ts
5
6
  /**
6
7
  * Lightweight byte-size estimation for cache entries.
@@ -156,39 +157,6 @@ function createSingleflight(opts) {
156
157
  } };
157
158
  }
158
159
  //#endregion
159
- //#region src/cache/fast-hash.ts
160
- /**
161
- * Fast non-cryptographic hash for cache keys.
162
- *
163
- * FNV-1a 64-bit produces a well-distributed hash with a collision
164
- * probability of ~1 in 5 billion at 77k keys (birthday paradox).
165
- * Not suitable for security, but ideal for cache key generation
166
- * where we need speed over crypto strength.
167
- *
168
- * Uses BigInt for 64-bit arithmetic — supported in all modern runtimes
169
- * including Cloudflare Workers. No node:crypto dependency.
170
- *
171
- * See TIM-370.
172
- */
173
- var FNV_OFFSET_BASIS = 14695981039346656037n;
174
- var FNV_PRIME = 1099511628211n;
175
- var MASK_64 = 18446744073709551615n;
176
- /**
177
- * Compute a 64-bit FNV-1a hash of a string, returned as a 16-char hex string.
178
- *
179
- * 64 bits gives ~5 billion keys before a 50% collision probability
180
- * (birthday paradox), making accidental collisions effectively impossible
181
- * for cache key use cases.
182
- */
183
- function fnv1aHash(input) {
184
- let hash = FNV_OFFSET_BASIS;
185
- for (let i = 0; i < input.length; i++) {
186
- hash ^= BigInt(input.charCodeAt(i));
187
- hash = hash * FNV_PRIME & MASK_64;
188
- }
189
- return hash.toString(16).padStart(16, "0");
190
- }
191
- //#endregion
192
160
  //#region src/cache/timber-cache.ts
193
161
  var defaultSingleflight = createSingleflight();
194
162
  /**
@@ -212,7 +180,8 @@ function resolveTags(opts, args) {
212
180
  if (Array.isArray(opts.tags)) return opts.tags;
213
181
  return opts.tags(...args);
214
182
  }
215
- var fnIdCounter = 0;
183
+ var unstableIdCounter = 0;
184
+ var hasWarnedUnstableId = false;
216
185
  /**
217
186
  * Creates a cached wrapper around an async function.
218
187
  *
@@ -225,8 +194,17 @@ var fnIdCounter = 0;
225
194
  * Cache hits/misses are recorded as OTEL span events on the enclosing
226
195
  * span (not child spans). The DevSpanProcessor reads these for dev log output.
227
196
  */
228
- function createCache(fn, opts, handler) {
229
- const fnId = `timber-cache:${fnIdCounter++}`;
197
+ function createCache(fn, opts, handler, stableId) {
198
+ let fnId;
199
+ if (stableId) fnId = stableId;
200
+ else if (opts.key) fnId = `timber-cache:${unstableIdCounter++}`;
201
+ else {
202
+ fnId = `timber-cache:unstable:${unstableIdCounter++}`;
203
+ if (!hasWarnedUnstableId && typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
204
+ hasWarnedUnstableId = true;
205
+ console.warn("[timber] cache() called without a stable ID or explicit key function. Cache keys will use a process-local counter which is unstable across cold starts. Run through the timber Vite plugin or provide an explicit `key` option to fix this.");
206
+ }
207
+ }
230
208
  const sf = opts.timeoutMs ? createSingleflight({ timeoutMs: opts.timeoutMs }) : defaultSingleflight;
231
209
  return (async (...args) => {
232
210
  const key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId, args);
@@ -298,8 +276,8 @@ createCache.invalidate = async function invalidate(handler, opts) {
298
276
  * );
299
277
  * ```
300
278
  */
301
- function cache(fn, opts) {
302
- return createCache(fn, opts, getCacheHandler());
279
+ function cache(fn, opts, stableId) {
280
+ return createCache(fn, opts, getCacheHandler(), stableId);
303
281
  }
304
282
  /**
305
283
  * Invalidate cache entries by tag or key.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/sizeof.ts","../../src/cache/redis-handler.ts","../../src/cache/stable-stringify.ts","../../src/cache/singleflight.ts","../../src/cache/fast-hash.ts","../../src/cache/timber-cache.ts","../../src/cache/cache-api.ts","../../src/cache/index.ts"],"sourcesContent":["/**\n * Lightweight byte-size estimation for cache entries.\n *\n * Estimates the in-memory byte cost of a JavaScript value using\n * JSON.stringify().length * 2 (UTF-16 char width). This is a rough\n * approximation — it doesn't account for V8 object overhead, Map\n * metadata, or non-serializable values — but it's fast and good\n * enough for cache budget enforcement.\n *\n * Values that fail JSON serialization (circular references, BigInt,\n * etc.) return 0, allowing the entry to be cached without counting\n * toward the byte budget. This is a conservative choice: it's better\n * to cache and undercount than to reject the entry.\n */\n\n/**\n * Estimate the byte size of a value.\n *\n * Uses `JSON.stringify(value).length * 2` to approximate UTF-16\n * in-memory size. Returns 0 if the value is not serializable.\n */\nexport function estimateByteSize(value: unknown): number {\n try {\n const json = JSON.stringify(value);\n if (json === undefined) return 0;\n // Each JS string character is 2 bytes (UTF-16)\n return json.length * 2;\n } catch (e) {\n // Circular reference or non-serializable value\n console.debug('[timber] sizeof estimation failed:', e instanceof Error ? e.message : e);\n return 0;\n }\n}\n","import type { CacheHandler } from './index';\n\n/**\n * Minimal Redis client interface — compatible with ioredis, node-redis, and\n * Cloudflare Workers Redis bindings. We depend on the interface, not the\n * implementation, so users bring their own Redis client.\n */\nexport interface RedisClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n del(key: string | string[]): Promise<number>;\n sadd(key: string, ...members: string[]): Promise<number>;\n smembers(key: string): Promise<string[]>;\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 — any client implementing the RedisClient\n * interface works (ioredis, node-redis, @upstash/redis, etc.).\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(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void> {\n const ck = this.cacheKey(key);\n const expiresAt = Date.now() + opts.ttl * 1000;\n const payload = JSON.stringify({ value, expiresAt });\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, 'EX', redisTtlSeconds);\n\n // Track key membership in each tag set\n for (const tag of opts.tags) {\n await this.client.sadd(this.tagKey(tag), key);\n }\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\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 if (keys.length > 0) {\n const cacheKeys = keys.map((k) => this.cacheKey(k));\n await this.client.del(cacheKeys);\n }\n\n // Clean up the tag set itself\n await this.client.del(tk);\n }\n }\n}\n","/**\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 */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) return JSON.stringify(value);\n if (typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return '[' + value.map((item) => stableStringify(item)).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 pairs.push(JSON.stringify(key) + ':' + stableStringify(obj[key]));\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: () => 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: () => Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing) return existing as Promise<T>;\n\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 promise = new Promise<T>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new SingleflightTimeoutError(key, timeoutMs)),\n timeoutMs\n );\n // Wrap in try/catch so a synchronous throw from fn()\n // (e.g. argument validation) still clears the timer.\n // Without this, the timer leaks until expiry.\n try {\n fn().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();\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 * Fast non-cryptographic hash for cache keys.\n *\n * FNV-1a 64-bit produces a well-distributed hash with a collision\n * probability of ~1 in 5 billion at 77k keys (birthday paradox).\n * Not suitable for security, but ideal for cache key generation\n * where we need speed over crypto strength.\n *\n * Uses BigInt for 64-bit arithmetic — supported in all modern runtimes\n * including Cloudflare Workers. No node:crypto dependency.\n *\n * See TIM-370.\n */\n\n// FNV-1a constants for 64-bit hash\nconst FNV_OFFSET_BASIS = 0xcbf29ce484222325n;\nconst FNV_PRIME = 0x100000001b3n;\nconst MASK_64 = 0xffffffffffffffffn;\n\n/**\n * Compute a 64-bit FNV-1a hash of a string, returned as a 16-char hex string.\n *\n * 64 bits gives ~5 billion keys before a 50% collision probability\n * (birthday paradox), making accidental collisions effectively impossible\n * for cache key use cases.\n */\nexport function fnv1aHash(input: string): string {\n let hash = FNV_OFFSET_BASIS;\n for (let i = 0; i < input.length; i++) {\n hash ^= BigInt(input.charCodeAt(i));\n hash = (hash * FNV_PRIME) & MASK_64;\n }\n return hash.toString(16).padStart(16, '0');\n}\n","import type { CacheHandler, CacheOptions } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport { addSpanEventSync } from '../server/tracing.js';\nimport { logSwrRefetchFailed } from '../server/logger.js';\nimport { fnv1aHash } from './fast-hash.js';\n\nconst defaultSingleflight = createSingleflight();\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// Counter for generating unique function IDs when no explicit key is provided.\nlet fnIdCounter = 0;\n\n/**\n * Creates a cached wrapper around an async function.\n *\n * - SHA-256 default keys with normalized JSON args\n * - Singleflight: concurrent misses → single execution\n * - SWR: serve stale immediately, background refetch\n * - Tags as string[] or function of args\n * - No ALS dependency\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 handler: CacheHandler\n): Fn {\n const fnId = `timber-cache:${fnIdCounter++}`;\n const sf = opts.timeoutMs\n ? createSingleflight({ timeoutMs: opts.timeoutMs })\n : defaultSingleflight;\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 key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId, args);\n\n const cacheStart = performance.now();\n const cached = await handler.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 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 sf.do(`swr:${key}`, async () => {\n try {\n const fresh = await fn(...args);\n const tags = resolveTags(opts, args);\n await handler.set(key, fresh, { ttl: opts.ttl, tags });\n } catch (err) {\n // Failed refetch — stale entry continues to be served.\n logSwrRefetchFailed({ cacheKey: key, error: err });\n }\n }).catch(() => {\n // Singleflight promise rejection handled — stale continues.\n });\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss (or stale without SWR) — execute with singleflight\n const result = await sf.do(key, () => fn(...args));\n const tags = resolveTags(opts, args);\n await handler.set(key, result, { ttl: opts.ttl, tags });\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 as Awaited<ReturnType<Fn>>;\n }) as unknown as Fn;\n}\n\n/**\n * Invalidate cache entries by tag or key.\n */\ncreateCache.invalidate = async function invalidate(\n handler: CacheHandler,\n opts: { key?: string; tag?: string }\n): Promise<void> {\n await handler.invalidate(opts);\n};\n","import type { CacheOptions } from './index';\nimport { createCache } from './timber-cache';\nimport { getCacheHandler } from './handler-store';\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): Fn {\n return createCache(fn, opts, getCacheHandler());\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 await getCacheHandler().invalidate(opts);\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(key: string, value: unknown, opts: { ttl: number; tags: string[] }): 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 maxEntries: number;\n private maxBytes: number | undefined;\n private maxEntryBytes: number | undefined;\n private currentBytes = 0;\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 }\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(key: string, value: unknown, opts: { ttl: number; tags: string[] }) {\n const byteSize = estimateByteSize(value);\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 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 }\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 }\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. */\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 }\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';\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// 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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/sizeof.ts","../../src/cache/redis-handler.ts","../../src/cache/stable-stringify.ts","../../src/cache/singleflight.ts","../../src/cache/timber-cache.ts","../../src/cache/cache-api.ts","../../src/cache/index.ts"],"sourcesContent":["/**\n * Lightweight byte-size estimation for cache entries.\n *\n * Estimates the in-memory byte cost of a JavaScript value using\n * JSON.stringify().length * 2 (UTF-16 char width). This is a rough\n * approximation — it doesn't account for V8 object overhead, Map\n * metadata, or non-serializable values — but it's fast and good\n * enough for cache budget enforcement.\n *\n * Values that fail JSON serialization (circular references, BigInt,\n * etc.) return 0, allowing the entry to be cached without counting\n * toward the byte budget. This is a conservative choice: it's better\n * to cache and undercount than to reject the entry.\n */\n\n/**\n * Estimate the byte size of a value.\n *\n * Uses `JSON.stringify(value).length * 2` to approximate UTF-16\n * in-memory size. Returns 0 if the value is not serializable.\n */\nexport function estimateByteSize(value: unknown): number {\n try {\n const json = JSON.stringify(value);\n if (json === undefined) return 0;\n // Each JS string character is 2 bytes (UTF-16)\n return json.length * 2;\n } catch (e) {\n // Circular reference or non-serializable value\n console.debug('[timber] sizeof estimation failed:', e instanceof Error ? e.message : e);\n return 0;\n }\n}\n","import type { CacheHandler } from './index';\n\n/**\n * Minimal Redis client interface — compatible with ioredis, node-redis, and\n * Cloudflare Workers Redis bindings. We depend on the interface, not the\n * implementation, so users bring their own Redis client.\n */\nexport interface RedisClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n del(key: string | string[]): Promise<number>;\n sadd(key: string, ...members: string[]): Promise<number>;\n smembers(key: string): Promise<string[]>;\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 — any client implementing the RedisClient\n * interface works (ioredis, node-redis, @upstash/redis, etc.).\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(key: string, value: unknown, opts: { ttl: number; tags: string[] }): Promise<void> {\n const ck = this.cacheKey(key);\n const expiresAt = Date.now() + opts.ttl * 1000;\n const payload = JSON.stringify({ value, expiresAt });\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, 'EX', redisTtlSeconds);\n\n // Track key membership in each tag set\n for (const tag of opts.tags) {\n await this.client.sadd(this.tagKey(tag), key);\n }\n }\n\n async invalidate(opts: { key?: string; tag?: string }): Promise<void> {\n if (opts.key) {\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 if (keys.length > 0) {\n const cacheKeys = keys.map((k) => this.cacheKey(k));\n await this.client.del(cacheKeys);\n }\n\n // Clean up the tag set itself\n await this.client.del(tk);\n }\n }\n}\n","/**\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 */\nexport function stableStringify(value: unknown): string {\n if (value === null || value === undefined) return JSON.stringify(value);\n if (typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return '[' + value.map((item) => stableStringify(item)).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 pairs.push(JSON.stringify(key) + ':' + stableStringify(obj[key]));\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: () => 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: () => Promise<T>): Promise<T> {\n const existing = inflight.get(key);\n if (existing) return existing as Promise<T>;\n\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 promise = new Promise<T>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new SingleflightTimeoutError(key, timeoutMs)),\n timeoutMs\n );\n // Wrap in try/catch so a synchronous throw from fn()\n // (e.g. argument validation) still clears the timer.\n // Without this, the timer leaks until expiry.\n try {\n fn().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();\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","import type { CacheHandler, CacheOptions } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport { addSpanEventSync } from '../server/tracing.js';\nimport { logSwrRefetchFailed } from '../server/logger.js';\nimport { fnv1aHash } from './fast-hash.js';\n\nconst defaultSingleflight = createSingleflight();\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\nlet unstableIdCounter = 0;\nlet hasWarnedUnstableId = false;\n\n/**\n * Creates a cached wrapper around an async function.\n *\n * - SHA-256 default keys with normalized JSON args\n * - Singleflight: concurrent misses → single execution\n * - SWR: serve stale immediately, background refetch\n * - Tags as string[] or function of args\n * - No ALS dependency\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 handler: CacheHandler,\n stableId?: string\n): Fn {\n let fnId: string;\n if (stableId) {\n fnId = stableId;\n } else if (opts.key) {\n // Explicit key function — fnId is only used for singleflight, not for\n // cache keys, so a counter is acceptable here.\n fnId = `timber-cache:${unstableIdCounter++}`;\n } else {\n fnId = `timber-cache:unstable:${unstableIdCounter++}`;\n if (\n !hasWarnedUnstableId &&\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV !== 'test'\n ) {\n hasWarnedUnstableId = true;\n console.warn(\n '[timber] cache() called without a stable ID or explicit key function. ' +\n 'Cache keys will use a process-local counter which is unstable across ' +\n 'cold starts. Run through the timber Vite plugin or provide an explicit ' +\n '`key` option to fix this.'\n );\n }\n }\n const sf = opts.timeoutMs\n ? createSingleflight({ timeoutMs: opts.timeoutMs })\n : defaultSingleflight;\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 key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId, args);\n\n const cacheStart = performance.now();\n const cached = await handler.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 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 sf.do(`swr:${key}`, async () => {\n try {\n const fresh = await fn(...args);\n const tags = resolveTags(opts, args);\n await handler.set(key, fresh, { ttl: opts.ttl, tags });\n } catch (err) {\n // Failed refetch — stale entry continues to be served.\n logSwrRefetchFailed({ cacheKey: key, error: err });\n }\n }).catch(() => {\n // Singleflight promise rejection handled — stale continues.\n });\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss (or stale without SWR) — execute with singleflight\n const result = await sf.do(key, () => fn(...args));\n const tags = resolveTags(opts, args);\n await handler.set(key, result, { ttl: opts.ttl, tags });\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 as Awaited<ReturnType<Fn>>;\n }) as unknown as Fn;\n}\n\n/**\n * Invalidate cache entries by tag or key.\n */\ncreateCache.invalidate = async function invalidate(\n handler: CacheHandler,\n opts: { key?: string; tag?: string }\n): Promise<void> {\n await handler.invalidate(opts);\n};\n","import type { CacheOptions } from './index';\nimport { createCache } from './timber-cache';\nimport { getCacheHandler } from './handler-store';\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 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 await getCacheHandler().invalidate(opts);\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(key: string, value: unknown, opts: { ttl: number; tags: string[] }): 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 maxEntries: number;\n private maxBytes: number | undefined;\n private maxEntryBytes: number | undefined;\n private currentBytes = 0;\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 }\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(key: string, value: unknown, opts: { ttl: number; tags: string[] }) {\n const byteSize = estimateByteSize(value);\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 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 }\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 }\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. */\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 }\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';\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// 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"],"mappings":"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"}
@@ -11,7 +11,7 @@ import type { CacheHandler, CacheOptions } from './index';
11
11
  * Cache hits/misses are recorded as OTEL span events on the enclosing
12
12
  * span (not child spans). The DevSpanProcessor reads these for dev log output.
13
13
  */
14
- export declare function createCache<Fn extends (...args: any[]) => Promise<any>>(fn: Fn, opts: CacheOptions<Fn>, handler: CacheHandler): Fn;
14
+ export declare function createCache<Fn extends (...args: any[]) => Promise<any>>(fn: Fn, opts: CacheOptions<Fn>, handler: CacheHandler, stableId?: string): Fn;
15
15
  export declare namespace createCache {
16
16
  var invalidate: (handler: CacheHandler, opts: {
17
17
  key?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"timber-cache.d.ts","sourceRoot":"","sources":["../../src/cache/timber-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAuC1D;;;;;;;;;;;GAWG;AAEH,wBAAgB,WAAW,CAAC,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,EACrE,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC,EACtB,OAAO,EAAE,YAAY,GACpB,EAAE,CA6DJ;yBAjEe,WAAW;8BAuEhB,YAAY,QACf;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC"}
1
+ {"version":3,"file":"timber-cache.d.ts","sourceRoot":"","sources":["../../src/cache/timber-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAuC1D;;;;;;;;;;;GAWG;AAEH,wBAAgB,WAAW,CAAC,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,EACrE,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,YAAY,CAAC,EAAE,CAAC,EACtB,OAAO,EAAE,YAAY,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,EAAE,CAmFJ;yBAxFe,WAAW;8BA8FhB,YAAY,QACf;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  "use client";
3
- import { n as getSsrData } from "../_chunks/ssr-data-DVX8S7vj.js";
4
- import { n as getRouterOrNull } from "../_chunks/router-ref-B2-2kIIC.js";
3
+ import { n as getSsrData } from "../_chunks/ssr-data-DxXyoRrz.js";
4
+ import { n as getRouterOrNull } from "../_chunks/router-ref-B50j42zB.js";
5
5
  import { Component, createElement } from "react";
6
6
  //#region src/client/error-boundary.tsx
7
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AASA,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC/F,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AA2CpF,MAAM,WAAW,YAAY;CAG5B;AACD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAOxD,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AASA,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGjD,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC/F,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AA4CpF,MAAM,WAAW,YAAY;CAG5B;AACD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAOxD,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
  import { n as classifyUrlSegment } from "../_chunks/segment-classify-BjfuctV2.js";
3
- import { t as getSearchParamsDefinition } from "../_chunks/registry-BI9cgCt7.js";
4
- import { n as getSsrData } from "../_chunks/ssr-data-DVX8S7vj.js";
5
- import { n as useSegmentParams, o as useNavigationContext } from "../_chunks/use-segment-params-CIeinIx2.js";
6
- import { i as mergePreservedSearchParams, r as validateNavigationHref, t as isInternalHref } from "../_chunks/href-validation-BJmD5mZh.js";
7
- import { n as getRouterOrNull } from "../_chunks/router-ref-B2-2kIIC.js";
8
- import { n as useSegmentContext } from "../_chunks/segment-context-Bg0pFbLT.js";
9
- import { t as getLinkCodec } from "../_chunks/codec-registry-DqkeAydt.js";
10
- import { n as usePendingNavigation } from "../_chunks/navigation-root-B5-RmsPi.js";
3
+ import { t as getSearchParamsDefinition } from "../_chunks/registry-Dp_hJL4s.js";
4
+ import { n as getSsrData } from "../_chunks/ssr-data-DxXyoRrz.js";
5
+ import { n as useSegmentParams, o as useNavigationContext } from "../_chunks/use-segment-params-x2poKmk8.js";
6
+ import { i as mergePreservedSearchParams, r as validateNavigationHref, t as isInternalHref } from "../_chunks/href-validation-2G-HZaQ8.js";
7
+ import { n as getRouterOrNull } from "../_chunks/router-ref-B50j42zB.js";
8
+ import { n as useSegmentContext } from "../_chunks/segment-context-BnxoPSFQ.js";
9
+ import { t as getLinkCodec } from "../_chunks/codec-registry-G9hD5LOq.js";
10
+ import { n as usePendingNavigation } from "../_chunks/navigation-root-BQqFQI-n.js";
11
11
  import { createContext, useActionState as useActionState$1, useContext, useTransition } from "react";
12
12
  import { jsx } from "react/jsx-runtime";
13
13
  //#region src/client/use-link-status.ts
@@ -236,8 +236,8 @@ var Link = function LinkImpl(props) {
236
236
  });
237
237
  const [isPending, startTransition] = useTransition();
238
238
  const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;
239
- const resolvedHref = preserveSearchParams ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams) : baseHref;
240
- const internal = isInternalHref(resolvedHref);
239
+ const internal = isInternalHref(baseHref);
240
+ const resolvedHref = preserveSearchParams && internal ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams) : baseHref;
241
241
  const handleClick = internal ? (event) => {
242
242
  userOnClick?.(event);
243
243
  if (!shouldInterceptClick(event, resolvedHref)) return;