better-effect 0.4.0 → 0.5.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/README.md +32 -5
- package/dist/adapters/iti.d.mts +11 -3
- package/dist/adapters/iti.d.mts.map +1 -1
- package/dist/adapters/iti.mjs +34 -14
- package/dist/adapters/iti.mjs.map +1 -1
- package/dist/errors-GR3K_nRu.mjs +74 -0
- package/dist/errors-GR3K_nRu.mjs.map +1 -0
- package/dist/index-D77AvuBl.d.mts +510 -0
- package/dist/index-D77AvuBl.d.mts.map +1 -0
- package/dist/index.d.mts +217 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +429 -153
- package/dist/index.mjs.map +1 -1
- package/dist/internal-identity-BnZC3Au-.mjs +26 -0
- package/dist/internal-identity-BnZC3Au-.mjs.map +1 -0
- package/dist/testing.d.mts +11 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +33 -12
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/errors-DlHCwICc.mjs +0 -67
- package/dist/errors-DlHCwICc.mjs.map +0 -1
- package/dist/index-BYQKfyeJ.d.mts +0 -235
- package/dist/index-BYQKfyeJ.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,28 +1,98 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as ServiceTagCollisionError, i as LayerRegistrationError, n as LayerDisposeError, o as ServiceNotFoundError, r as LayerGeneratorYieldError, s as ServiceRuntimeNotConfiguredError, t as DuplicateServiceError } from "./errors-GR3K_nRu.mjs";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { Result, TaggedError } from "better-result";
|
|
4
4
|
//#region src/service/runtime.ts
|
|
5
5
|
const storage$1 = new AsyncLocalStorage();
|
|
6
|
+
/** Provides the resolver context used by Service tokens during execution. */
|
|
6
7
|
var ServiceRuntime = class ServiceRuntime {
|
|
8
|
+
/**
|
|
9
|
+
* Run a callback with a resolver available to `yield* Service` expressions.
|
|
10
|
+
*
|
|
11
|
+
* The context is scoped to the callback and is restored afterward.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const value = ServiceRuntime.run(resolver, () => {
|
|
16
|
+
* return ServiceRuntime.resolve(Database)
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
7
20
|
static run(resolver, program) {
|
|
8
21
|
return storage$1.run(resolver, program);
|
|
9
22
|
}
|
|
23
|
+
/** Return the resolver active in the current execution context. */
|
|
10
24
|
static current() {
|
|
11
25
|
const resolver = storage$1.getStore();
|
|
12
26
|
if (!resolver) throw new ServiceRuntimeNotConfiguredError();
|
|
13
27
|
return resolver;
|
|
14
28
|
}
|
|
29
|
+
/** Resolve a Service token using the active resolver. */
|
|
15
30
|
static async resolve(token) {
|
|
16
31
|
return await ServiceRuntime.current().resolve(token);
|
|
17
32
|
}
|
|
18
33
|
};
|
|
19
34
|
//#endregion
|
|
20
35
|
//#region src/service/service.ts
|
|
36
|
+
/**
|
|
37
|
+
* Declare a class-backed Service with a stable string-literal identity.
|
|
38
|
+
*
|
|
39
|
+
* The returned class is simultaneously the implementation type, the runtime
|
|
40
|
+
* dependency token, and the value yielded by `yield*` in an Effect generator.
|
|
41
|
+
* The explicit self type preserves exact instance inference, while the second
|
|
42
|
+
* call captures the tag as a literal for Layer composition and diagnostics.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* class Database extends Service<Database>()('Database') {
|
|
47
|
+
* query(): string {
|
|
48
|
+
* return 'ok'
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* const database = yield* Database
|
|
53
|
+
* database.query()
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @typeParam Self The instance type implemented by the declared Service.
|
|
57
|
+
*/
|
|
21
58
|
function Service() {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
59
|
+
return function(tag) {
|
|
60
|
+
if (tag.length === 0) throw new TypeError("Service tags must not be empty");
|
|
61
|
+
class BaseService {
|
|
62
|
+
/** The stable logical identity used by Layers and resolver backends. */
|
|
63
|
+
static serviceTag = tag;
|
|
64
|
+
/**
|
|
65
|
+
* Type-check a structural implementation of this Service.
|
|
66
|
+
*
|
|
67
|
+
* This is an identity helper. It returns the supplied value unchanged
|
|
68
|
+
* and does not invoke a constructor or modify its prototype.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* class Database extends Service<Database>()('Database') {
|
|
73
|
+
* query(sql: string): string {
|
|
74
|
+
* return sql
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* const database = Database.of({
|
|
79
|
+
* query: (sql) => `Result: ${sql}`
|
|
80
|
+
* })
|
|
81
|
+
*
|
|
82
|
+
* database.query('SELECT 1')
|
|
83
|
+
* // 'Result: SELECT 1'
|
|
84
|
+
* // database is the original object, not an instance of Database
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
static of(implementation) {
|
|
88
|
+
return implementation;
|
|
89
|
+
}
|
|
90
|
+
/** Resolve this Service from the resolver active in the current runtime. */
|
|
91
|
+
static async *[Symbol.asyncIterator]() {
|
|
92
|
+
return await ServiceRuntime.resolve(this);
|
|
93
|
+
}
|
|
25
94
|
}
|
|
95
|
+
return BaseService;
|
|
26
96
|
};
|
|
27
97
|
}
|
|
28
98
|
//#endregion
|
|
@@ -39,25 +109,67 @@ const runLayerGenerator = async (service, factory) => {
|
|
|
39
109
|
};
|
|
40
110
|
//#endregion
|
|
41
111
|
//#region src/layer/layer.ts
|
|
112
|
+
/**
|
|
113
|
+
* Declarative collection of Service providers.
|
|
114
|
+
*
|
|
115
|
+
* A Layer describes how to acquire implementations; it does not execute
|
|
116
|
+
* providers until a `Runtime` is created. Use `merge` to compose distinct
|
|
117
|
+
* providers and `override` when replacing an existing provider intentionally.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* const AppLive = Layer.merge(
|
|
122
|
+
* Layer.succeed(Database, database),
|
|
123
|
+
* Layer.make(UserRepository)
|
|
124
|
+
* )
|
|
125
|
+
*
|
|
126
|
+
* const runtime = await Runtime.make(AppLive, backend)
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
42
129
|
var Layer = class Layer {
|
|
130
|
+
/** The provider registrations retained by this Layer. */
|
|
43
131
|
providers;
|
|
44
132
|
constructor(providers) {
|
|
45
133
|
this.providers = Object.freeze([...providers]);
|
|
46
134
|
}
|
|
47
135
|
static make(service, acquire) {
|
|
136
|
+
const defaultAcquire = () => {
|
|
137
|
+
return new service();
|
|
138
|
+
};
|
|
48
139
|
return new Layer([{
|
|
49
140
|
service,
|
|
50
|
-
acquire
|
|
141
|
+
acquire: acquire ?? defaultAcquire
|
|
51
142
|
}]);
|
|
52
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Create a Layer from an already-constructed Service instance.
|
|
146
|
+
*
|
|
147
|
+
* The instance is returned as-is whenever the Service is resolved.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const DatabaseLive = Layer.succeed(Database, database)
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
53
154
|
static succeed(service, instance) {
|
|
54
155
|
return Layer.make(service, () => instance);
|
|
55
156
|
}
|
|
56
157
|
/**
|
|
57
|
-
* Define a
|
|
158
|
+
* Define a provider with Runtime-root cleanup.
|
|
58
159
|
*
|
|
59
160
|
* The release callback intentionally keeps its compatibility-friendly
|
|
60
|
-
* one-argument shape
|
|
161
|
+
* one-argument shape and runs when the owning Runtime is disposed. Use
|
|
162
|
+
* `scopedGen` when acquisition needs contextual Services or cleanup needs
|
|
163
|
+
* `ScopeOutcome`.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* const DatabaseLive = Layer.scoped(
|
|
168
|
+
* Database,
|
|
169
|
+
* () => openDatabase(),
|
|
170
|
+
* (database) => database.close()
|
|
171
|
+
* )
|
|
172
|
+
* ```
|
|
61
173
|
*/
|
|
62
174
|
static scoped(service, acquire, release) {
|
|
63
175
|
return new Layer([{
|
|
@@ -66,7 +178,24 @@ var Layer = class Layer {
|
|
|
66
178
|
release: (instance) => release(instance)
|
|
67
179
|
}]);
|
|
68
180
|
}
|
|
69
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Define a provider whose acquisition can yield contextual Services.
|
|
183
|
+
*
|
|
184
|
+
* The release callback receives the acquired instance and the final
|
|
185
|
+
* `ScopeOutcome` selected by the owning Runtime.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* const RepositoryLive = Layer.scopedGen(
|
|
190
|
+
* UserRepository,
|
|
191
|
+
* async function* () {
|
|
192
|
+
* const database = yield* Database
|
|
193
|
+
* return new UserRepository(database)
|
|
194
|
+
* },
|
|
195
|
+
* (repository, outcome) => repository.close(outcome)
|
|
196
|
+
* )
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
70
199
|
static scopedGen(service, factory, release) {
|
|
71
200
|
return new Layer([{
|
|
72
201
|
service,
|
|
@@ -74,39 +203,84 @@ var Layer = class Layer {
|
|
|
74
203
|
release: (instance, outcome) => release(instance, outcome)
|
|
75
204
|
}]);
|
|
76
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Define a provider whose acquisition can yield contextual Services.
|
|
208
|
+
*
|
|
209
|
+
* Unlike `scopedGen`, this variant has no release callback. Use it for
|
|
210
|
+
* providers whose lifetime is managed elsewhere or that need no cleanup.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* const RepositoryLive = Layer.gen(UserRepository, async function* () {
|
|
215
|
+
* const database = yield* Database
|
|
216
|
+
* return new UserRepository(database)
|
|
217
|
+
* })
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
77
220
|
static gen(service, factory) {
|
|
78
221
|
return Layer.make(service, () => runLayerGenerator(service, factory));
|
|
79
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Compose Layers without replacing providers.
|
|
225
|
+
*
|
|
226
|
+
* Each Service tag may appear only once. Duplicate tags are rejected at
|
|
227
|
+
* runtime; use `override` when replacement is intentional.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* const AppLive = Layer.merge(DatabaseLive, RepositoryLive)
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
80
234
|
static merge(...layers) {
|
|
81
235
|
const providers = /* @__PURE__ */ new Map();
|
|
82
236
|
for (const layer of layers) for (const provider of layer.providers) {
|
|
83
237
|
const service = provider.service;
|
|
84
|
-
|
|
85
|
-
|
|
238
|
+
const existing = providers.get(service.serviceTag);
|
|
239
|
+
if (existing) {
|
|
240
|
+
if (existing.service !== service) throw new ServiceTagCollisionError(existing.service, service);
|
|
241
|
+
throw new DuplicateServiceError(service);
|
|
242
|
+
}
|
|
243
|
+
providers.set(service.serviceTag, provider);
|
|
86
244
|
}
|
|
87
245
|
return new Layer([...providers.values()]);
|
|
88
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Replace providers in a base Layer, using tag identity and compatible
|
|
249
|
+
* instance contracts.
|
|
250
|
+
*
|
|
251
|
+
* Overrides are applied from left to right; the last compatible provider for
|
|
252
|
+
* a tag wins. Incompatible same-tag replacements remain visible as a type
|
|
253
|
+
* diagnostic and cannot be passed as a complete Layer.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* const TestLive = Layer.override(AppLive, Layer.succeed(Database, fakeDb))
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
89
260
|
static override(base, ...overrides) {
|
|
90
261
|
const providers = /* @__PURE__ */ new Map();
|
|
91
|
-
for (const provider of base.providers) providers.set(provider.service, provider);
|
|
92
|
-
for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service, provider);
|
|
262
|
+
for (const provider of base.providers) providers.set(provider.service.serviceTag, provider);
|
|
263
|
+
for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service.serviceTag, provider);
|
|
93
264
|
return new Layer([...providers.values()]);
|
|
94
265
|
}
|
|
95
266
|
};
|
|
96
267
|
//#endregion
|
|
97
268
|
//#region src/scope/errors.ts
|
|
269
|
+
/** Thrown when Scope context is accessed outside an active Scope execution. */
|
|
98
270
|
var ScopeRuntimeNotConfiguredError = class extends Error {
|
|
99
271
|
constructor() {
|
|
100
272
|
super("No Scope is available in the current execution context");
|
|
101
273
|
this.name = "ScopeRuntimeNotConfiguredError";
|
|
102
274
|
}
|
|
103
275
|
};
|
|
276
|
+
/** Thrown when a resource or finalizer is added after Scope closure begins. */
|
|
104
277
|
var ScopeClosedError = class extends Error {
|
|
105
278
|
constructor() {
|
|
106
279
|
super("Cannot add resources or finalizers to a closed Scope");
|
|
107
280
|
this.name = "ScopeClosedError";
|
|
108
281
|
}
|
|
109
282
|
};
|
|
283
|
+
/** Aggregates finalizer failures encountered while closing a Scope. */
|
|
110
284
|
var ScopeCloseError = class extends Error {
|
|
111
285
|
causes;
|
|
112
286
|
constructor(causes) {
|
|
@@ -115,6 +289,7 @@ var ScopeCloseError = class extends Error {
|
|
|
115
289
|
this.name = "ScopeCloseError";
|
|
116
290
|
}
|
|
117
291
|
};
|
|
292
|
+
/** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
|
|
118
293
|
var ResourceNotDisposableError = class extends Error {
|
|
119
294
|
constructor() {
|
|
120
295
|
super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
|
|
@@ -124,6 +299,7 @@ var ResourceNotDisposableError = class extends Error {
|
|
|
124
299
|
//#endregion
|
|
125
300
|
//#region src/scope/disposable.ts
|
|
126
301
|
const SCOPE_SUCCESS$2 = { status: "success" };
|
|
302
|
+
/** Return a Scope finalizer for a value's async or sync disposal protocol. */
|
|
127
303
|
const getDisposeFinalizer = (resource) => {
|
|
128
304
|
const candidate = Object(resource);
|
|
129
305
|
const asyncDispose = candidate[Symbol.asyncDispose];
|
|
@@ -131,16 +307,20 @@ const getDisposeFinalizer = (resource) => {
|
|
|
131
307
|
const dispose = candidate[Symbol.dispose];
|
|
132
308
|
if (typeof dispose === "function") return () => dispose.call(resource);
|
|
133
309
|
};
|
|
310
|
+
/** Dispose a value immediately when it implements a disposal protocol. */
|
|
134
311
|
const disposeResource = (resource) => {
|
|
135
312
|
return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
|
|
136
313
|
};
|
|
137
314
|
//#endregion
|
|
138
315
|
//#region src/scope/runtime.ts
|
|
139
316
|
const storage = new AsyncLocalStorage();
|
|
317
|
+
/** Bridges the current Scope through async execution context. */
|
|
140
318
|
var ScopeRuntime = class {
|
|
319
|
+
/** Supply a Scope while invoking a callback. */
|
|
141
320
|
static run(scope, program) {
|
|
142
321
|
return storage.run(scope, program);
|
|
143
322
|
}
|
|
323
|
+
/** Return the Scope active in the current execution context. */
|
|
144
324
|
static current() {
|
|
145
325
|
const scope = storage.getStore();
|
|
146
326
|
if (!scope) throw new ScopeRuntimeNotConfiguredError();
|
|
@@ -286,15 +466,19 @@ var ScopeImpl = class ScopeImpl {
|
|
|
286
466
|
}
|
|
287
467
|
};
|
|
288
468
|
const Scope = {
|
|
469
|
+
/** Create an owned, initially open Scope. */
|
|
289
470
|
make() {
|
|
290
471
|
return new ScopeImpl();
|
|
291
472
|
},
|
|
473
|
+
/** Return the non-owning Scope available in the current execution context. */
|
|
292
474
|
current() {
|
|
293
475
|
return ScopeRuntime.current();
|
|
294
476
|
},
|
|
477
|
+
/** Run a callback with an existing Scope supplied as the current context. */
|
|
295
478
|
provide(scope, program) {
|
|
296
479
|
return ScopeRuntime.run(scope, program);
|
|
297
480
|
},
|
|
481
|
+
/** Resolve the current Scope through `yield* Scope` inside an Effect. */
|
|
298
482
|
*[Symbol.iterator]() {
|
|
299
483
|
return ScopeRuntime.current();
|
|
300
484
|
},
|
|
@@ -304,6 +488,14 @@ const Scope = {
|
|
|
304
488
|
* Scope is independent from `better-result`, so returned values—including
|
|
305
489
|
* `Result.err`—close this Scope with a successful outcome. Result-aware
|
|
306
490
|
* outcome classification belongs to `Runtime.run`.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* await Scope.run(async (scope) => {
|
|
495
|
+
* const connection = await scope.acquire(connect, (connection) => connection.close())
|
|
496
|
+
* return connection.query()
|
|
497
|
+
* })
|
|
498
|
+
* ```
|
|
307
499
|
*/
|
|
308
500
|
run(program) {
|
|
309
501
|
const scope = new ScopeImpl();
|
|
@@ -311,6 +503,182 @@ const Scope = {
|
|
|
311
503
|
}
|
|
312
504
|
};
|
|
313
505
|
//#endregion
|
|
506
|
+
//#region src/effect/combinators.ts
|
|
507
|
+
const isPromiseLike = (value) => {
|
|
508
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) return false;
|
|
509
|
+
return "then" in value && typeof value.then === "function";
|
|
510
|
+
};
|
|
511
|
+
const mapResult = (result, fn) => Result.map(result, fn);
|
|
512
|
+
const mapErrorResult = (result, fn) => Result.mapError(result, fn);
|
|
513
|
+
const andThenResult = (result, next) => Result.andThen(result, next);
|
|
514
|
+
const andThenAsyncResult = (result, next) => Result.andThenAsync(result, (value) => Promise.resolve(next(value)));
|
|
515
|
+
function map(first, second) {
|
|
516
|
+
if (typeof first === "function" && second === void 0) return (effect) => map(effect, first);
|
|
517
|
+
const fn = second;
|
|
518
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapResult(result, fn));
|
|
519
|
+
return mapResult(first, fn);
|
|
520
|
+
}
|
|
521
|
+
function mapError(first, second) {
|
|
522
|
+
if (typeof first === "function" && second === void 0) return (effect) => mapError(effect, first);
|
|
523
|
+
const fn = second;
|
|
524
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapErrorResult(result, fn));
|
|
525
|
+
return mapErrorResult(first, fn);
|
|
526
|
+
}
|
|
527
|
+
function andThen(first, second) {
|
|
528
|
+
if (typeof first === "function" && second === void 0) return (effect) => andThen(effect, first);
|
|
529
|
+
return andThenResult(first, second);
|
|
530
|
+
}
|
|
531
|
+
function andThenAsync(first, second) {
|
|
532
|
+
if (typeof first === "function" && second === void 0) return (effect) => andThenAsync(effect, first);
|
|
533
|
+
const next = second;
|
|
534
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => andThenAsyncResult(result, next));
|
|
535
|
+
return andThenAsyncResult(first, next);
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/effect/effect.ts
|
|
539
|
+
function gen(body) {
|
|
540
|
+
return Result.gen(body);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Acquire a resource in the current Scope and register its release callback.
|
|
544
|
+
*
|
|
545
|
+
* Acquisition failures are represented in the Effect Result error channel;
|
|
546
|
+
* release failures remain owned by Scope cleanup. The release callback
|
|
547
|
+
* receives the final outcome chosen by the enclosing execution boundary.
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```ts
|
|
551
|
+
* const connection = yield* Effect.acquireRelease(
|
|
552
|
+
* () => pool.connect(),
|
|
553
|
+
* (connection, outcome) => connection.close(outcome)
|
|
554
|
+
* )
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
function acquireRelease(acquire, release) {
|
|
558
|
+
const scope = Scope.current();
|
|
559
|
+
return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Register an already-acquired disposable resource in the current Scope.
|
|
563
|
+
*
|
|
564
|
+
* The resource is not acquired by this helper. Registration failures are
|
|
565
|
+
* represented in the Effect Result error channel; disposal failures remain
|
|
566
|
+
* owned by Scope cleanup.
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* const file = yield* Effect.add(await openFile('notes.txt'))
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
function add(resource) {
|
|
574
|
+
const scope = Scope.current();
|
|
575
|
+
return Result.await(Result.tryPromise(() => scope.add(resource)));
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Effect namespace containing generator, resource, and Result combinators.
|
|
579
|
+
*
|
|
580
|
+
* Prefer these helpers when a program needs typed Service requirements or
|
|
581
|
+
* Scope-aware acquisition and cleanup.
|
|
582
|
+
*/
|
|
583
|
+
const Effect = {
|
|
584
|
+
/** Compose a generator-based Effect program. */
|
|
585
|
+
gen,
|
|
586
|
+
/** Acquire and register a resource in the current Scope. */
|
|
587
|
+
acquireRelease,
|
|
588
|
+
/** Register an already-acquired disposable in the current Scope. */
|
|
589
|
+
add,
|
|
590
|
+
/** Map a successful Effect result. */
|
|
591
|
+
map,
|
|
592
|
+
/** Map an Effect error. */
|
|
593
|
+
mapError,
|
|
594
|
+
/** Chain a synchronous Effect result. */
|
|
595
|
+
andThen,
|
|
596
|
+
/** Chain an asynchronous Effect result. */
|
|
597
|
+
andThenAsync
|
|
598
|
+
};
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/function/pipe.ts
|
|
601
|
+
function pipe(value, ...operations) {
|
|
602
|
+
return operations.reduce((current, operation) => operation(current), value);
|
|
603
|
+
}
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/resource/errors.ts
|
|
606
|
+
/** Describes a failure encountered while releasing a Resource. */
|
|
607
|
+
var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/resource/internal.ts
|
|
610
|
+
const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
|
|
611
|
+
resource,
|
|
612
|
+
cause,
|
|
613
|
+
message: `Failed to release resource: ${resource}`
|
|
614
|
+
});
|
|
615
|
+
const runResult = async (operation) => {
|
|
616
|
+
return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
|
|
617
|
+
};
|
|
618
|
+
const normalizeReleaseOutcome = (name, outcome) => {
|
|
619
|
+
if (outcome === void 0) return Result.ok();
|
|
620
|
+
return outcome.mapError((cause) => toReleaseFailure(name, cause));
|
|
621
|
+
};
|
|
622
|
+
const runRelease = async (name, resource, release) => {
|
|
623
|
+
return (await Result.tryPromise({
|
|
624
|
+
try: () => Promise.resolve(release(resource)),
|
|
625
|
+
catch: (cause) => toReleaseFailure(name, cause)
|
|
626
|
+
})).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
|
|
627
|
+
};
|
|
628
|
+
const notifyReleaseFailure = async (observer, failure) => {
|
|
629
|
+
if (!observer) return;
|
|
630
|
+
try {
|
|
631
|
+
await observer(failure);
|
|
632
|
+
} catch {}
|
|
633
|
+
};
|
|
634
|
+
const combineUseAndRelease = async (used, released, onReleaseFailure) => {
|
|
635
|
+
if (Result.isError(used)) {
|
|
636
|
+
if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
637
|
+
return Result.err(used.error);
|
|
638
|
+
}
|
|
639
|
+
if (Result.isError(released)) {
|
|
640
|
+
await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
641
|
+
return Result.err(released.error);
|
|
642
|
+
}
|
|
643
|
+
return Result.ok(used.value);
|
|
644
|
+
};
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/resource/resource.ts
|
|
647
|
+
/**
|
|
648
|
+
* Acquire a resource, use it, and always attempt release afterward.
|
|
649
|
+
*
|
|
650
|
+
* Acquisition, use, and release may be synchronous or asynchronous Result
|
|
651
|
+
* operations. If both use and release fail, the use error remains primary and
|
|
652
|
+
* `onReleaseFailure` receives the cleanup failure as a diagnostic.
|
|
653
|
+
*
|
|
654
|
+
* When `release` is omitted, `Symbol.asyncDispose` is preferred over
|
|
655
|
+
* `Symbol.dispose`.
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* ```ts
|
|
659
|
+
* const result = await Resource.acquireUseRelease({
|
|
660
|
+
* name: 'database connection',
|
|
661
|
+
* acquire: () => connect(),
|
|
662
|
+
* use: (connection) => query(connection),
|
|
663
|
+
* release: (connection) => connection.close()
|
|
664
|
+
* })
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
|
|
668
|
+
const resource = yield* Result.await(runResult(acquire));
|
|
669
|
+
const scope = Scope.make();
|
|
670
|
+
let released = Result.ok();
|
|
671
|
+
scope.addFinalizer(async () => {
|
|
672
|
+
released = await runRelease(name, resource, release);
|
|
673
|
+
});
|
|
674
|
+
const used = await runResult(() => use(resource));
|
|
675
|
+
await scope.close();
|
|
676
|
+
return await combineUseAndRelease(used, released, onReleaseFailure);
|
|
677
|
+
});
|
|
678
|
+
const Resource = {
|
|
679
|
+
/** Acquire, use, and release a resource with deterministic error precedence. */
|
|
680
|
+
acquireUseRelease };
|
|
681
|
+
//#endregion
|
|
314
682
|
//#region src/runtime/outcome.ts
|
|
315
683
|
const isResultLike = (value) => typeof value === "object" && value !== null && "status" in value && (value.status === "ok" || value.status === "error");
|
|
316
684
|
const classifyRuntimeOutcome = (value) => {
|
|
@@ -323,6 +691,12 @@ const classifyRuntimeOutcome = (value) => {
|
|
|
323
691
|
//#endregion
|
|
324
692
|
//#region src/layer/runtime.ts
|
|
325
693
|
const SCOPE_SUCCESS = Object.freeze({ status: "success" });
|
|
694
|
+
var RuntimeHandleDisposedError = class extends Error {
|
|
695
|
+
constructor() {
|
|
696
|
+
super("Cannot run a program using a disposed Layer");
|
|
697
|
+
this.name = "RuntimeHandleDisposedError";
|
|
698
|
+
}
|
|
699
|
+
};
|
|
326
700
|
const normalizeDisposeCauses = (cause) => {
|
|
327
701
|
if (cause instanceof AggregateError) return [...cause.errors];
|
|
328
702
|
return [cause];
|
|
@@ -340,7 +714,7 @@ const bindProviderToScope = (provider, rootScope) => ({
|
|
|
340
714
|
return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
|
|
341
715
|
})
|
|
342
716
|
});
|
|
343
|
-
var
|
|
717
|
+
var RuntimeHandleImpl = class {
|
|
344
718
|
backend;
|
|
345
719
|
rootScope;
|
|
346
720
|
onCleanupFailure;
|
|
@@ -416,15 +790,11 @@ var BuiltLayerImpl = class {
|
|
|
416
790
|
}
|
|
417
791
|
}
|
|
418
792
|
assertActive() {
|
|
419
|
-
if (this.state !== "active") throw new
|
|
793
|
+
if (this.state !== "active") throw new RuntimeHandleDisposedError();
|
|
420
794
|
}
|
|
421
795
|
};
|
|
422
|
-
/**
|
|
423
|
-
|
|
424
|
-
*
|
|
425
|
-
* @deprecated Prefer `Runtime.make()` for application code.
|
|
426
|
-
*/
|
|
427
|
-
const buildLayer = async (layer, backend, options = {}) => {
|
|
796
|
+
/** Build a Runtime handle for a complete Layer and register its providers. */
|
|
797
|
+
const createRuntimeHandle = async (layer, backend, options = {}) => {
|
|
428
798
|
const rootScope = Scope.make();
|
|
429
799
|
let current;
|
|
430
800
|
try {
|
|
@@ -460,144 +830,50 @@ const buildLayer = async (layer, backend, options = {}) => {
|
|
|
460
830
|
else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
|
|
461
831
|
throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
|
|
462
832
|
}
|
|
463
|
-
return new
|
|
464
|
-
};
|
|
465
|
-
//#endregion
|
|
466
|
-
//#region src/effect/combinators.ts
|
|
467
|
-
const isPromiseLike = (value) => {
|
|
468
|
-
if (typeof value !== "object" && typeof value !== "function" || value === null) return false;
|
|
469
|
-
return "then" in value && typeof value.then === "function";
|
|
833
|
+
return new RuntimeHandleImpl(backend, rootScope, options.onCleanupFailure);
|
|
470
834
|
};
|
|
471
|
-
const mapResult = (result, fn) => Result.map(result, fn);
|
|
472
|
-
const mapErrorResult = (result, fn) => Result.mapError(result, fn);
|
|
473
|
-
const andThenResult = (result, next) => {
|
|
474
|
-
const chained = Result.andThen(result, next);
|
|
475
|
-
if (!isPromiseLike(chained)) return chained;
|
|
476
|
-
return Result.andThenAsync(result, () => Promise.resolve(chained));
|
|
477
|
-
};
|
|
478
|
-
function map(first, second) {
|
|
479
|
-
if (typeof first === "function" && second === void 0) return (effect) => map(effect, first);
|
|
480
|
-
const fn = second;
|
|
481
|
-
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapResult(result, fn));
|
|
482
|
-
return mapResult(first, fn);
|
|
483
|
-
}
|
|
484
|
-
function mapError(first, second) {
|
|
485
|
-
if (typeof first === "function" && second === void 0) return (effect) => mapError(effect, first);
|
|
486
|
-
const fn = second;
|
|
487
|
-
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => mapErrorResult(result, fn));
|
|
488
|
-
return mapErrorResult(first, fn);
|
|
489
|
-
}
|
|
490
|
-
function andThen(first, second) {
|
|
491
|
-
if (typeof first === "function" && second === void 0) return (effect) => andThen(effect, first);
|
|
492
|
-
const next = second;
|
|
493
|
-
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => andThenResult(result, next));
|
|
494
|
-
return andThenResult(first, next);
|
|
495
|
-
}
|
|
496
835
|
//#endregion
|
|
497
|
-
//#region src/
|
|
498
|
-
function gen(body) {
|
|
499
|
-
return Result.gen(body);
|
|
500
|
-
}
|
|
836
|
+
//#region src/runtime/runtime.ts
|
|
501
837
|
/**
|
|
502
|
-
*
|
|
838
|
+
* Long-lived execution environment backed by a complete Layer.
|
|
503
839
|
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
*
|
|
840
|
+
* A Runtime owns Layer resources until `dispose()` is called. Each `run()` is
|
|
841
|
+
* isolated in a child Scope, while Layer-scoped resources remain shared.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* ```ts
|
|
845
|
+
* const runtime = await Runtime.make(AppLive, new MemoryLayerBackend())
|
|
846
|
+
* const result = await runtime.run(loadUser('u1'))
|
|
847
|
+
* await runtime.dispose()
|
|
848
|
+
* ```
|
|
513
849
|
*
|
|
514
|
-
*
|
|
515
|
-
* disposal failures remain owned by Scope cleanup.
|
|
850
|
+
* @typeParam Provided The Service constructors supplied by the Layer.
|
|
516
851
|
*/
|
|
517
|
-
function add(resource) {
|
|
518
|
-
const scope = Scope.current();
|
|
519
|
-
return Result.await(Result.tryPromise(() => scope.add(resource)));
|
|
520
|
-
}
|
|
521
|
-
const Effect = {
|
|
522
|
-
gen,
|
|
523
|
-
acquireRelease,
|
|
524
|
-
add,
|
|
525
|
-
map,
|
|
526
|
-
mapError,
|
|
527
|
-
andThen
|
|
528
|
-
};
|
|
529
|
-
//#endregion
|
|
530
|
-
//#region src/function/pipe.ts
|
|
531
|
-
function pipe(value, ...operations) {
|
|
532
|
-
return operations.reduce((current, operation) => operation(current), value);
|
|
533
|
-
}
|
|
534
|
-
//#endregion
|
|
535
|
-
//#region src/resource/errors.ts
|
|
536
|
-
var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
|
|
537
|
-
//#endregion
|
|
538
|
-
//#region src/resource/internal.ts
|
|
539
|
-
const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
|
|
540
|
-
resource,
|
|
541
|
-
cause,
|
|
542
|
-
message: `Failed to release resource: ${resource}`
|
|
543
|
-
});
|
|
544
|
-
const runResult = async (operation) => {
|
|
545
|
-
return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
|
|
546
|
-
};
|
|
547
|
-
const normalizeReleaseOutcome = (name, outcome) => {
|
|
548
|
-
if (outcome === void 0) return Result.ok();
|
|
549
|
-
return outcome.mapError((cause) => toReleaseFailure(name, cause));
|
|
550
|
-
};
|
|
551
|
-
const runRelease = async (name, resource, release) => {
|
|
552
|
-
return (await Result.tryPromise({
|
|
553
|
-
try: () => Promise.resolve(release(resource)),
|
|
554
|
-
catch: (cause) => toReleaseFailure(name, cause)
|
|
555
|
-
})).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
|
|
556
|
-
};
|
|
557
|
-
const notifyReleaseFailure = async (observer, failure) => {
|
|
558
|
-
if (!observer) return;
|
|
559
|
-
try {
|
|
560
|
-
await observer(failure);
|
|
561
|
-
} catch {}
|
|
562
|
-
};
|
|
563
|
-
const combineUseAndRelease = async (used, released, onReleaseFailure) => {
|
|
564
|
-
if (Result.isError(used)) {
|
|
565
|
-
if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
566
|
-
return Result.err(used.error);
|
|
567
|
-
}
|
|
568
|
-
if (Result.isError(released)) {
|
|
569
|
-
await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
570
|
-
return Result.err(released.error);
|
|
571
|
-
}
|
|
572
|
-
return Result.ok(used.value);
|
|
573
|
-
};
|
|
574
|
-
//#endregion
|
|
575
|
-
//#region src/resource/resource.ts
|
|
576
|
-
const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
|
|
577
|
-
const resource = yield* Result.await(runResult(acquire));
|
|
578
|
-
const scope = Scope.make();
|
|
579
|
-
let released = Result.ok();
|
|
580
|
-
scope.addFinalizer(async () => {
|
|
581
|
-
released = await runRelease(name, resource, release);
|
|
582
|
-
});
|
|
583
|
-
const used = await runResult(() => use(resource));
|
|
584
|
-
await scope.close();
|
|
585
|
-
return await combineUseAndRelease(used, released, onReleaseFailure);
|
|
586
|
-
});
|
|
587
|
-
const Resource = { acquireUseRelease };
|
|
588
|
-
//#endregion
|
|
589
|
-
//#region src/runtime/runtime.ts
|
|
590
852
|
var Runtime = class Runtime {
|
|
591
|
-
|
|
592
|
-
constructor(
|
|
593
|
-
this.
|
|
853
|
+
handle;
|
|
854
|
+
constructor(handle) {
|
|
855
|
+
this.handle = handle;
|
|
594
856
|
}
|
|
595
|
-
/**
|
|
857
|
+
/**
|
|
858
|
+
* Create a long-lived Runtime that owns its Layer resources.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* ```ts
|
|
862
|
+
* const runtime = await Runtime.make(AppLive, backend)
|
|
863
|
+
* const result = await runtime.run(program)
|
|
864
|
+
* await runtime.dispose()
|
|
865
|
+
* ```
|
|
866
|
+
*/
|
|
596
867
|
static async make(layer, backend, options = {}) {
|
|
597
|
-
const
|
|
598
|
-
return new Runtime(
|
|
868
|
+
const handle = await createRuntimeHandle(layer, backend, options);
|
|
869
|
+
return new Runtime(handle);
|
|
599
870
|
}
|
|
600
|
-
/**
|
|
871
|
+
/**
|
|
872
|
+
* Run one program and dispose its Layer resources before resolving.
|
|
873
|
+
*
|
|
874
|
+
* This is convenient for request-style or command-style execution where a
|
|
875
|
+
* Runtime should not outlive the operation.
|
|
876
|
+
*/
|
|
601
877
|
static async run(layer, backend, program, options = {}) {
|
|
602
878
|
const runtime = await Runtime.make(layer, backend, options);
|
|
603
879
|
let value;
|
|
@@ -636,20 +912,20 @@ var Runtime = class Runtime {
|
|
|
636
912
|
}
|
|
637
913
|
/** Run one execution in this Runtime's child Scope. */
|
|
638
914
|
run(program) {
|
|
639
|
-
return this.
|
|
915
|
+
return this.handle.run(program);
|
|
640
916
|
}
|
|
641
917
|
runUnchecked(program) {
|
|
642
|
-
return this.
|
|
918
|
+
return this.handle.run(program);
|
|
643
919
|
}
|
|
644
920
|
/** Stop new executions and release the Runtime's Layer resources. */
|
|
645
921
|
dispose() {
|
|
646
|
-
return this.
|
|
922
|
+
return this.handle.dispose();
|
|
647
923
|
}
|
|
648
924
|
disposeWithOutcome(outcome) {
|
|
649
|
-
return this.
|
|
925
|
+
return this.handle.dispose(outcome);
|
|
650
926
|
}
|
|
651
927
|
};
|
|
652
928
|
//#endregion
|
|
653
|
-
export {
|
|
929
|
+
export { DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
|
|
654
930
|
|
|
655
931
|
//# sourceMappingURL=index.mjs.map
|