@timber-js/app 0.2.0-alpha.153 → 0.2.0-alpha.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/cli.d.ts +9 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +41 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  7. package/dist/server/internal.js +6 -2
  8. package/dist/server/internal.js.map +1 -1
  9. package/dist/server/pipeline-metadata.d.ts.map +1 -1
  10. package/dist/server/pipeline-phases.d.ts.map +1 -1
  11. package/dist/server/sitemap-handler.d.ts.map +1 -1
  12. package/docs/api/30-api-server.mdx +233 -0
  13. package/docs/api/31-api-client.mdx +161 -0
  14. package/docs/api/32-api-cache.mdx +193 -0
  15. package/docs/api/33-api-search-params.mdx +141 -0
  16. package/docs/api/34-api-config.mdx +189 -0
  17. package/docs/api/35-api-typescript.mdx +170 -0
  18. package/docs/api/36-cli.mdx +143 -0
  19. package/docs/learn/00-introduction.mdx +81 -0
  20. package/docs/learn/01-quick-start.mdx +94 -0
  21. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  22. package/docs/learn/03-fetching-data.mdx +211 -0
  23. package/docs/learn/03b-access-control.mdx +130 -0
  24. package/docs/learn/04-loading-states.mdx +67 -0
  25. package/docs/learn/04b-the-flush-point.mdx +115 -0
  26. package/docs/learn/05-typed-params.mdx +275 -0
  27. package/docs/learn/06-forms-and-actions.mdx +150 -0
  28. package/docs/learn/08-streaming.mdx +146 -0
  29. package/docs/learn/09-caching.mdx +121 -0
  30. package/docs/learn/10-middleware.mdx +156 -0
  31. package/docs/learn/11-error-handling.mdx +147 -0
  32. package/docs/learn/12-client-navigation.mdx +168 -0
  33. package/docs/learn/13-configuration.mdx +117 -0
  34. package/docs/learn/14-deploying.mdx +206 -0
  35. package/docs/more/01-advanced-routing.mdx +139 -0
  36. package/docs/more/02-advanced-forms.mdx +137 -0
  37. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  38. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  39. package/docs/more/04b-mdx.mdx +209 -0
  40. package/docs/more/05-content-collections.mdx +90 -0
  41. package/docs/more/06-instrumentation.mdx +188 -0
  42. package/docs/more/07-security.mdx +123 -0
  43. package/docs/more/40-why-timber.mdx +50 -0
  44. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  45. package/docs/more/42-timber-vs-others.mdx +68 -0
  46. package/llms.txt +55 -0
  47. package/package.json +6 -2
  48. package/src/cli.ts +62 -3
  49. package/src/client/browser-entry/action-dispatch.ts +14 -0
  50. package/src/server/pipeline-metadata.ts +1 -0
  51. package/src/server/pipeline-phases.ts +4 -1
  52. package/src/server/sitemap-handler.ts +12 -3
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline-metadata.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,OAAO,oBAAoB,EAAE,kBAAkB,GACzD,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CAoBR;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAMnE;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7C"}
1
+ {"version":3,"file":"pipeline-metadata.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,OAAO,oBAAoB,EAAE,kBAAkB,GACzD,OAAO,CAAC,QAAQ,CAAC,CAanB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CAoBR;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAMnE;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,CAqSnB"}
1
+ {"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,CAwSnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap-handler.d.ts","sourceRoot":"","sources":["../../src/server/sitemap-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAKvD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CAgH9D"}
1
+ {"version":3,"file":"sitemap-handler.d.ts","sourceRoot":"","sources":["../../src/server/sitemap-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAKvD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,EACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CAyH9D"}
@@ -0,0 +1,233 @@
1
+ ---
2
+ title: 'API: @timber-js/app/server'
3
+ description: 'Server-side exports — deny, redirect, getHeaders, getCookieJar, createActionClient, and context types.'
4
+ slug: 'api-server'
5
+ ---
6
+
7
+ # @timber-js/app/server
8
+
9
+ Server-side utilities and types. Import from `@timber-js/app/server`.
10
+
11
+ ## Navigation & Status
12
+
13
+ ### `deny(statusOrOptions?, data?)`
14
+
15
+ Universal denial/error primitive. Produces an HTTP error status and renders the matching status file. Accepts any 4xx or 5xx status code. Default status is 403.
16
+
17
+ ```ts
18
+ import { deny } from '@timber-js/app/server';
19
+
20
+ deny(); // 403 (default)
21
+ deny(404); // 404 Not Found
22
+ deny(503, { retry: true }); // 503 with data
23
+ deny({ status: 500, data: { title: 'Error' } }); // object form
24
+ ```
25
+
26
+ ### `redirect(url, type?)`
27
+
28
+ Redirect to a URL. Sends HTTP 307 (temporary) by default.
29
+
30
+ ```ts
31
+ import { redirect } from '@timber-js/app/server';
32
+
33
+ redirect('/login'); // 302 Temporary Redirect
34
+ redirect('/new-page', { permanent: true }); // 308 Permanent Redirect
35
+ redirect('/new-page', { status: 301 }); // 301 (explicit status)
36
+ redirect('/new-page', 301); // 301 (shorthand)
37
+ ```
38
+
39
+ The `permanent` option defaults to status **308** (preserves HTTP method). If you need 301 specifically, pass `status: 301`. When both `permanent` and `status` are provided, `status` wins.
40
+
41
+ ### `redirectExternal(url, allowList, status?)`
42
+
43
+ Redirect to an external URL. The target origin must be in the provided allow-list. Only `http:` and `https:` schemes are permitted — this prevents open redirect vulnerabilities and blocks `javascript:` scheme injection.
44
+
45
+ ```ts
46
+ import { redirectExternal } from '@timber-js/app/server';
47
+
48
+ // Redirect to an allowed external domain
49
+ redirectExternal('https://auth.example.com/login', ['https://auth.example.com']);
50
+
51
+ // With a custom status code
52
+ redirectExternal('https://docs.example.com', ['https://docs.example.com'], 301);
53
+ ```
54
+
55
+ | Parameter | Type | Default | Description |
56
+ | ----------- | ---------- | ------- | ---------------------------------------------------------- |
57
+ | `url` | `string` | — | Absolute URL to redirect to |
58
+ | `allowList` | `string[]` | — | Allowed origins (e.g. `['https://example.com']`) |
59
+ | `status` | `number` | `302` | HTTP redirect status code (3xx) |
60
+
61
+ Throws if the origin is not in the allow-list, the scheme isn't `http:` or `https:`, or the URL is invalid.
62
+
63
+ ## Request Context
64
+
65
+ ### `getHeaders()`
66
+
67
+ Returns the incoming request headers. ALS-backed — works in server components, middleware, access checks.
68
+
69
+ ```ts
70
+ import { getHeaders } from '@timber-js/app/server';
71
+
72
+ const h = getHeaders();
73
+ const auth = h.get('authorization');
74
+ ```
75
+
76
+ Returns `ReadonlyHeaders` — a read-only `Headers` interface (sync).
77
+
78
+ ### `getCookieJar()`
79
+
80
+ Returns the request cookies with mutation methods.
81
+
82
+ ```ts
83
+ import { getCookieJar } from '@timber-js/app/server';
84
+
85
+ const c = getCookieJar();
86
+ c.get('session'); // Read
87
+ c.set('theme', 'dark'); // Set (before flush only)
88
+ c.delete('session'); // Delete
89
+ c.clear(); // Clear all
90
+ ```
91
+
92
+ Mutations are only allowed before the response flushes. Attempting to set a cookie after flush throws in dev mode.
93
+
94
+ For typed cookies with validation and a client React hook, see [`defineCookie()`](/docs/typed-params) from `@timber-js/app/cookies`.
95
+
96
+ ## Actions
97
+
98
+ ### `createActionClient(options)`
99
+
100
+ Create a reusable action client with middleware for auth and validation.
101
+
102
+ ```ts
103
+ import { createActionClient, ActionError } from '@timber-js/app/server';
104
+ import { getUser } from '@/lib/auth';
105
+
106
+ const action = createActionClient({
107
+ middleware: async () => {
108
+ const user = await getUser();
109
+ if (!user) throw new ActionError('UNAUTHORIZED');
110
+ return { user };
111
+ },
112
+ });
113
+
114
+ // Usage:
115
+ const myAction = action.schema(z.object({ title: z.string() })).action(async ({ input, ctx }) => {
116
+ // ctx.user is typed from middleware return
117
+ });
118
+ ```
119
+
120
+ ### `ActionError`
121
+
122
+ Throw from action middleware or bodies to produce a typed error result.
123
+
124
+ ```ts
125
+ throw new ActionError('UNAUTHORIZED');
126
+ throw new ActionError('VALIDATION_FAILED', { field: 'email' });
127
+ ```
128
+
129
+ ### `revalidatePath(path)`
130
+
131
+ Re-render a path and return the RSC payload as part of the action response.
132
+
133
+ ### `revalidateTag(tag)`
134
+
135
+ Invalidate all cache entries with the given tag.
136
+
137
+ ## Segment Params
138
+
139
+ ### `getSegmentParams(segmentPath?)`
140
+
141
+ Returns route path params for the current request. Pass a `SEGMENT_PATH` string from `$segment` for exact types, or omit for `Partial<AllSegmentParams>`. ALS-backed — works in server components, middleware, access checks, and server actions.
142
+
143
+ ```ts
144
+ import { SEGMENT_PATH } from './$segment';
145
+ import { getSegmentParams } from '@timber-js/app/server';
146
+
147
+ const { id } = getSegmentParams(SEGMENT_PATH); // Exact types from schema
148
+ const params = getSegmentParams(); // Partial<AllSegmentParams>
149
+ ```
150
+
151
+ For typed coercion, define codecs in `app/schema.ts` with `defineSchema()`. See [Typed Params](/docs/typed-params).
152
+
153
+ ## Types
154
+
155
+ ### `RouteContext`
156
+
157
+ ```ts
158
+ interface RouteContext {
159
+ req: Request;
160
+ headers: Headers; // Response headers (writable)
161
+ }
162
+ ```
163
+
164
+ Segment params and query params are **not** on `RouteContext`. Read them via the same ALS-backed `.get()` accessors pages use — `segmentParams.get()` from `./params` and `searchParams.get()` from `./search-params`. For the raw query string, use `new URL(ctx.req.url).searchParams`.
165
+
166
+ ### `MiddlewareContext`
167
+
168
+ ```ts
169
+ interface MiddlewareContext {
170
+ req: Request;
171
+ segmentParams: Record<string, string | string[]>;
172
+ headers: Headers; // Response headers (writable)
173
+ requestHeaders: Headers; // Request headers (injectable)
174
+ }
175
+ ```
176
+
177
+ ### `AccessContext`
178
+
179
+ Empty interface — `access.ts` takes no arguments. Use ALS-backed imports like `getSegmentParams(SEGMENT_PATH)`.
180
+
181
+ ### `Metadata`
182
+
183
+ The full metadata type — see [Metadata & SEO](/docs/metadata-and-fonts) for usage.
184
+
185
+ ### `MetadataRoute`
186
+
187
+ Type for metadata route files (sitemap, robots, etc.).
188
+
189
+ ## Tracing
190
+
191
+ ### `getTraceId()`
192
+
193
+ Returns the current request's 32-char hex trace ID. Works in middleware, access checks, server components, and server actions.
194
+
195
+ ```ts
196
+ import { getTraceId } from '@timber-js/app/server';
197
+
198
+ const id = getTraceId();
199
+ ```
200
+
201
+ ### `getSpanId()`
202
+
203
+ Returns the current OTEL span ID, or `undefined` if OTEL is not active.
204
+
205
+ ### `withSpan(name, attributes, fn)`
206
+
207
+ Run a function within an OTEL span. No-op passthrough if OTEL is not available.
208
+
209
+ ```ts
210
+ import { withSpan } from '@timber-js/app/server';
211
+
212
+ declare const db: { query(...args: unknown[]): Promise<unknown> };
213
+
214
+ const result = await withSpan('db.query', { table: 'users' }, () => db.query());
215
+ ```
216
+
217
+ ### `addSpanEvent(name, attributes?)`
218
+
219
+ Add an event to the current active span.
220
+
221
+ See [Instrumentation & Tracing](/docs/instrumentation) for full documentation.
222
+
223
+ ## Lifecycle
224
+
225
+ ### `waitUntil(promise)`
226
+
227
+ Register a promise to be kept alive after the response is sent. Platform-dependent — works on Cloudflare Workers and other platforms that support it.
228
+
229
+ ```ts
230
+ import { waitUntil } from '@timber-js/app/server';
231
+
232
+ waitUntil(analytics.track(event));
233
+ ```
@@ -0,0 +1,161 @@
1
+ ---
2
+ title: 'API: @timber-js/app/client'
3
+ description: 'Client-side exports — Link, useRouter, usePathname, navigation hooks, and form utilities.'
4
+ slug: 'api-client'
5
+ ---
6
+
7
+ # @timber-js/app/client
8
+
9
+ Client-side components and hooks. Import from `@timber-js/app/client`.
10
+
11
+ ## Components
12
+
13
+ ### `<Link>`
14
+
15
+ Client-side navigation link. Type-checks `href` against the generated route map.
16
+
17
+ ```tsx
18
+ import { Link } from '@timber-js/app/client';
19
+
20
+ <Link href="/about">About</Link>
21
+ <Link href="/products/123" replace>Product</Link>
22
+ <Link href="/gallery" onNavigate={(e) => { /* intercept */ }}>Gallery</Link>
23
+ ```
24
+
25
+ #### Props
26
+
27
+ | Prop | Type | Description |
28
+ | ------------ | ------------------- | ------------------------------------------- |
29
+ | `href` | `string` | Target URL (type-checked against route map) |
30
+ | `replace` | `boolean` | Use `replaceState` instead of `pushState` |
31
+ | `prefetch` | `boolean` | Prefetch on hover/viewport |
32
+ | `onNavigate` | `OnNavigateHandler` | Intercept navigation before routing |
33
+
34
+ ### `<TimberErrorBoundary>`
35
+
36
+ Error boundary component with reset support.
37
+
38
+ ## Hooks
39
+
40
+ ### `useRouter()`
41
+
42
+ Programmatic navigation.
43
+
44
+ ```tsx
45
+ const router = useRouter();
46
+ router.push('/dashboard');
47
+ router.replace('/settings');
48
+ router.refresh();
49
+ router.back();
50
+ router.forward();
51
+ ```
52
+
53
+ #### `RouterInstance`
54
+
55
+ | Method | Description |
56
+ | --------------- | ---------------------------------- |
57
+ | `push(href)` | Navigate, add history entry |
58
+ | `replace(href)` | Navigate, replace history entry |
59
+ | `refresh()` | Re-fetch current route RSC payload |
60
+ | `back()` | Go back |
61
+ | `forward()` | Go forward |
62
+
63
+ ### `usePathname()`
64
+
65
+ Returns the current pathname as a string.
66
+
67
+ ```tsx
68
+ const pathname = usePathname(); // "/dashboard/settings"
69
+ ```
70
+
71
+ ### `useSelectedLayoutSegment()`
72
+
73
+ Returns the active child segment within a layout.
74
+
75
+ ```tsx
76
+ const segment = useSelectedLayoutSegment(); // "settings"
77
+ ```
78
+
79
+ ### `useSelectedLayoutSegments()`
80
+
81
+ Returns all active segments below the current layout.
82
+
83
+ ```tsx
84
+ const segments = useSelectedLayoutSegments(); // ["settings", "profile"]
85
+ ```
86
+
87
+ ### `usePendingNavigation()`
88
+
89
+ Returns whether a navigation is in flight. Use instead of wrapping `children` in `<Suspense>`.
90
+
91
+ ```tsx
92
+ const pending = usePendingNavigation();
93
+ ```
94
+
95
+ ### `useLinkStatus()`
96
+
97
+ Per-link pending state. Must be used inside a `<Link>` component's children.
98
+
99
+ ```tsx
100
+ const { pending } = useLinkStatus();
101
+ ```
102
+
103
+ ### `useSegmentParams(segmentPath?)`
104
+
105
+ Returns the current route segment params. Pass `SEGMENT_PATH` from `$segment` for exact types.
106
+
107
+ ```tsx
108
+ import { SEGMENT_PATH } from './$segment';
109
+ import { useSegmentParams } from '@timber-js/app/client';
110
+
111
+ const { id } = useSegmentParams(SEGMENT_PATH); // Exact types from schema
112
+ const params = useSegmentParams(); // Partial<AllSegmentParams>
113
+ ```
114
+
115
+ ## Form Hooks
116
+
117
+ ### `useActionState(action, initialState)`
118
+
119
+ Typed wrapper around React 19's `useActionState`. Returns `[result, formAction, isPending, errors]` — the 4th element auto-derives field and form-level errors from the result.
120
+
121
+ ```tsx
122
+ const [result, action, isPending, errors] = useActionState(createTodo, flash);
123
+
124
+ errors.hasErrors // boolean
125
+ errors.fieldErrors // Record<string, string[]>
126
+ errors.formErrors // string[]
127
+ errors.serverError // { code, data? } | null
128
+ errors.getFieldError('title') // string | null
129
+ ```
130
+
131
+ ### `useFormAction(action)`
132
+
133
+ Bind a server action to a form with pending state.
134
+
135
+ ## Utilities
136
+
137
+ ### `interpolateParams(pattern, params)`
138
+
139
+ Fill a route pattern with param values.
140
+
141
+ ```ts
142
+ interpolateParams('/products/[id]', { id: '123' }); // "/products/123"
143
+ ```
144
+
145
+ ### `defineSearchParams().useQueryStates()`
146
+
147
+ Sync typed search params to the URL. Called on a `defineSearchParams()` definition. See [Search Params](/docs/typed-params).
148
+
149
+ ## Cookie Hooks
150
+
151
+ ### `defineCookie().useCookie()`
152
+
153
+ Reactive cookie access in client components. Define a typed cookie with `defineCookie()` from `@timber-js/app/cookies`, then call `.useCookie()` in client components. Returns `[value, setter, deleter]`.
154
+
155
+ ```tsx
156
+ import { themeCookie } from '../lib/cookies';
157
+
158
+ const [theme, setTheme, deleteTheme] = themeCookie.useCookie();
159
+ ```
160
+
161
+ See [Cookies](/docs/typed-params) for full documentation.
@@ -0,0 +1,193 @@
1
+ ---
2
+ title: 'API: @timber-js/app/cache'
3
+ description: 'Cache exports — cache, CacheHandler, MemoryCacheHandler, and invalidation.'
4
+ slug: 'api-cache'
5
+ ---
6
+
7
+ # @timber-js/app/cache
8
+
9
+ Caching primitives and cache handler interface. Import from `@timber-js/app/cache`.
10
+
11
+ ## `cache(fn, options)`
12
+
13
+ Wrap an async function with cross-request caching.
14
+
15
+ ```ts
16
+ import { cache } from '@timber-js/app/cache';
17
+
18
+ declare const db: { users: { findUnique(opts: { where: { id: string } }): Promise<{ id: string; name: string }> } };
19
+
20
+ const getUser = cache(
21
+ async (userId: string) => {
22
+ return db.users.findUnique({ where: { id: userId } });
23
+ },
24
+ {
25
+ ttl: 60,
26
+ key: (userId) => `user:${userId}`,
27
+ staleWhileRevalidate: true,
28
+ tags: (userId) => [`user:${userId}`],
29
+ }
30
+ );
31
+ ```
32
+
33
+ ### `CacheOptions<Fn>`
34
+
35
+ | Option | Type | Description |
36
+ | ---------------------- | ----------------------------------- | ------------------------------------------ |
37
+ | `ttl` | `number` | Time-to-live in seconds |
38
+ | `key` | `(...args) => string` | Custom cache key function (optional) |
39
+ | `staleWhileRevalidate` | `boolean` | Serve stale while refetching in background |
40
+ | `tags` | `string[] \| (...args) => string[]` | Tags for invalidation |
41
+
42
+ Default key: FNV-1a hash of function identity + deterministic JSON serialization of arguments.
43
+
44
+ The function identity is derived at build time from the source text of the `cache(...)` call, so it is stable across restarts, instances, and deploys. With a persistent shared handler (Redis/KV), cached entries survive deploys as long as the call itself is unchanged — reordering calls or editing unrelated code preserves the cache, while editing the wrapped function or its options gives the call a fresh identity (old entries simply expire via TTL). Renaming or moving the file also resets identity for every call in it.
45
+
46
+ Build-time identity covers calls the bundler can see statically: named imports (including aliases like `import { cache as c }`) and namespace imports (`import * as timber` + `timber.cache(fn, opts)`). Calls it can't trace — a local alias like `const c = cache`, or `cache` re-exported through your own wrapper module — fall back to a hash of the function's source text at runtime. The fallback is still deterministic across instances of the same build, but it resets across deploys, and byte-identical closures produced by a factory can collide — pass an explicit `key` in those cases (timber logs a warning when the fallback is used).
47
+
48
+ ### `cache.invalidate(options)`
49
+
50
+ Invalidate cache entries by tag or key:
51
+
52
+ ```ts
53
+ import { cache } from '@timber-js/app/cache';
54
+
55
+ cache.invalidate({ tag: 'products' });
56
+ cache.invalidate({ key: 'user:abc' });
57
+ ```
58
+
59
+ ## `CacheHandler` Interface
60
+
61
+ Implement this to plug in a custom cache backend.
62
+
63
+ ```ts
64
+ interface CacheHandler {
65
+ get(key: string): Promise<{ value: unknown; tags: string[] } | null>;
66
+ set(key: string, value: unknown, options: { ttl: number; tags: string[] }): Promise<void>;
67
+ invalidate(options: { tag: string }): Promise<void>;
68
+ }
69
+ ```
70
+
71
+ ## `MemoryCacheHandler`
72
+
73
+ In-memory cache with LRU eviction. The default handler.
74
+
75
+ ```ts
76
+ import { MemoryCacheHandler } from '@timber-js/app/cache';
77
+
78
+ const handler = new MemoryCacheHandler({ maxSize: 1000 });
79
+ handler.size; // Current number of entries
80
+ ```
81
+
82
+ ### Options
83
+
84
+ | Option | Type | Default | Description |
85
+ | --------- | -------- | ------- | ----------------------------------- |
86
+ | `maxSize` | `number` | `1000` | Maximum entries before LRU eviction |
87
+
88
+ ## `RedisCacheHandler`
89
+
90
+ Redis-backed cache handler. Takes a client wrapped to timber's `RedisClient` contract — TTL is a required positional argument, so an adapter can't silently drop the expiry:
91
+
92
+ ```ts
93
+ interface RedisClient {
94
+ get(key: string): Promise<string | null>;
95
+ set(key: string, value: string, ttlSeconds: number): Promise<unknown>;
96
+ expire(key: string, seconds: number): Promise<unknown>;
97
+ del(key: string): Promise<unknown>; // single key
98
+ sadd(key: string, ...members: string[]): Promise<unknown>;
99
+ srem(key: string, ...members: string[]): Promise<unknown>;
100
+ smembers(key: string): Promise<string[]>;
101
+ ttl(key: string): Promise<number>;
102
+ }
103
+ ```
104
+
105
+ This is deliberately not the native shape of any client library — ioredis, node-redis, and @upstash/redis disagree on TTL argument shapes and method casing, and a contract that pattern-matches one client lets another silently write keys with no expiry. Wrap your client with one of these adapters:
106
+
107
+ ```ts title="ioredis"
108
+ import { RedisCacheHandler } from '@timber-js/app/cache';
109
+ import Redis from 'ioredis';
110
+
111
+ const redis = new Redis(process.env.REDIS_URL!);
112
+ const handler = new RedisCacheHandler({
113
+ get: (k) => redis.get(k),
114
+ set: (k, v, ttl) => redis.set(k, v, 'EX', ttl),
115
+ expire: (k, s) => redis.expire(k, s),
116
+ del: (k) => redis.del(k),
117
+ sadd: (k, ...m) => redis.sadd(k, ...m),
118
+ srem: (k, ...m) => redis.srem(k, ...m),
119
+ smembers: (k) => redis.smembers(k),
120
+ ttl: (k) => redis.ttl(k),
121
+ });
122
+ ```
123
+
124
+ ```ts title="node-redis"
125
+ import { RedisCacheHandler } from '@timber-js/app/cache';
126
+ import { createClient } from 'redis';
127
+
128
+ const redis = createClient({ url: process.env.REDIS_URL });
129
+ await redis.connect();
130
+ const handler = new RedisCacheHandler({
131
+ get: (k) => redis.get(k),
132
+ set: (k, v, ttl) => redis.set(k, v, { EX: ttl }),
133
+ expire: (k, s) => redis.expire(k, s),
134
+ del: (k) => redis.del(k),
135
+ sadd: (k, ...m) => redis.sAdd(k, m),
136
+ srem: (k, ...m) => redis.sRem(k, m),
137
+ smembers: (k) => redis.sMembers(k),
138
+ ttl: (k) => redis.ttl(k),
139
+ });
140
+ ```
141
+
142
+ ```ts title="@upstash/redis"
143
+ import { RedisCacheHandler } from '@timber-js/app/cache';
144
+ import { Redis } from '@upstash/redis';
145
+
146
+ // automaticDeserialization must be off: the handler stores JSON strings
147
+ // and parses them itself.
148
+ const redis = new Redis({
149
+ url: process.env.UPSTASH_REDIS_REST_URL!,
150
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!,
151
+ automaticDeserialization: false,
152
+ });
153
+ const handler = new RedisCacheHandler({
154
+ get: (k) => redis.get(k),
155
+ set: (k, v, ttl) => redis.set(k, v, { ex: ttl }),
156
+ expire: (k, s) => redis.expire(k, s),
157
+ del: (k) => redis.del(k),
158
+ sadd: (k, ...m) => redis.sadd(k, m[0], ...m.slice(1)),
159
+ srem: (k, ...m) => redis.srem(k, m[0], ...m.slice(1)),
160
+ smembers: (k) => redis.smembers(k),
161
+ ttl: (k) => redis.ttl(k),
162
+ });
163
+ ```
164
+
165
+ ## `CloudflareKVCacheHandler`
166
+
167
+ Cloudflare Workers KV–backed cache handler. Resolves the KV binding per-request via `getCloudflareBindings()`. Uses tag index keys for tag-based invalidation. Eventually consistent — TTL is the primary expiry mechanism.
168
+
169
+ ```ts
170
+ import { CloudflareKVCacheHandler } from '@timber-js/app/adapters/cloudflare/cache';
171
+
172
+ const handler = new CloudflareKVCacheHandler({
173
+ bindingName: 'TIMBER_CACHE',
174
+ prefix: 'tc:', // optional, default 'tc:'
175
+ });
176
+ ```
177
+
178
+ ## Invalidation
179
+
180
+ ### `revalidateTag(tag)`
181
+
182
+ Invalidate all cache entries with the given tag. Re-exported from `@timber-js/app/server` for convenience.
183
+
184
+ ```ts
185
+ import { revalidateTag } from '@timber-js/app/server';
186
+
187
+ revalidateTag('products');
188
+ revalidateTag(`user:${userId}`);
189
+ ```
190
+
191
+ ## Singleflight
192
+
193
+ Request coalescing is built into `cache()` automatically — when multiple concurrent calls miss the cache for the same key, only one executes the underlying function. The singleflight and stable-stringify utilities are internal implementation details and not part of the public API.