better-effect 0.1.0 → 0.3.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/dist/index.mjs CHANGED
@@ -1,17 +1,19 @@
1
- import { ItiLayerBackend } from "./adapters/iti.mjs";
1
+ import { a as LayerRegistrationError, i as LayerGeneratorYieldError, n as DuplicateServiceError, o as ServiceNotFoundError, r as LayerDisposeError, s as ServiceRuntimeNotConfiguredError, t as BuiltLayerDisposedError } from "./errors-CnvKqBpb.mjs";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { Result, TaggedError } from "better-result";
3
4
  //#region src/service/runtime.ts
4
- let currentResolver;
5
- var ServiceRuntime = class {
6
- static configure(resolver) {
7
- currentResolver = resolver;
5
+ const storage$1 = new AsyncLocalStorage();
6
+ var ServiceRuntime = class ServiceRuntime {
7
+ static run(resolver, program) {
8
+ return storage$1.run(resolver, program);
8
9
  }
9
- static async resolve(token) {
10
- if (!currentResolver) throw new Error("ServiceRuntime has not been configured");
11
- return await currentResolver.resolve(token);
10
+ static current() {
11
+ const resolver = storage$1.getStore();
12
+ if (!resolver) throw new ServiceRuntimeNotConfiguredError();
13
+ return resolver;
12
14
  }
13
- static reset() {
14
- currentResolver = void 0;
15
+ static async resolve(token) {
16
+ return await ServiceRuntime.current().resolve(token);
15
17
  }
16
18
  };
17
19
  //#endregion
@@ -24,11 +26,23 @@ function Service() {
24
26
  };
25
27
  }
26
28
  //#endregion
29
+ //#region src/layer/internal.ts
30
+ const runLayerGenerator = async (service, factory) => {
31
+ const iterator = factory();
32
+ const state = await iterator.next();
33
+ if (!state.done) try {
34
+ await iterator.return(void 0);
35
+ } finally {
36
+ throw new LayerGeneratorYieldError(service);
37
+ }
38
+ return state.value;
39
+ };
40
+ //#endregion
27
41
  //#region src/layer/layer.ts
28
42
  var Layer = class Layer {
29
43
  providers;
30
44
  constructor(providers) {
31
- this.providers = providers;
45
+ this.providers = Object.freeze([...providers]);
32
46
  }
33
47
  static make(service, acquire) {
34
48
  return new Layer([{
@@ -46,11 +60,21 @@ var Layer = class Layer {
46
60
  release: (instance) => release(instance)
47
61
  }]);
48
62
  }
63
+ static scopedGen(service, factory, release) {
64
+ return new Layer([{
65
+ service,
66
+ acquire: () => runLayerGenerator(service, factory),
67
+ release: (instance, outcome) => release(instance, outcome)
68
+ }]);
69
+ }
70
+ static gen(service, factory) {
71
+ return Layer.make(service, () => runLayerGenerator(service, factory));
72
+ }
49
73
  static merge(...layers) {
50
74
  const providers = /* @__PURE__ */ new Map();
51
75
  for (const layer of layers) for (const provider of layer.providers) {
52
76
  const service = provider.service;
53
- if (providers.has(service)) throw new Error(`Duplicate service "${service.name}" in Layer.merge()`);
77
+ if (providers.has(service)) throw new DuplicateServiceError(service);
54
78
  providers.set(service, provider);
55
79
  }
56
80
  return new Layer([...providers.values()]);
@@ -63,39 +87,402 @@ var Layer = class Layer {
63
87
  }
64
88
  };
65
89
  //#endregion
90
+ //#region src/scope/errors.ts
91
+ var ScopeRuntimeNotConfiguredError = class extends Error {
92
+ constructor() {
93
+ super("No Scope is available in the current execution context");
94
+ this.name = "ScopeRuntimeNotConfiguredError";
95
+ }
96
+ };
97
+ var ScopeClosedError = class extends Error {
98
+ constructor() {
99
+ super("Cannot add resources or finalizers to a closed Scope");
100
+ this.name = "ScopeClosedError";
101
+ }
102
+ };
103
+ var ScopeCloseError = class extends Error {
104
+ causes;
105
+ constructor(causes) {
106
+ super(`Failed to close Scope (${causes.length} finalizer${causes.length === 1 ? "" : "s"} failed)`);
107
+ this.causes = causes;
108
+ this.name = "ScopeCloseError";
109
+ }
110
+ };
111
+ var ResourceNotDisposableError = class extends Error {
112
+ constructor() {
113
+ super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
114
+ this.name = "ResourceNotDisposableError";
115
+ }
116
+ };
117
+ //#endregion
118
+ //#region src/scope/disposable.ts
119
+ const SCOPE_SUCCESS$2 = { status: "success" };
120
+ const getDisposeFinalizer = (resource) => {
121
+ const candidate = Object(resource);
122
+ const asyncDispose = candidate[Symbol.asyncDispose];
123
+ if (typeof asyncDispose === "function") return () => asyncDispose.call(resource);
124
+ const dispose = candidate[Symbol.dispose];
125
+ if (typeof dispose === "function") return () => dispose.call(resource);
126
+ };
127
+ const disposeResource = (resource) => {
128
+ return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$2);
129
+ };
130
+ //#endregion
131
+ //#region src/scope/runtime.ts
132
+ const storage = new AsyncLocalStorage();
133
+ var ScopeRuntime = class {
134
+ static run(scope, program) {
135
+ return storage.run(scope, program);
136
+ }
137
+ static current() {
138
+ const scope = storage.getStore();
139
+ if (!scope) throw new ScopeRuntimeNotConfiguredError();
140
+ return scope;
141
+ }
142
+ };
143
+ //#endregion
144
+ //#region src/scope/internal.ts
145
+ const notifyCleanupFailure = async (observer, diagnostic) => {
146
+ if (!observer) return;
147
+ try {
148
+ await observer(diagnostic);
149
+ } catch {}
150
+ };
151
+ const runScoped = async (scope, program, options) => {
152
+ let value;
153
+ let programFailed = false;
154
+ let programFailure;
155
+ try {
156
+ value = await ScopeRuntime.run(scope, program);
157
+ } catch (cause) {
158
+ programFailed = true;
159
+ programFailure = cause;
160
+ }
161
+ const outcome = programFailed ? {
162
+ status: "failure",
163
+ cause: programFailure
164
+ } : options.classify(value);
165
+ let cleanupFailed = false;
166
+ let cleanupFailure;
167
+ try {
168
+ await scope.close(outcome);
169
+ } catch (cause) {
170
+ cleanupFailed = true;
171
+ cleanupFailure = cause;
172
+ }
173
+ if (cleanupFailed) {
174
+ const error = cleanupFailure instanceof ScopeCloseError ? cleanupFailure : new ScopeCloseError([cleanupFailure]);
175
+ await notifyCleanupFailure(options.onCleanupFailure, {
176
+ outcome,
177
+ error
178
+ });
179
+ cleanupFailure = error;
180
+ }
181
+ if (programFailed) throw programFailure;
182
+ if (outcome.status === "failure") return value;
183
+ if (cleanupFailed) throw cleanupFailure;
184
+ return value;
185
+ };
186
+ //#endregion
187
+ //#region src/scope/scope.ts
188
+ const SCOPE_SUCCESS$1 = Object.freeze({ status: "success" });
189
+ var ScopeImpl = class ScopeImpl {
190
+ parent;
191
+ children = /* @__PURE__ */ new Set();
192
+ finalizers = [];
193
+ closePromise;
194
+ closeOutcome;
195
+ constructor(parent) {
196
+ this.parent = parent;
197
+ }
198
+ fork() {
199
+ this.assertOpen();
200
+ const child = new ScopeImpl(this);
201
+ this.children.add(child);
202
+ return child;
203
+ }
204
+ addFinalizer(finalizer) {
205
+ this.assertOpen();
206
+ this.finalizers.push(finalizer);
207
+ }
208
+ async acquire(acquire, release) {
209
+ this.assertOpen();
210
+ const resource = await acquire();
211
+ try {
212
+ this.addFinalizer((outcome) => release(resource, outcome));
213
+ return resource;
214
+ } catch (scopeFailure) {
215
+ try {
216
+ await release(resource, this.closeOutcome ?? SCOPE_SUCCESS$1);
217
+ } catch (releaseFailure) {
218
+ throw new AggregateError([scopeFailure, releaseFailure], "Scope closed while acquiring a resource and immediate cleanup also failed");
219
+ }
220
+ throw scopeFailure;
221
+ }
222
+ }
223
+ async add(resource) {
224
+ const finalizer = getDisposeFinalizer(resource);
225
+ if (!finalizer) throw new ResourceNotDisposableError();
226
+ try {
227
+ this.addFinalizer(finalizer);
228
+ return resource;
229
+ } catch (scopeFailure) {
230
+ try {
231
+ await finalizer(this.closeOutcome ?? SCOPE_SUCCESS$1);
232
+ } catch (releaseFailure) {
233
+ throw new AggregateError([scopeFailure, releaseFailure], "Scope closed while adding a disposable resource and cleanup also failed");
234
+ }
235
+ throw scopeFailure;
236
+ }
237
+ }
238
+ close(outcome = SCOPE_SUCCESS$1) {
239
+ if (this.closePromise) return this.closePromise;
240
+ this.closeOutcome = outcome;
241
+ this.closePromise = ScopeRuntime.run(this, () => this.closeInternal(outcome));
242
+ return this.closePromise;
243
+ }
244
+ async closeInternal(outcome) {
245
+ const failures = [];
246
+ const children = [...this.children];
247
+ this.children.clear();
248
+ for (let index = children.length - 1; index >= 0; index--) {
249
+ const child = children[index];
250
+ if (!child) continue;
251
+ try {
252
+ await child.close(outcome);
253
+ } catch (cause) {
254
+ if (cause instanceof ScopeCloseError) failures.push(...cause.causes);
255
+ else failures.push(cause);
256
+ }
257
+ }
258
+ for (let index = this.finalizers.length - 1; index >= 0; index--) {
259
+ const finalizer = this.finalizers[index];
260
+ if (!finalizer) continue;
261
+ try {
262
+ await finalizer(outcome);
263
+ } catch (cause) {
264
+ failures.push(cause);
265
+ }
266
+ }
267
+ this.finalizers.length = 0;
268
+ this.detach();
269
+ if (failures.length > 0) throw new ScopeCloseError(failures);
270
+ }
271
+ detach() {
272
+ const parent = this.parent;
273
+ if (!parent) return;
274
+ parent.children.delete(this);
275
+ this.parent = void 0;
276
+ }
277
+ assertOpen() {
278
+ if (this.closePromise) throw new ScopeClosedError();
279
+ }
280
+ };
281
+ const Scope = {
282
+ make() {
283
+ return new ScopeImpl();
284
+ },
285
+ current() {
286
+ return ScopeRuntime.current();
287
+ },
288
+ provide(scope, program) {
289
+ return ScopeRuntime.run(scope, program);
290
+ },
291
+ *[Symbol.iterator]() {
292
+ return ScopeRuntime.current();
293
+ },
294
+ run(program) {
295
+ const scope = new ScopeImpl();
296
+ return runScoped(scope, () => program(scope), { classify: () => SCOPE_SUCCESS$1 });
297
+ }
298
+ };
299
+ //#endregion
300
+ //#region src/runtime/outcome.ts
301
+ const isResultLike = (value) => typeof value === "object" && value !== null && "status" in value && (value.status === "ok" || value.status === "error");
302
+ const classifyRuntimeOutcome = (value) => {
303
+ if (isResultLike(value) && Result.isError(value)) return {
304
+ status: "failure",
305
+ cause: value.error
306
+ };
307
+ return { status: "success" };
308
+ };
309
+ //#endregion
66
310
  //#region src/layer/runtime.ts
67
- const buildLayer = async (layer, backend) => {
68
- for (const provider of layer.providers) await backend.register(provider);
69
- ServiceRuntime.configure(backend);
70
- return { async dispose() {
311
+ const SCOPE_SUCCESS = Object.freeze({ status: "success" });
312
+ const normalizeDisposeCauses = (cause) => {
313
+ if (cause instanceof AggregateError) return [...cause.errors];
314
+ return [cause];
315
+ };
316
+ const notifyShutdownFailure = async (observer, diagnostic) => {
317
+ if (!observer) return;
318
+ try {
319
+ await observer(diagnostic);
320
+ } catch {}
321
+ };
322
+ const bindProviderToScope = (provider, rootScope) => ({
323
+ service: provider.service,
324
+ acquire: () => ScopeRuntime.run(rootScope, async () => {
325
+ if (!provider.release) return await provider.acquire();
326
+ return await rootScope.acquire(() => provider.acquire(), (resource, outcome) => provider.release(resource, outcome));
327
+ })
328
+ });
329
+ var BuiltLayerImpl = class {
330
+ backend;
331
+ rootScope;
332
+ onCleanupFailure;
333
+ disposePromise;
334
+ executions = /* @__PURE__ */ new Set();
335
+ state = "active";
336
+ constructor(backend, rootScope, onCleanupFailure) {
337
+ this.backend = backend;
338
+ this.rootScope = rootScope;
339
+ this.onCleanupFailure = onCleanupFailure;
340
+ }
341
+ run(program) {
342
+ this.assertActive();
343
+ const executionScope = this.rootScope.fork();
344
+ let resolveExecution;
345
+ let rejectExecution;
346
+ const execution = new Promise((resolve, reject) => {
347
+ resolveExecution = resolve;
348
+ rejectExecution = reject;
349
+ });
350
+ this.executions.add(execution);
351
+ execution.then(() => {
352
+ this.executions.delete(execution);
353
+ }, () => {
354
+ this.executions.delete(execution);
355
+ });
356
+ try {
357
+ this.runExecution(executionScope, program).then((value) => {
358
+ resolveExecution(value);
359
+ }, (cause) => {
360
+ rejectExecution(cause);
361
+ });
362
+ } catch (cause) {
363
+ rejectExecution(cause);
364
+ }
365
+ return execution;
366
+ }
367
+ runExecution(executionScope, program) {
368
+ const options = this.onCleanupFailure ? {
369
+ classify: classifyRuntimeOutcome,
370
+ onCleanupFailure: this.onCleanupFailure
371
+ } : { classify: classifyRuntimeOutcome };
372
+ return runScoped(executionScope, () => ServiceRuntime.run(this.backend, program), options);
373
+ }
374
+ dispose(outcome = SCOPE_SUCCESS) {
375
+ if (this.disposePromise) return this.disposePromise;
376
+ this.state = "disposing";
377
+ const executions = [...this.executions];
378
+ this.disposePromise = this.performDispose(executions, outcome);
379
+ return this.disposePromise;
380
+ }
381
+ async performDispose(executions, outcome) {
382
+ const failures = [];
383
+ await Promise.allSettled(executions);
384
+ try {
385
+ await ServiceRuntime.run(this.backend, () => this.rootScope.close(outcome));
386
+ } catch (cause) {
387
+ failures.push(cause);
388
+ }
389
+ try {
390
+ await this.backend.disposeAll();
391
+ } catch (cause) {
392
+ failures.push(cause);
393
+ }
394
+ this.state = "disposed";
395
+ if (failures.length > 0) {
396
+ const error = new LayerDisposeError(failures.flatMap(normalizeDisposeCauses));
397
+ await notifyShutdownFailure(this.onCleanupFailure, {
398
+ outcome,
399
+ error
400
+ });
401
+ throw error;
402
+ }
403
+ }
404
+ assertActive() {
405
+ if (this.state !== "active") throw new BuiltLayerDisposedError();
406
+ }
407
+ };
408
+ const buildLayer = async (layer, backend, options = {}) => {
409
+ const rootScope = Scope.make();
410
+ let current;
411
+ try {
412
+ for (const provider of layer.providers) {
413
+ current = provider;
414
+ await backend.register(bindProviderToScope(provider, rootScope));
415
+ }
416
+ } catch (registrationCause) {
417
+ const outcome = {
418
+ status: "failure",
419
+ cause: registrationCause
420
+ };
421
+ const cleanupCauses = [];
422
+ try {
423
+ await ServiceRuntime.run(backend, () => rootScope.close(outcome));
424
+ } catch (cause) {
425
+ cleanupCauses.push(cause);
426
+ }
71
427
  try {
72
428
  await backend.disposeAll();
73
- } finally {
74
- ServiceRuntime.reset();
429
+ } catch (cause) {
430
+ cleanupCauses.push(cause);
431
+ }
432
+ if (cleanupCauses.length > 0) {
433
+ const shutdownError = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
434
+ await notifyShutdownFailure(options.onCleanupFailure, {
435
+ outcome,
436
+ error: shutdownError
437
+ });
75
438
  }
76
- } };
439
+ let cleanupCause;
440
+ if (cleanupCauses.length === 1) cleanupCause = cleanupCauses[0];
441
+ else if (cleanupCauses.length > 1) cleanupCause = new LayerDisposeError(cleanupCauses.flatMap(normalizeDisposeCauses));
442
+ throw new LayerRegistrationError(current?.service, registrationCause, cleanupCause);
443
+ }
444
+ return new BuiltLayerImpl(backend, rootScope, options.onCleanupFailure);
77
445
  };
78
- var RuntimeLayer = class {
79
- static build = buildLayer;
446
+ //#endregion
447
+ //#region src/effect/effect.ts
448
+ function gen(body) {
449
+ return Result.gen(body);
450
+ }
451
+ /**
452
+ * Acquire a resource in the current Scope and register its release callback.
453
+ *
454
+ * Acquisition failures are represented in the Effect Result error channel;
455
+ * release failures remain owned by Scope cleanup.
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.
463
+ *
464
+ * Registration failures are represented in the Effect Result error channel;
465
+ * disposal failures remain owned by Scope cleanup.
466
+ */
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
80
475
  };
81
476
  //#endregion
82
477
  //#region src/resource/errors.ts
83
478
  var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
84
479
  //#endregion
85
480
  //#region src/resource/internal.ts
86
- const disposeResource = (resource) => {
87
- const candidate = Object(resource);
88
- return (candidate[Symbol.asyncDispose] ?? candidate[Symbol.dispose])?.call(candidate);
89
- };
90
481
  const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
91
482
  resource,
92
483
  cause,
93
484
  message: `Failed to release resource: ${resource}`
94
485
  });
95
- /**
96
- * Executa uma operação que retorna Result e converte
97
- * exceptions/rejections inesperadas para UnhandledException.
98
- */
99
486
  const runResult = async (operation) => {
100
487
  return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
101
488
  };
@@ -109,31 +496,98 @@ const runRelease = async (name, resource, release) => {
109
496
  catch: (cause) => toReleaseFailure(name, cause)
110
497
  })).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
111
498
  };
112
- /**
113
- * Precedência:
114
- *
115
- * 1. erro ocorrido durante use
116
- * 2. erro ocorrido durante release
117
- * 3. valor produzido por use
118
- */
119
- const combineUseAndRelease = (used, released) => used.andThen((value) => released.map(() => value));
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
+ };
120
516
  //#endregion
121
517
  //#region src/resource/resource.ts
122
- const acquireUseRelease = ({ name, acquire, use, release = disposeResource }) => Result.gen(async function* () {
518
+ const acquireUseRelease = ({ name, acquire, use, release = disposeResource, onReleaseFailure }) => Result.gen(async function* () {
123
519
  const resource = yield* Result.await(runResult(acquire));
124
- /**
125
- * Não usamos yield* aqui.
126
- *
127
- * Se use retornar Err, o generator seria
128
- * encerrado imediatamente e o release
129
- * nunca seria executado.
130
- */
520
+ const scope = Scope.make();
521
+ let released = Result.ok();
522
+ scope.addFinalizer(async () => {
523
+ released = await runRelease(name, resource, release);
524
+ });
131
525
  const used = await runResult(() => use(resource));
132
- const released = await runRelease(name, resource, release);
133
- return combineUseAndRelease(used, released);
526
+ await scope.close();
527
+ return await combineUseAndRelease(used, released, onReleaseFailure);
134
528
  });
135
529
  const Resource = { acquireUseRelease };
136
530
  //#endregion
137
- export { ItiLayerBackend, Layer, Resource, ResourceReleaseFailure, RuntimeLayer, Service, ServiceRuntime, buildLayer };
531
+ //#region src/runtime/runtime.ts
532
+ var Runtime = class Runtime {
533
+ built;
534
+ constructor(built) {
535
+ this.built = built;
536
+ }
537
+ static async make(layer, backend, options = {}) {
538
+ const built = await buildLayer(layer, backend, options);
539
+ return new Runtime(built);
540
+ }
541
+ static async run(layer, backend, program, options = {}) {
542
+ const runtime = await Runtime.make(layer, backend, options);
543
+ let value;
544
+ let executionFailed = false;
545
+ let executionFailure;
546
+ let programOutcome;
547
+ try {
548
+ value = await runtime.runUnchecked(async () => {
549
+ try {
550
+ const programValue = await program();
551
+ programOutcome = classifyRuntimeOutcome(programValue);
552
+ return programValue;
553
+ } catch (cause) {
554
+ programOutcome = {
555
+ status: "failure",
556
+ cause
557
+ };
558
+ throw cause;
559
+ }
560
+ });
561
+ } catch (cause) {
562
+ executionFailed = true;
563
+ executionFailure = cause;
564
+ }
565
+ const outcome = programOutcome ?? {
566
+ status: "failure",
567
+ cause: executionFailure
568
+ };
569
+ try {
570
+ await runtime.disposeWithOutcome(outcome);
571
+ } catch (shutdownFailure) {
572
+ if (!executionFailed && outcome.status === "success") throw shutdownFailure;
573
+ }
574
+ if (executionFailed) throw executionFailure;
575
+ return value;
576
+ }
577
+ run(program) {
578
+ return this.built.run(program);
579
+ }
580
+ runUnchecked(program) {
581
+ return this.built.run(program);
582
+ }
583
+ dispose() {
584
+ return this.built.dispose();
585
+ }
586
+ disposeWithOutcome(outcome) {
587
+ return this.built.dispose(outcome);
588
+ }
589
+ };
590
+ //#endregion
591
+ export { BuiltLayerDisposedError, DuplicateServiceError, Effect, Layer, LayerDisposeError, LayerGeneratorYieldError, LayerRegistrationError, Resource, ResourceNotDisposableError, ResourceReleaseFailure, Runtime, Scope, ScopeCloseError, ScopeClosedError, ScopeRuntimeNotConfiguredError, Service, ServiceNotFoundError, ServiceRuntime, ServiceRuntimeNotConfiguredError, buildLayer };
138
592
 
139
593
  //# sourceMappingURL=index.mjs.map