@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,150 @@
1
+ /**
2
+ * In-Memory Segment Cache Store
3
+ *
4
+ * Simple in-memory implementation of SegmentCacheStore.
5
+ * Uses globalThis to survive HMR in development.
6
+ */
7
+
8
+ import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult } from "./types.js";
9
+ import type { RequestContext } from "../server/request-context.js";
10
+
11
+ const CACHE_GLOBAL_KEY = "__rsc_router_segment_cache_store__";
12
+
13
+ /**
14
+ * Options for MemorySegmentCacheStore
15
+ */
16
+ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
17
+ /**
18
+ * Default cache options for cache() boundaries.
19
+ * When cache() is called without explicit ttl/swr,
20
+ * these defaults are used.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const store = new MemorySegmentCacheStore({
25
+ * defaults: { ttl: 60, swr: 300 }
26
+ * });
27
+ * ```
28
+ */
29
+ defaults?: CacheDefaults;
30
+
31
+ /**
32
+ * Custom key generator applied to all cache operations.
33
+ * Receives the full RequestContext and the default-generated key.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * keyGenerator: (ctx, defaultKey) => {
38
+ * const locale = ctx.cookie('locale') || 'en';
39
+ * return `${locale}:${defaultKey}`;
40
+ * }
41
+ * ```
42
+ */
43
+ keyGenerator?: (
44
+ ctx: RequestContext<TEnv>,
45
+ defaultKey: string
46
+ ) => string | Promise<string>;
47
+ }
48
+
49
+ /**
50
+ * In-memory segment cache store.
51
+ *
52
+ * Suitable for development and single-instance deployments.
53
+ * For production with multiple instances, use a distributed store
54
+ * like Cloudflare KV or Redis.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // Basic usage
59
+ * const store = new MemorySegmentCacheStore();
60
+ *
61
+ * // With defaults for cache() boundaries
62
+ * const store = new MemorySegmentCacheStore({
63
+ * defaults: { ttl: 60 }
64
+ * });
65
+ *
66
+ * createRSCHandler({
67
+ * router,
68
+ * cache: { store }
69
+ * })
70
+ * ```
71
+ */
72
+ export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
73
+ private cache: Map<string, CachedEntryData>;
74
+ readonly defaults?: CacheDefaults;
75
+ readonly keyGenerator?: (
76
+ ctx: RequestContext<TEnv>,
77
+ defaultKey: string
78
+ ) => string | Promise<string>;
79
+
80
+ constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
81
+ // Use globalThis to survive HMR in development
82
+ this.cache =
83
+ (globalThis as any)[CACHE_GLOBAL_KEY] ??
84
+ ((globalThis as any)[CACHE_GLOBAL_KEY] = new Map<string, CachedEntryData>());
85
+ this.defaults = options?.defaults;
86
+ this.keyGenerator = options?.keyGenerator;
87
+ }
88
+
89
+ async get(key: string): Promise<CacheGetResult | null> {
90
+ const cached = this.cache.get(key);
91
+
92
+ if (!cached) {
93
+ return null;
94
+ }
95
+
96
+ // Check expiration
97
+ if (Date.now() > cached.expiresAt) {
98
+ this.cache.delete(key);
99
+ return null;
100
+ }
101
+
102
+ // Memory store doesn't support SWR - never triggers revalidation
103
+ return { data: cached, shouldRevalidate: false };
104
+ }
105
+
106
+ async set(key: string, data: CachedEntryData, ttl: number, _swr?: number): Promise<void> {
107
+ // Note: Memory store doesn't implement SWR - entries just expire at TTL
108
+ // For SWR support, use CFCacheStore or similar distributed cache
109
+ const entry: CachedEntryData = {
110
+ ...data,
111
+ expiresAt: Date.now() + ttl * 1000,
112
+ };
113
+ this.cache.set(key, entry);
114
+ }
115
+
116
+ async delete(key: string): Promise<boolean> {
117
+ return this.cache.delete(key);
118
+ }
119
+
120
+ async clear(): Promise<void> {
121
+ this.cache.clear();
122
+ }
123
+
124
+ /**
125
+ * Get cache statistics for debugging purposes.
126
+ * @internal
127
+ */
128
+ getStats(): { size: number; keys: string[] } {
129
+ return {
130
+ size: this.cache.size,
131
+ keys: Array.from(this.cache.keys()),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Reset the global cache state.
137
+ * Useful for test isolation - call this in beforeEach to ensure
138
+ * tests don't share cache state via globalThis.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * beforeEach(() => {
143
+ * MemorySegmentCacheStore.resetGlobalCache();
144
+ * });
145
+ * ```
146
+ */
147
+ static resetGlobalCache(): void {
148
+ delete (globalThis as any)[CACHE_GLOBAL_KEY];
149
+ }
150
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * In-Memory Cache Store
3
+ *
4
+ * Simple implementation for development and testing.
5
+ * Not suitable for production (no persistence, single-instance only).
6
+ *
7
+ * @internal This is reserved for future extensibility.
8
+ * For segment caching, use MemorySegmentCacheStore instead.
9
+ */
10
+
11
+ import type {
12
+ CacheStore,
13
+ CacheEntry,
14
+ CacheValue,
15
+ CachePutOptions,
16
+ CacheMetadata,
17
+ CacheValueType,
18
+ } from "./types.js";
19
+
20
+ // ============================================================================
21
+ // Constants
22
+ // ============================================================================
23
+
24
+ /** Default TTL when no explicit value is provided */
25
+ const DEFAULT_TTL_SECONDS = 60;
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ interface StoredEntry {
32
+ /** Stored value (streams/responses converted to ArrayBuffer) */
33
+ value: ArrayBuffer | string | object;
34
+ metadata: CacheMetadata;
35
+ }
36
+
37
+ /**
38
+ * In-memory cache store implementation
39
+ */
40
+ export class MemoryCacheStore implements CacheStore {
41
+ private cache = new Map<string, StoredEntry>();
42
+
43
+ async match<T = CacheValue>(key: string): Promise<CacheEntry<T> | undefined> {
44
+ const entry = this.cache.get(key);
45
+
46
+ if (!entry) {
47
+ return undefined;
48
+ }
49
+
50
+ // Check expiration
51
+ if (entry.metadata.expiresAt && Date.now() > entry.metadata.expiresAt) {
52
+ this.cache.delete(key);
53
+ return undefined;
54
+ }
55
+
56
+ // Reconstruct value based on original type
57
+ const value = this.reconstructValue(entry);
58
+
59
+ return {
60
+ value: value as T,
61
+ metadata: entry.metadata,
62
+ };
63
+ }
64
+
65
+ async put<T extends CacheValue>(
66
+ key: string,
67
+ value: T,
68
+ options?: CachePutOptions
69
+ ): Promise<void> {
70
+ const ttl = options?.ttl ?? DEFAULT_TTL_SECONDS;
71
+ const expiresAt = Date.now() + ttl * 1000;
72
+
73
+ // Detect value type and convert for storage
74
+ const { storedValue, valueType, responseHeaders, responseStatus } =
75
+ await this.prepareForStorage(value);
76
+
77
+ const metadata: CacheMetadata = {
78
+ ...options?.metadata,
79
+ expiresAt,
80
+ valueType,
81
+ responseHeaders,
82
+ responseStatus,
83
+ };
84
+
85
+ this.cache.set(key, {
86
+ value: storedValue,
87
+ metadata,
88
+ });
89
+ }
90
+
91
+ async delete(key: string): Promise<boolean> {
92
+ return this.cache.delete(key);
93
+ }
94
+
95
+ /**
96
+ * Clear all entries (useful for testing)
97
+ */
98
+ clear(): void {
99
+ this.cache.clear();
100
+ }
101
+
102
+ /**
103
+ * Get current cache size (useful for testing/debugging)
104
+ */
105
+ get size(): number {
106
+ return this.cache.size;
107
+ }
108
+
109
+ /**
110
+ * Manually purge expired entries
111
+ */
112
+ purgeExpired(): number {
113
+ const now = Date.now();
114
+ let purged = 0;
115
+
116
+ for (const [key, entry] of this.cache) {
117
+ if (entry.metadata.expiresAt && now > entry.metadata.expiresAt) {
118
+ this.cache.delete(key);
119
+ purged++;
120
+ }
121
+ }
122
+
123
+ return purged;
124
+ }
125
+
126
+ /**
127
+ * Prepare a value for storage
128
+ * Converts streams and responses to ArrayBuffer, detects type
129
+ */
130
+ private async prepareForStorage(value: CacheValue): Promise<{
131
+ storedValue: ArrayBuffer | string | object;
132
+ valueType: CacheValueType;
133
+ responseHeaders?: Record<string, string>;
134
+ responseStatus?: number;
135
+ }> {
136
+ // ReadableStream -> ArrayBuffer
137
+ if (value instanceof ReadableStream) {
138
+ return {
139
+ storedValue: await streamToArrayBuffer(value),
140
+ valueType: "stream",
141
+ };
142
+ }
143
+
144
+ // Response -> ArrayBuffer + headers/status
145
+ if (value instanceof Response) {
146
+ const headers: Record<string, string> = {};
147
+ value.headers.forEach((v, k) => {
148
+ headers[k] = v;
149
+ });
150
+
151
+ return {
152
+ storedValue: await value.clone().arrayBuffer(),
153
+ valueType: "response",
154
+ responseHeaders: headers,
155
+ responseStatus: value.status,
156
+ };
157
+ }
158
+
159
+ // ArrayBuffer -> store as-is
160
+ if (value instanceof ArrayBuffer) {
161
+ return {
162
+ storedValue: value,
163
+ valueType: "arraybuffer",
164
+ };
165
+ }
166
+
167
+ // String -> store as-is
168
+ if (typeof value === "string") {
169
+ return {
170
+ storedValue: value,
171
+ valueType: "string",
172
+ };
173
+ }
174
+
175
+ // Object -> store as-is (JSON-serializable)
176
+ return {
177
+ storedValue: value,
178
+ valueType: "object",
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Reconstruct original value type from stored entry
184
+ */
185
+ private reconstructValue(entry: StoredEntry): CacheValue {
186
+ const { value, metadata } = entry;
187
+
188
+ switch (metadata.valueType) {
189
+ case "stream":
190
+ return arrayBufferToStream(value as ArrayBuffer);
191
+
192
+ case "response": {
193
+ const status = metadata.responseStatus ?? 200;
194
+ // Status codes 204 (No Content) and 304 (Not Modified) cannot have a body
195
+ const isNullBodyStatus = status === 204 || status === 304;
196
+ return new Response(isNullBodyStatus ? null : (value as ArrayBuffer), {
197
+ status,
198
+ headers: metadata.responseHeaders,
199
+ });
200
+ }
201
+
202
+ case "arraybuffer":
203
+ case "string":
204
+ case "object":
205
+ default:
206
+ return value as CacheValue;
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Convert a ReadableStream to ArrayBuffer.
213
+ * @internal
214
+ */
215
+ async function streamToArrayBuffer(
216
+ stream: ReadableStream<Uint8Array>
217
+ ): Promise<ArrayBuffer> {
218
+ const chunks: Uint8Array[] = [];
219
+ const reader = stream.getReader();
220
+
221
+ while (true) {
222
+ const { done, value } = await reader.read();
223
+ if (done) break;
224
+ chunks.push(value);
225
+ }
226
+
227
+ // Concatenate chunks
228
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
229
+ const result = new Uint8Array(totalLength);
230
+ let offset = 0;
231
+
232
+ for (const chunk of chunks) {
233
+ result.set(chunk, offset);
234
+ offset += chunk.length;
235
+ }
236
+
237
+ return result.buffer;
238
+ }
239
+
240
+ /**
241
+ * Convert an ArrayBuffer to a ReadableStream.
242
+ * @internal
243
+ */
244
+ function arrayBufferToStream(buffer: ArrayBuffer): ReadableStream<Uint8Array> {
245
+ const uint8 = new Uint8Array(buffer);
246
+
247
+ return new ReadableStream({
248
+ start(controller) {
249
+ controller.enqueue(uint8);
250
+ controller.close();
251
+ },
252
+ });
253
+ }