better-effect 0.9.2 → 0.9.31
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 +95 -2
- package/dist/adapters/iti.d.mts +1 -1
- package/dist/effect-CZdZCZLW.mjs +522 -0
- package/dist/effect-CZdZCZLW.mjs.map +1 -0
- package/dist/hono.d.mts +88 -0
- package/dist/hono.d.mts.map +1 -0
- package/dist/hono.mjs +149 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/{index-BJFBEsm5.d.mts → index-BsPr7qHf.d.mts} +92 -92
- package/dist/index-BsPr7qHf.d.mts.map +1 -0
- package/dist/index-CITM15SE.d.mts +182 -0
- package/dist/index-CITM15SE.d.mts.map +1 -0
- package/dist/{index-CYAgpM_5.d.mts → index-EJlskfAW.d.mts} +4 -2
- package/dist/{index-CYAgpM_5.d.mts.map → index-EJlskfAW.d.mts.map} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -518
- package/dist/index.mjs.map +1 -1
- package/dist/standard-services-QseopL9g.mjs +340 -0
- package/dist/standard-services-QseopL9g.mjs.map +1 -0
- package/dist/standard-services.d.mts +3 -116
- package/dist/standard-services.mjs +3 -180
- package/package.json +7 -1
- package/dist/index-BJFBEsm5.d.mts.map +0 -1
- package/dist/standard-services.d.mts.map +0 -1
- package/dist/standard-services.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -188,6 +188,78 @@ const result = await runtime.runWith(RequestLive, handleRequest)
|
|
|
188
188
|
The request Layer may use root Services, while its scoped providers are closed
|
|
189
189
|
with the execution Scope and never change the shared Runtime environment.
|
|
190
190
|
|
|
191
|
+
### Hono request boundaries
|
|
192
|
+
|
|
193
|
+
The optional `better-effect/hono` entrypoint runs one Runtime execution and
|
|
194
|
+
Scope around each request. Handlers can yield Services directly; the adapter
|
|
195
|
+
provides `CurrentRequest`, forwards `Request.signal`, and converts Results to
|
|
196
|
+
Responses in one policy:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { Hono } from 'hono'
|
|
200
|
+
import { Result } from 'better-result'
|
|
201
|
+
import { HonoEffect } from 'better-effect/hono'
|
|
202
|
+
|
|
203
|
+
const http = HonoEffect.make(runtime, {
|
|
204
|
+
onFailure: (error, c) => c.json({ error: String(error) }, 400)
|
|
205
|
+
})
|
|
206
|
+
const app = new Hono()
|
|
207
|
+
|
|
208
|
+
app.use('*', http.middleware())
|
|
209
|
+
app.get(
|
|
210
|
+
'/work-orders',
|
|
211
|
+
http.gen(async function* () {
|
|
212
|
+
const workOrders = yield* WorkOrderService
|
|
213
|
+
const items = yield* Result.await(workOrders.list())
|
|
214
|
+
return Result.ok(items)
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Hono validators can precede the generator or handler callback. Their validated
|
|
220
|
+
`c.req.valid(...)` inputs are combined and inferred without a manual `Input`
|
|
221
|
+
helper:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { sValidator } from '@hono/standard-validator'
|
|
225
|
+
|
|
226
|
+
app.post(
|
|
227
|
+
'/work-orders',
|
|
228
|
+
http.gen(
|
|
229
|
+
sValidator('json', createWorkOrderSchema),
|
|
230
|
+
async function* (c) {
|
|
231
|
+
const input = c.req.valid('json')
|
|
232
|
+
const workOrders = yield* WorkOrderService
|
|
233
|
+
const workOrder = yield* Result.await(workOrders.create(input))
|
|
234
|
+
|
|
235
|
+
return Result.ok(workOrder)
|
|
236
|
+
},
|
|
237
|
+
{ status: 201 }
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
For multiple validators, pass them in order before the callback:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
app.post(
|
|
246
|
+
'/work-orders/:id',
|
|
247
|
+
http.gen(validateParam, validateHeader, validateCreateWorkOrder, async function* (c) {
|
|
248
|
+
const id = c.req.valid('param').id
|
|
249
|
+
const key = c.req.valid('header')['X-Idempotency-Key']
|
|
250
|
+
const input = c.req.valid('json')
|
|
251
|
+
return Result.ok({ id, key, input })
|
|
252
|
+
})
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The validator middleware runs before the Program and short-circuits with its
|
|
257
|
+
own `Response` when validation fails. `http.handler` accepts the same ordered
|
|
258
|
+
validator arguments followed by the program factory and options.
|
|
259
|
+
|
|
260
|
+
Install `hono` only when this subpath is used. The main entrypoint does not
|
|
261
|
+
load the framework.
|
|
262
|
+
|
|
191
263
|
Service and Scope access share one `RuntimeContext`. Node/Bun uses
|
|
192
264
|
`AsyncLocalStorage` by default; hosts without transparent async context can
|
|
193
265
|
pass `contextStorage: new ExplicitRuntimeContextStorage()` from
|
|
@@ -358,8 +430,29 @@ await runtime.dispose()
|
|
|
358
430
|
```
|
|
359
431
|
|
|
360
432
|
The entrypoint also provides `Random`/`RandomSeeded`, `Logger`/`LoggerTest`,
|
|
361
|
-
`CurrentRequest`, and the compatible `CurrentAbortSignal` bridge. None
|
|
362
|
-
installed implicitly; compose a normal Layer or use the provided test
|
|
433
|
+
`Config`, `CurrentRequest`, and the compatible `CurrentAbortSignal` bridge. None
|
|
434
|
+
is installed implicitly; compose a normal Layer or use the provided test
|
|
435
|
+
helpers.
|
|
436
|
+
|
|
437
|
+
For typed environment configuration, bind a Standard Schema directly to a
|
|
438
|
+
reusable descriptor:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import { Result } from 'better-result'
|
|
442
|
+
import { Effect } from 'better-effect'
|
|
443
|
+
import { Config } from 'better-effect/standard-services'
|
|
444
|
+
|
|
445
|
+
const AppConfig = Config.fromEnv({ schema: EnvSchema, dotEnvPath: '.env' })
|
|
446
|
+
|
|
447
|
+
const program = Effect.fn(async function* () {
|
|
448
|
+
const config = yield* AppConfig
|
|
449
|
+
return Result.ok(config)
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Use `Config.schema(schema)` with `Config.layer(source)` or
|
|
454
|
+
`Config.layerFromEnv(options)` when several descriptors should share an
|
|
455
|
+
explicitly replaceable provider.
|
|
363
456
|
|
|
364
457
|
---
|
|
365
458
|
|
package/dist/adapters/iti.d.mts
CHANGED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { a as runRuntimeContext, i as makeRuntimeContext, n as currentRuntimeContext, r as getRuntimeContext, t as activeRuntimeContextStorage } from "./context-B4yO5LaH.mjs";
|
|
2
|
+
import { t as isPromiseLike } from "./runtime-CDcCF5cb.mjs";
|
|
3
|
+
import { Result } from "better-result";
|
|
4
|
+
//#region src/scope/errors.ts
|
|
5
|
+
/** Thrown when Scope context is accessed outside an active Scope execution. */
|
|
6
|
+
var ScopeRuntimeNotConfiguredError = class extends Error {
|
|
7
|
+
constructor() {
|
|
8
|
+
super("No Scope is available in the current execution context");
|
|
9
|
+
this.name = "ScopeRuntimeNotConfiguredError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/** Thrown when a resource or finalizer is added after Scope closure begins. */
|
|
13
|
+
var ScopeClosedError = class extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Cannot add resources or finalizers to a closed Scope");
|
|
16
|
+
this.name = "ScopeClosedError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/** Aggregates finalizer failures encountered while closing a Scope. */
|
|
20
|
+
var ScopeCloseError = class extends Error {
|
|
21
|
+
causes;
|
|
22
|
+
constructor(causes) {
|
|
23
|
+
super(`Failed to close Scope (${causes.length} finalizer${causes.length === 1 ? "" : "s"} failed)`);
|
|
24
|
+
this.causes = causes;
|
|
25
|
+
this.name = "ScopeCloseError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Thrown when a value has neither Symbol.dispose nor Symbol.asyncDispose. */
|
|
29
|
+
var ResourceNotDisposableError = class extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("Resource does not implement Symbol.dispose or Symbol.asyncDispose");
|
|
32
|
+
this.name = "ResourceNotDisposableError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/scope/disposable.ts
|
|
37
|
+
const SCOPE_SUCCESS$1 = { status: "success" };
|
|
38
|
+
/** Return a Scope finalizer for a value's async or sync disposal protocol. */
|
|
39
|
+
const getDisposeFinalizer = (resource) => {
|
|
40
|
+
const candidate = Object(resource);
|
|
41
|
+
const asyncDispose = candidate[Symbol.asyncDispose];
|
|
42
|
+
if (asyncDispose instanceof Function) return () => asyncDispose.call(resource);
|
|
43
|
+
const dispose = candidate[Symbol.dispose];
|
|
44
|
+
if (dispose instanceof Function) return () => dispose.call(resource);
|
|
45
|
+
};
|
|
46
|
+
/** Dispose a value immediately when it implements a disposal protocol. */
|
|
47
|
+
const disposeResource = (resource) => {
|
|
48
|
+
return getDisposeFinalizer(resource)?.(SCOPE_SUCCESS$1);
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/scope/runtime.ts
|
|
52
|
+
const scopeStorages = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
/** Bridges the current Scope through async execution context. */
|
|
54
|
+
var ScopeRuntime = class {
|
|
55
|
+
/** Supply a Scope while invoking a callback. */
|
|
56
|
+
static run(scope, program, storage = scopeStorages.get(scope) ?? activeRuntimeContextStorage()) {
|
|
57
|
+
scopeStorages.set(scope, storage);
|
|
58
|
+
const current = getRuntimeContext(storage);
|
|
59
|
+
const context = makeRuntimeContext(current?.resolver, scope, current?.resolutionPath ?? [], current?.signal);
|
|
60
|
+
return runRuntimeContext(storage, context, program);
|
|
61
|
+
}
|
|
62
|
+
/** Return the Scope active in the current execution context. */
|
|
63
|
+
static current() {
|
|
64
|
+
let context;
|
|
65
|
+
try {
|
|
66
|
+
context = currentRuntimeContext();
|
|
67
|
+
} catch {
|
|
68
|
+
throw new ScopeRuntimeNotConfiguredError();
|
|
69
|
+
}
|
|
70
|
+
if (!context.scope) throw new ScopeRuntimeNotConfiguredError();
|
|
71
|
+
return context.scope;
|
|
72
|
+
}
|
|
73
|
+
/** Associate a Runtime-owned Scope with its context storage. */
|
|
74
|
+
static bind(scope, storage) {
|
|
75
|
+
scopeStorages.set(scope, storage);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/scope/internal.ts
|
|
80
|
+
const notifyCleanupFailure = async (observer, diagnostic) => {
|
|
81
|
+
if (!observer) return;
|
|
82
|
+
try {
|
|
83
|
+
await observer(diagnostic);
|
|
84
|
+
} catch {}
|
|
85
|
+
};
|
|
86
|
+
const runScoped = async (scope, program, options) => {
|
|
87
|
+
let value;
|
|
88
|
+
let programFailed = false;
|
|
89
|
+
let programFailure;
|
|
90
|
+
try {
|
|
91
|
+
const run = () => ScopeRuntime.run(scope, program, options.contextStorage);
|
|
92
|
+
value = await (options.context && options.contextStorage ? runRuntimeContext(options.contextStorage, options.context, run) : run());
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
programFailed = true;
|
|
95
|
+
programFailure = cause;
|
|
96
|
+
}
|
|
97
|
+
const outcome = programFailed ? {
|
|
98
|
+
status: "failure",
|
|
99
|
+
cause: programFailure
|
|
100
|
+
} : options.classify(value);
|
|
101
|
+
let cleanupFailed = false;
|
|
102
|
+
let cleanupFailure;
|
|
103
|
+
try {
|
|
104
|
+
await scope.close(outcome);
|
|
105
|
+
} catch (cause) {
|
|
106
|
+
cleanupFailed = true;
|
|
107
|
+
cleanupFailure = cause;
|
|
108
|
+
}
|
|
109
|
+
if (cleanupFailed) {
|
|
110
|
+
const error = cleanupFailure instanceof ScopeCloseError ? cleanupFailure : new ScopeCloseError([cleanupFailure]);
|
|
111
|
+
await notifyCleanupFailure(options.onCleanupFailure, {
|
|
112
|
+
outcome,
|
|
113
|
+
error
|
|
114
|
+
});
|
|
115
|
+
cleanupFailure = error;
|
|
116
|
+
}
|
|
117
|
+
if (programFailed) throw programFailure;
|
|
118
|
+
if (outcome.status === "failure") return value;
|
|
119
|
+
if (cleanupFailed) throw cleanupFailure;
|
|
120
|
+
return value;
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/scope/scope.ts
|
|
124
|
+
const SCOPE_SUCCESS = Object.freeze({ status: "success" });
|
|
125
|
+
var ScopeImpl = class ScopeImpl {
|
|
126
|
+
parent;
|
|
127
|
+
children = /* @__PURE__ */ new Set();
|
|
128
|
+
finalizers = [];
|
|
129
|
+
closePromise;
|
|
130
|
+
closeOutcome;
|
|
131
|
+
constructor(parent) {
|
|
132
|
+
this.parent = parent;
|
|
133
|
+
}
|
|
134
|
+
fork() {
|
|
135
|
+
this.assertOpen();
|
|
136
|
+
const child = new ScopeImpl(this);
|
|
137
|
+
this.children.add(child);
|
|
138
|
+
return child;
|
|
139
|
+
}
|
|
140
|
+
addFinalizer(finalizer) {
|
|
141
|
+
this.assertOpen();
|
|
142
|
+
this.finalizers.push(finalizer);
|
|
143
|
+
}
|
|
144
|
+
async acquire(acquire, release) {
|
|
145
|
+
this.assertOpen();
|
|
146
|
+
const resource = await acquire();
|
|
147
|
+
try {
|
|
148
|
+
this.addFinalizer((outcome) => release(resource, outcome));
|
|
149
|
+
return resource;
|
|
150
|
+
} catch (scopeFailure) {
|
|
151
|
+
try {
|
|
152
|
+
await release(resource, this.closeOutcome ?? SCOPE_SUCCESS);
|
|
153
|
+
} catch (releaseFailure) {
|
|
154
|
+
throw new AggregateError([scopeFailure, releaseFailure], "Scope closed while acquiring a resource and immediate cleanup also failed");
|
|
155
|
+
}
|
|
156
|
+
throw scopeFailure;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async add(resource) {
|
|
160
|
+
const finalizer = getDisposeFinalizer(resource);
|
|
161
|
+
if (!finalizer) throw new ResourceNotDisposableError();
|
|
162
|
+
try {
|
|
163
|
+
this.addFinalizer(finalizer);
|
|
164
|
+
return resource;
|
|
165
|
+
} catch (scopeFailure) {
|
|
166
|
+
try {
|
|
167
|
+
await finalizer(this.closeOutcome ?? SCOPE_SUCCESS);
|
|
168
|
+
} catch (releaseFailure) {
|
|
169
|
+
throw new AggregateError([scopeFailure, releaseFailure], "Scope closed while adding a disposable resource and cleanup also failed");
|
|
170
|
+
}
|
|
171
|
+
throw scopeFailure;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
close(outcome = SCOPE_SUCCESS) {
|
|
175
|
+
if (this.closePromise) return this.closePromise;
|
|
176
|
+
this.closeOutcome = outcome;
|
|
177
|
+
this.closePromise = ScopeRuntime.run(this, () => this.closeInternal(outcome));
|
|
178
|
+
return this.closePromise;
|
|
179
|
+
}
|
|
180
|
+
async closeInternal(outcome) {
|
|
181
|
+
const failures = [];
|
|
182
|
+
const children = [...this.children];
|
|
183
|
+
this.children.clear();
|
|
184
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
185
|
+
const child = children[index];
|
|
186
|
+
if (!child) continue;
|
|
187
|
+
try {
|
|
188
|
+
await child.close(outcome);
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
if (cause instanceof ScopeCloseError) failures.push(...cause.causes);
|
|
191
|
+
else failures.push(cause);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (let index = this.finalizers.length - 1; index >= 0; index--) {
|
|
195
|
+
const finalizer = this.finalizers[index];
|
|
196
|
+
if (!finalizer) continue;
|
|
197
|
+
try {
|
|
198
|
+
await finalizer(outcome);
|
|
199
|
+
} catch (cause) {
|
|
200
|
+
failures.push(cause);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
this.finalizers.length = 0;
|
|
204
|
+
this.detach();
|
|
205
|
+
if (failures.length > 0) throw new ScopeCloseError(failures);
|
|
206
|
+
}
|
|
207
|
+
detach() {
|
|
208
|
+
const parent = this.parent;
|
|
209
|
+
if (!parent) return;
|
|
210
|
+
parent.children.delete(this);
|
|
211
|
+
this.parent = void 0;
|
|
212
|
+
}
|
|
213
|
+
assertOpen() {
|
|
214
|
+
if (this.closePromise) throw new ScopeClosedError();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const Scope = {
|
|
218
|
+
/** Create an owned, initially open Scope. */
|
|
219
|
+
make() {
|
|
220
|
+
return new ScopeImpl();
|
|
221
|
+
},
|
|
222
|
+
/** Return the non-owning Scope available in the current execution context. */
|
|
223
|
+
current() {
|
|
224
|
+
return ScopeRuntime.current();
|
|
225
|
+
},
|
|
226
|
+
/** Run a callback with an existing Scope supplied as the current context. */
|
|
227
|
+
provide(scope, program) {
|
|
228
|
+
return ScopeRuntime.run(scope, program);
|
|
229
|
+
},
|
|
230
|
+
/** Resolve the current Scope through `yield* Scope` inside an Effect. */
|
|
231
|
+
*[Symbol.iterator]() {
|
|
232
|
+
return ScopeRuntime.current();
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* Run a program in a newly owned Scope.
|
|
236
|
+
*
|
|
237
|
+
* Scope is independent from `better-result`, so returned values—including
|
|
238
|
+
* `Result.err`—close this Scope with a successful outcome. Result-aware
|
|
239
|
+
* outcome classification belongs to `Runtime.run`.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* await Scope.run(async (scope) => {
|
|
244
|
+
* const connection = await scope.acquire(connect, (connection) => connection.close())
|
|
245
|
+
* return connection.query()
|
|
246
|
+
* })
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
run(program) {
|
|
250
|
+
const scope = new ScopeImpl();
|
|
251
|
+
return runScoped(scope, () => program(scope), { classify: () => SCOPE_SUCCESS });
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/effect/combinators.ts
|
|
256
|
+
const asResult = (value) => {
|
|
257
|
+
return value;
|
|
258
|
+
};
|
|
259
|
+
const mapResult = (result, fn) => {
|
|
260
|
+
return Result.map(result, fn);
|
|
261
|
+
};
|
|
262
|
+
const mapErrorResult = (result, fn) => {
|
|
263
|
+
return Result.mapError(result, fn);
|
|
264
|
+
};
|
|
265
|
+
const andThenResult = (result, next) => {
|
|
266
|
+
const resultNext = next;
|
|
267
|
+
return Result.andThen(result, resultNext);
|
|
268
|
+
};
|
|
269
|
+
const andThenAsyncResult = (result, next) => {
|
|
270
|
+
const resultNext = (value) => {
|
|
271
|
+
return Promise.resolve(next(value));
|
|
272
|
+
};
|
|
273
|
+
return Result.andThenAsync(result, resultNext);
|
|
274
|
+
};
|
|
275
|
+
function map(first, second) {
|
|
276
|
+
if (first instanceof Function && second === void 0) {
|
|
277
|
+
const callback = first;
|
|
278
|
+
return (effect) => {
|
|
279
|
+
return map(effect, callback);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const fn = second;
|
|
283
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
|
|
284
|
+
return mapResult(result, fn);
|
|
285
|
+
});
|
|
286
|
+
return mapResult(first, fn);
|
|
287
|
+
}
|
|
288
|
+
function mapError(first, second) {
|
|
289
|
+
if (first instanceof Function && second === void 0) {
|
|
290
|
+
const callback = first;
|
|
291
|
+
return (effect) => {
|
|
292
|
+
return mapError(effect, callback);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const fn = second;
|
|
296
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
|
|
297
|
+
return mapErrorResult(result, fn);
|
|
298
|
+
});
|
|
299
|
+
return mapErrorResult(first, fn);
|
|
300
|
+
}
|
|
301
|
+
function andThen(first, second) {
|
|
302
|
+
if (first instanceof Function && second === void 0) {
|
|
303
|
+
const callback = first;
|
|
304
|
+
return (effect) => {
|
|
305
|
+
return andThen(effect, callback);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return andThenResult(first, second);
|
|
309
|
+
}
|
|
310
|
+
function andThenAsync(first, second) {
|
|
311
|
+
if (first instanceof Function && second === void 0) {
|
|
312
|
+
const callback = first;
|
|
313
|
+
return (effect) => {
|
|
314
|
+
return andThenAsync(effect, callback);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const next = second;
|
|
318
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => {
|
|
319
|
+
return andThenAsyncResult(result, next);
|
|
320
|
+
});
|
|
321
|
+
return andThenAsyncResult(first, next);
|
|
322
|
+
}
|
|
323
|
+
const tapResult = (result, fn) => Result.tap(result, fn);
|
|
324
|
+
const tapErrorResult = (result, fn) => Result.tapError(result, fn);
|
|
325
|
+
const tapBothResult = (result, handlers) => Result.tapBoth(result, handlers);
|
|
326
|
+
const recoverResult = (result, fn) => Result.tryRecover(result, fn);
|
|
327
|
+
const recoverAsyncResult = (result, fn) => Result.tryRecoverAsync(result, (error) => Promise.resolve(fn(error)));
|
|
328
|
+
const flattenResult = (result) => Result.flatten(result);
|
|
329
|
+
const matchResult = (result, handlers) => Result.match(result, handlers);
|
|
330
|
+
const allResult = (results) => Result.all(results);
|
|
331
|
+
function tap(first, second) {
|
|
332
|
+
if (first instanceof Function && second === void 0) {
|
|
333
|
+
const callback = first;
|
|
334
|
+
return (effect) => tap(effect, callback);
|
|
335
|
+
}
|
|
336
|
+
if (second === void 0) throw new TypeError("Effect.tap requires a callback");
|
|
337
|
+
const fn = second;
|
|
338
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => tapResult(asResult(result), fn));
|
|
339
|
+
return tapResult(asResult(first), fn);
|
|
340
|
+
}
|
|
341
|
+
function tapError(first, second) {
|
|
342
|
+
if (first instanceof Function && second === void 0) {
|
|
343
|
+
const callback = first;
|
|
344
|
+
return (effect) => tapError(effect, callback);
|
|
345
|
+
}
|
|
346
|
+
if (second === void 0) throw new TypeError("Effect.tapError requires a callback");
|
|
347
|
+
const fn = second;
|
|
348
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => tapErrorResult(asResult(result), fn));
|
|
349
|
+
return tapErrorResult(asResult(first), fn);
|
|
350
|
+
}
|
|
351
|
+
function tapBoth(first, second) {
|
|
352
|
+
if (second === void 0) return (effect) => tapBoth(effect, first);
|
|
353
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => tapBothResult(result, second));
|
|
354
|
+
return tapBothResult(asResult(first), second);
|
|
355
|
+
}
|
|
356
|
+
function recover(first, second) {
|
|
357
|
+
if (first instanceof Function && second === void 0) {
|
|
358
|
+
const callback = first;
|
|
359
|
+
return (effect) => recover(effect, callback);
|
|
360
|
+
}
|
|
361
|
+
if (second === void 0) throw new TypeError("Effect.recover requires a callback");
|
|
362
|
+
const fn = second;
|
|
363
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => recoverResult(asResult(result), fn));
|
|
364
|
+
return recoverResult(asResult(first), fn);
|
|
365
|
+
}
|
|
366
|
+
function recoverAsync(first, second) {
|
|
367
|
+
if (first instanceof Function && second === void 0) {
|
|
368
|
+
const callback = first;
|
|
369
|
+
return (effect) => recoverAsync(effect, callback);
|
|
370
|
+
}
|
|
371
|
+
if (second === void 0) throw new TypeError("Effect.recoverAsync requires a callback");
|
|
372
|
+
const fn = second;
|
|
373
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => recoverAsyncResult(asResult(result), fn));
|
|
374
|
+
return recoverAsyncResult(asResult(first), fn);
|
|
375
|
+
}
|
|
376
|
+
/** Remove one nested Result/Effect layer. */
|
|
377
|
+
function flatten(effect) {
|
|
378
|
+
return flattenResult(asResult(effect));
|
|
379
|
+
}
|
|
380
|
+
function as(first, second) {
|
|
381
|
+
if (arguments.length < 2) return (effect) => as(effect, first);
|
|
382
|
+
return mapResult(asResult(first), () => second);
|
|
383
|
+
}
|
|
384
|
+
/** Replace a successful value with void. */
|
|
385
|
+
function asVoid(effect) {
|
|
386
|
+
return mapResult(asResult(effect), () => void 0);
|
|
387
|
+
}
|
|
388
|
+
function match(first, second) {
|
|
389
|
+
if (isPromiseLike(first)) return Promise.resolve(first).then((result) => match(asResult(result), second));
|
|
390
|
+
return matchResult(asResult(first), second);
|
|
391
|
+
}
|
|
392
|
+
/** Collect already-created Effects in input order. */
|
|
393
|
+
function all(results) {
|
|
394
|
+
return allResult(results);
|
|
395
|
+
}
|
|
396
|
+
/** Combine two already-created Effects in input order. */
|
|
397
|
+
function zip(left, right) {
|
|
398
|
+
return Result.all([left, right]);
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/effect/effect.ts
|
|
402
|
+
const runResultGenerator = Result.gen;
|
|
403
|
+
function gen(body) {
|
|
404
|
+
return runResultGenerator(body);
|
|
405
|
+
}
|
|
406
|
+
function fn(body) {
|
|
407
|
+
const program = () => runResultGenerator(body);
|
|
408
|
+
return program;
|
|
409
|
+
}
|
|
410
|
+
const validateProgramConcurrency = (concurrency) => {
|
|
411
|
+
if (concurrency !== void 0 && (!Number.isFinite(concurrency) || !Number.isInteger(concurrency) || concurrency <= 0)) throw new RangeError("Program.all concurrency must be a positive integer");
|
|
412
|
+
};
|
|
413
|
+
/** Build a lazy Program collection with optional bounded concurrency. */
|
|
414
|
+
function programAll(programs, options = {}) {
|
|
415
|
+
validateProgramConcurrency(options.concurrency);
|
|
416
|
+
const concurrency = options.concurrency;
|
|
417
|
+
const program = async () => {
|
|
418
|
+
const results = Array.from({ length: programs.length });
|
|
419
|
+
const failures = Array.from({ length: programs.length }, () => false);
|
|
420
|
+
const causes = Array.from({ length: programs.length });
|
|
421
|
+
let nextIndex = 0;
|
|
422
|
+
const worker = async () => {
|
|
423
|
+
while (true) {
|
|
424
|
+
const index = nextIndex++;
|
|
425
|
+
if (index >= programs.length) return;
|
|
426
|
+
try {
|
|
427
|
+
results[index] = await programs[index]();
|
|
428
|
+
} catch (cause) {
|
|
429
|
+
failures[index] = true;
|
|
430
|
+
causes[index] = cause;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const workers = Math.min(concurrency ?? programs.length, programs.length);
|
|
435
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
436
|
+
const failureIndex = failures.findIndex(Boolean);
|
|
437
|
+
if (failureIndex >= 0) throw causes[failureIndex];
|
|
438
|
+
return Result.all(results);
|
|
439
|
+
};
|
|
440
|
+
return program;
|
|
441
|
+
}
|
|
442
|
+
/** Value-level namespace for lazy Program combinators. */
|
|
443
|
+
const Program = { all: programAll };
|
|
444
|
+
/**
|
|
445
|
+
* Acquire a resource in the current Scope and register its release callback.
|
|
446
|
+
*
|
|
447
|
+
* Acquisition failures are represented in the Effect Result error channel;
|
|
448
|
+
* release failures remain owned by Scope cleanup. The release callback
|
|
449
|
+
* receives the final outcome chosen by the enclosing execution boundary.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```ts
|
|
453
|
+
* const connection = yield* Effect.acquireRelease(
|
|
454
|
+
* () => pool.connect(),
|
|
455
|
+
* (connection, outcome) => connection.close(outcome)
|
|
456
|
+
* )
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
function acquireRelease(acquire, release) {
|
|
460
|
+
const scope = Scope.current();
|
|
461
|
+
return Result.await(Result.tryPromise(() => scope.acquire(acquire, release)));
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Register an already-acquired disposable resource in the current Scope.
|
|
465
|
+
*
|
|
466
|
+
* The resource is not acquired by this helper. Registration failures are
|
|
467
|
+
* represented in the Effect Result error channel; disposal failures remain
|
|
468
|
+
* owned by Scope cleanup.
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```ts
|
|
472
|
+
* const file = yield* Effect.add(await openFile('notes.txt'))
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
function add(resource) {
|
|
476
|
+
const scope = Scope.current();
|
|
477
|
+
return Result.await(Result.tryPromise(() => scope.add(resource)));
|
|
478
|
+
}
|
|
479
|
+
const Effect = {
|
|
480
|
+
/** Compose a generator-based Effect program. */
|
|
481
|
+
gen,
|
|
482
|
+
/** Build a lazy Program from a generator. */
|
|
483
|
+
fn,
|
|
484
|
+
/** Acquire and register a resource in the current Scope. */
|
|
485
|
+
acquireRelease,
|
|
486
|
+
/** Register an already-acquired disposable in the current Scope. */
|
|
487
|
+
add,
|
|
488
|
+
/** Map a successful Effect result. */
|
|
489
|
+
map,
|
|
490
|
+
/** Map an Effect error. */
|
|
491
|
+
mapError,
|
|
492
|
+
/** Chain a synchronous Effect result. */
|
|
493
|
+
andThen,
|
|
494
|
+
/** Chain an asynchronous Effect result. */
|
|
495
|
+
andThenAsync,
|
|
496
|
+
/** Observe successful values without changing the Result. */
|
|
497
|
+
tap,
|
|
498
|
+
/** Observe error values without changing the Result. */
|
|
499
|
+
tapError,
|
|
500
|
+
/** Observe the active Result branch without changing the Result. */
|
|
501
|
+
tapBoth,
|
|
502
|
+
/** Recover an error with another Effect. */
|
|
503
|
+
recover,
|
|
504
|
+
/** Recover an error asynchronously with another Effect. */
|
|
505
|
+
recoverAsync,
|
|
506
|
+
/** Remove one nested Effect layer. */
|
|
507
|
+
flatten,
|
|
508
|
+
/** Replace a successful value. */
|
|
509
|
+
as,
|
|
510
|
+
/** Replace a successful value with void. */
|
|
511
|
+
asVoid,
|
|
512
|
+
/** Match either Result branch. */
|
|
513
|
+
match,
|
|
514
|
+
/** Collect Effects in input order. */
|
|
515
|
+
all,
|
|
516
|
+
/** Zip two Effects in input order. */
|
|
517
|
+
zip
|
|
518
|
+
};
|
|
519
|
+
//#endregion
|
|
520
|
+
export { ScopeRuntime as a, ScopeCloseError as c, runScoped as i, ScopeClosedError as l, Program as n, disposeResource as o, Scope as r, ResourceNotDisposableError as s, Effect as t, ScopeRuntimeNotConfiguredError as u };
|
|
521
|
+
|
|
522
|
+
//# sourceMappingURL=effect-CZdZCZLW.mjs.map
|