@sigil-dev/grimoire 0.7.4 → 0.7.6
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/.grimoire/_routes.dom.js +8 -0
- package/.grimoire/_routes.hydrate.js +8 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +59 -0
- package/.grimoire/types/api/hello/$types.d.ts +50 -0
- package/.grimoire/types/api/items/$types.d.ts +50 -0
- package/.grimoire/types/echo/$types.d.ts +50 -0
- package/.grimoire/types/env-private.d.ts +5 -0
- package/.grimoire/types/env-public.d.ts +5 -0
- package/.grimoire/types/mixed/$types.d.ts +50 -0
- package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
- package/.grimoire/types/reject/$types.d.ts +50 -0
- package/index.ts +34 -34
- package/package.json +8 -4
- package/preload.js +2 -0
- package/public/__grimoire__/hydrate.js +585 -0
- package/public/__grimoire__/index.js +490 -0
- package/src/client/head.ts +29 -0
- package/src/client/router.ts +224 -76
- package/src/env/index.ts +25 -0
- package/src/env/plugin.ts +13 -0
- package/src/env/private.ts +5 -0
- package/src/env/public.ts +7 -0
- package/src/env/typegen.ts +51 -0
- package/src/integrations/vite.ts +72 -72
- package/src/rendering/head.ts +22 -2
- package/src/rendering/hydrate.ts +81 -26
- package/src/rendering/index.ts +199 -186
- package/src/rendering/ssrPlugin.ts +53 -42
- package/src/routing/manifest-gen.ts +39 -26
- package/src/routing/router.ts +106 -98
- package/src/routing/scanner.ts +135 -129
- package/src/routing/transform-routes.ts +101 -96
- package/src/server/build.ts +147 -90
- package/src/server/coordinator.ts +306 -297
- package/src/server/hooks.ts +24 -3
- package/src/server/index.ts +148 -71
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +353 -340
- package/src/types.ts +269 -260
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -221
- package/test/rendering.test.ts +425 -425
- package/test/routing.test.ts +83 -45
- package/test/scanning.test.ts +181 -169
- package/test/server.test.ts +229 -229
- package/test/streaming.test.ts +106 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +19 -1
package/src/types.ts
CHANGED
|
@@ -1,260 +1,269 @@
|
|
|
1
|
-
import type { ServerWebSocket } from "bun";
|
|
2
|
-
import type { RouteFile, RouteTree } from "./routing/scanner";
|
|
3
|
-
|
|
4
|
-
/// <reference types="bun-types">
|
|
5
|
-
declare global {
|
|
6
|
-
namespace App {
|
|
7
|
-
// augment in your own project
|
|
8
|
-
interface Locals {}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
):
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Fires
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
*/
|
|
203
|
-
export
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
*
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import type { RouteFile, RouteTree } from "./routing/scanner";
|
|
3
|
+
|
|
4
|
+
/// <reference types="bun-types">
|
|
5
|
+
declare global {
|
|
6
|
+
namespace App {
|
|
7
|
+
// augment in your own project via src/app.d.ts
|
|
8
|
+
interface Locals {}
|
|
9
|
+
interface Error {
|
|
10
|
+
message: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
}
|
|
13
|
+
interface PageData {}
|
|
14
|
+
interface Platform {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Route {
|
|
19
|
+
path: string; // /blog/:slug
|
|
20
|
+
params: Record<string, string>;
|
|
21
|
+
filePath: string; // absolute path to +page.tsx
|
|
22
|
+
loadPath?: string; // absolute path to +page.ts
|
|
23
|
+
serverPath?: string; // absolute path to +server.ts
|
|
24
|
+
layoutPath?: string; // absolute path to +layout.tsx
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RouteInfo {
|
|
28
|
+
path: string; // /blog/:slug
|
|
29
|
+
filePath: string; // absolute path
|
|
30
|
+
type: string; // page, layout etc
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LoadContext {
|
|
34
|
+
request: Request;
|
|
35
|
+
params: Record<string, string>;
|
|
36
|
+
url: URL;
|
|
37
|
+
locals: Record<string, unknown>; // set by plugins/hooks
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TypedLoadContext<
|
|
41
|
+
P extends Record<string, string> = Record<string, string>,
|
|
42
|
+
> {
|
|
43
|
+
request: Request;
|
|
44
|
+
params: P;
|
|
45
|
+
url: URL;
|
|
46
|
+
locals: App.Locals;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RenderContext {
|
|
50
|
+
route: RouteInfo;
|
|
51
|
+
request: Request;
|
|
52
|
+
params: Record<string, string>;
|
|
53
|
+
data: unknown; // return value of load()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BuildResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
outputs: string[];
|
|
59
|
+
errors: string[];
|
|
60
|
+
}
|
|
61
|
+
export type Server = { port: number; hostname: string; stop(): void };
|
|
62
|
+
|
|
63
|
+
export interface GrimoirePlugin {
|
|
64
|
+
name: string;
|
|
65
|
+
|
|
66
|
+
// server lifecycle
|
|
67
|
+
onStart?(server: Server): void | Promise<void>;
|
|
68
|
+
onStop?(reason: "shutdown" | "restart"): void | Promise<void>;
|
|
69
|
+
|
|
70
|
+
// request pipeline
|
|
71
|
+
onRequest?(
|
|
72
|
+
req: Request,
|
|
73
|
+
next: () => Promise<Response>,
|
|
74
|
+
): Response | Promise<Response>;
|
|
75
|
+
|
|
76
|
+
// route lifecycle
|
|
77
|
+
onRouteLoad?(route: Route, context: LoadContext): void | Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Called after the page HTML is fully rendered.
|
|
80
|
+
* NOTE: When any plugin implements this hook, the streaming response is
|
|
81
|
+
* buffered into a single string. Streaming is preserved for routes where
|
|
82
|
+
* no plugin uses this hook. To avoid buffering, use onRequest to intercept
|
|
83
|
+
* at the Response level.
|
|
84
|
+
*/
|
|
85
|
+
onRouteRender?(
|
|
86
|
+
html: string,
|
|
87
|
+
context: RenderContext,
|
|
88
|
+
): string | Promise<string>;
|
|
89
|
+
|
|
90
|
+
// build
|
|
91
|
+
onBuildStart?(): void | Promise<void>;
|
|
92
|
+
onBuildEnd?(result: BuildResult): void | Promise<void>;
|
|
93
|
+
|
|
94
|
+
// config
|
|
95
|
+
config?: (config: GrimoireConfig) => GrimoireConfig | undefined;
|
|
96
|
+
|
|
97
|
+
// process lifecycle
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Runs AFTER Sigil/Babel compilation. Receives compiled JS, not source TSX.
|
|
101
|
+
* Suitable for string-level transforms (import rewriting, comment injection, etc.).
|
|
102
|
+
* Return null/undefined to pass through unchanged.
|
|
103
|
+
*/
|
|
104
|
+
transform?(
|
|
105
|
+
code: string,
|
|
106
|
+
id: string,
|
|
107
|
+
): string | null | undefined | Promise<string | null | undefined>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fires once after all workers are spawned and ready.
|
|
111
|
+
* Use for coordinator-level setup (e.g. opening a shared store).
|
|
112
|
+
*/
|
|
113
|
+
onCoordinatorStart?(ctx: CoordinatorContext): void | Promise<void>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fires for each worker before Bun.spawn is called.
|
|
117
|
+
* Return WorkerEnv to inject env vars or set the worker name.
|
|
118
|
+
* ALL plugins are called and results are merged.
|
|
119
|
+
*/
|
|
120
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: doesn't have to return a thing.
|
|
121
|
+
onWorkerSpawn?(
|
|
122
|
+
worker: WorkerDescriptor,
|
|
123
|
+
): WorkerEnv | void | Promise<WorkerEnv | void>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fires when a worker process is up and accepting requests.
|
|
127
|
+
*/
|
|
128
|
+
onWorkerReady?(worker: WorkerDescriptor): void | Promise<void>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fires when a worker process exits.
|
|
132
|
+
* reason "crash" = unexpected exit, "intentional" = coordinator stopped it.
|
|
133
|
+
* Use for cleanup or respawn logic (see plugin-scale).
|
|
134
|
+
*/
|
|
135
|
+
onWorkerDeath?(
|
|
136
|
+
worker: WorkerDescriptor,
|
|
137
|
+
reason: "crash" | "intentional",
|
|
138
|
+
): void | Promise<void>;
|
|
139
|
+
|
|
140
|
+
// ── Locals boundary ────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Called by coordinator before forwarding a request to a worker.
|
|
144
|
+
* Return a serialized string representation of locals.
|
|
145
|
+
* First plugin to return non-null wins. Default: signed JSON header.
|
|
146
|
+
*/
|
|
147
|
+
serializeLocals?(locals: App.Locals): string | Promise<string>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Called by worker on receiving a forwarded request.
|
|
151
|
+
* Return the deserialized locals object.
|
|
152
|
+
* First plugin to return non-null wins. Default: verify + JSON.parse.
|
|
153
|
+
*/
|
|
154
|
+
deserializeLocals?(raw: string): App.Locals | Promise<App.Locals>;
|
|
155
|
+
|
|
156
|
+
// ── Routing policy ─────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Called by coordinator to decide which worker handles a request.
|
|
160
|
+
* First plugin to return non-null wins.
|
|
161
|
+
* Default: ws routes → consistent hash, everything else → round robin.
|
|
162
|
+
* routes is the full RouteTree so plugins can make route-aware decisions.
|
|
163
|
+
*/
|
|
164
|
+
routeRequest?(
|
|
165
|
+
req: Request,
|
|
166
|
+
workers: WorkerDescriptor[],
|
|
167
|
+
routes: RouteTree,
|
|
168
|
+
): WorkerDescriptor | Promise<WorkerDescriptor>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface GrimoireConfig {
|
|
172
|
+
port?: number;
|
|
173
|
+
host?: string;
|
|
174
|
+
plugins?: GrimoirePlugin[];
|
|
175
|
+
routes?: string; // glob, default 'src/routes/**';
|
|
176
|
+
dev?: boolean;
|
|
177
|
+
vitePort?: number;
|
|
178
|
+
alias?: Record<string, string>; // e.g. { "$lib": "src/lib" } — resolved relative to cwd
|
|
179
|
+
cspNonce?: string; // CSP nonce for <script>/<style> tags
|
|
180
|
+
bundleStrategy?: "split" | "single"; // 'split' (default) or 'single' bundle
|
|
181
|
+
_skipBuild?: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function defineConfig(config: GrimoireConfig): GrimoireConfig {
|
|
185
|
+
return config;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Generic load type for +page.server.ts. For per-route typed params import PageServerLoad from './$types'. */
|
|
189
|
+
export type PageServerLoad<
|
|
190
|
+
P extends Record<string, string> = Record<string, string>,
|
|
191
|
+
> = (
|
|
192
|
+
ctx: TypedLoadContext<P>,
|
|
193
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
194
|
+
|
|
195
|
+
/** Generic load type for +layout.server.ts. For per-route typed params import LayoutServerLoad from './$types'. */
|
|
196
|
+
export type LayoutServerLoad<
|
|
197
|
+
P extends Record<string, string> = Record<string, string>,
|
|
198
|
+
> = (
|
|
199
|
+
ctx: TypedLoadContext<P>,
|
|
200
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
201
|
+
|
|
202
|
+
/** Generic handler type for +server.ts API routes. For per-route typed params import RequestHandler from './$types'. */
|
|
203
|
+
export type RequestHandler<
|
|
204
|
+
P extends Record<string, string> = Record<string, string>,
|
|
205
|
+
> = (ctx: TypedLoadContext<P>) => Response | Promise<Response>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Shape of the `websocket` export in a +server.ts file.
|
|
209
|
+
* T is the resolved ws.data shape (params + upgrade() return value).
|
|
210
|
+
* For per-route typing import WebSocketHandler from './$types'.
|
|
211
|
+
*/
|
|
212
|
+
export interface WsRouteHandler<T = Record<string, unknown>> {
|
|
213
|
+
open?(ws: ServerWebSocket<T>): void | Promise<void>;
|
|
214
|
+
message?(ws: ServerWebSocket<T>, data: string | Buffer): void | Promise<void>;
|
|
215
|
+
close?(
|
|
216
|
+
ws: ServerWebSocket<T>,
|
|
217
|
+
code: number,
|
|
218
|
+
reason?: string,
|
|
219
|
+
): void | Promise<void>;
|
|
220
|
+
drain?(ws: ServerWebSocket<T>): void | Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ***BIG ONE***
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Built-in worker modes understood by the coordinator.
|
|
227
|
+
* Plugins may define custom modes via WorkerMode.
|
|
228
|
+
*/
|
|
229
|
+
export type BuiltinWorkerMode = "api" | "frontend" | "ws" | "full";
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Open union — (string & {}) preserves autocomplete for built-in
|
|
233
|
+
* values while allowing plugin-defined modes like "db" or "queue".
|
|
234
|
+
*/
|
|
235
|
+
export type WorkerMode = BuiltinWorkerMode | (string & {});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Describes a single worker process managed by the coordinator.
|
|
239
|
+
* Coordinator derives behavior from routes, not mode string —
|
|
240
|
+
* mode is informational and for plugin use only.
|
|
241
|
+
*/
|
|
242
|
+
export interface WorkerDescriptor {
|
|
243
|
+
mode: WorkerMode;
|
|
244
|
+
index: number; // index within this mode (api-0, api-1...)
|
|
245
|
+
globalIndex: number; // index across all workers
|
|
246
|
+
internalUrl: string; // coordinator → worker base URL
|
|
247
|
+
name?: string; // set by namepool or onWorkerSpawn
|
|
248
|
+
routes: RouteFile[]; // route slice assigned by coordinator
|
|
249
|
+
pid?: number; // set after Bun.spawn
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Return value of onWorkerSpawn. All plugins are called and results
|
|
254
|
+
* are merged — env from all plugins is combined, last name wins.
|
|
255
|
+
*/
|
|
256
|
+
export interface WorkerEnv {
|
|
257
|
+
env?: Record<string, string>;
|
|
258
|
+
name?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Passed to onCoordinatorStart once the coordinator is fully initialized.
|
|
263
|
+
*/
|
|
264
|
+
export interface CoordinatorContext {
|
|
265
|
+
workers: WorkerDescriptor[];
|
|
266
|
+
port: number;
|
|
267
|
+
secret: string; // ephemeral signing secret, generated per-run
|
|
268
|
+
routes: RouteTree;
|
|
269
|
+
}
|
package/test/context.test.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
// packages/grimoire/src/context.test.ts
|
|
2
|
-
import { describe, expect, test } from "bun:test";
|
|
3
|
-
import { createContext, getContext, setContext } from "@sigil-dev/runtime";
|
|
4
|
-
import { runWithContext } from "../src/server/context";
|
|
5
|
-
|
|
6
|
-
const UserKey = createContext<string>();
|
|
7
|
-
const ThemeKey = createContext<string>();
|
|
8
|
-
|
|
9
|
-
// 1. Basic: does it work at all
|
|
10
|
-
test("getContext returns value set in same context", async () => {
|
|
11
|
-
await runWithContext(async () => {
|
|
12
|
-
setContext(UserKey, "cane");
|
|
13
|
-
expect(getContext(UserKey)).toBe("cane");
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// 2. Isolation: the whole point of AsyncLocalStorage
|
|
18
|
-
test("concurrent requests do not bleed context", async () => {
|
|
19
|
-
let resolveA!: () => void;
|
|
20
|
-
let resolveB!: () => void;
|
|
21
|
-
|
|
22
|
-
const barrier = {
|
|
23
|
-
a: new Promise<void>((r) => (resolveA = r)),
|
|
24
|
-
b: new Promise<void>((r) => (resolveB = r)),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const requestA = runWithContext(async () => {
|
|
28
|
-
setContext(UserKey, "request-A");
|
|
29
|
-
await barrier.a; // suspend, let B run
|
|
30
|
-
// if context bleeds, this returns "request-B"
|
|
31
|
-
return getContext(UserKey);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const requestB = runWithContext(async () => {
|
|
35
|
-
setContext(UserKey, "request-B");
|
|
36
|
-
resolveA(); // wake A up
|
|
37
|
-
await barrier.b;
|
|
38
|
-
return getContext(UserKey);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
resolveB();
|
|
42
|
-
const [a, b] = await Promise.all([requestA, requestB]);
|
|
43
|
-
|
|
44
|
-
expect(a).toBe("request-A"); // fails if bleed
|
|
45
|
-
expect(b).toBe("request-B");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// 3. Outside context: should not throw, returns undefined
|
|
49
|
-
test("getContext outside runWithContext returns undefined gracefully", () => {
|
|
50
|
-
const val = getContext(UserKey);
|
|
51
|
-
expect(val).toBeUndefined();
|
|
52
|
-
});
|
|
1
|
+
// packages/grimoire/src/context.test.ts
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { createContext, getContext, setContext } from "@sigil-dev/runtime";
|
|
4
|
+
import { runWithContext } from "../src/server/context";
|
|
5
|
+
|
|
6
|
+
const UserKey = createContext<string>();
|
|
7
|
+
const ThemeKey = createContext<string>();
|
|
8
|
+
|
|
9
|
+
// 1. Basic: does it work at all
|
|
10
|
+
test("getContext returns value set in same context", async () => {
|
|
11
|
+
await runWithContext(async () => {
|
|
12
|
+
setContext(UserKey, "cane");
|
|
13
|
+
expect(getContext(UserKey)).toBe("cane");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// 2. Isolation: the whole point of AsyncLocalStorage
|
|
18
|
+
test("concurrent requests do not bleed context", async () => {
|
|
19
|
+
let resolveA!: () => void;
|
|
20
|
+
let resolveB!: () => void;
|
|
21
|
+
|
|
22
|
+
const barrier = {
|
|
23
|
+
a: new Promise<void>((r) => (resolveA = r)),
|
|
24
|
+
b: new Promise<void>((r) => (resolveB = r)),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const requestA = runWithContext(async () => {
|
|
28
|
+
setContext(UserKey, "request-A");
|
|
29
|
+
await barrier.a; // suspend, let B run
|
|
30
|
+
// if context bleeds, this returns "request-B"
|
|
31
|
+
return getContext(UserKey);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const requestB = runWithContext(async () => {
|
|
35
|
+
setContext(UserKey, "request-B");
|
|
36
|
+
resolveA(); // wake A up
|
|
37
|
+
await barrier.b;
|
|
38
|
+
return getContext(UserKey);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
resolveB();
|
|
42
|
+
const [a, b] = await Promise.all([requestA, requestB]);
|
|
43
|
+
|
|
44
|
+
expect(a).toBe("request-A"); // fails if bleed
|
|
45
|
+
expect(b).toBe("request-B");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 3. Outside context: should not throw, returns undefined
|
|
49
|
+
test("getContext outside runWithContext returns undefined gracefully", () => {
|
|
50
|
+
const val = getContext(UserKey);
|
|
51
|
+
expect(val).toBeUndefined();
|
|
52
|
+
});
|