@dxos/effect 0.8.4-main.67995b8 → 0.8.4-main.69d29f4

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 (75) hide show
  1. package/dist/lib/browser/chunk-CGS2ULMK.mjs +11 -0
  2. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +418 -230
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing.mjs +38 -0
  7. package/dist/lib/browser/testing.mjs.map +7 -0
  8. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  9. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  10. package/dist/lib/node-esm/index.mjs +418 -230
  11. package/dist/lib/node-esm/index.mjs.map +4 -4
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing.mjs +38 -0
  14. package/dist/lib/node-esm/testing.mjs.map +7 -0
  15. package/dist/types/src/RuntimeProvider.d.ts +21 -0
  16. package/dist/types/src/RuntimeProvider.d.ts.map +1 -0
  17. package/dist/types/src/ast.d.ts +36 -22
  18. package/dist/types/src/ast.d.ts.map +1 -1
  19. package/dist/types/src/atom-kvs.d.ts +19 -0
  20. package/dist/types/src/atom-kvs.d.ts.map +1 -0
  21. package/dist/types/src/context.d.ts +2 -1
  22. package/dist/types/src/context.d.ts.map +1 -1
  23. package/dist/types/src/dynamic-runtime.d.ts +56 -0
  24. package/dist/types/src/dynamic-runtime.d.ts.map +1 -0
  25. package/dist/types/src/dynamic-runtime.test.d.ts +2 -0
  26. package/dist/types/src/dynamic-runtime.test.d.ts.map +1 -0
  27. package/dist/types/src/errors.d.ts +35 -1
  28. package/dist/types/src/errors.d.ts.map +1 -1
  29. package/dist/types/src/index.d.ts +5 -3
  30. package/dist/types/src/index.d.ts.map +1 -1
  31. package/dist/types/src/interrupt.test.d.ts +2 -0
  32. package/dist/types/src/interrupt.test.d.ts.map +1 -0
  33. package/dist/types/src/{jsonPath.d.ts → json-path.d.ts} +12 -4
  34. package/dist/types/src/json-path.d.ts.map +1 -0
  35. package/dist/types/src/json-path.test.d.ts +2 -0
  36. package/dist/types/src/json-path.test.d.ts.map +1 -0
  37. package/dist/types/src/layers.test.d.ts +2 -0
  38. package/dist/types/src/layers.test.d.ts.map +1 -0
  39. package/dist/types/src/otel.d.ts +17 -0
  40. package/dist/types/src/otel.d.ts.map +1 -0
  41. package/dist/types/src/otel.test.d.ts +2 -0
  42. package/dist/types/src/otel.test.d.ts.map +1 -0
  43. package/dist/types/src/resource.d.ts +6 -2
  44. package/dist/types/src/resource.d.ts.map +1 -1
  45. package/dist/types/src/testing.d.ts +33 -1
  46. package/dist/types/src/testing.d.ts.map +1 -1
  47. package/dist/types/src/url.d.ts +3 -1
  48. package/dist/types/src/url.d.ts.map +1 -1
  49. package/dist/types/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +35 -11
  51. package/src/RuntimeProvider.ts +35 -0
  52. package/src/ast.test.ts +14 -11
  53. package/src/ast.ts +119 -92
  54. package/src/atom-kvs.ts +35 -0
  55. package/src/context.ts +2 -1
  56. package/src/dynamic-runtime.test.ts +465 -0
  57. package/src/dynamic-runtime.ts +195 -0
  58. package/src/errors.test.ts +1 -1
  59. package/src/errors.ts +90 -22
  60. package/src/index.ts +5 -3
  61. package/src/interrupt.test.ts +35 -0
  62. package/src/{jsonPath.test.ts → json-path.test.ts} +47 -8
  63. package/src/{jsonPath.ts → json-path.ts} +29 -4
  64. package/src/layers.test.ts +112 -0
  65. package/src/otel.test.ts +126 -0
  66. package/src/otel.ts +45 -0
  67. package/src/resource.test.ts +5 -4
  68. package/src/resource.ts +10 -5
  69. package/src/sanity.test.ts +30 -15
  70. package/src/testing.ts +53 -1
  71. package/src/url.test.ts +1 -1
  72. package/src/url.ts +5 -2
  73. package/dist/types/src/jsonPath.d.ts.map +0 -1
  74. package/dist/types/src/jsonPath.test.d.ts +0 -2
  75. package/dist/types/src/jsonPath.test.d.ts.map +0 -1
@@ -0,0 +1,465 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Context from 'effect/Context';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Exit from 'effect/Exit';
8
+ import * as Layer from 'effect/Layer';
9
+ import * as ManagedRuntime from 'effect/ManagedRuntime';
10
+ import * as Runtime from 'effect/Runtime';
11
+ import { describe, expect, test } from 'vitest';
12
+
13
+ import * as DynamicRuntime from './dynamic-runtime';
14
+ import { runAndForwardErrors } from './errors';
15
+
16
+ // Test service tags
17
+ class Database extends Context.Tag('Database')<Database, { query: (sql: string) => Effect.Effect<string[]> }>() {}
18
+
19
+ class Logger extends Context.Tag('Logger')<Logger, { log: (msg: string) => Effect.Effect<void> }>() {}
20
+
21
+ class Cache extends Context.Tag('Cache')<Cache, { get: (key: string) => Effect.Effect<string | undefined> }>() {}
22
+
23
+ describe('DynamicRuntime', () => {
24
+ describe('Success Cases', () => {
25
+ test('single tag validation success with runPromise', async () => {
26
+ const layer = Layer.succeed(Database, {
27
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
28
+ });
29
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
30
+
31
+ const program = Effect.gen(function* () {
32
+ const db = yield* Database;
33
+ return yield* db.query('SELECT * FROM users');
34
+ });
35
+
36
+ const result = await runtime.runPromise(program);
37
+ expect(result).toEqual(['result: SELECT * FROM users']);
38
+ });
39
+
40
+ test('single tag validation success with runSync', () => {
41
+ const layer = Layer.succeed(Database, {
42
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
43
+ });
44
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
45
+
46
+ const program = Effect.gen(function* () {
47
+ const db = yield* Database;
48
+ return yield* db.query('SELECT * FROM users');
49
+ });
50
+
51
+ const result = runtime.runSync(program);
52
+ expect(result).toEqual(['result: SELECT * FROM users']);
53
+ });
54
+
55
+ test('single tag validation success with runSyncExit', () => {
56
+ const layer = Layer.succeed(Database, {
57
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
58
+ });
59
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
60
+
61
+ const program = Effect.gen(function* () {
62
+ const db = yield* Database;
63
+ return yield* db.query('SELECT * FROM users');
64
+ });
65
+
66
+ const exit = runtime.runSyncExit(program);
67
+ expect(Exit.isSuccess(exit)).toBe(true);
68
+ if (Exit.isSuccess(exit)) {
69
+ expect(exit.value).toEqual(['result: SELECT * FROM users']);
70
+ }
71
+ });
72
+
73
+ test('single tag validation success with runPromiseExit', async () => {
74
+ const layer = Layer.succeed(Database, {
75
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
76
+ });
77
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
78
+
79
+ const program = Effect.gen(function* () {
80
+ const db = yield* Database;
81
+ return yield* db.query('SELECT * FROM users');
82
+ });
83
+
84
+ const exit = await runtime.runPromiseExit(program);
85
+ expect(Exit.isSuccess(exit)).toBe(true);
86
+ if (Exit.isSuccess(exit)) {
87
+ expect(exit.value).toEqual(['result: SELECT * FROM users']);
88
+ }
89
+ });
90
+
91
+ test('multiple tags validation success', async () => {
92
+ const layer = Layer.mergeAll(
93
+ Layer.succeed(Database, {
94
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
95
+ }),
96
+ Layer.succeed(Logger, {
97
+ log: (msg: string) => Effect.sync(() => console.log(msg)),
98
+ }),
99
+ );
100
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database, Logger]);
101
+
102
+ const program = Effect.gen(function* () {
103
+ const db = yield* Database;
104
+ const logger = yield* Logger;
105
+ yield* logger.log('Querying database');
106
+ return yield* db.query('SELECT * FROM users');
107
+ });
108
+
109
+ const result = await runtime.runPromise(program);
110
+ expect(result).toEqual(['result: SELECT * FROM users']);
111
+ });
112
+
113
+ test('effect requiring subset of tags executes successfully', async () => {
114
+ const layer = Layer.mergeAll(
115
+ Layer.succeed(Database, {
116
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
117
+ }),
118
+ Layer.succeed(Logger, {
119
+ log: (msg: string) => Effect.sync(() => console.log(msg)),
120
+ }),
121
+ );
122
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database, Logger]);
123
+
124
+ // Effect only requires Database, not Logger
125
+ const program = Effect.gen(function* () {
126
+ const db = yield* Database;
127
+ return yield* db.query('SELECT * FROM users');
128
+ });
129
+
130
+ const result = await runtime.runPromise(program);
131
+ expect(result).toEqual(['result: SELECT * FROM users']);
132
+ });
133
+
134
+ test('runFork with valid tags', async () => {
135
+ const layer = Layer.succeed(Database, {
136
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
137
+ });
138
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
139
+
140
+ const program = Effect.gen(function* () {
141
+ const db = yield* Database;
142
+ return yield* db.query('SELECT * FROM users');
143
+ });
144
+
145
+ const fiber = runtime.runFork(program);
146
+ const exit = await runAndForwardErrors(fiber.await);
147
+ expect(Exit.isSuccess(exit)).toBe(true);
148
+ if (Exit.isSuccess(exit)) {
149
+ expect(exit.value).toEqual(['result: SELECT * FROM users']);
150
+ }
151
+ });
152
+
153
+ test('runtimeEffect returns runtime with correct context', async () => {
154
+ const layer = Layer.succeed(Database, {
155
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
156
+ });
157
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
158
+
159
+ const rt = await runtime.runPromise(runtime.runtimeEffect);
160
+ expect(rt).toBeDefined();
161
+ // Verify we can use the runtime directly
162
+ const program = Effect.gen(function* () {
163
+ const db = yield* Database;
164
+ return yield* db.query('test');
165
+ });
166
+ const result = await Runtime.runPromise(rt)(program);
167
+ expect(result).toEqual(['result: test']);
168
+ });
169
+
170
+ test('complex effect composition with multiple tags', async () => {
171
+ const layer = Layer.mergeAll(
172
+ Layer.succeed(Database, {
173
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
174
+ }),
175
+ Layer.succeed(Logger, {
176
+ log: (msg: string) => Effect.sync(() => console.log(msg)),
177
+ }),
178
+ Layer.succeed(Cache, {
179
+ get: (key: string) => Effect.succeed(key === 'key1' ? 'cached' : undefined),
180
+ }),
181
+ );
182
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database, Logger, Cache]);
183
+
184
+ const program = Effect.gen(function* () {
185
+ const db = yield* Database;
186
+ const logger = yield* Logger;
187
+ const cache = yield* Cache;
188
+
189
+ yield* logger.log('Checking cache');
190
+ const cached = yield* cache.get('key1');
191
+ if (cached) {
192
+ yield* logger.log('Cache hit');
193
+ return [cached];
194
+ }
195
+ yield* logger.log('Cache miss, querying database');
196
+ return yield* db.query('SELECT * FROM users');
197
+ });
198
+
199
+ const result = await runtime.runPromise(program);
200
+ expect(result).toEqual(['cached']);
201
+ });
202
+
203
+ test('empty tag array edge case', async () => {
204
+ const layer = Layer.empty;
205
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), []);
206
+
207
+ const program = Effect.succeed('no dependencies');
208
+
209
+ const result = await runtime.runPromise(program);
210
+ expect(result).toBe('no dependencies');
211
+ });
212
+ });
213
+
214
+ describe('Failure Cases', () => {
215
+ test('missing single tag throws error with runPromise', async () => {
216
+ const layer = Layer.empty; // No tags provided
217
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
218
+
219
+ const program = Effect.gen(function* () {
220
+ const db = yield* Database;
221
+ return yield* db.query('SELECT * FROM users');
222
+ });
223
+
224
+ await expect(runtime.runPromise(program)).rejects.toThrow(/Missing required tags in runtime: Database/);
225
+ });
226
+
227
+ test('missing single tag throws error with runSync', () => {
228
+ const layer = Layer.empty; // No tags provided
229
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
230
+
231
+ const program = Effect.gen(function* () {
232
+ const db = yield* Database;
233
+ return yield* db.query('SELECT * FROM users');
234
+ });
235
+
236
+ expect(() => runtime.runSync(program)).toThrow(/Missing required tags in runtime: Database/);
237
+ });
238
+
239
+ test('missing single tag throws error with runSyncExit', () => {
240
+ const layer = Layer.empty; // No tags provided
241
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
242
+
243
+ const program = Effect.gen(function* () {
244
+ const db = yield* Database;
245
+ return yield* db.query('SELECT * FROM users');
246
+ });
247
+
248
+ const exit = runtime.runSyncExit(program);
249
+ expect(Exit.isFailure(exit)).toBe(true);
250
+ if (Exit.isFailure(exit)) {
251
+ // Exit.cause is available directly when Exit.isFailure is true
252
+ expect(exit.cause).toBeDefined();
253
+ }
254
+ });
255
+
256
+ test('missing single tag throws error with runPromiseExit', async () => {
257
+ const layer = Layer.empty; // No tags provided
258
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
259
+
260
+ const program = Effect.gen(function* () {
261
+ const db = yield* Database;
262
+ return yield* db.query('SELECT * FROM users');
263
+ });
264
+
265
+ const exit = await runtime.runPromiseExit(program);
266
+ expect(Exit.isFailure(exit)).toBe(true);
267
+ });
268
+
269
+ test('missing multiple tags lists all missing tags', async () => {
270
+ const layer = Layer.empty; // No tags provided
271
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database, Logger, Cache]);
272
+
273
+ const program = Effect.gen(function* () {
274
+ const db = yield* Database;
275
+ const logger = yield* Logger;
276
+ yield* logger.log('test');
277
+ return yield* db.query('SELECT * FROM users');
278
+ });
279
+
280
+ await expect(runtime.runPromise(program)).rejects.toThrow(/Missing required tags in runtime/);
281
+ try {
282
+ await runtime.runPromise(program);
283
+ } catch (error: any) {
284
+ expect(error.message).toContain('Database');
285
+ expect(error.message).toContain('Logger');
286
+ expect(error.message).toContain('Cache');
287
+ }
288
+ });
289
+
290
+ test('partial tag availability - only missing tags listed', async () => {
291
+ const layer = Layer.succeed(Database, {
292
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
293
+ });
294
+ // Only Database is provided, Logger and Cache are missing
295
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database, Logger, Cache]);
296
+
297
+ const program = Effect.gen(function* () {
298
+ const db = yield* Database;
299
+ const logger = yield* Logger;
300
+ yield* logger.log('test');
301
+ return yield* db.query('SELECT * FROM users');
302
+ });
303
+
304
+ await expect(runtime.runPromise(program)).rejects.toThrow(/Missing required tags in runtime/);
305
+ try {
306
+ await runtime.runPromise(program);
307
+ } catch (error: any) {
308
+ // Should only list missing tags, not Database
309
+ expect(error.message).toContain('Logger');
310
+ expect(error.message).toContain('Cache');
311
+ expect(error.message).not.toContain('Database');
312
+ }
313
+ });
314
+
315
+ test('runFork with missing tags throws error', async () => {
316
+ const layer = Layer.empty; // No tags provided
317
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
318
+
319
+ const program = Effect.gen(function* () {
320
+ const db = yield* Database;
321
+ return yield* db.query('SELECT * FROM users');
322
+ });
323
+
324
+ expect(() => runtime.runFork(program)).toThrow(/Missing required tags in runtime: Database/);
325
+ });
326
+
327
+ test('runtimeEffect with missing tags throws error', async () => {
328
+ const layer = Layer.empty; // No tags provided
329
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
330
+
331
+ await expect(runtime.runPromise(runtime.runtimeEffect)).rejects.toThrow(
332
+ /Missing required tags in runtime: Database/,
333
+ );
334
+ });
335
+ });
336
+
337
+ describe('Integration Tests', () => {
338
+ test('real-world layer composition with dependencies', async () => {
339
+ // Simulate a real-world scenario with layer dependencies
340
+ const DatabaseLayer = Layer.effect(
341
+ Database,
342
+ Effect.gen(function* () {
343
+ return {
344
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
345
+ };
346
+ }),
347
+ );
348
+
349
+ const LoggerLayer = Layer.effect(
350
+ Logger,
351
+ Effect.gen(function* () {
352
+ return {
353
+ log: (msg: string) => Effect.sync(() => console.log(msg)),
354
+ };
355
+ }),
356
+ );
357
+
358
+ const layer = Layer.mergeAll(DatabaseLayer, LoggerLayer);
359
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database, Logger]);
360
+
361
+ const program = Effect.gen(function* () {
362
+ const logger = yield* Logger;
363
+ yield* logger.log('Starting query');
364
+ const db = yield* Database;
365
+ const result = yield* db.query('SELECT * FROM users');
366
+ yield* logger.log('Query completed');
367
+ return result;
368
+ });
369
+
370
+ const result = await runtime.runPromise(program);
371
+ expect(result).toEqual(['result: SELECT * FROM users']);
372
+ });
373
+
374
+ test('runtime lifecycle - dispose properly cleans up', async () => {
375
+ const layer = Layer.succeed(Database, {
376
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
377
+ });
378
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
379
+
380
+ const program = Effect.gen(function* () {
381
+ const db = yield* Database;
382
+ return yield* db.query('test');
383
+ });
384
+
385
+ const result = await runtime.runPromise(program);
386
+ expect(result).toEqual(['result: test']);
387
+
388
+ // Dispose should not throw
389
+ await expect(runtime.dispose()).resolves.toBeUndefined();
390
+ });
391
+
392
+ test('validation happens lazily on first use', async () => {
393
+ const layer = Layer.empty; // No tags provided
394
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
395
+
396
+ // Creating the runtime should not throw
397
+ expect(runtime).toBeDefined();
398
+
399
+ // First use should trigger validation and throw
400
+ const program = Effect.gen(function* () {
401
+ const db = yield* Database;
402
+ return yield* db.query('test');
403
+ });
404
+
405
+ await expect(runtime.runPromise(program)).rejects.toThrow();
406
+ });
407
+
408
+ test('validation is cached for async operations', async () => {
409
+ const layer = Layer.empty; // No tags provided
410
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
411
+
412
+ const program = Effect.gen(function* () {
413
+ const db = yield* Database;
414
+ return yield* db.query('test');
415
+ });
416
+
417
+ // First call should trigger validation
418
+ const promise1 = runtime.runPromise(program);
419
+ // Second call should use cached validation
420
+ const promise2 = runtime.runPromise(program);
421
+
422
+ // Both should fail with the same error
423
+ await expect(promise1).rejects.toThrow(/Missing required tags in runtime: Database/);
424
+ await expect(promise2).rejects.toThrow(/Missing required tags in runtime: Database/);
425
+ });
426
+ });
427
+
428
+ describe('Type Safety', () => {
429
+ test('effects with correct context types are accepted', () => {
430
+ const layer = Layer.succeed(Database, {
431
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
432
+ });
433
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
434
+
435
+ // This should compile without errors
436
+ const program: Effect.Effect<string[], never, Database> = Effect.gen(function* () {
437
+ const db = yield* Database;
438
+ return yield* db.query('test');
439
+ });
440
+
441
+ // TypeScript should accept this
442
+ expect(() => runtime.runSync(program)).not.toThrow();
443
+ });
444
+
445
+ test('type safety prevents effects requiring wrong tags', () => {
446
+ const layer = Layer.succeed(Database, {
447
+ query: (sql: string) => Effect.succeed([`result: ${sql}`]),
448
+ });
449
+ const runtime = DynamicRuntime.make(ManagedRuntime.make(layer), [Database]);
450
+
451
+ // This effect requires Logger but runtime only provides Database
452
+ // TypeScript correctly prevents this at compile time - the type system enforces
453
+ // that effects must only require tags that are in the runtime's tag list.
454
+ // If you uncomment the lines below, TypeScript will error:
455
+ // const program = Effect.gen(function* () {
456
+ // const logger = yield* Logger; // Error: Logger is not assignable to Database
457
+ // yield* logger.log('test');
458
+ // });
459
+ // runtime.runSync(program); // Error: Effect<void, never, Logger> is not assignable to Effect<void, never, Database>
460
+
461
+ // The type system successfully prevents this, which is the desired behavior
462
+ expect(runtime).toBeDefined();
463
+ });
464
+ });
465
+ });
@@ -0,0 +1,195 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Context from 'effect/Context';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Exit from 'effect/Exit';
8
+ import type * as Fiber from 'effect/Fiber';
9
+ import type * as ManagedRuntime from 'effect/ManagedRuntime';
10
+ import * as Option from 'effect/Option';
11
+ import * as Runtime from 'effect/Runtime';
12
+
13
+ import { unwrapExit } from './errors';
14
+
15
+ /**
16
+ * Helper type to construct a union of tag identifiers from an array of tags.
17
+ */
18
+ export type TagsToContext<Tags extends ReadonlyArray<Context.Tag<any, any>>> = Tags extends readonly [
19
+ infer Head,
20
+ ...infer Tail,
21
+ ]
22
+ ? Head extends Context.Tag<infer Id, any>
23
+ ? Tail extends ReadonlyArray<Context.Tag<any, any>>
24
+ ? Id | TagsToContext<Tail>
25
+ : Id
26
+ : never
27
+ : never;
28
+
29
+ /**
30
+ * A runtime wrapper that validates required tags are available at runtime
31
+ * while providing type-level guarantees that effects require those tags.
32
+ */
33
+ export interface DynamicRuntime<Tags extends ReadonlyArray<Context.Tag<any, any>>> {
34
+ /**
35
+ * Run an effect as a promise that requires the specified tags.
36
+ */
37
+ readonly runPromise: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Promise<A>;
38
+
39
+ /**
40
+ * Run an effect synchronously that requires the specified tags.
41
+ */
42
+ readonly runSync: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => A;
43
+
44
+ /**
45
+ * Run an effect synchronously returning exit that requires the specified tags.
46
+ */
47
+ readonly runSyncExit: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Exit.Exit<A, E>;
48
+
49
+ /**
50
+ * Run an effect as a promise returning exit that requires the specified tags.
51
+ */
52
+ readonly runPromiseExit: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Promise<Exit.Exit<A, E>>;
53
+
54
+ /**
55
+ * Fork an effect that requires the specified tags.
56
+ */
57
+ readonly runFork: <A, E>(effect: Effect.Effect<A, E, TagsToContext<Tags>>) => Fiber.RuntimeFiber<A, E>;
58
+
59
+ /**
60
+ * Get the runtime as an effect that requires the specified tags.
61
+ */
62
+ readonly runtimeEffect: Effect.Effect<Runtime.Runtime<TagsToContext<Tags>>, never, never>;
63
+
64
+ /**
65
+ * Dispose the underlying managed runtime.
66
+ */
67
+ readonly dispose: () => Promise<void>;
68
+
69
+ /**
70
+ * Get the underlying managed runtime.
71
+ */
72
+ readonly managedRuntime: ManagedRuntime.ManagedRuntime<any, any>;
73
+ }
74
+
75
+ /**
76
+ * Validate that all required tags are present in the runtime context.
77
+ */
78
+ const validateTags = <Tags extends ReadonlyArray<Context.Tag<any, any>>>(
79
+ context: Context.Context<any>,
80
+ tags: Tags,
81
+ ): Effect.Effect<void> =>
82
+ Effect.gen(function* () {
83
+ const missingTags: string[] = [];
84
+ for (const tag of tags) {
85
+ const option = Context.getOption(context, tag);
86
+ if (Option.isNone(option)) {
87
+ missingTags.push(tag.key);
88
+ }
89
+ }
90
+
91
+ if (missingTags.length > 0) {
92
+ return yield* Effect.die(new Error(`Missing required tags in runtime: ${missingTags.join(', ')}`));
93
+ }
94
+ });
95
+
96
+ /**
97
+ * Create a dynamic runtime from a managed runtime and validate required tags.
98
+ */
99
+ export function make<const Tags extends ReadonlyArray<Context.Tag<any, any>>>(
100
+ managedRuntime: ManagedRuntime.ManagedRuntime<any, any> | ManagedRuntime.ManagedRuntime<never, never>,
101
+ tags: Tags,
102
+ ): DynamicRuntime<Tags> {
103
+ type RequiredContext = TagsToContext<Tags>;
104
+ const managedRuntimeAny = managedRuntime as ManagedRuntime.ManagedRuntime<any, any>;
105
+
106
+ // Cache for the validated runtime - once resolved, can be used synchronously.
107
+ let cachedRuntime: Runtime.Runtime<RequiredContext> | undefined;
108
+
109
+ // Cache validated runtime for async operations.
110
+ let validatedRuntimePromise: Promise<Runtime.Runtime<RequiredContext>> | undefined;
111
+
112
+ const getValidatedRuntimeAsync = async (): Promise<Runtime.Runtime<RequiredContext>> => {
113
+ if (!validatedRuntimePromise) {
114
+ validatedRuntimePromise = managedRuntimeAny.runPromise(
115
+ Effect.gen(function* () {
116
+ const rt = yield* managedRuntimeAny.runtimeEffect;
117
+ yield* validateTags(rt.context, tags);
118
+ return rt as Runtime.Runtime<RequiredContext>;
119
+ }),
120
+ );
121
+ }
122
+ return validatedRuntimePromise;
123
+ };
124
+
125
+ // Get validated runtime for sync operations.
126
+ const getValidatedRuntime = (): Runtime.Runtime<RequiredContext> => {
127
+ const validationExit = managedRuntimeAny.runSyncExit(
128
+ Effect.gen(function* () {
129
+ const rt = yield* managedRuntimeAny.runtimeEffect;
130
+ yield* validateTags(rt.context, tags);
131
+ return rt as Runtime.Runtime<RequiredContext>;
132
+ }),
133
+ );
134
+ return unwrapExit(validationExit);
135
+ };
136
+
137
+ return {
138
+ managedRuntime: managedRuntimeAny,
139
+ runPromise: async <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Promise<A> => {
140
+ const runtime = await getValidatedRuntimeAsync();
141
+ return Runtime.runPromise(runtime)(effect);
142
+ },
143
+ runSync: <A, E>(effect: Effect.Effect<A, E, RequiredContext>): A => {
144
+ const runtime = getValidatedRuntime();
145
+ return Runtime.runSync(runtime)(effect);
146
+ },
147
+ runSyncExit: <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Exit.Exit<A, E> => {
148
+ const validationExit = managedRuntimeAny.runSyncExit(
149
+ Effect.gen(function* () {
150
+ const rt = yield* managedRuntimeAny.runtimeEffect;
151
+ yield* validateTags(rt.context, tags);
152
+ return rt as Runtime.Runtime<RequiredContext>;
153
+ }),
154
+ );
155
+ if (Exit.isSuccess(validationExit)) {
156
+ const runtime = validationExit.value;
157
+ return Runtime.runSyncExit(runtime)(effect);
158
+ }
159
+ return validationExit as Exit.Exit<A, E>;
160
+ },
161
+ runPromiseExit: async <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Promise<Exit.Exit<A, E>> => {
162
+ try {
163
+ const runtime = await getValidatedRuntimeAsync();
164
+ return Runtime.runPromiseExit(runtime)(effect);
165
+ } catch (error) {
166
+ // If validation failed, return a failure exit
167
+ return Exit.die(error);
168
+ }
169
+ },
170
+ runFork: <A, E>(effect: Effect.Effect<A, E, RequiredContext>): Fiber.RuntimeFiber<A, E> => {
171
+ const runtime = getValidatedRuntime();
172
+ return Runtime.runFork(runtime)(effect);
173
+ },
174
+ runtimeEffect: Effect.gen(function* () {
175
+ // Return cached runtime if available.
176
+ if (cachedRuntime) {
177
+ return cachedRuntime;
178
+ }
179
+ const rt = yield* managedRuntimeAny.runtimeEffect;
180
+ yield* validateTags(rt.context, tags);
181
+ const runtime = rt as Runtime.Runtime<RequiredContext>;
182
+ // Cache for future sync calls.
183
+ cachedRuntime = runtime;
184
+ return runtime;
185
+ }).pipe(
186
+ Effect.catchAll(() =>
187
+ // This should never happen since validateTags uses Effect.die
188
+ Effect.die(new Error('Unexpected error in runtimeEffect validation')),
189
+ ),
190
+ ),
191
+ dispose: async (): Promise<void> => {
192
+ await managedRuntimeAny.dispose();
193
+ },
194
+ };
195
+ }
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Data } from 'effect';
5
+ import * as Data from 'effect/Data';
6
6
  import { test } from 'vitest';
7
7
 
8
8
  class MyError extends Data.TaggedError('MyError')<{