better-effect 0.3.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 +167 -486
- 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 +263 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +444 -106
- 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 +6 -9
- package/dist/errors-CnvKqBpb.mjs +0 -63
- package/dist/errors-CnvKqBpb.mjs.map +0 -1
- package/dist/index-Bg6ofRE1.d.mts +0 -232
- package/dist/index-Bg6ofRE1.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,20 +109,68 @@ 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
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Define a provider with Runtime-root cleanup.
|
|
159
|
+
*
|
|
160
|
+
* The release callback intentionally keeps its compatibility-friendly
|
|
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
|
+
* ```
|
|
173
|
+
*/
|
|
56
174
|
static scoped(service, acquire, release) {
|
|
57
175
|
return new Layer([{
|
|
58
176
|
service,
|
|
@@ -60,6 +178,24 @@ var Layer = class Layer {
|
|
|
60
178
|
release: (instance) => release(instance)
|
|
61
179
|
}]);
|
|
62
180
|
}
|
|
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
|
+
*/
|
|
63
199
|
static scopedGen(service, factory, release) {
|
|
64
200
|
return new Layer([{
|
|
65
201
|
service,
|
|
@@ -67,39 +203,84 @@ var Layer = class Layer {
|
|
|
67
203
|
release: (instance, outcome) => release(instance, outcome)
|
|
68
204
|
}]);
|
|
69
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
|
+
*/
|
|
70
220
|
static gen(service, factory) {
|
|
71
221
|
return Layer.make(service, () => runLayerGenerator(service, factory));
|
|
72
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
|
+
*/
|
|
73
234
|
static merge(...layers) {
|
|
74
235
|
const providers = /* @__PURE__ */ new Map();
|
|
75
236
|
for (const layer of layers) for (const provider of layer.providers) {
|
|
76
237
|
const service = provider.service;
|
|
77
|
-
|
|
78
|
-
|
|
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);
|
|
79
244
|
}
|
|
80
245
|
return new Layer([...providers.values()]);
|
|
81
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
|
+
*/
|
|
82
260
|
static override(base, ...overrides) {
|
|
83
261
|
const providers = /* @__PURE__ */ new Map();
|
|
84
|
-
for (const provider of base.providers) providers.set(provider.service, provider);
|
|
85
|
-
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);
|
|
86
264
|
return new Layer([...providers.values()]);
|
|
87
265
|
}
|
|
88
266
|
};
|
|
89
267
|
//#endregion
|
|
90
268
|
//#region src/scope/errors.ts
|
|
269
|
+
/** Thrown when Scope context is accessed outside an active Scope execution. */
|
|
91
270
|
var ScopeRuntimeNotConfiguredError = class extends Error {
|
|
92
271
|
constructor() {
|
|
93
272
|
super("No Scope is available in the current execution context");
|
|
94
273
|
this.name = "ScopeRuntimeNotConfiguredError";
|
|
95
274
|
}
|
|
96
275
|
};
|
|
276
|
+
/** Thrown when a resource or finalizer is added after Scope closure begins. */
|
|
97
277
|
var ScopeClosedError = class extends Error {
|
|
98
278
|
constructor() {
|
|
99
279
|
super("Cannot add resources or finalizers to a closed Scope");
|
|
100
280
|
this.name = "ScopeClosedError";
|
|
101
281
|
}
|
|
102
282
|
};
|
|
283
|
+
/** Aggregates finalizer failures encountered while closing a Scope. */
|
|
103
284
|
var ScopeCloseError = class extends Error {
|
|
104
285
|
causes;
|
|
105
286
|
constructor(causes) {
|
|
@@ -108,6 +289,7 @@ var ScopeCloseError = class extends Error {
|
|
|
108
289
|
this.name = "ScopeCloseError";
|
|
109
290
|
}
|
|
110
291
|
};
|
|
292
|
+
/** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
|
|
111
293
|
var ResourceNotDisposableError = class extends Error {
|
|
112
294
|
constructor() {
|
|
113
295
|
super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
|
|
@@ -117,6 +299,7 @@ var ResourceNotDisposableError = class extends Error {
|
|
|
117
299
|
//#endregion
|
|
118
300
|
//#region src/scope/disposable.ts
|
|
119
301
|
const SCOPE_SUCCESS$2 = { status: "success" };
|
|
302
|
+
/** Return a Scope finalizer for a value's async or sync disposal protocol. */
|
|
120
303
|
const getDisposeFinalizer = (resource) => {
|
|
121
304
|
const candidate = Object(resource);
|
|
122
305
|
const asyncDispose = candidate[Symbol.asyncDispose];
|
|
@@ -124,16 +307,20 @@ const getDisposeFinalizer = (resource) => {
|
|
|
124
307
|
const dispose = candidate[Symbol.dispose];
|
|
125
308
|
if (typeof dispose === "function") return () => dispose.call(resource);
|
|
126
309
|
};
|
|
310
|
+
/** Dispose a value immediately when it implements a disposal protocol. */
|
|
127
311
|
const disposeResource = (resource) => {
|
|
128
312
|
return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
|
|
129
313
|
};
|
|
130
314
|
//#endregion
|
|
131
315
|
//#region src/scope/runtime.ts
|
|
132
316
|
const storage = new AsyncLocalStorage();
|
|
317
|
+
/** Bridges the current Scope through async execution context. */
|
|
133
318
|
var ScopeRuntime = class {
|
|
319
|
+
/** Supply a Scope while invoking a callback. */
|
|
134
320
|
static run(scope, program) {
|
|
135
321
|
return storage.run(scope, program);
|
|
136
322
|
}
|
|
323
|
+
/** Return the Scope active in the current execution context. */
|
|
137
324
|
static current() {
|
|
138
325
|
const scope = storage.getStore();
|
|
139
326
|
if (!scope) throw new ScopeRuntimeNotConfiguredError();
|
|
@@ -279,24 +466,219 @@ var ScopeImpl = class ScopeImpl {
|
|
|
279
466
|
}
|
|
280
467
|
};
|
|
281
468
|
const Scope = {
|
|
469
|
+
/** Create an owned, initially open Scope. */
|
|
282
470
|
make() {
|
|
283
471
|
return new ScopeImpl();
|
|
284
472
|
},
|
|
473
|
+
/** Return the non-owning Scope available in the current execution context. */
|
|
285
474
|
current() {
|
|
286
475
|
return ScopeRuntime.current();
|
|
287
476
|
},
|
|
477
|
+
/** Run a callback with an existing Scope supplied as the current context. */
|
|
288
478
|
provide(scope, program) {
|
|
289
479
|
return ScopeRuntime.run(scope, program);
|
|
290
480
|
},
|
|
481
|
+
/** Resolve the current Scope through `yield* Scope` inside an Effect. */
|
|
291
482
|
*[Symbol.iterator]() {
|
|
292
483
|
return ScopeRuntime.current();
|
|
293
484
|
},
|
|
485
|
+
/**
|
|
486
|
+
* Run a program in a newly owned Scope.
|
|
487
|
+
*
|
|
488
|
+
* Scope is independent from `better-result`, so returned values—including
|
|
489
|
+
* `Result.err`—close this Scope with a successful outcome. Result-aware
|
|
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
|
+
* ```
|
|
499
|
+
*/
|
|
294
500
|
run(program) {
|
|
295
501
|
const scope = new ScopeImpl();
|
|
296
502
|
return runScoped(scope, () => program(scope), { classify: () => SCOPE_SUCCESS$1 });
|
|
297
503
|
}
|
|
298
504
|
};
|
|
299
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
|
|
300
682
|
//#region src/runtime/outcome.ts
|
|
301
683
|
const isResultLike = (value) => typeof value === "object" && value !== null && "status" in value && (value.status === "ok" || value.status === "error");
|
|
302
684
|
const classifyRuntimeOutcome = (value) => {
|
|
@@ -309,6 +691,12 @@ const classifyRuntimeOutcome = (value) => {
|
|
|
309
691
|
//#endregion
|
|
310
692
|
//#region src/layer/runtime.ts
|
|
311
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
|
+
};
|
|
312
700
|
const normalizeDisposeCauses = (cause) => {
|
|
313
701
|
if (cause instanceof AggregateError) return [...cause.errors];
|
|
314
702
|
return [cause];
|
|
@@ -326,7 +714,7 @@ const bindProviderToScope = (provider, rootScope) => ({
|
|
|
326
714
|
return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
|
|
327
715
|
})
|
|
328
716
|
});
|
|
329
|
-
var
|
|
717
|
+
var RuntimeHandleImpl = class {
|
|
330
718
|
backend;
|
|
331
719
|
rootScope;
|
|
332
720
|
onCleanupFailure;
|
|
@@ -402,10 +790,11 @@ var BuiltLayerImpl = class {
|
|
|
402
790
|
}
|
|
403
791
|
}
|
|
404
792
|
assertActive() {
|
|
405
|
-
if (this.state !== "active") throw new
|
|
793
|
+
if (this.state !== "active") throw new RuntimeHandleDisposedError();
|
|
406
794
|
}
|
|
407
795
|
};
|
|
408
|
-
|
|
796
|
+
/** Build a Runtime handle for a complete Layer and register its providers. */
|
|
797
|
+
const createRuntimeHandle = async (layer, backend, options = {}) => {
|
|
409
798
|
const rootScope = Scope.make();
|
|
410
799
|
let current;
|
|
411
800
|
try {
|
|
@@ -441,103 +830,50 @@ const buildLayer = async (layer, backend, options = {}) => {
|
|
|
441
830
|
else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
|
|
442
831
|
throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
|
|
443
832
|
}
|
|
444
|
-
return new
|
|
833
|
+
return new RuntimeHandleImpl(backend, rootScope, options.onCleanupFailure);
|
|
445
834
|
};
|
|
446
835
|
//#endregion
|
|
447
|
-
//#region src/
|
|
448
|
-
function gen(body) {
|
|
449
|
-
return Result.gen(body);
|
|
450
|
-
}
|
|
836
|
+
//#region src/runtime/runtime.ts
|
|
451
837
|
/**
|
|
452
|
-
*
|
|
838
|
+
* Long-lived execution environment backed by a complete Layer.
|
|
453
839
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*/
|
|
457
|
-
function acquireRelease(acquire, release) {
|
|
458
|
-
const scope = Scope.current();
|
|
459
|
-
return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Register an already-acquired disposable resource in the current Scope.
|
|
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.
|
|
463
842
|
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
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
|
+
* ```
|
|
849
|
+
*
|
|
850
|
+
* @typeParam Provided The Service constructors supplied by the Layer.
|
|
466
851
|
*/
|
|
467
|
-
function add(resource) {
|
|
468
|
-
const scope = Scope.current();
|
|
469
|
-
return Result.await(Result.tryPromise(() => scope.add(resource)));
|
|
470
|
-
}
|
|
471
|
-
const Effect = {
|
|
472
|
-
gen,
|
|
473
|
-
acquireRelease,
|
|
474
|
-
add
|
|
475
|
-
};
|
|
476
|
-
//#endregion
|
|
477
|
-
//#region src/resource/errors.ts
|
|
478
|
-
var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
|
|
479
|
-
//#endregion
|
|
480
|
-
//#region src/resource/internal.ts
|
|
481
|
-
const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
|
|
482
|
-
resource,
|
|
483
|
-
cause,
|
|
484
|
-
message: `Failed to release resource: ${resource}`
|
|
485
|
-
});
|
|
486
|
-
const runResult = async (operation) => {
|
|
487
|
-
return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
|
|
488
|
-
};
|
|
489
|
-
const normalizeReleaseOutcome = (name, outcome) => {
|
|
490
|
-
if (outcome === void 0) return Result.ok();
|
|
491
|
-
return outcome.mapError((cause) => toReleaseFailure(name, cause));
|
|
492
|
-
};
|
|
493
|
-
const runRelease = async (name, resource, release) => {
|
|
494
|
-
return (await Result.tryPromise({
|
|
495
|
-
try: () => Promise.resolve(release(resource)),
|
|
496
|
-
catch: (cause) => toReleaseFailure(name, cause)
|
|
497
|
-
})).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
|
|
498
|
-
};
|
|
499
|
-
const notifyReleaseFailure = async (observer, failure) => {
|
|
500
|
-
if (!observer) return;
|
|
501
|
-
try {
|
|
502
|
-
await observer(failure);
|
|
503
|
-
} catch {}
|
|
504
|
-
};
|
|
505
|
-
const combineUseAndRelease = async (used, released, onReleaseFailure) => {
|
|
506
|
-
if (Result.isError(used)) {
|
|
507
|
-
if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
508
|
-
return Result.err(used.error);
|
|
509
|
-
}
|
|
510
|
-
if (Result.isError(released)) {
|
|
511
|
-
await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
512
|
-
return Result.err(released.error);
|
|
513
|
-
}
|
|
514
|
-
return Result.ok(used.value);
|
|
515
|
-
};
|
|
516
|
-
//#endregion
|
|
517
|
-
//#region src/resource/resource.ts
|
|
518
|
-
const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
|
|
519
|
-
const resource = yield* Result.await(runResult(acquire));
|
|
520
|
-
const scope = Scope.make();
|
|
521
|
-
let released = Result.ok();
|
|
522
|
-
scope.addFinalizer(async () => {
|
|
523
|
-
released = await runRelease(name, resource, release);
|
|
524
|
-
});
|
|
525
|
-
const used = await runResult(() => use(resource));
|
|
526
|
-
await scope.close();
|
|
527
|
-
return await combineUseAndRelease(used, released, onReleaseFailure);
|
|
528
|
-
});
|
|
529
|
-
const Resource = { acquireUseRelease };
|
|
530
|
-
//#endregion
|
|
531
|
-
//#region src/runtime/runtime.ts
|
|
532
852
|
var Runtime = class Runtime {
|
|
533
|
-
|
|
534
|
-
constructor(
|
|
535
|
-
this.
|
|
536
|
-
}
|
|
853
|
+
handle;
|
|
854
|
+
constructor(handle) {
|
|
855
|
+
this.handle = handle;
|
|
856
|
+
}
|
|
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
|
+
*/
|
|
537
867
|
static async make(layer, backend, options = {}) {
|
|
538
|
-
const
|
|
539
|
-
return new Runtime(
|
|
540
|
-
}
|
|
868
|
+
const handle = await createRuntimeHandle(layer, backend, options);
|
|
869
|
+
return new Runtime(handle);
|
|
870
|
+
}
|
|
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
|
+
*/
|
|
541
877
|
static async run(layer, backend, program, options = {}) {
|
|
542
878
|
const runtime = await Runtime.make(layer, backend, options);
|
|
543
879
|
let value;
|
|
@@ -574,20 +910,22 @@ var Runtime = class Runtime {
|
|
|
574
910
|
if (executionFailed) throw executionFailure;
|
|
575
911
|
return value;
|
|
576
912
|
}
|
|
913
|
+
/** Run one execution in this Runtime's child Scope. */
|
|
577
914
|
run(program) {
|
|
578
|
-
return this.
|
|
915
|
+
return this.handle.run(program);
|
|
579
916
|
}
|
|
580
917
|
runUnchecked(program) {
|
|
581
|
-
return this.
|
|
918
|
+
return this.handle.run(program);
|
|
582
919
|
}
|
|
920
|
+
/** Stop new executions and release the Runtime's Layer resources. */
|
|
583
921
|
dispose() {
|
|
584
|
-
return this.
|
|
922
|
+
return this.handle.dispose();
|
|
585
923
|
}
|
|
586
924
|
disposeWithOutcome(outcome) {
|
|
587
|
-
return this.
|
|
925
|
+
return this.handle.dispose(outcome);
|
|
588
926
|
}
|
|
589
927
|
};
|
|
590
928
|
//#endregion
|
|
591
|
-
export {
|
|
929
|
+
export { DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
|
|
592
930
|
|
|
593
931
|
//# sourceMappingURL=index.mjs.map
|