@dxos/effect 0.8.3 → 0.8.4-main.16b68245aa

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 (91) 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 +536 -154
  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 +31 -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 +536 -154
  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 +31 -0
  14. package/dist/lib/node-esm/testing.mjs.map +7 -0
  15. package/dist/types/src/Performance.d.ts +25 -0
  16. package/dist/types/src/Performance.d.ts.map +1 -0
  17. package/dist/types/src/RuntimeProvider.d.ts +21 -0
  18. package/dist/types/src/RuntimeProvider.d.ts.map +1 -0
  19. package/dist/types/src/ast.d.ts +42 -18
  20. package/dist/types/src/ast.d.ts.map +1 -1
  21. package/dist/types/src/async-task-tagging.d.ts +6 -0
  22. package/dist/types/src/async-task-tagging.d.ts.map +1 -0
  23. package/dist/types/src/atom-kvs.d.ts +19 -0
  24. package/dist/types/src/atom-kvs.d.ts.map +1 -0
  25. package/dist/types/src/context.d.ts +5 -0
  26. package/dist/types/src/context.d.ts.map +1 -0
  27. package/dist/types/src/dynamic-runtime.d.ts +56 -0
  28. package/dist/types/src/dynamic-runtime.d.ts.map +1 -0
  29. package/dist/types/src/dynamic-runtime.test.d.ts +2 -0
  30. package/dist/types/src/dynamic-runtime.test.d.ts.map +1 -0
  31. package/dist/types/src/errors.d.ts +57 -0
  32. package/dist/types/src/errors.d.ts.map +1 -0
  33. package/dist/types/src/errors.test.d.ts +2 -0
  34. package/dist/types/src/errors.test.d.ts.map +1 -0
  35. package/dist/types/src/index.d.ts +10 -1
  36. package/dist/types/src/index.d.ts.map +1 -1
  37. package/dist/types/src/interrupt.test.d.ts +2 -0
  38. package/dist/types/src/interrupt.test.d.ts.map +1 -0
  39. package/dist/types/src/{jsonPath.d.ts → json-path.d.ts} +12 -4
  40. package/dist/types/src/json-path.d.ts.map +1 -0
  41. package/dist/types/src/json-path.test.d.ts +2 -0
  42. package/dist/types/src/json-path.test.d.ts.map +1 -0
  43. package/dist/types/src/layers.test.d.ts +2 -0
  44. package/dist/types/src/layers.test.d.ts.map +1 -0
  45. package/dist/types/src/otel.d.ts +17 -0
  46. package/dist/types/src/otel.d.ts.map +1 -0
  47. package/dist/types/src/otel.test.d.ts +2 -0
  48. package/dist/types/src/otel.test.d.ts.map +1 -0
  49. package/dist/types/src/resource.d.ts +8 -0
  50. package/dist/types/src/resource.d.ts.map +1 -0
  51. package/dist/types/src/resource.test.d.ts +2 -0
  52. package/dist/types/src/resource.test.d.ts.map +1 -0
  53. package/dist/types/src/testing.d.ts +58 -0
  54. package/dist/types/src/testing.d.ts.map +1 -0
  55. package/dist/types/src/types.d.ts +8 -0
  56. package/dist/types/src/types.d.ts.map +1 -0
  57. package/dist/types/src/url.d.ts +3 -1
  58. package/dist/types/src/url.d.ts.map +1 -1
  59. package/dist/types/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +33 -11
  61. package/src/Performance.ts +45 -0
  62. package/src/RuntimeProvider.ts +35 -0
  63. package/src/ast.test.ts +55 -10
  64. package/src/ast.ts +151 -84
  65. package/src/async-task-tagging.ts +51 -0
  66. package/src/atom-kvs.ts +35 -0
  67. package/src/context.ts +16 -0
  68. package/src/dynamic-runtime.test.ts +465 -0
  69. package/src/dynamic-runtime.ts +195 -0
  70. package/src/errors.test.ts +22 -0
  71. package/src/errors.ts +252 -0
  72. package/src/index.ts +10 -1
  73. package/src/interrupt.test.ts +35 -0
  74. package/src/{jsonPath.test.ts → json-path.test.ts} +47 -8
  75. package/src/{jsonPath.ts → json-path.ts} +29 -4
  76. package/src/layers.test.ts +112 -0
  77. package/src/otel.test.ts +126 -0
  78. package/src/otel.ts +45 -0
  79. package/src/resource.test.ts +32 -0
  80. package/src/resource.ts +30 -0
  81. package/src/sanity.test.ts +30 -15
  82. package/src/testing.ts +86 -0
  83. package/src/types.ts +11 -0
  84. package/src/url.test.ts +1 -1
  85. package/src/url.ts +5 -2
  86. package/dist/lib/node/index.cjs +0 -487
  87. package/dist/lib/node/index.cjs.map +0 -7
  88. package/dist/lib/node/meta.json +0 -1
  89. package/dist/types/src/jsonPath.d.ts.map +0 -1
  90. package/dist/types/src/jsonPath.test.d.ts +0 -2
  91. package/dist/types/src/jsonPath.test.d.ts.map +0 -1
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import * as BrowserKeyValueStore from '@effect/platform-browser/BrowserKeyValueStore';
7
+ import type * as Schema from 'effect/Schema';
8
+
9
+ // TODO(wittjosiah): This is currently provided for convenience but maybe should be removed.
10
+ const defaultRuntime = Atom.runtime(BrowserKeyValueStore.layerLocalStorage);
11
+
12
+ /**
13
+ * Creates a KVS-backed atom for structured settings using Atom.kvs.
14
+ * The entire object is stored as a single localStorage key with JSON serialization.
15
+ *
16
+ * @param options.key - The localStorage key to store the value under.
17
+ * @param options.schema - Effect Schema for the value type.
18
+ * @param options.defaultValue - Function returning the default value.
19
+ * @param options.runtime - Optional custom Atom runtime (defaults to localStorage).
20
+ * @returns A writable atom that persists to localStorage.
21
+ */
22
+ export const createKvsStore = <T extends Record<string, any>>(options: {
23
+ key: string;
24
+ schema: Schema.Schema<T>;
25
+ defaultValue: () => T;
26
+ runtime?: ReturnType<typeof Atom.runtime>;
27
+ }): Atom.Writable<T> => {
28
+ const runtime = options.runtime ?? defaultRuntime;
29
+ return Atom.kvs({
30
+ runtime,
31
+ key: options.key,
32
+ schema: options.schema,
33
+ defaultValue: options.defaultValue,
34
+ }).pipe(Atom.keepAlive);
35
+ };
package/src/context.ts ADDED
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import type * as Scope from 'effect/Scope';
7
+
8
+ import { Context } from '@dxos/context';
9
+
10
+ // TODO(dmaretskyi): Error handling.
11
+ export const contextFromScope = (): Effect.Effect<Context, never, Scope.Scope> =>
12
+ Effect.gen(function* () {
13
+ const ctx = new Context();
14
+ yield* Effect.addFinalizer(() => Effect.promise(() => ctx.dispose()));
15
+ return ctx;
16
+ });
@@ -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
+ });