better-effect 0.9.31 → 0.11.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/LICENSE +21 -0
- package/README.md +277 -16
- package/dist/adapters/iti.d.mts +10 -5
- package/dist/adapters/iti.d.mts.map +1 -1
- package/dist/adapters/iti.mjs +30 -5
- package/dist/adapters/iti.mjs.map +1 -1
- package/dist/context-BUEf1qjL.mjs +152 -0
- package/dist/context-BUEf1qjL.mjs.map +1 -0
- package/dist/effect-2ZcGZI8A.mjs +513 -0
- package/dist/effect-2ZcGZI8A.mjs.map +1 -0
- package/dist/errors-BKcsQ0ip.d.mts +12 -0
- package/dist/errors-BKcsQ0ip.d.mts.map +1 -0
- package/dist/hono.d.mts +39 -21
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +25 -20
- package/dist/hono.mjs.map +1 -1
- package/dist/{index-BsPr7qHf.d.mts → index-C7Qild0_.d.mts} +158 -28
- package/dist/index-C7Qild0_.d.mts.map +1 -0
- package/dist/{index-CITM15SE.d.mts → index-DStz0JMN.d.mts} +4 -4
- package/dist/{index-CITM15SE.d.mts.map → index-DStz0JMN.d.mts.map} +1 -1
- package/dist/{index-1NLNdkJy.d.mts → index-heuaRmXR.d.mts} +8 -8
- package/dist/{index-1NLNdkJy.d.mts.map → index-heuaRmXR.d.mts.map} +1 -1
- package/dist/{index-EJlskfAW.d.mts → index-rQhZk3Nt.d.mts} +130 -23
- package/dist/index-rQhZk3Nt.d.mts.map +1 -0
- package/dist/index.d.mts +5 -6
- package/dist/index.mjs +6 -540
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/explicit.d.mts +11 -7
- package/dist/runtime/explicit.d.mts.map +1 -1
- package/dist/runtime/explicit.mjs +96 -23
- package/dist/runtime/explicit.mjs.map +1 -1
- package/dist/runtime/node.d.mts +2 -2
- package/dist/runtime/node.d.mts.map +1 -1
- package/dist/runtime/node.mjs +10 -6
- package/dist/runtime/node.mjs.map +1 -1
- package/dist/runtime-DnMn0X0X.mjs +613 -0
- package/dist/runtime-DnMn0X0X.mjs.map +1 -0
- package/dist/scope-GGnmTQck.mjs +245 -0
- package/dist/scope-GGnmTQck.mjs.map +1 -0
- package/dist/{signal-Cl9tqGyX.mjs → signal-B97cs85Z.mjs} +86 -6
- package/dist/signal-B97cs85Z.mjs.map +1 -0
- package/dist/{standard-services-QseopL9g.mjs → standard-services-BFBq-4lo.mjs} +2 -2
- package/dist/{standard-services-QseopL9g.mjs.map → standard-services-BFBq-4lo.mjs.map} +1 -1
- package/dist/standard-services.d.mts +2 -2
- package/dist/standard-services.mjs +2 -2
- package/dist/testing.d.mts +198 -2
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +976 -2
- package/dist/testing.mjs.map +1 -0
- package/package.json +6 -3
- package/dist/context-B4yO5LaH.mjs +0 -80
- package/dist/context-B4yO5LaH.mjs.map +0 -1
- package/dist/effect-CZdZCZLW.mjs +0 -522
- package/dist/effect-CZdZCZLW.mjs.map +0 -1
- package/dist/errors-BXKc7juX.d.mts +0 -8
- package/dist/errors-BXKc7juX.d.mts.map +0 -1
- package/dist/index-BsPr7qHf.d.mts.map +0 -1
- package/dist/index-EJlskfAW.d.mts.map +0 -1
- package/dist/map-layer-backend-DMJauecV.d.mts +0 -42
- package/dist/map-layer-backend-DMJauecV.d.mts.map +0 -1
- package/dist/map-layer-backend-gal-mcRv.mjs +0 -53
- package/dist/map-layer-backend-gal-mcRv.mjs.map +0 -1
- package/dist/signal-Cl9tqGyX.mjs.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Alves
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -10,6 +10,10 @@ Use Services directly inside `Effect.fn` Programs (or eager `Effect.gen` workflo
|
|
|
10
10
|
bun add better-effect better-result
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
The published Runtime entrypoint is officially supported on Node.js and Bun.
|
|
14
|
+
The tested runtime matrix is Node.js 24 and Bun 1.3.14, and the default runtime
|
|
15
|
+
context uses Node/Bun async context propagation.
|
|
16
|
+
|
|
13
17
|
## TypeScript knows what your application needs
|
|
14
18
|
|
|
15
19
|
```ts
|
|
@@ -95,6 +99,40 @@ const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
|
|
|
95
99
|
const runtime = await Runtime.make(AppLive)
|
|
96
100
|
```
|
|
97
101
|
|
|
102
|
+
Use `Layer.empty` when a composition root intentionally has no providers. It is
|
|
103
|
+
stable and has the exact `Layer<never, never>` type:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const EmptyLive = Layer.empty
|
|
107
|
+
const runtime = await Runtime.make(EmptyLive)
|
|
108
|
+
await runtime.run(() => 'no Services required')
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For a port/adapter boundary, `Layer.alias` exposes one compatible implementation
|
|
112
|
+
under another Service token without constructing, cloning, or proxying it:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
class SqlUserRepository extends Service<SqlUserRepository>()('SqlUserRepository') {
|
|
116
|
+
findById(id: string): string {
|
|
117
|
+
return `sql:${id}`
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class UserRepository extends Service<UserRepository>()('UserRepository') {
|
|
122
|
+
declare findById: SqlUserRepository['findById']
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const UserRepositoryPort = Layer.alias({
|
|
126
|
+
from: SqlUserRepository,
|
|
127
|
+
to: UserRepository
|
|
128
|
+
})
|
|
129
|
+
const ApplicationLive = Layer.merge(Layer.empty, Layer.make(SqlUserRepository), UserRepositoryPort)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The alias lazily resolves `from`, returns the same object under `to`, and checks
|
|
133
|
+
that the source satisfies the target's `Service.Contract`. Its source remains an
|
|
134
|
+
external Layer requirement until the source provider is composed.
|
|
135
|
+
|
|
98
136
|
And the contract does not disappear after startup.
|
|
99
137
|
|
|
100
138
|
A Runtime also knows which Services exist in its environment:
|
|
@@ -109,20 +147,117 @@ const inspectDatabase = Effect.fn(async function* () {
|
|
|
109
147
|
await runtime.run(inspectDatabase)
|
|
110
148
|
```
|
|
111
149
|
|
|
150
|
+
Runtime boundaries inspect only nominal `better-result` values: `Result.err`
|
|
151
|
+
becomes a failed execution, while a plain object with `status: 'error'` is
|
|
152
|
+
still a successful value. Intermediate Results do not close the execution
|
|
153
|
+
Scope.
|
|
154
|
+
|
|
112
155
|
`Effect.gen` remains eager for code that already runs inside a resolver and
|
|
113
156
|
Scope. `Effect.fn` captures the generator as a lazy `Program` for Runtime
|
|
114
157
|
boundaries; the callback form remains supported for compatibility.
|
|
115
158
|
|
|
116
159
|
`Program.all` keeps a collection lazy until the returned Program is run. Pass
|
|
117
160
|
`{ concurrency: n }` for a positive bounded FIFO worker pool; values retain
|
|
118
|
-
input order
|
|
119
|
-
|
|
161
|
+
input order. If a Program returns an error or throws, scheduling stops, already-
|
|
162
|
+
started Programs are allowed to settle, and the deterministic primary failure remains selected;
|
|
163
|
+
there is no cancellation or Fiber scheduler.
|
|
164
|
+
|
|
165
|
+
Use `Program.forEach` when each item needs a lazy Program factory. The callback
|
|
166
|
+
receives the item and its input index, and the returned Program produces a
|
|
167
|
+
readonly collection in input order:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const synchronized = Program.forEach(userIds, (userId, index) => synchronizeUser(userId, index), {
|
|
171
|
+
concurrency: 8
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const result = await runtime.run(synchronized)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use `Program.allResults` when typed validation errors should be retained rather
|
|
178
|
+
than short-circuiting. It returns every exact child `Result` as a successful
|
|
179
|
+
collection element; defects still stop new work and reject the outer Program:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const validations = Program.allResults(
|
|
183
|
+
[validateIdentity, validateAddress, validateDocuments] as const,
|
|
184
|
+
{ concurrency: 3 }
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
const results = await runtime.run(validations)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Both helpers use the same lazy bounded scheduler as `Program.all`: indexes are
|
|
191
|
+
claimed in order, output order is stable, and already-started work is always
|
|
192
|
+
allowed to settle.
|
|
193
|
+
|
|
194
|
+
Use `Effect.*` to transform an already-created Result, and `Program.*` to compose
|
|
195
|
+
an `Effect.fn` Program without starting it. `Program.map`, `mapError`, `tap`, and
|
|
196
|
+
`tapError` preserve that laziness; `andThen` and `recover` accept an Effect, a
|
|
197
|
+
Promise of an Effect, or another Program only after their matching Result branch
|
|
198
|
+
is selected. `Program.andThen` unions its source and continuation error and
|
|
199
|
+
Service requirement channels. `Program.recover` handles and removes the source
|
|
200
|
+
`Err` channel, exposes the recovery error channel, and unions the source and
|
|
201
|
+
recovery Service requirements. Taps preserve the original Result object on
|
|
202
|
+
success.
|
|
120
203
|
|
|
121
204
|
`Runtime.make(AppLive)` and `Runtime.run(AppLive, program)` use the built-in
|
|
122
205
|
`MapLayerBackend`. Pass `{ backend: new ItiLayerBackend() }` when an external
|
|
123
206
|
container is needed; `MemoryLayerBackend` remains its compatibility alias from
|
|
124
207
|
`better-effect/testing`.
|
|
125
208
|
|
|
209
|
+
### Verify custom adapters
|
|
210
|
+
|
|
211
|
+
`better-effect/testing` provides runner-neutral conformance scenarios for
|
|
212
|
+
third-party `LayerBackend` and `RuntimeContextStorage` implementations. Each
|
|
213
|
+
scenario has a stable `name` and `run` callback, creates a fresh adapter, and
|
|
214
|
+
runs the optional adapter cleanup after every assertion outcome. Backends must
|
|
215
|
+
synchronously pass the actual readonly pending acquisition Promise collection to
|
|
216
|
+
`disposeAll`'s `onPendingAcquisitions` hook, await the callback, then await the
|
|
217
|
+
acquisitions before clearing state. Declare the backend's acquisition-failure
|
|
218
|
+
policy explicitly: `MapLayerBackend` retries,
|
|
219
|
+
while ITI keeps an asynchronous failure cached until disposal.
|
|
220
|
+
|
|
221
|
+
Register the scenarios with Bun:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { describe, test } from 'bun:test'
|
|
225
|
+
import { MapLayerBackend } from 'better-effect'
|
|
226
|
+
import { layerBackendContract } from 'better-effect/testing'
|
|
227
|
+
|
|
228
|
+
describe('My backend', () => {
|
|
229
|
+
for (const scenario of layerBackendContract({
|
|
230
|
+
makeBackend: () => new MapLayerBackend(),
|
|
231
|
+
acquisitionFailure: 'retry'
|
|
232
|
+
})) {
|
|
233
|
+
test(scenario.name, scenario.run)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The same scenarios work with Vitest without adding a runner dependency to the
|
|
239
|
+
published entrypoint:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { describe, it } from 'vitest'
|
|
243
|
+
import { NodeRuntimeContextStorage } from 'better-effect/runtime/node'
|
|
244
|
+
import { runtimeContextStorageContract } from 'better-effect/testing'
|
|
245
|
+
|
|
246
|
+
describe('My context storage', () => {
|
|
247
|
+
for (const scenario of runtimeContextStorageContract({
|
|
248
|
+
makeStorage: () => new NodeRuntimeContextStorage(),
|
|
249
|
+
concurrency: 'concurrent'
|
|
250
|
+
})) {
|
|
251
|
+
it(scenario.name, scenario.run)
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Use `concurrency: 'sequential'` for a storage that rejects overlapping roots
|
|
257
|
+
with `RuntimeContextOverlapError`; pass `makeCompanionStorage` to also verify
|
|
258
|
+
that it does not leak frames into a Node or explicit storage. See the adapter
|
|
259
|
+
guide for the complete `LayerBackend` contract.
|
|
260
|
+
|
|
126
261
|
Runtimes are async disposables, so request-scoped code can use:
|
|
127
262
|
|
|
128
263
|
```ts
|
|
@@ -136,6 +271,42 @@ Or let `Runtime.use` own the lifetime:
|
|
|
136
271
|
const result = await Runtime.use(AppLive, (runtime) => runtime.run(program))
|
|
137
272
|
```
|
|
138
273
|
|
|
274
|
+
For isolated application tests, use the testing facade over that same Layer and
|
|
275
|
+
Runtime. It installs only the controlled Services you pass, records lifecycle
|
|
276
|
+
events, and disposes automatically:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { ClockTest, LoggerTest, TestRuntime } from 'better-effect/testing'
|
|
280
|
+
|
|
281
|
+
const logger = new LoggerTest()
|
|
282
|
+
const result = await TestRuntime.use(
|
|
283
|
+
AppLive,
|
|
284
|
+
{
|
|
285
|
+
overrides: [DatabaseTest],
|
|
286
|
+
clock: new ClockTest(Date.UTC(2026, 0, 1)),
|
|
287
|
+
logger
|
|
288
|
+
},
|
|
289
|
+
async (test) => {
|
|
290
|
+
const value = await test.run(loadDashboard)
|
|
291
|
+
expect(test.observer.executionEnds).toHaveLength(1)
|
|
292
|
+
return value
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
expect(logger.events).toHaveLength(1)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
A long-lived test boundary supports integration-style request Layers as well:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
await using test = await TestRuntime.make(AppLive, { overrides: [DatabaseTest] })
|
|
303
|
+
const result = await test.runWith(RequestLive, handleRequest)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`TestRuntime.use` preserves program-vs-cleanup failure precedence. The default
|
|
307
|
+
recorder is available as `test.observer`; use `test.runtime` only when an
|
|
308
|
+
advanced test explicitly needs the underlying Runtime.
|
|
309
|
+
|
|
139
310
|
Layer providers remain lazy unless startup validation is requested. Warm them
|
|
140
311
|
all before accepting work with either form:
|
|
141
312
|
|
|
@@ -166,9 +337,38 @@ const runtime = await Runtime.make(AppLive, {
|
|
|
166
337
|
})
|
|
167
338
|
```
|
|
168
339
|
|
|
340
|
+
For lifecycle assertions, `better-effect/testing` provides a recorder and a
|
|
341
|
+
best-effort composition utility:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { RecordedRuntimeObserver, RuntimeObserver } from 'better-effect/testing'
|
|
345
|
+
|
|
346
|
+
const recorded = RecordedRuntimeObserver.make()
|
|
347
|
+
const runtime = await Runtime.make(AppTest, {
|
|
348
|
+
observers: [
|
|
349
|
+
RuntimeObserver.compose(recorded, {
|
|
350
|
+
onExecutionEnd: ({ outcome }) => console.debug(outcome.status)
|
|
351
|
+
})
|
|
352
|
+
]
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await runtime.run(program)
|
|
356
|
+
const snapshot = recorded.snapshot()
|
|
357
|
+
expect(snapshot.executionEnds).toHaveLength(1)
|
|
358
|
+
expect(snapshot.timeline).toContain(snapshot.executionEnds[0])
|
|
359
|
+
|
|
360
|
+
await runtime.dispose()
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`RecordedRuntimeObserver` preserves event identity in immutable category views
|
|
364
|
+
and its ordered `timeline`; call `clear()` to reuse it. Composition invokes
|
|
365
|
+
observers in declaration order and isolates thrown or rejected observer
|
|
366
|
+
failures from the Runtime result.
|
|
367
|
+
|
|
169
368
|
Cancellation is cooperative and uses `AbortSignal`; no scheduler or fibers are
|
|
170
369
|
created. Pass a signal to one execution and read it from the program when an
|
|
171
|
-
I/O operation supports cancellation
|
|
370
|
+
I/O operation supports cancellation. Runtime disposal waits for active work;
|
|
371
|
+
it does not forcibly terminate arbitrary Promises:
|
|
172
372
|
|
|
173
373
|
```ts
|
|
174
374
|
const result = await runtime.run(program, { signal: request.signal })
|
|
@@ -186,7 +386,9 @@ const result = await runtime.runWith(RequestLive, handleRequest)
|
|
|
186
386
|
```
|
|
187
387
|
|
|
188
388
|
The request Layer may use root Services, while its scoped providers are closed
|
|
189
|
-
with the execution Scope and never change the shared Runtime environment.
|
|
389
|
+
with the execution Scope and never change the shared Runtime environment. Its
|
|
390
|
+
external requirements must be provided by the Runtime, and the Hono failure
|
|
391
|
+
handler/request-Layer types are checked at the adapter boundary.
|
|
190
392
|
|
|
191
393
|
### Hono request boundaries
|
|
192
394
|
|
|
@@ -201,7 +403,7 @@ import { Result } from 'better-result'
|
|
|
201
403
|
import { HonoEffect } from 'better-effect/hono'
|
|
202
404
|
|
|
203
405
|
const http = HonoEffect.make(runtime, {
|
|
204
|
-
onFailure: (
|
|
406
|
+
onFailure: (_error, c) => c.json({ error: 'Request failed' }, 400)
|
|
205
407
|
})
|
|
206
408
|
const app = new Hono()
|
|
207
409
|
|
|
@@ -216,9 +418,9 @@ app.get(
|
|
|
216
418
|
)
|
|
217
419
|
```
|
|
218
420
|
|
|
219
|
-
Hono validators can precede the generator or handler callback
|
|
220
|
-
`c.req.valid(...)` inputs are
|
|
221
|
-
helper:
|
|
421
|
+
One or more Hono validators can precede the generator or handler callback, in
|
|
422
|
+
the order they should run. Their validated `c.req.valid(...)` inputs are
|
|
423
|
+
combined and inferred without a manual `Input` helper:
|
|
222
424
|
|
|
223
425
|
```ts
|
|
224
426
|
import { sValidator } from '@hono/standard-validator'
|
|
@@ -258,12 +460,21 @@ own `Response` when validation fails. `http.handler` accepts the same ordered
|
|
|
258
460
|
validator arguments followed by the program factory and options.
|
|
259
461
|
|
|
260
462
|
Install `hono` only when this subpath is used. The main entrypoint does not
|
|
261
|
-
load the framework.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
`
|
|
265
|
-
|
|
266
|
-
|
|
463
|
+
load the framework. The default Hono failure policy redacts every non-`Response`
|
|
464
|
+
failure value to `{ error: 'Internal Server Error' }` with status 500. An
|
|
465
|
+
explicitly returned `Response` failure is intentionally passed through
|
|
466
|
+
unchanged. A custom `onFailure` policy should serialize only safe, intentional
|
|
467
|
+
domain details.
|
|
468
|
+
|
|
469
|
+
Service and Scope access share one `RuntimeContext`. The published Runtime
|
|
470
|
+
entrypoint is officially supported on Node.js and Bun, where `AsyncLocalStorage`
|
|
471
|
+
is the default context storage. The `better-effect/runtime/explicit` subpath
|
|
472
|
+
provides `ExplicitRuntimeContextStorage` as a manually managed, sequential
|
|
473
|
+
strategy only when the package entrypoint and host can load it. One instance
|
|
474
|
+
supports one non-overlapping async flow and rejects concurrent overlap. Using
|
|
475
|
+
it does not make the package generally usable in browsers, Deno, Cloudflare
|
|
476
|
+
Workers, or other non-Node hosts; separate explicit instances are not a general
|
|
477
|
+
concurrent-isolation strategy.
|
|
267
478
|
|
|
268
479
|
If a program asks that Runtime for a Service its environment does not provide, TypeScript rejects the call.
|
|
269
480
|
|
|
@@ -386,7 +597,9 @@ Some dependencies are values.
|
|
|
386
597
|
|
|
387
598
|
Others own connections, sessions, files or other resources.
|
|
388
599
|
|
|
389
|
-
`Layer.scoped`, `Layer.scopedGen`, `
|
|
600
|
+
`Layer.scoped`, `Layer.scopedGen`, `Layer.scopedDisposable`, `Effect.acquireRelease`,
|
|
601
|
+
`Effect.acquireReleaseResult`, `Effect.acquireDisposable`, `Effect.add` and `Scope` make their
|
|
602
|
+
lifetime explicit.
|
|
390
603
|
|
|
391
604
|
```ts
|
|
392
605
|
const DatabaseLive = Layer.scoped(
|
|
@@ -400,6 +613,35 @@ Runtime owns the application lifetime and safely releases scoped resources when
|
|
|
400
613
|
|
|
401
614
|
Resources acquired during an individual execution belong to that execution instead.
|
|
402
615
|
|
|
616
|
+
When an existing API already returns a `Result`, keep its typed failure channel while
|
|
617
|
+
registering only successful acquisitions:
|
|
618
|
+
|
|
619
|
+
```ts
|
|
620
|
+
const connection =
|
|
621
|
+
yield *
|
|
622
|
+
Effect.acquireReleaseResult(
|
|
623
|
+
() => pool.connect(),
|
|
624
|
+
(connection, outcome) => connection.close(outcome)
|
|
625
|
+
)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
An `Err` is returned unchanged and is never released. Thrown or rejected acquisition
|
|
629
|
+
defects use the normal `UnhandledException` channel; release failures remain Scope
|
|
630
|
+
cleanup failures rather than widening the acquisition error type.
|
|
631
|
+
|
|
632
|
+
For values that implement JavaScript disposal, use the disposable helpers instead of
|
|
633
|
+
repeating a release callback. Async disposal is preferred when both protocols exist:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
const file = yield * Effect.acquireDisposable(() => openFile(path))
|
|
637
|
+
|
|
638
|
+
const DatabaseLive = Layer.scopedDisposable(Database, () => Database.connect())
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
`Effect.acquireDisposable` belongs to the current execution Scope. `Layer.scopedDisposable`
|
|
642
|
+
keeps the client alive across executions and disposes it with the Runtime root; the DI
|
|
643
|
+
backend never owns that release.
|
|
644
|
+
|
|
403
645
|
### Keep your runtime choices
|
|
404
646
|
|
|
405
647
|
`better-effect` is not a replacement implementation of Effect.
|
|
@@ -550,7 +792,26 @@ still rejects it when its Layer does not provide every required Service.
|
|
|
550
792
|
|
|
551
793
|
Observation helpers such as `Effect.tap`, `Effect.tapError`, and `Effect.tapBoth`
|
|
552
794
|
run only the active branch and return the original Result, so logging or metrics
|
|
553
|
-
do not change the pipeline's value or requirement channel.
|
|
795
|
+
do not change the pipeline's value or requirement channel. The async variants
|
|
796
|
+
`Effect.tapAsync`, `Effect.tapErrorAsync`, and `Effect.tapBothAsync` accept
|
|
797
|
+
`PromiseLike<void>` observers, always return a Promise, and preserve the source
|
|
798
|
+
requirements. They delegate branch selection and defect handling to
|
|
799
|
+
`better-result`; only the active observer runs, and a successful observation
|
|
800
|
+
returns the exact original Result. They do not create a Scope or resolve
|
|
801
|
+
Services inside the callback.
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
const audited = pipe(
|
|
805
|
+
loadUser(userId),
|
|
806
|
+
Effect.tapAsync((user) => metrics.recordUserLoaded(user.id)),
|
|
807
|
+
Effect.tapErrorAsync((error) => metrics.recordUserFailure(error))
|
|
808
|
+
)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`Effect.matchError` exhaustively maps a tagged `Err` union, while
|
|
812
|
+
`Effect.matchErrorPartial` maps selected tags and retains unhandled variants in
|
|
813
|
+
the resulting error union. Both delegate to `better-result`'s tagged-error
|
|
814
|
+
matchers and preserve the source success and requirement channels.
|
|
554
815
|
|
|
555
816
|
Use `Effect.recover` or `Effect.recoverAsync` for an explicit fallback Result;
|
|
556
817
|
the fallback is evaluated only when the input is an `Err`, and its Service
|
package/dist/adapters/iti.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { N as AnyServiceToken } from "../index-
|
|
2
|
-
import "../index-
|
|
3
|
-
import { a as LayerRegistration, n as LayerBackend } from "../map-layer-backend-DMJauecV.mjs";
|
|
1
|
+
import { N as AnyServiceToken } from "../index-heuaRmXR.mjs";
|
|
2
|
+
import { v as LayerBackend, x as LayerRegistration, y as LayerBackendDisposeOptions } from "../index-rQhZk3Nt.mjs";
|
|
4
3
|
//#region src/adapters/iti.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* ITI-backed Layer backend.
|
|
@@ -12,13 +11,19 @@ declare class ItiLayerBackend implements LayerBackend {
|
|
|
12
11
|
private container;
|
|
13
12
|
private readonly keys;
|
|
14
13
|
private readonly registered;
|
|
14
|
+
/**
|
|
15
|
+
* Track async gets so disposal cannot reset ITI while a provider is acquiring.
|
|
16
|
+
* ITI caches rejected acquisitions; replacing the container is the explicit
|
|
17
|
+
* retry boundary for that sticky failure behavior.
|
|
18
|
+
*/
|
|
19
|
+
private readonly pending;
|
|
15
20
|
private keyFor;
|
|
16
21
|
/** Register a Layer provider under its deterministic Service-tag key. */
|
|
17
22
|
register(registration: LayerRegistration): void;
|
|
18
23
|
/** Resolve a registered Service through the ITI container. */
|
|
19
24
|
resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>;
|
|
20
|
-
/**
|
|
21
|
-
disposeAll(): Promise<void>;
|
|
25
|
+
/** Reset container-owned ITI state; Scope owns Layer provider releases. */
|
|
26
|
+
disposeAll(options?: LayerBackendDisposeOptions): Promise<void>;
|
|
22
27
|
}
|
|
23
28
|
//#endregion
|
|
24
29
|
export { ItiLayerBackend };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"iti.d.mts","names":[],"sources":["../../src/adapters/iti.ts"],"mappings":";;;;;;;;;cAuBa,2BAA2B;UAC9B;mBAES;mBAEA;;;;;;mBAOA;UAET;;EAgBR,SAAS,cAAc;;EAuBvB,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;;EAiCnF,WAAW,UAAU,6BAA6B"}
|
package/dist/adapters/iti.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as ServiceTagCollisionError, c as ServiceNotFoundError, t as DuplicateServiceError } from "../errors-Dnjhzbt0.mjs";
|
|
2
|
-
import { t as assertServiceCompatibility } from "../internal-identity-DmUpBeeL.mjs";
|
|
3
2
|
import { t as isPromiseLike } from "../runtime-CDcCF5cb.mjs";
|
|
3
|
+
import { t as assertServiceCompatibility } from "../internal-identity-DmUpBeeL.mjs";
|
|
4
4
|
import { createContainer } from "iti";
|
|
5
5
|
//#region src/adapters/iti.ts
|
|
6
6
|
/**
|
|
@@ -13,6 +13,12 @@ var ItiLayerBackend = class {
|
|
|
13
13
|
container = createContainer();
|
|
14
14
|
keys = /* @__PURE__ */ new Map();
|
|
15
15
|
registered = /* @__PURE__ */ new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Track async gets so disposal cannot reset ITI while a provider is acquiring.
|
|
18
|
+
* ITI caches rejected acquisitions; replacing the container is the explicit
|
|
19
|
+
* retry boundary for that sticky failure behavior.
|
|
20
|
+
*/
|
|
21
|
+
pending = /* @__PURE__ */ new Map();
|
|
16
22
|
keyFor(token) {
|
|
17
23
|
const tag = token.serviceTag;
|
|
18
24
|
const existing = this.keys.get(tag);
|
|
@@ -42,12 +48,31 @@ var ItiLayerBackend = class {
|
|
|
42
48
|
assertServiceCompatibility(token, registered, instance);
|
|
43
49
|
return instance;
|
|
44
50
|
};
|
|
45
|
-
if (isPromiseLike(resolved))
|
|
51
|
+
if (isPromiseLike(resolved)) {
|
|
52
|
+
const pending = Promise.resolve(resolved).then(validate);
|
|
53
|
+
this.pending.set(pending, registered);
|
|
54
|
+
pending.then(() => this.pending.delete(pending), () => this.pending.delete(pending));
|
|
55
|
+
return pending;
|
|
56
|
+
}
|
|
46
57
|
return validate(resolved);
|
|
47
58
|
}
|
|
48
|
-
/**
|
|
49
|
-
async disposeAll() {
|
|
50
|
-
|
|
59
|
+
/** Reset container-owned ITI state; Scope owns Layer provider releases. */
|
|
60
|
+
async disposeAll(options) {
|
|
61
|
+
const container = this.container;
|
|
62
|
+
const acquisitions = [...this.pending.keys()];
|
|
63
|
+
try {
|
|
64
|
+
if (acquisitions.length > 0) {
|
|
65
|
+
const observePending = options?.onPendingAcquisitions;
|
|
66
|
+
if (observePending) await observePending(acquisitions);
|
|
67
|
+
await Promise.allSettled(acquisitions);
|
|
68
|
+
}
|
|
69
|
+
await container.disposeAll();
|
|
70
|
+
} finally {
|
|
71
|
+
this.container = createContainer();
|
|
72
|
+
this.registered.clear();
|
|
73
|
+
this.keys.clear();
|
|
74
|
+
this.pending.clear();
|
|
75
|
+
}
|
|
51
76
|
}
|
|
52
77
|
};
|
|
53
78
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iti.mjs","names":[],"sources":["../../src/adapters/iti.ts"],"sourcesContent":["import { createContainer } from 'iti'\n\nimport {\n DuplicateServiceError,\n ServiceTagCollisionError,\n type LayerBackend,\n type LayerRegistration\n} from '../layer'\n\nimport { ServiceNotFoundError, type AnyServiceToken } from '../service'\n\nimport { assertServiceCompatibility } from '../layer/internal-identity'\nimport { isPromiseLike } from '../utils/runtime'\n\ntype LayerAcquiredValue = Awaited<ReturnType<LayerRegistration['acquire']>>\n\n/**\n * ITI-backed Layer backend.\n *\n * Install `iti` as the optional peer dependency and pass an instance to\n * `Runtime.make` when using ITI's container implementation.\n */\nexport class ItiLayerBackend implements LayerBackend {\n private container: any = createContainer()\n\n private readonly keys = new Map<string, string>()\n\n private readonly registered = new Map<string, AnyServiceToken>()\n\n private keyFor(token: AnyServiceToken): string {\n const tag = token.serviceTag\n const existing = this.keys.get(tag)\n\n if (existing) {\n return existing\n }\n\n const key = `better-effect:${tag}`\n\n this.keys.set(tag, key)\n\n return key\n }\n\n /** Register a Layer provider under its deterministic Service-tag key. */\n register(registration: LayerRegistration): void {\n const token = registration.service\n const tag = token.serviceTag\n const existing = this.registered.get(tag)\n\n if (existing === token) {\n throw new DuplicateServiceError(token)\n }\n\n if (existing) {\n throw new ServiceTagCollisionError(existing, token)\n }\n\n const key = this.keyFor(token)\n\n this.container = this.container.add({\n [key]: registration.acquire\n })\n\n this.registered.set(tag, token)\n }\n\n /** Resolve a registered Service through the ITI container. */\n resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>> {\n const registered = this.registered.get(token.serviceTag)\n\n if (registered === undefined) {\n throw new ServiceNotFoundError(token)\n }\n\n const key = this.keyFor(token)\n const resolved = this.container.get(key)\n\n const validate = (instance: LayerAcquiredValue): InstanceType<T> => {\n assertServiceCompatibility(token, registered, instance)\n\n // SAFETY: The registered tag and compatibility check establish the constructor-to-instance relationship after ITI erases it.\n return instance as InstanceType<T>\n }\n\n if (isPromiseLike(resolved)) {\n
|
|
1
|
+
{"version":3,"file":"iti.mjs","names":[],"sources":["../../src/adapters/iti.ts"],"sourcesContent":["import { createContainer } from 'iti'\n\nimport {\n DuplicateServiceError,\n ServiceTagCollisionError,\n type LayerBackend,\n type LayerBackendDisposeOptions,\n type LayerRegistration\n} from '../layer'\n\nimport { ServiceNotFoundError, type AnyServiceToken } from '../service'\n\nimport { assertServiceCompatibility } from '../layer/internal-identity'\nimport { isPromiseLike } from '../utils/runtime'\n\ntype LayerAcquiredValue = Awaited<ReturnType<LayerRegistration['acquire']>>\n\n/**\n * ITI-backed Layer backend.\n *\n * Install `iti` as the optional peer dependency and pass an instance to\n * `Runtime.make` when using ITI's container implementation.\n */\nexport class ItiLayerBackend implements LayerBackend {\n private container: any = createContainer()\n\n private readonly keys = new Map<string, string>()\n\n private readonly registered = new Map<string, AnyServiceToken>()\n\n /**\n * Track async gets so disposal cannot reset ITI while a provider is acquiring.\n * ITI caches rejected acquisitions; replacing the container is the explicit\n * retry boundary for that sticky failure behavior.\n */\n private readonly pending = new Map<Promise<unknown>, AnyServiceToken>()\n\n private keyFor(token: AnyServiceToken): string {\n const tag = token.serviceTag\n const existing = this.keys.get(tag)\n\n if (existing) {\n return existing\n }\n\n const key = `better-effect:${tag}`\n\n this.keys.set(tag, key)\n\n return key\n }\n\n /** Register a Layer provider under its deterministic Service-tag key. */\n register(registration: LayerRegistration): void {\n const token = registration.service\n const tag = token.serviceTag\n const existing = this.registered.get(tag)\n\n if (existing === token) {\n throw new DuplicateServiceError(token)\n }\n\n if (existing) {\n throw new ServiceTagCollisionError(existing, token)\n }\n\n const key = this.keyFor(token)\n\n this.container = this.container.add({\n [key]: registration.acquire\n })\n\n this.registered.set(tag, token)\n }\n\n /** Resolve a registered Service through the ITI container. */\n resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>> {\n const registered = this.registered.get(token.serviceTag)\n\n if (registered === undefined) {\n throw new ServiceNotFoundError(token)\n }\n\n const key = this.keyFor(token)\n const resolved = this.container.get(key)\n\n const validate = (instance: LayerAcquiredValue): InstanceType<T> => {\n assertServiceCompatibility(token, registered, instance)\n\n // SAFETY: The registered tag and compatibility check establish the constructor-to-instance relationship after ITI erases it.\n return instance as InstanceType<T>\n }\n\n if (isPromiseLike(resolved)) {\n const pending = Promise.resolve(resolved).then(validate)\n\n this.pending.set(pending, registered)\n void pending.then(\n () => this.pending.delete(pending),\n () => this.pending.delete(pending)\n )\n\n return pending\n }\n\n return validate(resolved)\n }\n\n /** Reset container-owned ITI state; Scope owns Layer provider releases. */\n async disposeAll(options?: LayerBackendDisposeOptions): Promise<void> {\n const container = this.container\n const acquisitions = [...this.pending.keys()]\n\n try {\n if (acquisitions.length > 0) {\n const observePending = options?.onPendingAcquisitions\n\n if (observePending) {\n await observePending(acquisitions)\n }\n\n await Promise.allSettled(acquisitions)\n }\n await container.disposeAll()\n } finally {\n this.container = createContainer()\n this.registered.clear()\n this.keys.clear()\n this.pending.clear()\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAuBA,IAAa,kBAAb,MAAqD;CACnD,YAAyB,gBAAgB;CAEzC,uBAAwB,IAAI,IAAoB;CAEhD,6BAA8B,IAAI,IAA6B;;;;;;CAO/D,0BAA2B,IAAI,IAAuC;CAEtE,OAAe,OAAgC;EAC7C,MAAM,MAAM,MAAM;EAClB,MAAM,WAAW,KAAK,KAAK,IAAI,GAAG;EAElC,IAAI,UACF,OAAO;EAGT,MAAM,MAAM,iBAAiB;EAE7B,KAAK,KAAK,IAAI,KAAK,GAAG;EAEtB,OAAO;CACT;;CAGA,SAAS,cAAuC;EAC9C,MAAM,QAAQ,aAAa;EAC3B,MAAM,MAAM,MAAM;EAClB,MAAM,WAAW,KAAK,WAAW,IAAI,GAAG;EAExC,IAAI,aAAa,OACf,MAAM,IAAI,sBAAsB,KAAK;EAGvC,IAAI,UACF,MAAM,IAAI,yBAAyB,UAAU,KAAK;EAGpD,MAAM,MAAM,KAAK,OAAO,KAAK;EAE7B,KAAK,YAAY,KAAK,UAAU,IAAI,GACjC,MAAM,aAAa,QACtB,CAAC;EAED,KAAK,WAAW,IAAI,KAAK,KAAK;CAChC;;CAGA,QAAmC,OAA0D;EAC3F,MAAM,aAAa,KAAK,WAAW,IAAI,MAAM,UAAU;EAEvD,IAAI,eAAe,KAAA,GACjB,MAAM,IAAI,qBAAqB,KAAK;EAGtC,MAAM,MAAM,KAAK,OAAO,KAAK;EAC7B,MAAM,WAAW,KAAK,UAAU,IAAI,GAAG;EAEvC,MAAM,YAAY,aAAkD;GAClE,2BAA2B,OAAO,YAAY,QAAQ;GAGtD,OAAO;EACT;EAEA,IAAI,cAAc,QAAQ,GAAG;GAC3B,MAAM,UAAU,QAAQ,QAAQ,QAAQ,CAAC,CAAC,KAAK,QAAQ;GAEvD,KAAK,QAAQ,IAAI,SAAS,UAAU;GACpC,QAAa,WACL,KAAK,QAAQ,OAAO,OAAO,SAC3B,KAAK,QAAQ,OAAO,OAAO,CACnC;GAEA,OAAO;EACT;EAEA,OAAO,SAAS,QAAQ;CAC1B;;CAGA,MAAM,WAAW,SAAqD;EACpE,MAAM,YAAY,KAAK;EACvB,MAAM,eAAe,CAAC,GAAG,KAAK,QAAQ,KAAK,CAAC;EAE5C,IAAI;GACF,IAAI,aAAa,SAAS,GAAG;IAC3B,MAAM,iBAAiB,SAAS;IAEhC,IAAI,gBACF,MAAM,eAAe,YAAY;IAGnC,MAAM,QAAQ,WAAW,YAAY;GACvC;GACA,MAAM,UAAU,WAAW;EAC7B,UAAU;GACR,KAAK,YAAY,gBAAgB;GACjC,KAAK,WAAW,MAAM;GACtB,KAAK,KAAK,MAAM;GAChB,KAAK,QAAQ,MAAM;EACrB;CACF;AACF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { t as isPromiseLike } from "./runtime-CDcCF5cb.mjs";
|
|
2
|
+
//#region src/runtime/errors.ts
|
|
3
|
+
/** Thrown when no RuntimeContext is active in the selected storage. */
|
|
4
|
+
var RuntimeContextNotConfiguredError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("No RuntimeContext is available in the current execution context");
|
|
7
|
+
this.name = "RuntimeContextNotConfiguredError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
/** Thrown when ExplicitRuntimeContextStorage cannot safely overlap lineages. */
|
|
11
|
+
var RuntimeContextOverlapError = class extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("ExplicitRuntimeContextStorage does not support overlapping root or derived context runs; use NodeRuntimeContextStorage for concurrent async branches");
|
|
14
|
+
this.name = "RuntimeContextOverlapError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/runtime/context.ts
|
|
19
|
+
const unconfiguredRuntimeContextStorage = {
|
|
20
|
+
run: (_context, program) => program(),
|
|
21
|
+
current: () => {
|
|
22
|
+
throw new RuntimeContextNotConfiguredError();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const contextLineages = /* @__PURE__ */ new WeakMap();
|
|
26
|
+
/** Associate a derived context with the root context of its execution lineage. */
|
|
27
|
+
const inheritRuntimeContextLineage = (context, parent) => {
|
|
28
|
+
const lineage = contextLineages.get(parent) ?? parent;
|
|
29
|
+
contextLineages.set(context, lineage);
|
|
30
|
+
return context;
|
|
31
|
+
};
|
|
32
|
+
/** Return the root context associated with a contextual value, when known. */
|
|
33
|
+
const getRuntimeContextLineage = (context) => contextLineages.get(context);
|
|
34
|
+
/** Associate a newly-entered root context with itself. */
|
|
35
|
+
const setRuntimeContextLineage = (context) => {
|
|
36
|
+
contextLineages.set(context, context);
|
|
37
|
+
return context;
|
|
38
|
+
};
|
|
39
|
+
const hasPathPrefix = (parent, child) => child.length >= parent.length && parent.every((token, index) => child[index] === token);
|
|
40
|
+
/** Recognize contexts created by a nested Service, Scope, or Layer operation. */
|
|
41
|
+
const isDerivedRuntimeContext = (parent, child) => parent.resolver !== void 0 && child.resolver === parent.resolver && parent.resolutionPath.length > 0 && hasPathPrefix(parent.resolutionPath, child.resolutionPath) && (child.resolutionPath.length > parent.resolutionPath.length || child.scope !== parent.scope);
|
|
42
|
+
function makeRuntimeContext(resolver, scope, resolutionPath, signal, parent) {
|
|
43
|
+
const context = { resolutionPath };
|
|
44
|
+
if (resolver !== void 0) Object.assign(context, { resolver });
|
|
45
|
+
if (scope !== void 0) Object.assign(context, { scope });
|
|
46
|
+
if (signal !== void 0) Object.assign(context, { signal });
|
|
47
|
+
if (parent !== void 0) inheritRuntimeContextLineage(context, parent);
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
let defaultStorage = unconfiguredRuntimeContextStorage;
|
|
51
|
+
let fallbackFrame;
|
|
52
|
+
const fallbackFrames = /* @__PURE__ */ new Set();
|
|
53
|
+
const settleWithRuntimeStorage = (value, restore) => {
|
|
54
|
+
let promiseLike;
|
|
55
|
+
try {
|
|
56
|
+
promiseLike = isPromiseLike(value);
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
restore();
|
|
59
|
+
throw cause;
|
|
60
|
+
}
|
|
61
|
+
if (!promiseLike) {
|
|
62
|
+
restore();
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return Promise.resolve(value).then((resolved) => {
|
|
67
|
+
restore();
|
|
68
|
+
return resolved;
|
|
69
|
+
}, (cause) => {
|
|
70
|
+
restore();
|
|
71
|
+
throw cause;
|
|
72
|
+
});
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
restore();
|
|
75
|
+
throw cause;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const restoreFallbackFrame = (frame) => {
|
|
79
|
+
fallbackFrames.delete(frame);
|
|
80
|
+
if (fallbackFrame !== frame) return;
|
|
81
|
+
let parent = frame.parent;
|
|
82
|
+
while (parent !== void 0 && !fallbackFrames.has(parent)) parent = parent.parent;
|
|
83
|
+
fallbackFrame = parent;
|
|
84
|
+
};
|
|
85
|
+
const fallbackRuntimeContextFrameCarrier = {
|
|
86
|
+
run(frame, program) {
|
|
87
|
+
fallbackFrames.add(frame);
|
|
88
|
+
fallbackFrame = frame;
|
|
89
|
+
const restore = () => restoreFallbackFrame(frame);
|
|
90
|
+
try {
|
|
91
|
+
return settleWithRuntimeStorage(program(), restore);
|
|
92
|
+
} catch (cause) {
|
|
93
|
+
restore();
|
|
94
|
+
throw cause;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
current: () => fallbackFrame
|
|
98
|
+
};
|
|
99
|
+
let runtimeContextFrameCarrier = fallbackRuntimeContextFrameCarrier;
|
|
100
|
+
/** Install host async-context propagation for Runtime context frames. */
|
|
101
|
+
const setRuntimeContextFrameCarrier = (carrier) => {
|
|
102
|
+
runtimeContextFrameCarrier = carrier;
|
|
103
|
+
};
|
|
104
|
+
/** Return whether a host async-context carrier has been installed. */
|
|
105
|
+
const isRuntimeContextFrameCarrierInstalled = () => runtimeContextFrameCarrier !== fallbackRuntimeContextFrameCarrier;
|
|
106
|
+
/** Return the frame associated with the executing callback. */
|
|
107
|
+
const currentRuntimeContextFrame = () => runtimeContextFrameCarrier.current();
|
|
108
|
+
/** Install the host default used by the main Runtime entrypoint. */
|
|
109
|
+
const setDefaultRuntimeContextStorage = (storage) => {
|
|
110
|
+
defaultStorage = storage;
|
|
111
|
+
};
|
|
112
|
+
/** Return the storage currently associated with the executing callback. */
|
|
113
|
+
const activeRuntimeContextStorage = () => currentRuntimeContextFrame()?.storage ?? defaultStorage;
|
|
114
|
+
/** Return the active context, or undefined when the storage has not been entered. */
|
|
115
|
+
const getRuntimeContext = (storage = activeRuntimeContextStorage()) => {
|
|
116
|
+
const frame = currentRuntimeContextFrame();
|
|
117
|
+
if (frame?.storage === storage) return frame.context;
|
|
118
|
+
try {
|
|
119
|
+
return storage.current();
|
|
120
|
+
} catch (cause) {
|
|
121
|
+
if (cause instanceof RuntimeContextNotConfiguredError) return;
|
|
122
|
+
throw cause;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** Return the active context or throw the storage's standard missing-context error. */
|
|
126
|
+
const currentRuntimeContext = () => {
|
|
127
|
+
return currentRuntimeContextFrame()?.context ?? defaultStorage.current();
|
|
128
|
+
};
|
|
129
|
+
/** Keep a storage discoverable to Service and Scope compatibility bridges. */
|
|
130
|
+
const withActiveRuntimeContextStorage = (storage, context, program) => {
|
|
131
|
+
const current = currentRuntimeContextFrame();
|
|
132
|
+
if (current?.storage === storage && current.context === context) return program();
|
|
133
|
+
const frame = current === void 0 ? {
|
|
134
|
+
storage,
|
|
135
|
+
context
|
|
136
|
+
} : {
|
|
137
|
+
storage,
|
|
138
|
+
context,
|
|
139
|
+
parent: current
|
|
140
|
+
};
|
|
141
|
+
return runtimeContextFrameCarrier.run(frame, program);
|
|
142
|
+
};
|
|
143
|
+
/** Run a callback in a context while keeping the selected storage discoverable to bridges. */
|
|
144
|
+
const runRuntimeContext = (storage, context, program) => {
|
|
145
|
+
const current = getRuntimeContext(storage);
|
|
146
|
+
const contextual = current !== void 0 && isDerivedRuntimeContext(current, context) ? inheritRuntimeContextLineage(context, current) : context;
|
|
147
|
+
return storage.run(contextual, () => withActiveRuntimeContextStorage(storage, contextual, program));
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
export { getRuntimeContextLineage as a, isRuntimeContextFrameCarrierInstalled as c, setDefaultRuntimeContextStorage as d, setRuntimeContextFrameCarrier as f, RuntimeContextOverlapError as g, RuntimeContextNotConfiguredError as h, getRuntimeContext as i, makeRuntimeContext as l, withActiveRuntimeContextStorage as m, currentRuntimeContext as n, inheritRuntimeContextLineage as o, setRuntimeContextLineage as p, currentRuntimeContextFrame as r, isDerivedRuntimeContext as s, activeRuntimeContextStorage as t, runRuntimeContext as u };
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=context-BUEf1qjL.mjs.map
|