@sylphx/lens-server 1.11.3 → 2.1.0
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/index.d.ts +1262 -262
- package/dist/index.js +1714 -1154
- package/package.json +2 -2
- package/src/context/index.test.ts +425 -0
- package/src/context/index.ts +90 -0
- package/src/e2e/server.test.ts +215 -433
- package/src/handlers/framework.ts +294 -0
- package/src/handlers/http.test.ts +215 -0
- package/src/handlers/http.ts +189 -0
- package/src/handlers/index.ts +55 -0
- package/src/handlers/unified.ts +114 -0
- package/src/handlers/ws-types.ts +126 -0
- package/src/handlers/ws.ts +669 -0
- package/src/index.ts +127 -24
- package/src/plugin/index.ts +41 -0
- package/src/plugin/op-log.ts +286 -0
- package/src/plugin/optimistic.ts +375 -0
- package/src/plugin/types.ts +551 -0
- package/src/reconnect/index.ts +9 -0
- package/src/reconnect/operation-log.test.ts +480 -0
- package/src/reconnect/operation-log.ts +450 -0
- package/src/server/create.test.ts +256 -2193
- package/src/server/create.ts +285 -1481
- package/src/server/dataloader.ts +60 -0
- package/src/server/selection.ts +123 -0
- package/src/server/types.ts +306 -0
- package/src/sse/handler.ts +123 -56
- package/src/state/index.ts +9 -11
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +279 -0
- package/src/storage/types.ts +205 -0
- package/src/state/graph-state-manager.test.ts +0 -1105
- package/src/state/graph-state-manager.ts +0 -890
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Server runtime for Lens API framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "SylphxAI",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@sylphx/lens-core": "^
|
|
33
|
+
"@sylphx/lens-core": "^2.0.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.9.3",
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-core - Context System Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for AsyncLocalStorage-based context.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
extendContext,
|
|
11
|
+
hasContext,
|
|
12
|
+
runWithContext,
|
|
13
|
+
runWithContextAsync,
|
|
14
|
+
tryUseContext,
|
|
15
|
+
useContext,
|
|
16
|
+
} from "./index.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Test Fixtures
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
interface TestContext {
|
|
23
|
+
db: { query: (sql: string) => string[] };
|
|
24
|
+
currentUser: { id: string; name: string } | null;
|
|
25
|
+
requestId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const mockDb = {
|
|
29
|
+
query: (sql: string) => [`result for: ${sql}`],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mockUser = { id: "user-1", name: "John" };
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Test: createContext & useContext
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
describe("createContext & useContext", () => {
|
|
39
|
+
it("creates a context and accesses it", () => {
|
|
40
|
+
const ctx = createContext<TestContext>();
|
|
41
|
+
|
|
42
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
43
|
+
const context = useContext<TestContext>();
|
|
44
|
+
expect(context.db).toBe(mockDb);
|
|
45
|
+
expect(context.currentUser).toBe(mockUser);
|
|
46
|
+
expect(context.requestId).toBe("req-1");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("useContext throws outside of context", () => {
|
|
51
|
+
expect(() => useContext()).toThrow("useContext() called outside of context");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("useContext throws with correct error message", () => {
|
|
55
|
+
expect(() => useContext()).toThrow(
|
|
56
|
+
"useContext() called outside of context. Make sure to wrap your code with runWithContext() or use explicit ctx parameter.",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("tryUseContext returns undefined outside of context", () => {
|
|
61
|
+
const ctx = tryUseContext<TestContext>();
|
|
62
|
+
expect(ctx).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("tryUseContext returns context when available", () => {
|
|
66
|
+
const ctx = createContext<TestContext>();
|
|
67
|
+
|
|
68
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
69
|
+
const context = tryUseContext<TestContext>();
|
|
70
|
+
expect(context).toBeDefined();
|
|
71
|
+
expect(context?.currentUser).toBe(mockUser);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("useContext without type parameter uses default ContextValue type", () => {
|
|
76
|
+
const ctx = createContext<{ value: string }>();
|
|
77
|
+
|
|
78
|
+
runWithContext(ctx, { value: "test" }, () => {
|
|
79
|
+
const context = useContext();
|
|
80
|
+
expect(context).toHaveProperty("value");
|
|
81
|
+
expect((context as { value: string }).value).toBe("test");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("tryUseContext without type parameter uses default ContextValue type", () => {
|
|
86
|
+
const ctx = createContext<{ value: string }>();
|
|
87
|
+
|
|
88
|
+
runWithContext(ctx, { value: "test" }, () => {
|
|
89
|
+
const context = tryUseContext();
|
|
90
|
+
expect(context).toBeDefined();
|
|
91
|
+
expect((context as { value: string }).value).toBe("test");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("createContext returns a ContextStore", () => {
|
|
96
|
+
const ctx = createContext<TestContext>();
|
|
97
|
+
expect(ctx).toBeDefined();
|
|
98
|
+
expect(typeof ctx.run).toBe("function");
|
|
99
|
+
expect(typeof ctx.getStore).toBe("function");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("multiple contexts share the same underlying store", () => {
|
|
103
|
+
const ctx1 = createContext<TestContext>();
|
|
104
|
+
const ctx2 = createContext<TestContext>();
|
|
105
|
+
|
|
106
|
+
// Both should be the same store instance (type-casted)
|
|
107
|
+
expect(ctx1).toBe(ctx2);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Test: runWithContext
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
describe("runWithContext", () => {
|
|
116
|
+
it("runs synchronous function with context", () => {
|
|
117
|
+
const ctx = createContext<TestContext>();
|
|
118
|
+
|
|
119
|
+
const result = runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
120
|
+
const context = useContext<TestContext>();
|
|
121
|
+
return context.currentUser?.name;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result).toBe("John");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("runs async function with context", async () => {
|
|
128
|
+
const ctx = createContext<TestContext>();
|
|
129
|
+
|
|
130
|
+
const result = await runWithContextAsync(
|
|
131
|
+
ctx,
|
|
132
|
+
{ db: mockDb, currentUser: mockUser, requestId: "req-1" },
|
|
133
|
+
async () => {
|
|
134
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
135
|
+
const context = useContext<TestContext>();
|
|
136
|
+
return context.currentUser?.name;
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result).toBe("John");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("context is isolated between runs", () => {
|
|
144
|
+
const ctx = createContext<TestContext>();
|
|
145
|
+
|
|
146
|
+
const results: string[] = [];
|
|
147
|
+
|
|
148
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
149
|
+
results.push(useContext<TestContext>().requestId);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
runWithContext(ctx, { db: mockDb, currentUser: null, requestId: "req-2" }, () => {
|
|
153
|
+
results.push(useContext<TestContext>().requestId);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(results).toEqual(["req-1", "req-2"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("nested contexts work correctly", () => {
|
|
160
|
+
const ctx = createContext<TestContext>();
|
|
161
|
+
|
|
162
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "outer" }, () => {
|
|
163
|
+
expect(useContext<TestContext>().requestId).toBe("outer");
|
|
164
|
+
|
|
165
|
+
runWithContext(ctx, { db: mockDb, currentUser: null, requestId: "inner" }, () => {
|
|
166
|
+
expect(useContext<TestContext>().requestId).toBe("inner");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// After inner context, outer is restored
|
|
170
|
+
expect(useContext<TestContext>().requestId).toBe("outer");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns the function result", () => {
|
|
175
|
+
const ctx = createContext<TestContext>();
|
|
176
|
+
|
|
177
|
+
const result = runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
178
|
+
return 42;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result).toBe(42);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("propagates errors thrown in function", () => {
|
|
185
|
+
const ctx = createContext<TestContext>();
|
|
186
|
+
|
|
187
|
+
expect(() => {
|
|
188
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
189
|
+
throw new Error("Test error");
|
|
190
|
+
});
|
|
191
|
+
}).toThrow("Test error");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("propagates errors in async function", async () => {
|
|
195
|
+
const ctx = createContext<TestContext>();
|
|
196
|
+
|
|
197
|
+
await expect(
|
|
198
|
+
runWithContextAsync(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, async () => {
|
|
199
|
+
throw new Error("Async test error");
|
|
200
|
+
}),
|
|
201
|
+
).rejects.toThrow("Async test error");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("cleans up context after error", () => {
|
|
205
|
+
const ctx = createContext<TestContext>();
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
209
|
+
throw new Error("Error");
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// Error expected
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Context should be cleaned up after error
|
|
216
|
+
expect(() => useContext()).toThrow("useContext() called outside of context");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("works with different context types", () => {
|
|
220
|
+
interface OtherContext {
|
|
221
|
+
config: { apiUrl: string };
|
|
222
|
+
logger: { log: (msg: string) => void };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const ctx = createContext<OtherContext>();
|
|
226
|
+
const config = { apiUrl: "https://api.example.com" };
|
|
227
|
+
const logger = { log: (msg: string) => msg };
|
|
228
|
+
|
|
229
|
+
const result = runWithContext(ctx, { config, logger }, () => {
|
|
230
|
+
const context = useContext<OtherContext>();
|
|
231
|
+
return context.config.apiUrl;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result).toBe("https://api.example.com");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("runWithContextAsync is an alias for runWithContext with async", async () => {
|
|
238
|
+
const ctx = createContext<TestContext>();
|
|
239
|
+
|
|
240
|
+
const result1 = await runWithContextAsync(
|
|
241
|
+
ctx,
|
|
242
|
+
{ db: mockDb, currentUser: mockUser, requestId: "req-1" },
|
|
243
|
+
async () => {
|
|
244
|
+
return "async result";
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const result2 = await runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-2" }, async () => {
|
|
249
|
+
return "async result";
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result1).toBe(result2);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// =============================================================================
|
|
257
|
+
// Test: Utilities
|
|
258
|
+
// =============================================================================
|
|
259
|
+
|
|
260
|
+
describe("hasContext", () => {
|
|
261
|
+
it("returns false outside context", () => {
|
|
262
|
+
expect(hasContext()).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns true inside context", () => {
|
|
266
|
+
const ctx = createContext<TestContext>();
|
|
267
|
+
|
|
268
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
269
|
+
expect(hasContext()).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("returns false after context exits", () => {
|
|
274
|
+
const ctx = createContext<TestContext>();
|
|
275
|
+
|
|
276
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
277
|
+
expect(hasContext()).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// After context exits, should return false
|
|
281
|
+
expect(hasContext()).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("extendContext", () => {
|
|
286
|
+
it("extends context with additional values", () => {
|
|
287
|
+
const ctx = createContext<TestContext>();
|
|
288
|
+
|
|
289
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
290
|
+
const current = useContext<TestContext>();
|
|
291
|
+
const extended = extendContext(current, { extra: "value" });
|
|
292
|
+
|
|
293
|
+
expect(extended.db).toBe(mockDb);
|
|
294
|
+
expect(extended.currentUser).toBe(mockUser);
|
|
295
|
+
expect(extended.extra).toBe("value");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("can be used with nested runWithContext", () => {
|
|
300
|
+
const ctx = createContext<TestContext>();
|
|
301
|
+
|
|
302
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
303
|
+
const current = useContext<TestContext>();
|
|
304
|
+
const extended = extendContext(current, { requestId: "req-extended" });
|
|
305
|
+
|
|
306
|
+
runWithContext(ctx, extended, () => {
|
|
307
|
+
const inner = useContext<TestContext>();
|
|
308
|
+
expect(inner.db).toBe(mockDb);
|
|
309
|
+
expect(inner.requestId).toBe("req-extended");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("overrides properties with same keys", () => {
|
|
315
|
+
const ctx = createContext<TestContext>();
|
|
316
|
+
|
|
317
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "original" }, () => {
|
|
318
|
+
const current = useContext<TestContext>();
|
|
319
|
+
const extended = extendContext(current, { requestId: "overridden", currentUser: null });
|
|
320
|
+
|
|
321
|
+
expect(extended.requestId).toBe("overridden");
|
|
322
|
+
expect(extended.currentUser).toBeNull();
|
|
323
|
+
expect(extended.db).toBe(mockDb); // unchanged
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("does not mutate original context", () => {
|
|
328
|
+
const original = { db: mockDb, currentUser: mockUser, requestId: "req-1" };
|
|
329
|
+
const extension = { extra: "value" };
|
|
330
|
+
|
|
331
|
+
const extended = extendContext(original, extension);
|
|
332
|
+
|
|
333
|
+
expect(original).not.toHaveProperty("extra");
|
|
334
|
+
expect(extended).toHaveProperty("extra");
|
|
335
|
+
expect(original.db).toBe(mockDb);
|
|
336
|
+
expect(extended.db).toBe(mockDb);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("works with empty extension object", () => {
|
|
340
|
+
const ctx = createContext<TestContext>();
|
|
341
|
+
|
|
342
|
+
runWithContext(ctx, { db: mockDb, currentUser: mockUser, requestId: "req-1" }, () => {
|
|
343
|
+
const current = useContext<TestContext>();
|
|
344
|
+
const extended = extendContext(current, {});
|
|
345
|
+
|
|
346
|
+
expect(extended.db).toBe(mockDb);
|
|
347
|
+
expect(extended.currentUser).toBe(mockUser);
|
|
348
|
+
expect(extended.requestId).toBe("req-1");
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// Test: Real-world Usage Pattern
|
|
355
|
+
// =============================================================================
|
|
356
|
+
|
|
357
|
+
describe("Real-world usage pattern", () => {
|
|
358
|
+
interface AppContext {
|
|
359
|
+
db: typeof mockDb;
|
|
360
|
+
currentUser: { id: string; name: string } | null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Define composables like in real app
|
|
364
|
+
const useDB = () => useContext<AppContext>().db;
|
|
365
|
+
const useCurrentUser = () => useContext<AppContext>().currentUser;
|
|
366
|
+
|
|
367
|
+
// Simulate a resolver function
|
|
368
|
+
async function getUserPosts() {
|
|
369
|
+
const db = useDB();
|
|
370
|
+
const user = useCurrentUser();
|
|
371
|
+
if (!user) throw new Error("Not authenticated");
|
|
372
|
+
return db.query(`SELECT * FROM posts WHERE authorId = '${user.id}'`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
it("works with resolver pattern", async () => {
|
|
376
|
+
const ctx = createContext<AppContext>();
|
|
377
|
+
|
|
378
|
+
const posts = await runWithContextAsync(ctx, { db: mockDb, currentUser: mockUser }, async () => {
|
|
379
|
+
return getUserPosts();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(posts).toEqual(["result for: SELECT * FROM posts WHERE authorId = 'user-1'"]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("throws when not authenticated", async () => {
|
|
386
|
+
const ctx = createContext<AppContext>();
|
|
387
|
+
|
|
388
|
+
await expect(
|
|
389
|
+
runWithContextAsync(ctx, { db: mockDb, currentUser: null }, async () => {
|
|
390
|
+
return getUserPosts();
|
|
391
|
+
}),
|
|
392
|
+
).rejects.toThrow("Not authenticated");
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// =============================================================================
|
|
397
|
+
// Test: Concurrent Requests
|
|
398
|
+
// =============================================================================
|
|
399
|
+
|
|
400
|
+
describe("Concurrent requests", () => {
|
|
401
|
+
it("maintains isolation between concurrent async operations", async () => {
|
|
402
|
+
const ctx = createContext<TestContext>();
|
|
403
|
+
const results: string[] = [];
|
|
404
|
+
|
|
405
|
+
const task1 = runWithContextAsync(ctx, { db: mockDb, currentUser: mockUser, requestId: "task-1" }, async () => {
|
|
406
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
407
|
+
results.push(`1: ${useContext<TestContext>().requestId}`);
|
|
408
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
409
|
+
results.push(`1: ${useContext<TestContext>().requestId}`);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const task2 = runWithContextAsync(ctx, { db: mockDb, currentUser: null, requestId: "task-2" }, async () => {
|
|
413
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
414
|
+
results.push(`2: ${useContext<TestContext>().requestId}`);
|
|
415
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
416
|
+
results.push(`2: ${useContext<TestContext>().requestId}`);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await Promise.all([task1, task2]);
|
|
420
|
+
|
|
421
|
+
// Each task should see its own context throughout
|
|
422
|
+
expect(results.filter((r) => r.startsWith("1:"))).toEqual(["1: task-1", "1: task-1"]);
|
|
423
|
+
expect(results.filter((r) => r.startsWith("2:"))).toEqual(["2: task-2", "2: task-2"]);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-server - Context System
|
|
3
|
+
*
|
|
4
|
+
* AsyncLocalStorage-based context for implicit dependency injection.
|
|
5
|
+
* Server-side implementation of the context pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
|
+
import type { ContextStore, ContextValue } from "@sylphx/lens-core";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Global Context Store
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/** Global context store - single AsyncLocalStorage instance */
|
|
16
|
+
const globalContextStore = new AsyncLocalStorage<ContextValue>();
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Context Functions
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a typed context reference.
|
|
24
|
+
* This doesn't create a new AsyncLocalStorage, but provides type information.
|
|
25
|
+
*/
|
|
26
|
+
export function createContext<T extends ContextValue>(): ContextStore<T> {
|
|
27
|
+
return globalContextStore as unknown as ContextStore<T>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the current context value.
|
|
32
|
+
* Throws if called outside of runWithContext.
|
|
33
|
+
*/
|
|
34
|
+
export function useContext<T extends ContextValue = ContextValue>(): T {
|
|
35
|
+
const ctx = globalContextStore.getStore();
|
|
36
|
+
if (!ctx) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"useContext() called outside of context. " +
|
|
39
|
+
"Make sure to wrap your code with runWithContext() or use explicit ctx parameter.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return ctx as T;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Try to get the current context value.
|
|
47
|
+
* Returns undefined if called outside of runWithContext.
|
|
48
|
+
*/
|
|
49
|
+
export function tryUseContext<T extends ContextValue = ContextValue>(): T | undefined {
|
|
50
|
+
return globalContextStore.getStore() as T | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run a function with the given context.
|
|
55
|
+
*/
|
|
56
|
+
export function runWithContext<T extends ContextValue, R>(
|
|
57
|
+
_context: ContextStore<T>,
|
|
58
|
+
value: T,
|
|
59
|
+
fn: () => R,
|
|
60
|
+
): R {
|
|
61
|
+
return globalContextStore.run(value, fn);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run an async function with the given context.
|
|
66
|
+
*/
|
|
67
|
+
export async function runWithContextAsync<T extends ContextValue, R>(
|
|
68
|
+
context: ContextStore<T>,
|
|
69
|
+
value: T,
|
|
70
|
+
fn: () => Promise<R>,
|
|
71
|
+
): Promise<R> {
|
|
72
|
+
return runWithContext(context, value, fn);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if currently running within a context.
|
|
77
|
+
*/
|
|
78
|
+
export function hasContext(): boolean {
|
|
79
|
+
return globalContextStore.getStore() !== undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extend the current context with additional values.
|
|
84
|
+
*/
|
|
85
|
+
export function extendContext<T extends ContextValue, E extends ContextValue>(
|
|
86
|
+
current: T,
|
|
87
|
+
extension: E,
|
|
88
|
+
): T & E {
|
|
89
|
+
return { ...current, ...extension };
|
|
90
|
+
}
|