@timber-js/app 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/als-registry-c0AGnbqS.js +39 -0
- package/dist/_chunks/als-registry-c0AGnbqS.js.map +1 -0
- package/dist/_chunks/{interception-c-a3uODY.js → interception-DGDIjDbR.js} +10 -3
- package/dist/_chunks/interception-DGDIjDbR.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BDnswgRO.js → metadata-routes-CQCnF4VK.js} +14 -2
- package/dist/_chunks/metadata-routes-CQCnF4VK.js.map +1 -0
- package/dist/_chunks/{request-context-BzES06i1.js → request-context-C69VW4xS.js} +2 -4
- package/dist/_chunks/request-context-C69VW4xS.js.map +1 -0
- package/dist/_chunks/ssr-data-B2yikEEB.js +90 -0
- package/dist/_chunks/ssr-data-B2yikEEB.js.map +1 -0
- package/dist/_chunks/{tracing-BtOwb8O6.js → tracing-tIvqStk8.js} +2 -3
- package/dist/_chunks/tracing-tIvqStk8.js.map +1 -0
- package/dist/_chunks/{use-cookie-D2cZu0jK.js → use-cookie-D5aS4slY.js} +2 -2
- package/dist/_chunks/{use-cookie-D2cZu0jK.js.map → use-cookie-D5aS4slY.js.map} +1 -1
- package/dist/_chunks/{use-query-states-wEXY2JQB.js → use-query-states-DAhgj8Gx.js} +1 -1
- package/dist/_chunks/{use-query-states-wEXY2JQB.js.map → use-query-states-DAhgj8Gx.js.map} +1 -1
- package/dist/cache/index.js +2 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +35 -25
- package/dist/client/index.js.map +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +3 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +47 -0
- package/dist/client/state.d.ts.map +1 -0
- package/dist/client/types.d.ts +10 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +3 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +3 -0
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +3 -0
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/cookies/index.js +4 -2
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/rsc-runtime/browser.d.ts +13 -0
- package/dist/rsc-runtime/browser.d.ts.map +1 -0
- package/dist/rsc-runtime/rsc.d.ts +14 -0
- package/dist/rsc-runtime/rsc.d.ts.map +1 -0
- package/dist/rsc-runtime/ssr.d.ts +13 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -0
- package/dist/search-params/builtin-codecs.d.ts +105 -0
- package/dist/search-params/builtin-codecs.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +1 -0
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +167 -2
- package/dist/search-params/index.js.map +1 -1
- package/dist/server/actions.d.ts +2 -7
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +80 -0
- package/dist/server/als-registry.d.ts.map +1 -0
- package/dist/server/early-hints-sender.d.ts.map +1 -1
- package/dist/server/form-flash.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +242 -76
- package/dist/server/index.js.map +1 -1
- package/dist/server/metadata-routes.d.ts +27 -0
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +7 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +14 -6
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +2 -32
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +5 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +25 -0
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts +43 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts +52 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -0
- package/dist/server/rsc-prop-warnings.d.ts +53 -0
- package/dist/server/rsc-prop-warnings.d.ts.map +1 -0
- package/dist/server/server-timing.d.ts +49 -0
- package/dist/server/server-timing.d.ts.map +1 -0
- package/dist/server/tracing.d.ts +2 -6
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/types.d.ts +11 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +1 -1
- package/src/client/index.ts +1 -1
- package/src/client/router-ref.ts +6 -12
- package/src/client/router.ts +25 -20
- package/src/client/ssr-data.ts +25 -9
- package/src/client/state.ts +83 -0
- package/src/client/types.ts +18 -1
- package/src/client/unload-guard.ts +6 -3
- package/src/client/use-params.ts +10 -13
- package/src/client/use-search-params.ts +9 -5
- package/src/plugins/shims.ts +26 -2
- package/src/routing/scanner.ts +18 -2
- package/src/rsc-runtime/browser.ts +18 -0
- package/src/rsc-runtime/rsc.ts +19 -0
- package/src/rsc-runtime/ssr.ts +13 -0
- package/src/search-params/builtin-codecs.ts +228 -0
- package/src/search-params/index.ts +11 -0
- package/src/server/action-handler.ts +1 -1
- package/src/server/actions.ts +4 -10
- package/src/server/als-registry.ts +116 -0
- package/src/server/deny-renderer.ts +1 -1
- package/src/server/early-hints-sender.ts +1 -3
- package/src/server/form-flash.ts +1 -5
- package/src/server/index.ts +1 -0
- package/src/server/metadata-routes.ts +61 -0
- package/src/server/pipeline.ts +164 -38
- package/src/server/primitives.ts +110 -6
- package/src/server/request-context.ts +8 -36
- package/src/server/route-matcher.ts +25 -2
- package/src/server/rsc-entry/error-renderer.ts +1 -1
- package/src/server/rsc-entry/index.ts +42 -380
- package/src/server/rsc-entry/rsc-payload.ts +126 -0
- package/src/server/rsc-entry/rsc-stream.ts +162 -0
- package/src/server/rsc-entry/ssr-renderer.ts +228 -0
- package/src/server/rsc-prop-warnings.ts +187 -0
- package/src/server/server-timing.ts +132 -0
- package/src/server/ssr-entry.ts +1 -1
- package/src/server/tracing.ts +3 -11
- package/src/server/types.ts +16 -0
- package/dist/_chunks/interception-c-a3uODY.js.map +0 -1
- package/dist/_chunks/metadata-routes-BDnswgRO.js.map +0 -1
- package/dist/_chunks/request-context-BzES06i1.js.map +0 -1
- package/dist/_chunks/ssr-data-BgSwMbN9.js +0 -38
- package/dist/_chunks/ssr-data-BgSwMbN9.js.map +0 -1
- package/dist/_chunks/tracing-BtOwb8O6.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-query-states-
|
|
1
|
+
{"version":3,"file":"use-query-states-DAhgj8Gx.js","names":[],"sources":["../../src/search-params/registry.ts","../../src/client/use-query-states.ts"],"sourcesContent":["/**\n * Runtime registry for route-scoped search params definitions.\n *\n * When a route's modules load, the framework registers its search-params\n * definition here. useQueryStates('/route') resolves codecs from this map.\n *\n * Design doc: design/23-search-params.md §\"Runtime: Registration at Route Load\"\n */\n\nimport type { SearchParamsDefinition } from './create.js';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<string, SearchParamsDefinition<any>>();\n\n/**\n * Register a route's search params definition.\n * Called by the generated route manifest loader when a route's modules load.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function registerSearchParams(route: string, definition: SearchParamsDefinition<any>): void {\n registry.set(route, definition);\n}\n\n/**\n * Look up a route's search params definition.\n * Returns undefined if the route hasn't been loaded yet.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getSearchParams(route: string): SearchParamsDefinition<any> | undefined {\n return registry.get(route);\n}\n","/**\n * useQueryStates — client-side hook for URL-synced search params.\n *\n * Delegates to nuqs for URL synchronization, batching, React 19 transitions,\n * and throttled URL writes. Bridges timber's SearchParamCodec protocol to\n * nuqs-compatible parsers.\n *\n * Design doc: design/23-search-params.md §\"Codec Bridge\"\n */\n\n'use client';\n\nimport { useQueryStates as nuqsUseQueryStates } from 'nuqs';\nimport type { SingleParser } from 'nuqs';\nimport type {\n SearchParamCodec,\n SearchParamsDefinition,\n SetParams,\n QueryStatesOptions,\n} from '#/search-params/create.js';\nimport { getSearchParams } from '#/search-params/registry.js';\n\n// ─── Codec Bridge ─────────────────────────────────────────────────\n\n/**\n * Bridge a timber SearchParamCodec to a nuqs-compatible SingleParser.\n *\n * nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }\n * timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }\n */\nfunction bridgeCodec<T>(codec: SearchParamCodec<T>): SingleParser<T> & { defaultValue: T } {\n return {\n parse: (v: string) => codec.parse(v),\n serialize: (v: T) => codec.serialize(v) ?? '',\n defaultValue: codec.parse(undefined) as T,\n eq: (a: T, b: T) => codec.serialize(a) === codec.serialize(b),\n };\n}\n\n/**\n * Bridge an entire codec map to nuqs-compatible parsers.\n */\nfunction bridgeCodecs<T extends Record<string, unknown>>(codecs: {\n [K in keyof T]: SearchParamCodec<T[K]>;\n}) {\n const result: Record<string, SingleParser<unknown> & { defaultValue: unknown }> = {};\n for (const key of Object.keys(codecs)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result[key] = bridgeCodec(codecs[key as keyof T]) as any;\n }\n return result as { [K in keyof T]: SingleParser<T[K]> & { defaultValue: T[K] } };\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────\n\n/**\n * Read and write typed search params from/to the URL.\n *\n * Delegates to nuqs internally. The timber nuqs adapter (auto-injected in\n * browser-entry.ts) handles RSC navigation on non-shallow updates.\n *\n * Usage:\n * ```ts\n * // Via a SearchParamsDefinition\n * const [params, setParams] = definition.useQueryStates()\n *\n * // Standalone with inline codecs\n * const [params, setParams] = useQueryStates({\n * page: fromSchema(z.coerce.number().int().min(1).default(1)),\n * })\n * ```\n */\nexport function useQueryStates<T extends Record<string, unknown>>(\n codecsOrRoute: { [K in keyof T]: SearchParamCodec<T[K]> } | string,\n _options?: QueryStatesOptions,\n urlKeys?: Readonly<Record<string, string>>\n): [T, SetParams<T>] {\n // Route-string overload: resolve codecs from the registry\n let codecs: { [K in keyof T]: SearchParamCodec<T[K]> };\n let resolvedUrlKeys = urlKeys;\n if (typeof codecsOrRoute === 'string') {\n const definition = getSearchParams(codecsOrRoute);\n if (!definition) {\n throw new Error(\n `useQueryStates('${codecsOrRoute}'): no search params registered for this route. ` +\n `Either the route has no search-params.ts file, or it hasn't been loaded yet. ` +\n `For cross-route usage, import the definition explicitly.`\n );\n }\n codecs = definition.codecs as { [K in keyof T]: SearchParamCodec<T[K]> };\n resolvedUrlKeys = definition.urlKeys;\n } else {\n codecs = codecsOrRoute;\n }\n\n const bridged = bridgeCodecs(codecs);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const nuqsOptions: any = {};\n if (resolvedUrlKeys && Object.keys(resolvedUrlKeys).length > 0) {\n nuqsOptions.urlKeys = resolvedUrlKeys;\n }\n\n let values: Record<string, unknown>;\n let setValues: Function;\n try {\n [values, setValues] = nuqsUseQueryStates(bridged, nuqsOptions);\n } catch (err) {\n if (err instanceof Error && /Invalid hook call|cannot be called|Cannot read properties of null/i.test(err.message)) {\n throw new Error(\n 'useQueryStates is a client component hook and cannot be called outside a React component. ' +\n 'Use definition.parse(searchParams) in server components instead.'\n );\n }\n throw err;\n }\n\n // Wrap the nuqs setter to match timber's SetParams<T> signature.\n // nuqs's setter accepts Partial<Nullable<Values>> | UpdaterFn | null.\n // timber's setter accepts Partial<T> with optional SetParamsOptions.\n const setParams: SetParams<T> = (partial, setOptions?) => {\n const nuqsSetOptions: Record<string, unknown> = {};\n if (setOptions?.shallow !== undefined) nuqsSetOptions.shallow = setOptions.shallow;\n if (setOptions?.scroll !== undefined) nuqsSetOptions.scroll = setOptions.scroll;\n if (setOptions?.history !== undefined) nuqsSetOptions.history = setOptions.history;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void setValues(partial as any, nuqsSetOptions);\n };\n\n return [values as T, setParams];\n}\n\n// ─── Definition binding ───────────────────────────────────────────\n\n/**\n * Create a useQueryStates binding for a SearchParamsDefinition.\n * This is used internally by SearchParamsDefinition.useQueryStates().\n */\nexport function bindUseQueryStates<T extends Record<string, unknown>>(\n definition: SearchParamsDefinition<T>\n): (options?: QueryStatesOptions) => [T, SetParams<T>] {\n return (options?: QueryStatesOptions) => {\n return useQueryStates<T>(definition.codecs, options, definition.urlKeys);\n };\n}\n"],"mappings":";;AAYA,IAAM,2BAAW,IAAI,KAA0C;;;;;AAO/D,SAAgB,qBAAqB,OAAe,YAA+C;AACjG,UAAS,IAAI,OAAO,WAAW;;;;;;AAQjC,SAAgB,gBAAgB,OAAwD;AACtF,QAAO,SAAS,IAAI,MAAM;;;;;;;;;;;;;;;;;;;ACC5B,SAAS,YAAe,OAAmE;AACzF,QAAO;EACL,QAAQ,MAAc,MAAM,MAAM,EAAE;EACpC,YAAY,MAAS,MAAM,UAAU,EAAE,IAAI;EAC3C,cAAc,MAAM,MAAM,KAAA,EAAU;EACpC,KAAK,GAAM,MAAS,MAAM,UAAU,EAAE,KAAK,MAAM,UAAU,EAAE;EAC9D;;;;;AAMH,SAAS,aAAgD,QAEtD;CACD,MAAM,SAA4E,EAAE;AACpF,MAAK,MAAM,OAAO,OAAO,KAAK,OAAO,CAEnC,QAAO,OAAO,YAAY,OAAO,KAAgB;AAEnD,QAAO;;;;;;;;;;;;;;;;;;;AAsBT,SAAgB,iBACd,eACA,UACA,SACmB;CAEnB,IAAI;CACJ,IAAI,kBAAkB;AACtB,KAAI,OAAO,kBAAkB,UAAU;EACrC,MAAM,aAAa,gBAAgB,cAAc;AACjD,MAAI,CAAC,WACH,OAAM,IAAI,MACR,mBAAmB,cAAc,uLAGlC;AAEH,WAAS,WAAW;AACpB,oBAAkB,WAAW;OAE7B,UAAS;CAGX,MAAM,UAAU,aAAa,OAAO;CAGpC,MAAM,cAAmB,EAAE;AAC3B,KAAI,mBAAmB,OAAO,KAAK,gBAAgB,CAAC,SAAS,EAC3D,aAAY,UAAU;CAGxB,IAAI;CACJ,IAAI;AACJ,KAAI;AACF,GAAC,QAAQ,aAAa,eAAmB,SAAS,YAAY;UACvD,KAAK;AACZ,MAAI,eAAe,SAAS,qEAAqE,KAAK,IAAI,QAAQ,CAChH,OAAM,IAAI,MACR,6JAED;AAEH,QAAM;;CAMR,MAAM,aAA2B,SAAS,eAAgB;EACxD,MAAM,iBAA0C,EAAE;AAClD,MAAI,YAAY,YAAY,KAAA,EAAW,gBAAe,UAAU,WAAW;AAC3E,MAAI,YAAY,WAAW,KAAA,EAAW,gBAAe,SAAS,WAAW;AACzE,MAAI,YAAY,YAAY,KAAA,EAAW,gBAAe,UAAU,WAAW;AAEtE,YAAU,SAAgB,eAAe;;AAGhD,QAAO,CAAC,QAAa,UAAU;;;;;;AASjC,SAAgB,mBACd,YACqD;AACrD,SAAQ,YAAiC;AACvC,SAAO,iBAAkB,WAAW,QAAQ,SAAS,WAAW,QAAQ"}
|
package/dist/cache/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "../_chunks/als-registry-c0AGnbqS.js";
|
|
2
|
+
import { t as addSpanEvent } from "../_chunks/tracing-tIvqStk8.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
//#region src/cache/redis-handler.ts
|
|
4
5
|
var KEY_PREFIX = "timber:cache:";
|
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/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","import { createHash } from 'node:crypto';\nimport type { CacheHandler, CacheOptions } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport { addSpanEvent } from '#/server/tracing.js';\n\nconst singleflight = createSingleflight();\n\n/**\n * Generate a SHA-256 cache key from function identity and serialized args.\n */\nfunction defaultKeyGenerator(fnId: string, args: unknown[]): string {\n const raw = fnId + ':' + stableStringify(args);\n return createHash('sha256').update(raw).digest('hex');\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 await addSpanEvent('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\n await addSpanEvent('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\n await addSpanEvent('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 { createHash } from 'node:crypto';\nimport type { CacheHandler } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\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 SHA-256 cache key from a stable function ID and serialized args.\n */\nfunction generateKey(id: string, args: unknown[]): string {\n const raw = id + ':' + stableStringify(args);\n return createHash('sha256').update(raw).digest('hex');\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/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","import { createHash } from 'node:crypto';\nimport type { CacheHandler, CacheOptions } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\nimport { addSpanEvent } from '#/server/tracing.js';\n\nconst singleflight = createSingleflight();\n\n/**\n * Generate a SHA-256 cache key from function identity and serialized args.\n */\nfunction defaultKeyGenerator(fnId: string, args: unknown[]): string {\n const raw = fnId + ':' + stableStringify(args);\n return createHash('sha256').update(raw).digest('hex');\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 await addSpanEvent('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\n await addSpanEvent('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\n await addSpanEvent('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 { createHash } from 'node:crypto';\nimport type { CacheHandler } from './index';\nimport { stableStringify } from './stable-stringify';\nimport { createSingleflight } from './singleflight';\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 SHA-256 cache key from a stable function ID and serialized args.\n */\nfunction generateKey(id: string, args: unknown[]): string {\n const raw = id + ':' + stableStringify(args);\n return createHash('sha256').update(raw).digest('hex');\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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"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { RenderErrorDigest } from './types';
|
|
1
|
+
export type { JsonSerializable, RenderErrorDigest } from './types';
|
|
2
2
|
export { Link, interpolateParams, resolveHref, validateLinkHref, buildLinkProps } from './link';
|
|
3
3
|
export type { LinkProps, LinkPropsWithHref, LinkPropsWithParams } from './link';
|
|
4
4
|
export type { OnNavigateHandler, OnNavigateEvent } from './link-navigate-interceptor';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,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,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChF,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,UAAU,EACV,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,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG3D,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;AAGtE,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,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { n as getSsrData, r as setSsrData, t as clearSsrData } from "../_chunks/ssr-data-
|
|
3
|
-
import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-
|
|
4
|
-
import { t as useCookie } from "../_chunks/use-cookie-
|
|
2
|
+
import { a as _setCurrentParams, c as cachedSearchParams, d as paramsListeners, i as _setCachedSearch, l as currentParams, n as getSsrData, o as _setGlobalRouter, r as setSsrData, s as cachedSearch, t as clearSsrData, u as globalRouter } from "../_chunks/ssr-data-B2yikEEB.js";
|
|
3
|
+
import { n as useQueryStates, t as bindUseQueryStates } from "../_chunks/use-query-states-DAhgj8Gx.js";
|
|
4
|
+
import { t as useCookie } from "../_chunks/use-cookie-D5aS4slY.js";
|
|
5
5
|
import { TimberErrorBoundary } from "./error-boundary.js";
|
|
6
6
|
import { createContext, createElement, startTransition, useActionState as useActionState$1, useContext, useEffect, useMemo, useRef, useSyncExternalStore, useTransition } from "react";
|
|
7
7
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
8
|
+
import { flushSync } from "react-dom";
|
|
8
9
|
//#region src/client/link-navigate-interceptor.tsx
|
|
9
10
|
var _jsxFileName$2 = "/Users/dsaewitz/y/timber-js-fresh/packages/timber-app/src/client/link-navigate-interceptor.tsx";
|
|
10
11
|
/** Symbol used to store the onNavigate callback on anchor elements. */
|
|
@@ -73,13 +74,11 @@ function useLinkStatus() {
|
|
|
73
74
|
}
|
|
74
75
|
//#endregion
|
|
75
76
|
//#region src/client/router-ref.ts
|
|
76
|
-
/** Module-level singleton — set once during bootstrap. */
|
|
77
|
-
var globalRouter = null;
|
|
78
77
|
/**
|
|
79
78
|
* Set the global router instance. Called once during bootstrap.
|
|
80
79
|
*/
|
|
81
80
|
function setGlobalRouter(router) {
|
|
82
|
-
|
|
81
|
+
_setGlobalRouter(router);
|
|
83
82
|
}
|
|
84
83
|
/**
|
|
85
84
|
* Get the global router instance. Throws if called before bootstrap.
|
|
@@ -407,17 +406,18 @@ var HistoryStack = class {
|
|
|
407
406
|
* params change during client-side navigation. This matches the pattern
|
|
408
407
|
* used by usePathname() and useSearchParams().
|
|
409
408
|
*
|
|
409
|
+
* All mutable state is delegated to client/state.ts for singleton guarantees.
|
|
410
|
+
* See design/18-build-system.md §"Singleton State Registry"
|
|
411
|
+
*
|
|
410
412
|
* Design doc: design/09-typescript.md §"Typed Routes"
|
|
411
413
|
*/
|
|
412
|
-
var currentParams = {};
|
|
413
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
414
414
|
/**
|
|
415
415
|
* Subscribe to params changes. Called by useSyncExternalStore.
|
|
416
416
|
* Exported for testing — not intended for direct use by app code.
|
|
417
417
|
*/
|
|
418
418
|
function subscribe$2(callback) {
|
|
419
|
-
|
|
420
|
-
return () =>
|
|
419
|
+
paramsListeners.add(callback);
|
|
420
|
+
return () => paramsListeners.delete(callback);
|
|
421
421
|
}
|
|
422
422
|
/**
|
|
423
423
|
* Get the current params snapshot (client).
|
|
@@ -450,7 +450,7 @@ function getServerSnapshot() {
|
|
|
450
450
|
* module-level fallback path.
|
|
451
451
|
*/
|
|
452
452
|
function setCurrentParams(params) {
|
|
453
|
-
|
|
453
|
+
_setCurrentParams(params);
|
|
454
454
|
}
|
|
455
455
|
/**
|
|
456
456
|
* Notify all useSyncExternalStore subscribers that params have changed.
|
|
@@ -460,7 +460,7 @@ function setCurrentParams(params) {
|
|
|
460
460
|
* {old tree, new params} intermediate state.
|
|
461
461
|
*/
|
|
462
462
|
function notifyParamsListeners() {
|
|
463
|
-
for (const listener of
|
|
463
|
+
for (const listener of paramsListeners) listener();
|
|
464
464
|
}
|
|
465
465
|
function useParams(_route) {
|
|
466
466
|
try {
|
|
@@ -683,8 +683,10 @@ function createRouter(deps) {
|
|
|
683
683
|
});
|
|
684
684
|
updateSegmentCache(result.segmentInfo);
|
|
685
685
|
updateParams(result.params);
|
|
686
|
-
|
|
687
|
-
|
|
686
|
+
flushSync(() => {
|
|
687
|
+
renderPayload(result.payload);
|
|
688
|
+
notifyParamsListeners();
|
|
689
|
+
});
|
|
688
690
|
applyHead(result.headElements);
|
|
689
691
|
window.dispatchEvent(new Event("timber:navigation-end"));
|
|
690
692
|
afterPaint(() => {
|
|
@@ -716,8 +718,10 @@ function createRouter(deps) {
|
|
|
716
718
|
});
|
|
717
719
|
updateSegmentCache(result.segmentInfo);
|
|
718
720
|
updateParams(result.params);
|
|
719
|
-
|
|
720
|
-
|
|
721
|
+
flushSync(() => {
|
|
722
|
+
renderPayload(result.payload);
|
|
723
|
+
notifyParamsListeners();
|
|
724
|
+
});
|
|
721
725
|
applyHead(result.headElements);
|
|
722
726
|
} finally {
|
|
723
727
|
setPending(false);
|
|
@@ -727,8 +731,10 @@ function createRouter(deps) {
|
|
|
727
731
|
const entry = historyStack.get(url);
|
|
728
732
|
if (entry && entry.payload !== null) {
|
|
729
733
|
updateParams(entry.params);
|
|
730
|
-
|
|
731
|
-
|
|
734
|
+
flushSync(() => {
|
|
735
|
+
renderPayload(entry.payload);
|
|
736
|
+
notifyParamsListeners();
|
|
737
|
+
});
|
|
732
738
|
applyHead(entry.headElements);
|
|
733
739
|
afterPaint(() => {
|
|
734
740
|
deps.scrollTo(0, scrollY);
|
|
@@ -745,8 +751,10 @@ function createRouter(deps) {
|
|
|
745
751
|
headElements: result.headElements,
|
|
746
752
|
params: result.params
|
|
747
753
|
});
|
|
748
|
-
|
|
749
|
-
|
|
754
|
+
flushSync(() => {
|
|
755
|
+
renderPayload(result.payload);
|
|
756
|
+
notifyParamsListeners();
|
|
757
|
+
});
|
|
750
758
|
applyHead(result.headElements);
|
|
751
759
|
afterPaint(() => {
|
|
752
760
|
deps.scrollTo(0, scrollY);
|
|
@@ -960,6 +968,9 @@ function usePathname() {
|
|
|
960
968
|
*
|
|
961
969
|
* During SSR, reads the request search params from the SSR ALS context
|
|
962
970
|
* (populated by ssr-entry.ts) instead of window.location.
|
|
971
|
+
*
|
|
972
|
+
* All mutable state is delegated to client/state.ts for singleton guarantees.
|
|
973
|
+
* See design/18-build-system.md §"Singleton State Registry"
|
|
963
974
|
*/
|
|
964
975
|
function getSearch() {
|
|
965
976
|
if (typeof window !== "undefined") return window.location.search;
|
|
@@ -978,15 +989,14 @@ function subscribe(callback) {
|
|
|
978
989
|
window.addEventListener("popstate", callback);
|
|
979
990
|
return () => window.removeEventListener("popstate", callback);
|
|
980
991
|
}
|
|
981
|
-
var cachedSearch = "";
|
|
982
|
-
var cachedParams = new URLSearchParams();
|
|
983
992
|
function getSearchParams() {
|
|
984
993
|
const search = getSearch();
|
|
985
994
|
if (search !== cachedSearch) {
|
|
986
|
-
|
|
987
|
-
|
|
995
|
+
const params = new URLSearchParams(search);
|
|
996
|
+
_setCachedSearch(search, params);
|
|
997
|
+
return params;
|
|
988
998
|
}
|
|
989
|
-
return
|
|
999
|
+
return cachedSearchParams;
|
|
990
1000
|
}
|
|
991
1001
|
function getServerSearchParams() {
|
|
992
1002
|
const data = getSsrData();
|