better-effect 0.10.0 → 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/README.md +244 -5
- package/dist/adapters/iti.d.mts +3 -4
- package/dist/adapters/iti.d.mts.map +1 -1
- package/dist/adapters/iti.mjs +9 -4
- package/dist/adapters/iti.mjs.map +1 -1
- package/dist/effect-2ZcGZI8A.mjs +513 -0
- package/dist/effect-2ZcGZI8A.mjs.map +1 -0
- package/dist/hono.d.mts +18 -13
- package/dist/hono.d.mts.map +1 -1
- package/dist/hono.mjs +9 -5
- package/dist/hono.mjs.map +1 -1
- package/dist/{index-BafDna0B.d.mts → index-C7Qild0_.d.mts} +158 -28
- package/dist/index-C7Qild0_.d.mts.map +1 -0
- package/dist/{index-DklNCz7w.d.mts → index-DStz0JMN.d.mts} +4 -4
- package/dist/{index-DklNCz7w.d.mts.map → index-DStz0JMN.d.mts.map} +1 -1
- package/dist/{index-CULgSzUw.d.mts → index-heuaRmXR.d.mts} +5 -5
- package/dist/{index-CULgSzUw.d.mts.map → index-heuaRmXR.d.mts.map} +1 -1
- package/dist/{index-C7KX5rAP.d.mts → index-rQhZk3Nt.d.mts} +127 -24
- package/dist/index-rQhZk3Nt.d.mts.map +1 -0
- package/dist/index.d.mts +4 -5
- package/dist/index.mjs +7 -538
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/explicit.d.mts +1 -1
- package/dist/runtime/node.d.mts +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-C1bagvrO.mjs → signal-B97cs85Z.mjs} +81 -2
- package/dist/signal-B97cs85Z.mjs.map +1 -0
- package/dist/{standard-services-DW-i4UuA.mjs → standard-services-BFBq-4lo.mjs} +2 -2
- package/dist/{standard-services-DW-i4UuA.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 +1 -1
- package/dist/effect-DAMqvegy.mjs +0 -544
- package/dist/effect-DAMqvegy.mjs.map +0 -1
- package/dist/index-BafDna0B.d.mts.map +0 -1
- package/dist/index-C7KX5rAP.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/map-layer-backend-rNwfH0Bz.d.mts +0 -42
- package/dist/map-layer-backend-rNwfH0Bz.d.mts.map +0 -1
- package/dist/signal-C1bagvrO.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -99,6 +99,40 @@ const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
|
|
|
99
99
|
const runtime = await Runtime.make(AppLive)
|
|
100
100
|
```
|
|
101
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
|
+
|
|
102
136
|
And the contract does not disappear after startup.
|
|
103
137
|
|
|
104
138
|
A Runtime also knows which Services exist in its environment:
|
|
@@ -128,11 +162,102 @@ input order. If a Program returns an error or throws, scheduling stops, already-
|
|
|
128
162
|
started Programs are allowed to settle, and the deterministic primary failure remains selected;
|
|
129
163
|
there is no cancellation or Fiber scheduler.
|
|
130
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.
|
|
203
|
+
|
|
131
204
|
`Runtime.make(AppLive)` and `Runtime.run(AppLive, program)` use the built-in
|
|
132
205
|
`MapLayerBackend`. Pass `{ backend: new ItiLayerBackend() }` when an external
|
|
133
206
|
container is needed; `MemoryLayerBackend` remains its compatibility alias from
|
|
134
207
|
`better-effect/testing`.
|
|
135
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
|
+
|
|
136
261
|
Runtimes are async disposables, so request-scoped code can use:
|
|
137
262
|
|
|
138
263
|
```ts
|
|
@@ -146,6 +271,42 @@ Or let `Runtime.use` own the lifetime:
|
|
|
146
271
|
const result = await Runtime.use(AppLive, (runtime) => runtime.run(program))
|
|
147
272
|
```
|
|
148
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
|
+
|
|
149
310
|
Layer providers remain lazy unless startup validation is requested. Warm them
|
|
150
311
|
all before accepting work with either form:
|
|
151
312
|
|
|
@@ -176,6 +337,34 @@ const runtime = await Runtime.make(AppLive, {
|
|
|
176
337
|
})
|
|
177
338
|
```
|
|
178
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
|
+
|
|
179
368
|
Cancellation is cooperative and uses `AbortSignal`; no scheduler or fibers are
|
|
180
369
|
created. Pass a signal to one execution and read it from the program when an
|
|
181
370
|
I/O operation supports cancellation. Runtime disposal waits for active work;
|
|
@@ -229,9 +418,9 @@ app.get(
|
|
|
229
418
|
)
|
|
230
419
|
```
|
|
231
420
|
|
|
232
|
-
Hono validators can precede the generator or handler callback
|
|
233
|
-
`c.req.valid(...)` inputs are
|
|
234
|
-
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:
|
|
235
424
|
|
|
236
425
|
```ts
|
|
237
426
|
import { sValidator } from '@hono/standard-validator'
|
|
@@ -408,7 +597,9 @@ Some dependencies are values.
|
|
|
408
597
|
|
|
409
598
|
Others own connections, sessions, files or other resources.
|
|
410
599
|
|
|
411
|
-
`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.
|
|
412
603
|
|
|
413
604
|
```ts
|
|
414
605
|
const DatabaseLive = Layer.scoped(
|
|
@@ -422,6 +613,35 @@ Runtime owns the application lifetime and safely releases scoped resources when
|
|
|
422
613
|
|
|
423
614
|
Resources acquired during an individual execution belong to that execution instead.
|
|
424
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
|
+
|
|
425
645
|
### Keep your runtime choices
|
|
426
646
|
|
|
427
647
|
`better-effect` is not a replacement implementation of Effect.
|
|
@@ -572,7 +792,26 @@ still rejects it when its Layer does not provide every required Service.
|
|
|
572
792
|
|
|
573
793
|
Observation helpers such as `Effect.tap`, `Effect.tapError`, and `Effect.tapBoth`
|
|
574
794
|
run only the active branch and return the original Result, so logging or metrics
|
|
575
|
-
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.
|
|
576
815
|
|
|
577
816
|
Use `Effect.recover` or `Effect.recoverAsync` for an explicit fallback Result;
|
|
578
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-rNwfH0Bz.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.
|
|
@@ -24,7 +23,7 @@ declare class ItiLayerBackend implements LayerBackend {
|
|
|
24
23
|
/** Resolve a registered Service through the ITI container. */
|
|
25
24
|
resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>;
|
|
26
25
|
/** Reset container-owned ITI state; Scope owns Layer provider releases. */
|
|
27
|
-
disposeAll(): Promise<void>;
|
|
26
|
+
disposeAll(options?: LayerBackendDisposeOptions): Promise<void>;
|
|
28
27
|
}
|
|
29
28
|
//#endregion
|
|
30
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
|
@@ -18,7 +18,7 @@ var ItiLayerBackend = class {
|
|
|
18
18
|
* ITI caches rejected acquisitions; replacing the container is the explicit
|
|
19
19
|
* retry boundary for that sticky failure behavior.
|
|
20
20
|
*/
|
|
21
|
-
pending = /* @__PURE__ */ new
|
|
21
|
+
pending = /* @__PURE__ */ new Map();
|
|
22
22
|
keyFor(token) {
|
|
23
23
|
const tag = token.serviceTag;
|
|
24
24
|
const existing = this.keys.get(tag);
|
|
@@ -50,17 +50,22 @@ var ItiLayerBackend = class {
|
|
|
50
50
|
};
|
|
51
51
|
if (isPromiseLike(resolved)) {
|
|
52
52
|
const pending = Promise.resolve(resolved).then(validate);
|
|
53
|
-
this.pending.
|
|
53
|
+
this.pending.set(pending, registered);
|
|
54
54
|
pending.then(() => this.pending.delete(pending), () => this.pending.delete(pending));
|
|
55
55
|
return pending;
|
|
56
56
|
}
|
|
57
57
|
return validate(resolved);
|
|
58
58
|
}
|
|
59
59
|
/** Reset container-owned ITI state; Scope owns Layer provider releases. */
|
|
60
|
-
async disposeAll() {
|
|
60
|
+
async disposeAll(options) {
|
|
61
61
|
const container = this.container;
|
|
62
|
+
const acquisitions = [...this.pending.keys()];
|
|
62
63
|
try {
|
|
63
|
-
|
|
64
|
+
if (acquisitions.length > 0) {
|
|
65
|
+
const observePending = options?.onPendingAcquisitions;
|
|
66
|
+
if (observePending) await observePending(acquisitions);
|
|
67
|
+
await Promise.allSettled(acquisitions);
|
|
68
|
+
}
|
|
64
69
|
await container.disposeAll();
|
|
65
70
|
} finally {
|
|
66
71
|
this.container = createContainer();
|
|
@@ -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 /**\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
|
|
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"}
|