@timber-js/app 0.2.0-alpha.36 → 0.2.0-alpha.38
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/LICENSE +8 -0
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -1
- package/dist/_chunks/{define-cookie-w5GWm_bL.js → define-cookie-BmKbSyp0.js} +4 -4
- package/dist/_chunks/{define-cookie-w5GWm_bL.js.map → define-cookie-BmKbSyp0.js.map} +1 -1
- package/dist/_chunks/{error-boundary-TYEQJZ1-.js → error-boundary-BAN3751q.js} +1 -1
- package/dist/_chunks/{error-boundary-TYEQJZ1-.js.map → error-boundary-BAN3751q.js.map} +1 -1
- package/dist/_chunks/{request-context-CZz_T0Bc.js → request-context-BxYIJM24.js} +59 -4
- package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
- package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
- package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
- package/dist/_chunks/{tracing-BPyIzIdu.js → tracing-CuXiCP5p.js} +1 -1
- package/dist/_chunks/{tracing-BPyIzIdu.js.map → tracing-CuXiCP5p.js.map} +1 -1
- package/dist/_chunks/{wrappers-C1SN725w.js → wrappers-C6J0nNji.js} +2 -2
- package/dist/_chunks/{wrappers-C1SN725w.js.map → wrappers-C6J0nNji.js.map} +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +27 -4
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +33 -9
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +25 -8
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +15 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/search-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +14 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +2 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +68 -26
- package/dist/server/index.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +13 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +39 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.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 +3 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +2 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +7 -4
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/package.json +6 -7
- package/src/adapters/nitro.ts +27 -4
- package/src/cache/index.ts +5 -2
- package/src/cache/singleflight.ts +54 -4
- package/src/cache/timber-cache.ts +17 -16
- package/src/cli.ts +0 -0
- package/src/client/index.ts +1 -0
- package/src/client/link.tsx +57 -3
- package/src/index.ts +12 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +1 -0
- package/src/server/access-gate.tsx +6 -5
- package/src/server/als-registry.ts +14 -0
- package/src/server/html-injectors.ts +32 -7
- package/src/server/index.ts +7 -0
- package/src/server/node-stream-transforms.ts +49 -13
- package/src/server/pipeline.ts +6 -0
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +69 -1
- package/src/server/route-element-builder.ts +10 -16
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +34 -4
- package/src/server/rsc-entry/rsc-payload.ts +11 -3
- package/src/server/rsc-entry/rsc-stream.ts +24 -3
- package/src/server/slot-resolver.ts +10 -19
- package/src/server/ssr-entry.ts +9 -2
- package/src/server/ssr-render.ts +94 -13
- package/src/server/tree-builder.ts +13 -15
- package/src/shared/merge-search-params.ts +48 -0
- package/dist/_chunks/request-context-CZz_T0Bc.js.map +0 -1
- package/dist/_chunks/segment-context-Dpq2XOKg.js +0 -34
- package/dist/_chunks/segment-context-Dpq2XOKg.js.map +0 -1
package/dist/cache/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../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/register-cached-function.ts","../../src/cache/index.ts"],"sourcesContent":["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 */\nexport interface Singleflight {\n do<T>(key: string, fn: () => Promise<T>): Promise<T>;\n}\n\nexport function createSingleflight(): Singleflight {\n const inflight = new Map<string, Promise<unknown>>();\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 const promise = fn().finally(() => {\n inflight.delete(key);\n });\n inflight.set(key, promise);\n return promise;\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 { fnv1aHash } from './fast-hash.js';\n\nconst singleflight = 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): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {\n const fnId = `timber-cache:${fnIdCounter++}`;\n\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 singleflight\n .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 {\n // Failed refetch — stale entry continues to be served.\n // Error is swallowed per design doc: \"Error is logged.\"\n }\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 singleflight.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 };\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 { CacheHandler } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport { fnv1aHash } from './fast-hash.js';\n\nconst singleflight = createSingleflight();\n\n// Prop names that suggest request-specific data — triggers dev warning for \"use cache\" components.\nconst REQUEST_SPECIFIC_PROPS = new Set([\n 'cookies',\n 'cookie',\n 'session',\n 'sessionId',\n 'token',\n 'authorization',\n 'auth',\n 'headers',\n]);\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface RegisterCachedFunctionOptions<Fn extends (...args: any[]) => any> {\n ttl: number;\n id: string;\n tags?: string[] | ((...args: Parameters<Fn>) => string[]);\n /** True when the cached function is a React component (PascalCase name). */\n isComponent?: boolean;\n}\n\n/**\n * Generate a cache key from a stable function ID and serialized args.\n *\n * Uses FNV-1a (fast non-crypto hash) instead of SHA-256. The id prefix\n * provides namespace isolation; the hash covers the args.\n * See TIM-370.\n */\nfunction generateKey(id: string, args: unknown[]): string {\n const raw = id + ':' + stableStringify(args);\n return id + ':' + fnv1aHash(raw);\n}\n\n/**\n * Resolve tags from 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: RegisterCachedFunctionOptions<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/**\n * Checks if component props contain request-specific keys and emits a dev warning.\n * Only runs when process.env.NODE_ENV !== 'production'.\n */\nfunction warnRequestSpecificProps(id: string, props: unknown): void {\n if (typeof props !== 'object' || props === null) return;\n const keys = Object.keys(props);\n const suspicious = keys.filter((k) => REQUEST_SPECIFIC_PROPS.has(k.toLowerCase()));\n if (suspicious.length > 0) {\n console.warn(\n `[timber] \"use cache\" component ${id} received request-specific props: ${suspicious.join(', ')}. ` +\n `This may serve one user's cached render to another user. ` +\n `Remove request-specific data from props or remove \"use cache\".`\n );\n }\n}\n\n/**\n * Runtime for the \"use cache\" directive transform. Wraps an async function\n * with caching using the same cache handler as timber.cache.\n *\n * The stable `id` (file path + function name) ensures cache keys are consistent\n * across builds. Args/props are hashed with SHA-256 for the per-call key.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function registerCachedFunction<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: RegisterCachedFunctionOptions<Fn>,\n handler: CacheHandler\n): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {\n return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {\n // Dev-mode warning for components with request-specific props\n if (opts.isComponent && process.env.NODE_ENV !== 'production' && args.length > 0) {\n warnRequestSpecificProps(opts.id, args[0]);\n }\n\n const key = generateKey(opts.id, args);\n const cached = await handler.get(key);\n\n if (cached && !cached.stale) {\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss or stale — execute with singleflight\n const result = await singleflight.do(key, () => fn(...args));\n const tags = resolveTags(opts, args);\n await handler.set(key, result, { ttl: opts.ttl, tags });\n return result as Awaited<ReturnType<Fn>>;\n };\n}\n","// @timber-js/app/cache — Caching primitives\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}\n\nexport interface MemoryCacheHandlerOptions {\n /** Maximum number of entries. Oldest accessed entries are evicted first. Default: 1000. */\n maxSize?: number;\n}\n\nexport class MemoryCacheHandler implements CacheHandler {\n private store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n private maxSize: number;\n\n constructor(opts?: MemoryCacheHandlerOptions) {\n this.maxSize = opts?.maxSize ?? 1000;\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 // If key already exists, delete first to refresh insertion order\n if (this.store.has(key)) {\n this.store.delete(key);\n }\n\n // Evict oldest entries (front of Map) if at capacity\n while (this.store.size >= this.maxSize) {\n const oldest = this.store.keys().next().value;\n if (oldest !== undefined) {\n this.store.delete(oldest);\n } else {\n break;\n }\n }\n\n this.store.set(key, {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n });\n }\n\n async invalidate(opts: { key?: string; tag?: string }) {\n if (opts.key) {\n this.store.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.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\nexport { RedisCacheHandler } from './redis-handler';\nexport type { RedisClient } from './redis-handler';\nexport { createCache } from './timber-cache';\nexport { registerCachedFunction } from './register-cached-function';\nexport type { RegisterCachedFunctionOptions } from './register-cached-function';\nexport { stableStringify } from './stable-stringify';\nexport { createSingleflight } from './singleflight';\nexport type { Singleflight } from './singleflight';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../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/register-cached-function.ts","../../src/cache/index.ts"],"sourcesContent":["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 fn().then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n }\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 { 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): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {\n const fnId = `timber-cache:${fnIdCounter++}`;\n const sf = opts.timeoutMs\n ? createSingleflight({ timeoutMs: opts.timeoutMs })\n : defaultSingleflight;\n\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 {\n // Failed refetch — stale entry continues to be served.\n // Error is swallowed per design doc: \"Error is logged.\"\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 };\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 { CacheHandler } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport { fnv1aHash } from './fast-hash.js';\n\nconst singleflight = createSingleflight();\n\n// Prop names that suggest request-specific data — triggers dev warning for \"use cache\" components.\nconst REQUEST_SPECIFIC_PROPS = new Set([\n 'cookies',\n 'cookie',\n 'session',\n 'sessionId',\n 'token',\n 'authorization',\n 'auth',\n 'headers',\n]);\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport interface RegisterCachedFunctionOptions<Fn extends (...args: any[]) => any> {\n ttl: number;\n id: string;\n tags?: string[] | ((...args: Parameters<Fn>) => string[]);\n /** True when the cached function is a React component (PascalCase name). */\n isComponent?: boolean;\n}\n\n/**\n * Generate a cache key from a stable function ID and serialized args.\n *\n * Uses FNV-1a (fast non-crypto hash) instead of SHA-256. The id prefix\n * provides namespace isolation; the hash covers the args.\n * See TIM-370.\n */\nfunction generateKey(id: string, args: unknown[]): string {\n const raw = id + ':' + stableStringify(args);\n return id + ':' + fnv1aHash(raw);\n}\n\n/**\n * Resolve tags from 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: RegisterCachedFunctionOptions<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/**\n * Checks if component props contain request-specific keys and emits a dev warning.\n * Only runs when process.env.NODE_ENV !== 'production'.\n */\nfunction warnRequestSpecificProps(id: string, props: unknown): void {\n if (typeof props !== 'object' || props === null) return;\n const keys = Object.keys(props);\n const suspicious = keys.filter((k) => REQUEST_SPECIFIC_PROPS.has(k.toLowerCase()));\n if (suspicious.length > 0) {\n console.warn(\n `[timber] \"use cache\" component ${id} received request-specific props: ${suspicious.join(', ')}. ` +\n `This may serve one user's cached render to another user. ` +\n `Remove request-specific data from props or remove \"use cache\".`\n );\n }\n}\n\n/**\n * Runtime for the \"use cache\" directive transform. Wraps an async function\n * with caching using the same cache handler as timber.cache.\n *\n * The stable `id` (file path + function name) ensures cache keys are consistent\n * across builds. Args/props are hashed with SHA-256 for the per-call key.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function registerCachedFunction<Fn extends (...args: any[]) => Promise<any>>(\n fn: Fn,\n opts: RegisterCachedFunctionOptions<Fn>,\n handler: CacheHandler\n): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {\n return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {\n // Dev-mode warning for components with request-specific props\n if (opts.isComponent && process.env.NODE_ENV !== 'production' && args.length > 0) {\n warnRequestSpecificProps(opts.id, args[0]);\n }\n\n const key = generateKey(opts.id, args);\n const cached = await handler.get(key);\n\n if (cached && !cached.stale) {\n return cached.value as Awaited<ReturnType<Fn>>;\n }\n\n // Cache miss or stale — execute with singleflight\n const result = await singleflight.do(key, () => fn(...args));\n const tags = resolveTags(opts, args);\n await handler.set(key, result, { ttl: opts.ttl, tags });\n return result as Awaited<ReturnType<Fn>>;\n };\n}\n","// @timber-js/app/cache — Caching primitives\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 maxSize?: number;\n}\n\nexport class MemoryCacheHandler implements CacheHandler {\n private store = new Map<string, { value: unknown; expiresAt: number; tags: string[] }>();\n private maxSize: number;\n\n constructor(opts?: MemoryCacheHandlerOptions) {\n this.maxSize = opts?.maxSize ?? 1000;\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 // If key already exists, delete first to refresh insertion order\n if (this.store.has(key)) {\n this.store.delete(key);\n }\n\n // Evict oldest entries (front of Map) if at capacity\n while (this.store.size >= this.maxSize) {\n const oldest = this.store.keys().next().value;\n if (oldest !== undefined) {\n this.store.delete(oldest);\n } else {\n break;\n }\n }\n\n this.store.set(key, {\n value,\n expiresAt: Date.now() + opts.ttl * 1000,\n tags: opts.tags,\n });\n }\n\n async invalidate(opts: { key?: string; tag?: string }) {\n if (opts.key) {\n this.store.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.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\nexport { RedisCacheHandler } from './redis-handler';\nexport type { RedisClient } from './redis-handler';\nexport { createCache } from './timber-cache';\nexport { registerCachedFunction } from './register-cached-function';\nexport type { RegisterCachedFunctionOptions } from './register-cached-function';\nexport { stableStringify } from './stable-stringify';\nexport { createSingleflight, SingleflightTimeoutError } from './singleflight';\nexport type { Singleflight, SingleflightOptions } from './singleflight';\n"],"mappings":"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"}
|
|
@@ -3,9 +3,26 @@
|
|
|
3
3
|
* execution. All callers receive the same result (or error).
|
|
4
4
|
*
|
|
5
5
|
* Per-process, in-memory. Each process coalesces independently.
|
|
6
|
+
*
|
|
7
|
+
* An optional `timeoutMs` prevents hung `fn()` calls from permanently
|
|
8
|
+
* blocking all future callers for that key. When set, `fn()` is raced
|
|
9
|
+
* against a timeout — if the timeout fires first, the promise rejects
|
|
10
|
+
* with `SingleflightTimeoutError`, `finally` cleans up the key, and
|
|
11
|
+
* subsequent callers can retry. See TIM-518.
|
|
6
12
|
*/
|
|
13
|
+
export interface SingleflightOptions {
|
|
14
|
+
/** Maximum time (ms) a coalesced call may run before being rejected. */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
7
17
|
export interface Singleflight {
|
|
8
18
|
do<T>(key: string, fn: () => Promise<T>): Promise<T>;
|
|
9
19
|
}
|
|
10
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Error thrown when a singleflight call exceeds `timeoutMs`.
|
|
22
|
+
* Exported so callers can distinguish timeout from other errors.
|
|
23
|
+
*/
|
|
24
|
+
export declare class SingleflightTimeoutError extends Error {
|
|
25
|
+
constructor(key: string, timeoutMs: number);
|
|
26
|
+
}
|
|
27
|
+
export declare function createSingleflight(opts?: SingleflightOptions): Singleflight;
|
|
11
28
|
//# sourceMappingURL=singleflight.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"singleflight.d.ts","sourceRoot":"","sources":["../../src/cache/singleflight.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"singleflight.d.ts","sourceRoot":"","sources":["../../src/cache/singleflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACtD;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAI3C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,YAAY,CA0C3E"}
|
|
@@ -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;AAsC1D;;;;;;;;;;;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,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,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;AAsC1D;;;;;;;;;;;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,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CA0D/D;yBA9De,WAAW;8BAoEhB,YAAY,QACf;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { JsonSerializable, RenderErrorDigest } from './types';
|
|
2
2
|
export { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';
|
|
3
|
+
export { mergePreservedSearchParams } from '#/shared/merge-search-params.js';
|
|
3
4
|
export type { LinkProps, LinkPropsWithHref, LinkPropsWithParams } from './link';
|
|
4
5
|
export type { OnNavigateHandler, OnNavigateEvent } from './link';
|
|
5
6
|
export { createRouter } from './router';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,UAAU,EACV,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,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,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,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,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGlE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAClG,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAUtE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAChG,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,UAAU,EACV,YAAY,GACb,MAAM,UAAU,CAAC;AAClB,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,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,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,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGpG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGvF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGlE,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAClG,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAUtE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { n as __exportAll } from "../_chunks/chunk-DYhsFzuS.js";
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
3
|
+
import { n as useSegmentContext, r as mergePreservedSearchParams, t as SegmentProvider } from "../_chunks/segment-context-C6byCyZU.js";
|
|
4
|
+
import { a as _setCachedSearch, c as cachedSearch, d as globalRouter, i as setSsrData, l as cachedSearchParams, n as clearSsrData, o as _setCurrentParams, r as getSsrData, s as _setGlobalRouter, t as TimberErrorBoundary, u as currentParams } from "../_chunks/error-boundary-BAN3751q.js";
|
|
5
5
|
import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-BvW0TKDn.js";
|
|
6
|
-
import { t as _registerUseCookieModule } from "../_chunks/define-cookie-
|
|
6
|
+
import { t as _registerUseCookieModule } from "../_chunks/define-cookie-BmKbSyp0.js";
|
|
7
7
|
import React, { cloneElement, createContext, createElement, isValidElement, useActionState as useActionState$1, useContext, useSyncExternalStore, useTransition } from "react";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
|
9
9
|
//#region src/client/use-link-status.ts
|
|
@@ -226,6 +226,18 @@ function getRouterOrNull() {
|
|
|
226
226
|
//#endregion
|
|
227
227
|
//#region src/client/link.tsx
|
|
228
228
|
/**
|
|
229
|
+
* Read the current URL's search string without requiring a React hook.
|
|
230
|
+
* On the client, reads window.location.search. During SSR, reads from
|
|
231
|
+
* the request context (getSsrData). Returns empty string if unavailable.
|
|
232
|
+
*/
|
|
233
|
+
function getCurrentSearch() {
|
|
234
|
+
if (typeof window !== "undefined") return window.location.search;
|
|
235
|
+
const data = getSsrData();
|
|
236
|
+
if (!data) return "";
|
|
237
|
+
const str = new URLSearchParams(data.searchParams).toString();
|
|
238
|
+
return str ? `?${str}` : "";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
229
241
|
* Reject dangerous URL schemes that could execute script.
|
|
230
242
|
* Security: design/13-security.md § Link scheme injection (test #9)
|
|
231
243
|
*/
|
|
@@ -328,12 +340,13 @@ function shouldInterceptClick(event, resolvedHref) {
|
|
|
328
340
|
* - `segmentParams` prop interpolates dynamic segments in the href pattern
|
|
329
341
|
* - `searchParams` prop serializes query parameters via a SearchParamsDefinition
|
|
330
342
|
*/
|
|
331
|
-
function Link({ href, prefetch, scroll, segmentParams, searchParams, onNavigate, onClick: userOnClick, onMouseEnter: userOnMouseEnter, children, ...rest }) {
|
|
332
|
-
const { href:
|
|
343
|
+
function Link({ href, prefetch, scroll, segmentParams, searchParams, preserveSearchParams, onNavigate, onClick: userOnClick, onMouseEnter: userOnMouseEnter, children, ...rest }) {
|
|
344
|
+
const { href: baseHref } = buildLinkProps({
|
|
333
345
|
href,
|
|
334
346
|
params: segmentParams,
|
|
335
347
|
searchParams
|
|
336
348
|
});
|
|
349
|
+
const resolvedHref = preserveSearchParams ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams) : baseHref;
|
|
337
350
|
const internal = isInternalHref(resolvedHref);
|
|
338
351
|
const handleClick = internal ? (event) => {
|
|
339
352
|
userOnClick?.(event);
|
|
@@ -352,12 +365,16 @@ function Link({ href, prefetch, scroll, segmentParams, searchParams, onNavigate,
|
|
|
352
365
|
if (!router) return;
|
|
353
366
|
event.preventDefault();
|
|
354
367
|
const shouldScroll = scroll !== false;
|
|
355
|
-
|
|
368
|
+
const navHref = preserveSearchParams ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams) : resolvedHref;
|
|
369
|
+
router.navigate(navHref, { scroll: shouldScroll });
|
|
356
370
|
} : userOnClick;
|
|
357
371
|
const handleMouseEnter = internal && prefetch ? (event) => {
|
|
358
372
|
userOnMouseEnter?.(event);
|
|
359
373
|
const router = getRouterOrNull();
|
|
360
|
-
if (router)
|
|
374
|
+
if (router) {
|
|
375
|
+
const prefetchHref = preserveSearchParams ? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams) : resolvedHref;
|
|
376
|
+
router.prefetch(prefetchHref);
|
|
377
|
+
}
|
|
361
378
|
} : userOnMouseEnter;
|
|
362
379
|
return /* @__PURE__ */ jsx("a", {
|
|
363
380
|
...rest,
|
|
@@ -1692,6 +1709,6 @@ function useCookie(name, defaultOptions) {
|
|
|
1692
1709
|
//#region src/client/index.ts
|
|
1693
1710
|
_registerUseCookieModule(use_cookie_exports);
|
|
1694
1711
|
//#endregion
|
|
1695
|
-
export { HistoryStack, Link, LinkStatusContext, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getNavigationState, getRouter, getSsrData, interpolateParams, resolveHref, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSegmentParams, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
|
|
1712
|
+
export { HistoryStack, Link, LinkStatusContext, NavigationProvider, PrefetchCache, SegmentCache, SegmentProvider, TimberErrorBoundary, bindUseQueryStates, buildLinkProps, clearSsrData, createRouter, getNavigationState, getRouter, getSsrData, interpolateParams, mergePreservedSearchParams, resolveHref, setCurrentParams, setGlobalRouter, setNavigationState, setSsrData, useActionState, useCookie, useFormAction, useFormErrors, useLinkStatus, useNavigationPending, usePathname, useQueryStates, useRouter, useSearchParams, useSegmentContext, useSegmentParams, useSelectedLayoutSegment, useSelectedLayoutSegments, validateLinkHref };
|
|
1696
1713
|
|
|
1697
1714
|
//# sourceMappingURL=index.js.map
|