@ivogt/rsc-router 0.0.0-experimental.1

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 (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Cache Store Types
3
+ *
4
+ * Generic caching interface supporting multiple value types.
5
+ * Designed to be implemented by different backends:
6
+ * - MemoryCacheStore (dev/testing)
7
+ * - Cloudflare Cache API adapter
8
+ * - Cloudflare KV adapter
9
+ * - Redis adapter
10
+ */
11
+
12
+ import type { ResolvedSegment } from "../types.js";
13
+ import type { RequestContext } from "../server/request-context.js";
14
+
15
+ // ============================================================================
16
+ // Segment Cache Store (low-level storage interface)
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Result from cache get() including data and revalidation status
21
+ */
22
+ export interface CacheGetResult {
23
+ /** The cached entry data */
24
+ data: CachedEntryData;
25
+ /**
26
+ * Whether the caller should trigger background revalidation.
27
+ * True when entry is stale AND not already being revalidated.
28
+ * The store atomically marks the entry as REVALIDATING when returning true.
29
+ */
30
+ shouldRevalidate: boolean;
31
+ }
32
+
33
+ /**
34
+ * Low-level segment cache store interface.
35
+ *
36
+ * Implementations handle the actual storage (memory, KV, Redis, etc.).
37
+ * The store deals with serialized data - RSC serialization is handled
38
+ * by the cache provider layer.
39
+ *
40
+ * @typeParam TEnv - Platform bindings type (e.g., Cloudflare env)
41
+ */
42
+ export interface SegmentCacheStore<TEnv = unknown> {
43
+ /**
44
+ * Default cache options for this store.
45
+ * Used by cache() boundaries when ttl/swr are not explicitly specified.
46
+ */
47
+ readonly defaults?: CacheDefaults;
48
+
49
+ /**
50
+ * Custom key generator applied to all cache operations using this store.
51
+ * Receives the full RequestContext and the default-generated key.
52
+ * Return value becomes the final cache key (unless route overrides with `key` option).
53
+ *
54
+ * Resolution priority:
55
+ * 1. Route-level `key` function (full override)
56
+ * 2. Store-level `keyGenerator` (modifies default key)
57
+ * 3. Default key generation (prefix:pathname:params)
58
+ *
59
+ * @example Using headers for cache segmentation
60
+ * ```typescript
61
+ * keyGenerator: (ctx, defaultKey) => {
62
+ * const segment = ctx.request.headers.get('x-user-segment') || 'default';
63
+ * return `${segment}:${defaultKey}`;
64
+ * }
65
+ * ```
66
+ *
67
+ * @example Using env bindings (Cloudflare)
68
+ * ```typescript
69
+ * keyGenerator: (ctx, defaultKey) => {
70
+ * const region = ctx.env.REGION || 'us';
71
+ * return `${region}:${defaultKey}`;
72
+ * }
73
+ * ```
74
+ *
75
+ * @example Using cookies for locale
76
+ * ```typescript
77
+ * keyGenerator: (ctx, defaultKey) => {
78
+ * const locale = ctx.cookie('locale') || 'en';
79
+ * return `${locale}:${defaultKey}`;
80
+ * }
81
+ * ```
82
+ */
83
+ readonly keyGenerator?: (
84
+ ctx: RequestContext<TEnv>,
85
+ defaultKey: string
86
+ ) => string | Promise<string>;
87
+
88
+ /**
89
+ * Get cached entry data by key
90
+ * @returns Cache result with data and staleness, or null if not found/expired
91
+ */
92
+ get(key: string): Promise<CacheGetResult | null>;
93
+
94
+ /**
95
+ * Store entry data with TTL
96
+ * @param key - Cache key
97
+ * @param data - Serialized entry data
98
+ * @param ttl - Time-to-live in seconds
99
+ * @param swr - Optional stale-while-revalidate window in seconds
100
+ */
101
+ set(key: string, data: CachedEntryData, ttl: number, swr?: number): Promise<void>;
102
+
103
+ /**
104
+ * Delete a cached entry
105
+ * @returns true if deleted, false if not found
106
+ */
107
+ delete(key: string): Promise<boolean>;
108
+
109
+ /**
110
+ * Clear all cached entries (optional, for testing)
111
+ */
112
+ clear?(): Promise<void>;
113
+ }
114
+
115
+ /**
116
+ * Serialized segment data stored in cache
117
+ * Note: loading is preserved to ensure consistent tree structure between cached and fresh renders
118
+ */
119
+ export interface SerializedSegmentData {
120
+ /** RSC-encoded component string */
121
+ encoded: string;
122
+ /** RSC-encoded layout string (if present) */
123
+ encodedLayout?: string;
124
+ /** RSC-encoded loading skeleton string (if present), or "null" for explicit null */
125
+ encodedLoading?: string;
126
+ /** RSC-encoded loaderData (if present) */
127
+ encodedLoaderData?: string;
128
+ /** RSC-encoded loaderDataPromise (if present) */
129
+ encodedLoaderDataPromise?: string;
130
+ /** Segment metadata (everything except component, layout, loading, and loader data) */
131
+ metadata: Omit<ResolvedSegment, "component" | "layout" | "loading" | "loaderData" | "loaderDataPromise">;
132
+ }
133
+
134
+ /**
135
+ * Raw data stored in cache for an entry
136
+ */
137
+ export interface CachedEntryData {
138
+ /** Serialized segments for this entry */
139
+ segments: SerializedSegmentData[];
140
+ /** Handle data keyed by segment ID */
141
+ handles: Record<string, SegmentHandleData>;
142
+ /** Expiration timestamp (ms since epoch) */
143
+ expiresAt: number;
144
+ }
145
+
146
+ // ============================================================================
147
+ // Cache Configuration
148
+ // ============================================================================
149
+
150
+ /**
151
+ * Default cache options applied to all cache() boundaries.
152
+ * Individual cache() calls can override any of these values.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const store = new CFCacheStore({
157
+ * defaults: { ttl: 60, swr: 300 }
158
+ * });
159
+ * ```
160
+ */
161
+ export interface CacheDefaults {
162
+ /**
163
+ * Default time-to-live in seconds.
164
+ * After TTL expires, cached entry is considered stale.
165
+ */
166
+ ttl?: number;
167
+ /**
168
+ * Default stale-while-revalidate window in seconds.
169
+ * During SWR window, stale content is served while revalidating in background.
170
+ */
171
+ swr?: number;
172
+ }
173
+
174
+ /**
175
+ * Cache configuration for RSC handler
176
+ */
177
+ export interface CacheConfig {
178
+ /** Cache store implementation (includes defaults) */
179
+ store: SegmentCacheStore;
180
+ /** Enable/disable caching (default: true) */
181
+ enabled?: boolean;
182
+ }
183
+
184
+ /**
185
+ * Cache configuration - can be static or a function receiving env
186
+ */
187
+ export type CacheConfigOrFactory<TEnv> =
188
+ | CacheConfig
189
+ | ((env: TEnv) => CacheConfig);
190
+
191
+ // ============================================================================
192
+ // Segment Cache Provider (request-level interface)
193
+ // ============================================================================
194
+
195
+ /**
196
+ * Handle data for a single segment
197
+ * Structure: { handleName: [values...] }
198
+ */
199
+ export type SegmentHandleData = Record<string, unknown[]>;
200
+
201
+ /**
202
+ * Result from cache get() including segments and their handle data
203
+ * Each entry can produce multiple segments (main + parallels)
204
+ */
205
+ export interface CachedEntryResult {
206
+ /** All segments for this entry (main segment + parallels) */
207
+ segments: ResolvedSegment[];
208
+ /** Handle data keyed by segment ID */
209
+ handles: Record<string, SegmentHandleData>;
210
+ }
211
+
212
+
213
+ /**
214
+ * Segment cache provider interface
215
+ *
216
+ * Used by router to check/store segment cache during matching.
217
+ * Accessed via request context - if not present, caching is disabled.
218
+ *
219
+ * @internal Not currently implemented - CacheScope is used directly.
220
+ * Reserved for future extensibility.
221
+ */
222
+ export interface SegmentCacheProvider {
223
+ /** Whether caching is enabled for this request */
224
+ readonly enabled: boolean;
225
+
226
+ /**
227
+ * Get cached segments and restore handles/loaders.
228
+ *
229
+ * Combines cache get with handle replay and loader data restoration.
230
+ * Returns tuple of [segments, segmentIds] if cache hit, null if miss or disabled.
231
+ *
232
+ * @param cacheKey - Cache key to look up
233
+ * @param params - Route params for cache key generation
234
+ * @param loaderPromises - Map to restore loader data into
235
+ * @returns Tuple of [segments, segmentIds] or null if miss
236
+ */
237
+ restore(
238
+ cacheKey: string,
239
+ params: Record<string, string>,
240
+ loaderPromises: Map<string, Promise<any>>
241
+ ): Promise<[ResolvedSegment[], string[]] | null>;
242
+
243
+ /**
244
+ * Cache entry with automatic handle collection (non-blocking).
245
+ *
246
+ * Schedules caching via waitUntil - handles are collected after they settle.
247
+ * Validates segments have actual components before caching.
248
+ *
249
+ * @param cacheKey - The cache key to store under
250
+ * @param segments - All resolved segments for this entry
251
+ */
252
+ cacheEntry(cacheKey: string, segments: ResolvedSegment[]): void;
253
+ }
254
+
255
+ // ============================================================================
256
+ // Generic Cache Store (for future extensibility)
257
+ // ============================================================================
258
+ // These types support a general-purpose cache interface that can be used
259
+ // for caching arbitrary values (responses, streams, objects). Currently,
260
+ // the segment caching system uses SegmentCacheStore directly, but these
261
+ // types enable future use cases like response caching or data caching.
262
+
263
+ /**
264
+ * Supported cache value types for the generic CacheStore interface.
265
+ * @internal Reserved for future extensibility
266
+ */
267
+ export type CacheValue =
268
+ | ReadableStream<Uint8Array>
269
+ | Response
270
+ | ArrayBuffer
271
+ | string
272
+ | Record<string, unknown>; // JSON-serializable object
273
+
274
+ /**
275
+ * Cache entry returned by match().
276
+ * @internal Reserved for future extensibility
277
+ */
278
+ export interface CacheEntry<T = CacheValue> {
279
+ /** The cached value */
280
+ value: T;
281
+ /** Optional metadata stored with the entry */
282
+ metadata?: CacheMetadata;
283
+ }
284
+
285
+ /**
286
+ * Original value type for reconstruction.
287
+ * @internal Reserved for future extensibility
288
+ */
289
+ export type CacheValueType =
290
+ | "stream"
291
+ | "response"
292
+ | "arraybuffer"
293
+ | "string"
294
+ | "object";
295
+
296
+ /**
297
+ * Metadata associated with a cache entry.
298
+ * @internal Reserved for future extensibility
299
+ */
300
+ export interface CacheMetadata {
301
+ /** Timestamp when entry expires (ms since epoch) */
302
+ expiresAt?: number;
303
+ /** Tags for bulk invalidation */
304
+ tags?: string[];
305
+ /** Original value type for reconstruction on read */
306
+ valueType?: CacheValueType;
307
+ /** Response headers (preserved when caching Response) */
308
+ responseHeaders?: Record<string, string>;
309
+ /** Response status (preserved when caching Response) */
310
+ responseStatus?: number;
311
+ /** Custom metadata */
312
+ [key: string]: unknown;
313
+ }
314
+
315
+ /**
316
+ * Options for put().
317
+ * @internal Reserved for future extensibility
318
+ */
319
+ export interface CachePutOptions {
320
+ /** Time-to-live in seconds */
321
+ ttl?: number;
322
+ /** Metadata to store with entry */
323
+ metadata?: Omit<CacheMetadata, "expiresAt">;
324
+ }
325
+
326
+ /**
327
+ * Generic cache store interface for arbitrary value types.
328
+ *
329
+ * This interface is designed for future extensibility to support caching
330
+ * responses, streams, and other values. Currently, segment caching uses
331
+ * the SegmentCacheStore interface directly.
332
+ *
333
+ * Implementations must handle:
334
+ * - Stream values (clone before storing, streams can only be read once)
335
+ * - Promise values (await before storing)
336
+ * - Expiration/TTL
337
+ *
338
+ * @internal Reserved for future extensibility
339
+ */
340
+ export interface CacheStore {
341
+ /**
342
+ * Retrieve a cached entry by key.
343
+ * @param key - Cache key
344
+ * @returns The cached entry or undefined if not found/expired
345
+ */
346
+ match<T = CacheValue>(key: string): Promise<CacheEntry<T> | undefined>;
347
+
348
+ /**
349
+ * Store a value in the cache.
350
+ * @param key - Cache key
351
+ * @param value - Value to cache (stream, response, string, object, etc.)
352
+ * @param options - TTL, metadata, etc.
353
+ */
354
+ put<T extends CacheValue>(
355
+ key: string,
356
+ value: T,
357
+ options?: CachePutOptions
358
+ ): Promise<void>;
359
+
360
+ /**
361
+ * Delete a cached entry.
362
+ * @param key - Cache key
363
+ * @returns true if entry was deleted, false if not found
364
+ */
365
+ delete(key: string): Promise<boolean>;
366
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * RSC-environment version of client exports
3
+ *
4
+ * This file is used when importing "rsc-router/client" from RSC (server components).
5
+ * It re-exports the server's createLoader so that loader definitions work in both
6
+ * environments with the same import.
7
+ *
8
+ * The bundler uses the "react-server" export condition to select this file
9
+ * in RSC context, while the regular client.tsx is used in client components.
10
+ */
11
+
12
+ // Re-export everything from client.tsx (Outlet, useLoader, etc.)
13
+ // These are safe to use in RSC context
14
+ export {
15
+ Outlet,
16
+ ParallelOutlet,
17
+ OutletProvider,
18
+ useOutlet,
19
+ useLoader,
20
+ useLoaderData,
21
+ ErrorBoundary,
22
+ type ErrorBoundaryProps,
23
+ } from "./client.js";
24
+
25
+ // Re-export the server's createLoader for RSC context
26
+ // This version includes the actual loader function
27
+ export { createLoader } from "./route-definition.js";
28
+
29
+ // Re-export Link component (can be used in server components)
30
+ export {
31
+ Link,
32
+ type LinkProps,
33
+ type PrefetchStrategy,
34
+ } from "./browser/react/Link.js";
35
+
36
+ // Re-export ScrollRestoration (can be used in server components)
37
+ export {
38
+ ScrollRestoration,
39
+ type ScrollRestorationProps,
40
+ } from "./browser/react/ScrollRestoration.js";
41
+
42
+ // Re-export NavigationProvider (needed for setup)
43
+ export {
44
+ NavigationProvider,
45
+ type NavigationProviderProps,
46
+ } from "./browser/react/NavigationProvider.js";
47
+
48
+ // Re-export href function (can be used in server components)
49
+ export { href } from "./href-client.js";
50
+
51
+ // Note: useNavigation, useAction, useClientCache are NOT re-exported here
52
+ // because they use client-side state and should only be used in client components
53
+
54
+ // Handle API - for accumulating data across route segments
55
+ // Works in both RSC and client contexts
56
+ export { createHandle, isHandle, type Handle } from "./handle.js";
57
+
58
+ // Built-in handles
59
+ // Meta handle works in RSC context
60
+ export { Meta } from "./handles/meta.js";
61
+ // MetaTags is a "use client" component that can be imported from RSC
62
+ export { MetaTags } from "./handles/MetaTags.js";
63
+ export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
64
+
65
+ // Location state - createLocationState works in RSC (just creates definition)
66
+ // useLocationState is NOT exported here as it uses client hooks
67
+ export {
68
+ createLocationState,
69
+ type LocationStateDefinition,
70
+ type LocationStateEntry,
71
+ } from "./browser/react/location-state-shared.js";
72
+
73
+ // Stub exports for client-only hooks
74
+ // These satisfy esbuild's dependency scan but throw if accidentally used in RSC
75
+ function clientOnlyHookError(hookName: string): never {
76
+ throw new Error(
77
+ `${hookName}() can only be used in client components. ` +
78
+ `Add "use client" directive at the top of your file.`
79
+ );
80
+ }
81
+
82
+ export function useHandle(): never {
83
+ return clientOnlyHookError("useHandle");
84
+ }
85
+
86
+ export function useLocationState(): never {
87
+ return clientOnlyHookError("useLocationState");
88
+ }