better-effect 0.4.0 → 0.6.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 +92 -11
- package/dist/adapters/iti.d.mts +11 -3
- package/dist/adapters/iti.d.mts.map +1 -1
- package/dist/adapters/iti.mjs +35 -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-DMfjhNR_.d.mts +500 -0
- package/dist/index-DMfjhNR_.d.mts.map +1 -0
- package/dist/index.d.mts +225 -32
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +419 -168
- package/dist/index.mjs.map +1 -1
- package/dist/internal-identity-C6Awrc33.mjs +27 -0
- package/dist/internal-identity-C6Awrc33.mjs.map +1 -0
- package/dist/runtime-CDcCF5cb.mjs +10 -0
- package/dist/runtime-CDcCF5cb.mjs.map +1 -0
- package/dist/testing.d.mts +11 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +34 -12
- package/dist/testing.mjs.map +1 -1
- package/package.json +13 -5
- 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,99 @@
|
|
|
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
|
+
import { t as isPromiseLike } from "./runtime-CDcCF5cb.mjs";
|
|
2
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
import { Result, TaggedError } from "better-result";
|
|
4
5
|
//#region src/service/runtime.ts
|
|
5
6
|
const storage$1 = new AsyncLocalStorage();
|
|
7
|
+
/** Provides the resolver context used by Service tokens during execution. */
|
|
6
8
|
var ServiceRuntime = class ServiceRuntime {
|
|
9
|
+
/**
|
|
10
|
+
* Run a callback with a resolver available to `yield* Service` expressions.
|
|
11
|
+
*
|
|
12
|
+
* The context is scoped to the callback and is restored afterward.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const value = ServiceRuntime.run(resolver, () => {
|
|
17
|
+
* return ServiceRuntime.resolve(Database)
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
7
21
|
static run(resolver, program) {
|
|
8
22
|
return storage$1.run(resolver, program);
|
|
9
23
|
}
|
|
24
|
+
/** Return the resolver active in the current execution context. */
|
|
10
25
|
static current() {
|
|
11
26
|
const resolver = storage$1.getStore();
|
|
12
27
|
if (!resolver) throw new ServiceRuntimeNotConfiguredError();
|
|
13
28
|
return resolver;
|
|
14
29
|
}
|
|
30
|
+
/** Resolve a Service token using the active resolver. */
|
|
15
31
|
static async resolve(token) {
|
|
16
32
|
return await ServiceRuntime.current().resolve(token);
|
|
17
33
|
}
|
|
18
34
|
};
|
|
19
35
|
//#endregion
|
|
20
36
|
//#region src/service/service.ts
|
|
37
|
+
/**
|
|
38
|
+
* Declare a class-backed Service with a stable string-literal identity.
|
|
39
|
+
*
|
|
40
|
+
* The returned class is simultaneously the implementation type, the runtime
|
|
41
|
+
* dependency token, and the value yielded by `yield*` in an Effect generator.
|
|
42
|
+
* The explicit self type preserves exact instance inference, while the second
|
|
43
|
+
* call captures the tag as a literal for Layer composition and diagnostics.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* class Database extends Service<Database>()('Database') {
|
|
48
|
+
* query(): string {
|
|
49
|
+
* return 'ok'
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* const database = yield* Database
|
|
54
|
+
* database.query()
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @typeParam Self The instance type implemented by the declared Service.
|
|
58
|
+
*/
|
|
21
59
|
function Service() {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
60
|
+
return function(tag) {
|
|
61
|
+
if (tag.length === 0) throw new TypeError("Service tags must not be empty");
|
|
62
|
+
class BaseService {
|
|
63
|
+
/** The stable logical identity used by Layers and resolver backends. */
|
|
64
|
+
static serviceTag = tag;
|
|
65
|
+
/**
|
|
66
|
+
* Type-check a structural implementation of this Service.
|
|
67
|
+
*
|
|
68
|
+
* This is an identity helper. It returns the supplied value unchanged
|
|
69
|
+
* and does not invoke a constructor or modify its prototype.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* class Database extends Service<Database>()('Database') {
|
|
74
|
+
* query(sql: string): string {
|
|
75
|
+
* return sql
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* const database = Database.of({
|
|
80
|
+
* query: (sql) => `Result: ${sql}`
|
|
81
|
+
* })
|
|
82
|
+
*
|
|
83
|
+
* database.query('SELECT 1')
|
|
84
|
+
* // 'Result: SELECT 1'
|
|
85
|
+
* // database is the original object, not an instance of Database
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
static of(implementation) {
|
|
89
|
+
return implementation;
|
|
90
|
+
}
|
|
91
|
+
/** Resolve this Service from the resolver active in the current runtime. */
|
|
92
|
+
static async *[Symbol.asyncIterator]() {
|
|
93
|
+
return await ServiceRuntime.resolve(this);
|
|
94
|
+
}
|
|
25
95
|
}
|
|
96
|
+
return BaseService;
|
|
26
97
|
};
|
|
27
98
|
}
|
|
28
99
|
//#endregion
|
|
@@ -39,74 +110,116 @@ const runLayerGenerator = async (service, factory) => {
|
|
|
39
110
|
};
|
|
40
111
|
//#endregion
|
|
41
112
|
//#region src/layer/layer.ts
|
|
113
|
+
/**
|
|
114
|
+
* Declarative collection of Service providers.
|
|
115
|
+
*
|
|
116
|
+
* A Layer describes how to acquire implementations; it does not execute
|
|
117
|
+
* providers until a `Runtime` is created. Use `merge` to compose distinct
|
|
118
|
+
* providers and `override` when replacing an existing provider intentionally.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const AppLive = Layer.merge(
|
|
123
|
+
* Layer.succeed(Database, database),
|
|
124
|
+
* Layer.make(UserRepository)
|
|
125
|
+
* )
|
|
126
|
+
*
|
|
127
|
+
* const runtime = await Runtime.make(AppLive, backend)
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
42
130
|
var Layer = class Layer {
|
|
131
|
+
/** The provider registrations retained by this Layer. */
|
|
43
132
|
providers;
|
|
44
133
|
constructor(providers) {
|
|
45
134
|
this.providers = Object.freeze([...providers]);
|
|
46
135
|
}
|
|
47
136
|
static make(service, acquire) {
|
|
137
|
+
const defaultAcquire = () => {
|
|
138
|
+
return new service();
|
|
139
|
+
};
|
|
140
|
+
const normalizedAcquire = normalizeAcquire(acquire ?? defaultAcquire);
|
|
48
141
|
return new Layer([{
|
|
49
142
|
service,
|
|
50
|
-
acquire
|
|
143
|
+
acquire: normalizedAcquire
|
|
51
144
|
}]);
|
|
52
145
|
}
|
|
146
|
+
/** Create a Layer from an already-constructed Service instance. */
|
|
53
147
|
static succeed(service, instance) {
|
|
54
|
-
|
|
148
|
+
const normalizedAcquire = normalizeAcquire(() => instance);
|
|
149
|
+
return new Layer([{
|
|
150
|
+
service,
|
|
151
|
+
acquire: normalizedAcquire
|
|
152
|
+
}]);
|
|
55
153
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Define a dependency-free provider with Runtime-root cleanup.
|
|
58
|
-
*
|
|
59
|
-
* The release callback intentionally keeps its compatibility-friendly
|
|
60
|
-
* one-argument shape. Use `scopedGen` when cleanup needs `ScopeOutcome`.
|
|
61
|
-
*/
|
|
154
|
+
/** Define a provider with Runtime-root cleanup. */
|
|
62
155
|
static scoped(service, acquire, release) {
|
|
63
156
|
return new Layer([{
|
|
64
157
|
service,
|
|
65
|
-
acquire,
|
|
66
|
-
release: (instance) =>
|
|
158
|
+
acquire: normalizeAcquire(acquire),
|
|
159
|
+
release: (instance) => {
|
|
160
|
+
return release(instance);
|
|
161
|
+
}
|
|
67
162
|
}]);
|
|
68
163
|
}
|
|
69
|
-
/** Define a
|
|
164
|
+
/** Define a provider whose acquisition can yield contextual Services. */
|
|
70
165
|
static scopedGen(service, factory, release) {
|
|
71
166
|
return new Layer([{
|
|
72
167
|
service,
|
|
73
168
|
acquire: () => runLayerGenerator(service, factory),
|
|
74
|
-
release: (instance, outcome) =>
|
|
169
|
+
release: (instance, outcome) => {
|
|
170
|
+
return release(instance, outcome);
|
|
171
|
+
}
|
|
75
172
|
}]);
|
|
76
173
|
}
|
|
174
|
+
/** Define a provider whose acquisition can yield contextual Services. */
|
|
77
175
|
static gen(service, factory) {
|
|
78
|
-
return Layer
|
|
176
|
+
return new Layer([{
|
|
177
|
+
service,
|
|
178
|
+
acquire: () => runLayerGenerator(service, factory)
|
|
179
|
+
}]);
|
|
79
180
|
}
|
|
181
|
+
/** Compose Layers without replacing providers. */
|
|
80
182
|
static merge(...layers) {
|
|
81
183
|
const providers = /* @__PURE__ */ new Map();
|
|
82
184
|
for (const layer of layers) for (const provider of layer.providers) {
|
|
83
185
|
const service = provider.service;
|
|
84
|
-
|
|
85
|
-
|
|
186
|
+
const existing = providers.get(service.serviceTag);
|
|
187
|
+
if (existing) {
|
|
188
|
+
if (existing.service !== service) throw new ServiceTagCollisionError(existing.service, service);
|
|
189
|
+
throw new DuplicateServiceError(service);
|
|
190
|
+
}
|
|
191
|
+
providers.set(service.serviceTag, provider);
|
|
86
192
|
}
|
|
87
193
|
return new Layer([...providers.values()]);
|
|
88
194
|
}
|
|
195
|
+
/** Replace providers in a base Layer, using tag identity and compatible contracts. */
|
|
89
196
|
static override(base, ...overrides) {
|
|
90
197
|
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);
|
|
198
|
+
for (const provider of base.providers) providers.set(provider.service.serviceTag, provider);
|
|
199
|
+
for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service.serviceTag, provider);
|
|
93
200
|
return new Layer([...providers.values()]);
|
|
94
201
|
}
|
|
95
202
|
};
|
|
203
|
+
const normalizeAcquire = (acquire) => () => {
|
|
204
|
+
return acquire();
|
|
205
|
+
};
|
|
96
206
|
//#endregion
|
|
97
207
|
//#region src/scope/errors.ts
|
|
208
|
+
/** Thrown when Scope context is accessed outside an active Scope execution. */
|
|
98
209
|
var ScopeRuntimeNotConfiguredError = class extends Error {
|
|
99
210
|
constructor() {
|
|
100
211
|
super("No Scope is available in the current execution context");
|
|
101
212
|
this.name = "ScopeRuntimeNotConfiguredError";
|
|
102
213
|
}
|
|
103
214
|
};
|
|
215
|
+
/** Thrown when a resource or finalizer is added after Scope closure begins. */
|
|
104
216
|
var ScopeClosedError = class extends Error {
|
|
105
217
|
constructor() {
|
|
106
218
|
super("Cannot add resources or finalizers to a closed Scope");
|
|
107
219
|
this.name = "ScopeClosedError";
|
|
108
220
|
}
|
|
109
221
|
};
|
|
222
|
+
/** Aggregates finalizer failures encountered while closing a Scope. */
|
|
110
223
|
var ScopeCloseError = class extends Error {
|
|
111
224
|
causes;
|
|
112
225
|
constructor(causes) {
|
|
@@ -115,6 +228,7 @@ var ScopeCloseError = class extends Error {
|
|
|
115
228
|
this.name = "ScopeCloseError";
|
|
116
229
|
}
|
|
117
230
|
};
|
|
231
|
+
/** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
|
|
118
232
|
var ResourceNotDisposableError = class extends Error {
|
|
119
233
|
constructor() {
|
|
120
234
|
super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
|
|
@@ -124,23 +238,28 @@ var ResourceNotDisposableError = class extends Error {
|
|
|
124
238
|
//#endregion
|
|
125
239
|
//#region src/scope/disposable.ts
|
|
126
240
|
const SCOPE_SUCCESS$2 = { status: "success" };
|
|
241
|
+
/** Return a Scope finalizer for a value's async or sync disposal protocol. */
|
|
127
242
|
const getDisposeFinalizer = (resource) => {
|
|
128
243
|
const candidate = Object(resource);
|
|
129
244
|
const asyncDispose = candidate[Symbol.asyncDispose];
|
|
130
|
-
if (
|
|
245
|
+
if (asyncDispose instanceof Function) return () => asyncDispose.call(resource);
|
|
131
246
|
const dispose = candidate[Symbol.dispose];
|
|
132
|
-
if (
|
|
247
|
+
if (dispose instanceof Function) return () => dispose.call(resource);
|
|
133
248
|
};
|
|
249
|
+
/** Dispose a value immediately when it implements a disposal protocol. */
|
|
134
250
|
const disposeResource = (resource) => {
|
|
135
251
|
return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
|
|
136
252
|
};
|
|
137
253
|
//#endregion
|
|
138
254
|
//#region src/scope/runtime.ts
|
|
139
255
|
const storage = new AsyncLocalStorage();
|
|
256
|
+
/** Bridges the current Scope through async execution context. */
|
|
140
257
|
var ScopeRuntime = class {
|
|
258
|
+
/** Supply a Scope while invoking a callback. */
|
|
141
259
|
static run(scope, program) {
|
|
142
260
|
return storage.run(scope, program);
|
|
143
261
|
}
|
|
262
|
+
/** Return the Scope active in the current execution context. */
|
|
144
263
|
static current() {
|
|
145
264
|
const scope = storage.getStore();
|
|
146
265
|
if (!scope) throw new ScopeRuntimeNotConfiguredError();
|
|
@@ -286,15 +405,19 @@ var ScopeImpl = class ScopeImpl {
|
|
|
286
405
|
}
|
|
287
406
|
};
|
|
288
407
|
const Scope = {
|
|
408
|
+
/** Create an owned, initially open Scope. */
|
|
289
409
|
make() {
|
|
290
410
|
return new ScopeImpl();
|
|
291
411
|
},
|
|
412
|
+
/** Return the non-owning Scope available in the current execution context. */
|
|
292
413
|
current() {
|
|
293
414
|
return ScopeRuntime.current();
|
|
294
415
|
},
|
|
416
|
+
/** Run a callback with an existing Scope supplied as the current context. */
|
|
295
417
|
provide(scope, program) {
|
|
296
418
|
return ScopeRuntime.run(scope, program);
|
|
297
419
|
},
|
|
420
|
+
/** Resolve the current Scope through `yield* Scope` inside an Effect. */
|
|
298
421
|
*[Symbol.iterator]() {
|
|
299
422
|
return ScopeRuntime.current();
|
|
300
423
|
},
|
|
@@ -304,6 +427,14 @@ const Scope = {
|
|
|
304
427
|
* Scope is independent from `better-result`, so returned values—including
|
|
305
428
|
* `Result.err`—close this Scope with a successful outcome. Result-aware
|
|
306
429
|
* outcome classification belongs to `Runtime.run`.
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```ts
|
|
433
|
+
* await Scope.run(async (scope) => {
|
|
434
|
+
* const connection = await scope.acquire(connect, (connection) => connection.close())
|
|
435
|
+
* return connection.query()
|
|
436
|
+
* })
|
|
437
|
+
* ```
|
|
307
438
|
*/
|
|
308
439
|
run(program) {
|
|
309
440
|
const scope = new ScopeImpl();
|
|
@@ -311,8 +442,222 @@ const Scope = {
|
|
|
311
442
|
}
|
|
312
443
|
};
|
|
313
444
|
//#endregion
|
|
445
|
+
//#region src/effect/combinators.ts
|
|
446
|
+
const mapResult = (result, fn) => {
|
|
447
|
+
return Result.map(result, fn);
|
|
448
|
+
};
|
|
449
|
+
const mapErrorResult = (result, fn) => {
|
|
450
|
+
return Result.mapError(result, fn);
|
|
451
|
+
};
|
|
452
|
+
const andThenResult = (result, next) => {
|
|
453
|
+
const resultNext = next;
|
|
454
|
+
return Result.andThen(result, resultNext);
|
|
455
|
+
};
|
|
456
|
+
const andThenAsyncResult = (result, next) => {
|
|
457
|
+
const resultNext = (value) => {
|
|
458
|
+
return Promise.resolve(next(value));
|
|
459
|
+
};
|
|
460
|
+
return Result.andThenAsync(result, resultNext);
|
|
461
|
+
};
|
|
462
|
+
function map(first, second) {
|
|
463
|
+
if (first instanceof Function && second === void 0) {
|
|
464
|
+
const callback = first;
|
|
465
|
+
return (effect) => {
|
|
466
|
+
return map(effect, callback);
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const fn = second;
|
|
470
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
|
|
471
|
+
return mapResult(result, fn);
|
|
472
|
+
});
|
|
473
|
+
return mapResult(first, fn);
|
|
474
|
+
}
|
|
475
|
+
function mapError(first, second) {
|
|
476
|
+
if (first instanceof Function && second === void 0) {
|
|
477
|
+
const callback = first;
|
|
478
|
+
return (effect) => {
|
|
479
|
+
return mapError(effect, callback);
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const fn = second;
|
|
483
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
|
|
484
|
+
return mapErrorResult(result, fn);
|
|
485
|
+
});
|
|
486
|
+
return mapErrorResult(first, fn);
|
|
487
|
+
}
|
|
488
|
+
function andThen(first, second) {
|
|
489
|
+
if (first instanceof Function && second === void 0) {
|
|
490
|
+
const callback = first;
|
|
491
|
+
return (effect) => {
|
|
492
|
+
return andThen(effect, callback);
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return andThenResult(first, second);
|
|
496
|
+
}
|
|
497
|
+
function andThenAsync(first, second) {
|
|
498
|
+
if (first instanceof Function && second === void 0) {
|
|
499
|
+
const callback = first;
|
|
500
|
+
return (effect) => {
|
|
501
|
+
return andThenAsync(effect, callback);
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const next = second;
|
|
505
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
|
|
506
|
+
return andThenAsyncResult(result, next);
|
|
507
|
+
});
|
|
508
|
+
return andThenAsyncResult(first, next);
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/effect/effect.ts
|
|
512
|
+
function gen(body) {
|
|
513
|
+
const runResultGenerator = Result.gen;
|
|
514
|
+
return runResultGenerator(body);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Acquire a resource in the current Scope and register its release callback.
|
|
518
|
+
*
|
|
519
|
+
* Acquisition failures are represented in the Effect Result error channel;
|
|
520
|
+
* release failures remain owned by Scope cleanup. The release callback
|
|
521
|
+
* receives the final outcome chosen by the enclosing execution boundary.
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* ```ts
|
|
525
|
+
* const connection = yield* Effect.acquireRelease(
|
|
526
|
+
* () => pool.connect(),
|
|
527
|
+
* (connection, outcome) => connection.close(outcome)
|
|
528
|
+
* )
|
|
529
|
+
* ```
|
|
530
|
+
*/
|
|
531
|
+
function acquireRelease(acquire, release) {
|
|
532
|
+
const scope = Scope.current();
|
|
533
|
+
return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Register an already-acquired disposable resource in the current Scope.
|
|
537
|
+
*
|
|
538
|
+
* The resource is not acquired by this helper. Registration failures are
|
|
539
|
+
* represented in the Effect Result error channel; disposal failures remain
|
|
540
|
+
* owned by Scope cleanup.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* const file = yield* Effect.add(await openFile('notes.txt'))
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
547
|
+
function add(resource) {
|
|
548
|
+
const scope = Scope.current();
|
|
549
|
+
return Result.await(Result.tryPromise(() => scope.add(resource)));
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Effect namespace containing generator, resource, and Result combinators.
|
|
553
|
+
*
|
|
554
|
+
* Prefer these helpers when a program needs typed Service requirements or
|
|
555
|
+
* Scope-aware acquisition and cleanup.
|
|
556
|
+
*/
|
|
557
|
+
const Effect = {
|
|
558
|
+
/** Compose a generator-based Effect program. */
|
|
559
|
+
gen,
|
|
560
|
+
/** Acquire and register a resource in the current Scope. */
|
|
561
|
+
acquireRelease,
|
|
562
|
+
/** Register an already-acquired disposable in the current Scope. */
|
|
563
|
+
add,
|
|
564
|
+
/** Map a successful Effect result. */
|
|
565
|
+
map,
|
|
566
|
+
/** Map an Effect error. */
|
|
567
|
+
mapError,
|
|
568
|
+
/** Chain a synchronous Effect result. */
|
|
569
|
+
andThen,
|
|
570
|
+
/** Chain an asynchronous Effect result. */
|
|
571
|
+
andThenAsync
|
|
572
|
+
};
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/function/pipe.ts
|
|
575
|
+
function pipe(value, ...operations) {
|
|
576
|
+
return operations.reduce((current, operation) => operation(current), value);
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/resource/errors.ts
|
|
580
|
+
/** Describes a failure encountered while releasing a Resource. */
|
|
581
|
+
var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/resource/internal.ts
|
|
584
|
+
const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
|
|
585
|
+
resource,
|
|
586
|
+
cause,
|
|
587
|
+
message: `Failed to release resource: ${resource}`
|
|
588
|
+
});
|
|
589
|
+
const runResult = async (operation) => {
|
|
590
|
+
return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
|
|
591
|
+
};
|
|
592
|
+
const normalizeReleaseOutcome = (name, outcome) => {
|
|
593
|
+
if (outcome === void 0) return Result.ok();
|
|
594
|
+
return outcome.mapError((cause) => toReleaseFailure(name, cause));
|
|
595
|
+
};
|
|
596
|
+
const runRelease = async (name, resource, release) => {
|
|
597
|
+
return (await Result.tryPromise({
|
|
598
|
+
try: () => Promise.resolve(release(resource)),
|
|
599
|
+
catch: (cause) => toReleaseFailure(name, cause)
|
|
600
|
+
})).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
|
|
601
|
+
};
|
|
602
|
+
const notifyReleaseFailure = async (observer, failure) => {
|
|
603
|
+
if (!observer) return;
|
|
604
|
+
try {
|
|
605
|
+
await observer(failure);
|
|
606
|
+
} catch {}
|
|
607
|
+
};
|
|
608
|
+
const combineUseAndRelease = async (used, released, onReleaseFailure) => {
|
|
609
|
+
if (Result.isError(used)) {
|
|
610
|
+
if (Result.isError(released)) await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
611
|
+
return Result.err(used.error);
|
|
612
|
+
}
|
|
613
|
+
if (Result.isError(released)) {
|
|
614
|
+
await notifyReleaseFailure(onReleaseFailure, released.error);
|
|
615
|
+
return Result.err(released.error);
|
|
616
|
+
}
|
|
617
|
+
return Result.ok(used.value);
|
|
618
|
+
};
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/resource/resource.ts
|
|
621
|
+
/**
|
|
622
|
+
* Acquire a resource, use it, and always attempt release afterward.
|
|
623
|
+
*
|
|
624
|
+
* Acquisition, use, and release may be synchronous or asynchronous Result
|
|
625
|
+
* operations. If both use and release fail, the use error remains primary and
|
|
626
|
+
* `onReleaseFailure` receives the cleanup failure as a diagnostic.
|
|
627
|
+
*
|
|
628
|
+
* When `release` is omitted, `Symbol.asyncDispose` is preferred over
|
|
629
|
+
* `Symbol.dispose`.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```ts
|
|
633
|
+
* const result = await Resource.acquireUseRelease({
|
|
634
|
+
* name: 'database connection',
|
|
635
|
+
* acquire: () => connect(),
|
|
636
|
+
* use: (connection) => query(connection),
|
|
637
|
+
* release: (connection) => connection.close()
|
|
638
|
+
* })
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
|
|
642
|
+
const resource = yield* Result.await(runResult(acquire));
|
|
643
|
+
const scope = Scope.make();
|
|
644
|
+
let released = Result.ok();
|
|
645
|
+
scope.addFinalizer(async () => {
|
|
646
|
+
released = await runRelease(name, resource, release);
|
|
647
|
+
});
|
|
648
|
+
const used = await runResult(() => use(resource));
|
|
649
|
+
await scope.close();
|
|
650
|
+
return await combineUseAndRelease(used, released, onReleaseFailure);
|
|
651
|
+
});
|
|
652
|
+
const Resource = {
|
|
653
|
+
/** Acquire, use, and release a resource with deterministic error precedence. */
|
|
654
|
+
acquireUseRelease };
|
|
655
|
+
//#endregion
|
|
314
656
|
//#region src/runtime/outcome.ts
|
|
315
|
-
const isResultLike = (value) =>
|
|
657
|
+
const isResultLike = (value) => {
|
|
658
|
+
const candidate = Object(value);
|
|
659
|
+
return Object.prototype.toString.call(value) !== "[object Function]" && "status" in candidate && (candidate.status === "ok" || candidate.status === "error");
|
|
660
|
+
};
|
|
316
661
|
const classifyRuntimeOutcome = (value) => {
|
|
317
662
|
if (isResultLike(value) && Result.isError(value)) return {
|
|
318
663
|
status: "failure",
|
|
@@ -323,6 +668,12 @@ const classifyRuntimeOutcome = (value) => {
|
|
|
323
668
|
//#endregion
|
|
324
669
|
//#region src/layer/runtime.ts
|
|
325
670
|
const SCOPE_SUCCESS = Object.freeze({ status: "success" });
|
|
671
|
+
var RuntimeHandleDisposedError = class extends Error {
|
|
672
|
+
constructor() {
|
|
673
|
+
super("Cannot run a program using a disposed Layer");
|
|
674
|
+
this.name = "RuntimeHandleDisposedError";
|
|
675
|
+
}
|
|
676
|
+
};
|
|
326
677
|
const normalizeDisposeCauses = (cause) => {
|
|
327
678
|
if (cause instanceof AggregateError) return [...cause.errors];
|
|
328
679
|
return [cause];
|
|
@@ -340,7 +691,7 @@ const bindProviderToScope = (provider, rootScope) => ({
|
|
|
340
691
|
return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
|
|
341
692
|
})
|
|
342
693
|
});
|
|
343
|
-
var
|
|
694
|
+
var RuntimeHandleImpl = class {
|
|
344
695
|
backend;
|
|
345
696
|
rootScope;
|
|
346
697
|
onCleanupFailure;
|
|
@@ -416,15 +767,11 @@ var BuiltLayerImpl = class {
|
|
|
416
767
|
}
|
|
417
768
|
}
|
|
418
769
|
assertActive() {
|
|
419
|
-
if (this.state !== "active") throw new
|
|
770
|
+
if (this.state !== "active") throw new RuntimeHandleDisposedError();
|
|
420
771
|
}
|
|
421
772
|
};
|
|
422
|
-
/**
|
|
423
|
-
|
|
424
|
-
*
|
|
425
|
-
* @deprecated Prefer `Runtime.make()` for application code.
|
|
426
|
-
*/
|
|
427
|
-
const buildLayer = async (layer, backend, options = {}) => {
|
|
773
|
+
/** Build a Runtime handle for a complete Layer and register its providers. */
|
|
774
|
+
const createRuntimeHandle = async (layer, backend, options = {}) => {
|
|
428
775
|
const rootScope = Scope.make();
|
|
429
776
|
let current;
|
|
430
777
|
try {
|
|
@@ -455,149 +802,53 @@ const buildLayer = async (layer, backend, options = {}) => {
|
|
|
455
802
|
error: shutdownError
|
|
456
803
|
});
|
|
457
804
|
}
|
|
458
|
-
|
|
459
|
-
if (cleanupCauses.length === 1) cleanupCause = cleanupCauses[0];
|
|
460
|
-
else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
|
|
805
|
+
const cleanupCause = cleanupCauses.length === 1 ? cleanupCauses[0] : cleanupCauses.length > 1 ? new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses)) : void 0;
|
|
461
806
|
throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
|
|
462
807
|
}
|
|
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";
|
|
470
|
-
};
|
|
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));
|
|
808
|
+
return new RuntimeHandleImpl(backend, rootScope, options.onCleanupFailure);
|
|
477
809
|
};
|
|
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
810
|
//#endregion
|
|
497
|
-
//#region src/
|
|
498
|
-
function gen(body) {
|
|
499
|
-
return Result.gen(body);
|
|
500
|
-
}
|
|
811
|
+
//#region src/runtime/runtime.ts
|
|
501
812
|
/**
|
|
502
|
-
*
|
|
813
|
+
* Long-lived execution environment backed by a complete Layer.
|
|
503
814
|
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*/
|
|
507
|
-
function acquireRelease(acquire, release) {
|
|
508
|
-
const scope = Scope.current();
|
|
509
|
-
return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Register an already-acquired disposable resource in the current Scope.
|
|
815
|
+
* A Runtime owns Layer resources until `dispose()` is called. Each `run()` is
|
|
816
|
+
* isolated in a child Scope, while Layer-scoped resources remain shared.
|
|
513
817
|
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```ts
|
|
820
|
+
* const runtime = await Runtime.make(AppLive, new MemoryLayerBackend())
|
|
821
|
+
* const result = await runtime.run(loadUser('u1'))
|
|
822
|
+
* await runtime.dispose()
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @typeParam Provided The branded Service instances supplied by the Layer.
|
|
516
826
|
*/
|
|
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
827
|
var Runtime = class Runtime {
|
|
591
|
-
|
|
592
|
-
constructor(
|
|
593
|
-
this.
|
|
828
|
+
handle;
|
|
829
|
+
constructor(handle) {
|
|
830
|
+
this.handle = handle;
|
|
594
831
|
}
|
|
595
|
-
/**
|
|
832
|
+
/**
|
|
833
|
+
* Create a long-lived Runtime that owns its Layer resources.
|
|
834
|
+
*
|
|
835
|
+
* @example
|
|
836
|
+
* ```ts
|
|
837
|
+
* const runtime = await Runtime.make(AppLive, backend)
|
|
838
|
+
* const result = await runtime.run(program)
|
|
839
|
+
* await runtime.dispose()
|
|
840
|
+
* ```
|
|
841
|
+
*/
|
|
596
842
|
static async make(layer, backend, options = {}) {
|
|
597
|
-
const
|
|
598
|
-
return new Runtime(
|
|
843
|
+
const handle = await createRuntimeHandle(layer, backend, options);
|
|
844
|
+
return new Runtime(handle);
|
|
599
845
|
}
|
|
600
|
-
/**
|
|
846
|
+
/**
|
|
847
|
+
* Run one program and dispose its Layer resources before resolving.
|
|
848
|
+
*
|
|
849
|
+
* This is convenient for request-style or command-style execution where a
|
|
850
|
+
* Runtime should not outlive the operation.
|
|
851
|
+
*/
|
|
601
852
|
static async run(layer, backend, program, options = {}) {
|
|
602
853
|
const runtime = await Runtime.make(layer, backend, options);
|
|
603
854
|
let value;
|
|
@@ -636,20 +887,20 @@ var Runtime = class Runtime {
|
|
|
636
887
|
}
|
|
637
888
|
/** Run one execution in this Runtime's child Scope. */
|
|
638
889
|
run(program) {
|
|
639
|
-
return this.
|
|
890
|
+
return this.handle.run(program);
|
|
640
891
|
}
|
|
641
892
|
runUnchecked(program) {
|
|
642
|
-
return this.
|
|
893
|
+
return this.handle.run(program);
|
|
643
894
|
}
|
|
644
895
|
/** Stop new executions and release the Runtime's Layer resources. */
|
|
645
896
|
dispose() {
|
|
646
|
-
return this.
|
|
897
|
+
return this.handle.dispose();
|
|
647
898
|
}
|
|
648
899
|
disposeWithOutcome(outcome) {
|
|
649
|
-
return this.
|
|
900
|
+
return this.handle.dispose(outcome);
|
|
650
901
|
}
|
|
651
902
|
};
|
|
652
903
|
//#endregion
|
|
653
|
-
export {
|
|
904
|
+
export { DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, ServiceTagCollisionError, pipe };
|
|
654
905
|
|
|
655
906
|
//# sourceMappingURL=index.mjs.map
|