better-effect 0.1.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 ADDED
@@ -0,0 +1,336 @@
1
+ # better-effect
2
+
3
+ Lightweight Effect-inspired primitives built around [`better-result`](https://www.npmjs.com/package/better-result).
4
+
5
+ `better-effect` focuses on a small set of ideas that are useful in application code without bringing in a full effect runtime:
6
+
7
+ - **Service** — contextual dependency access with `yield*`
8
+ - **Layer** — declarative composition of live/test environments
9
+ - **Resource** — safe acquire/use/release lifecycle
10
+ - **DI adapters** — dependency resolution is delegated to an external container instead of being reimplemented by the library
11
+
12
+ The goal is not to recreate Effect. The goal is to provide a small, composable layer on top of `better-result`.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ bun add better-effect better-result
18
+ ```
19
+
20
+ If you want the ITI adapter:
21
+
22
+ ```bash
23
+ bun add iti
24
+ ```
25
+
26
+ ## Service
27
+
28
+ A service is a class that also acts as its own dependency token.
29
+
30
+ ```ts
31
+ import { Result } from 'better-result'
32
+ import { Service } from 'better-effect'
33
+
34
+ export class Database extends Service<Database>() {
35
+ findUser(email: string) {
36
+ return Promise.resolve({
37
+ id: '1',
38
+ email
39
+ })
40
+ }
41
+ }
42
+
43
+ export class UserRepository extends Service<UserRepository>() {
44
+ findByEmail(email: string) {
45
+ return Result.gen(async function* () {
46
+ const database = yield* Database
47
+
48
+ const user = await database.findUser(email)
49
+
50
+ return Result.ok(user)
51
+ })
52
+ }
53
+ }
54
+ ```
55
+
56
+ There are no string tokens:
57
+
58
+ ```ts
59
+ const database = yield * Database
60
+ ```
61
+
62
+ The class itself is the identity used by the resolver, and the result is inferred as `Database`.
63
+
64
+ ### Why classes are tokens
65
+
66
+ Using constructors directly avoids duplicated identifiers such as:
67
+
68
+ ```ts
69
+ Service<AuthService>()('authService')
70
+ ```
71
+
72
+ and prevents string-key collisions or typos.
73
+
74
+ Conceptually:
75
+
76
+ ```text
77
+ yield* AuthService
78
+
79
+
80
+ ServiceRuntime
81
+
82
+
83
+ ServiceResolver
84
+
85
+
86
+ AuthService instance
87
+ ```
88
+
89
+ ## Layer
90
+
91
+ A Layer describes which implementations form an application environment.
92
+
93
+ It does **not** implement dependency resolution. That remains the responsibility of the configured backend.
94
+
95
+ ```ts
96
+ import { Layer } from 'better-effect'
97
+
98
+ export const DatabaseLive = Layer.scoped(
99
+ Database,
100
+ async () => {
101
+ const database = new Database()
102
+
103
+ await database.connect()
104
+
105
+ return database
106
+ },
107
+ (database) => database.close()
108
+ )
109
+
110
+ export const UserRepositoryLive = Layer.make(UserRepository, () => new UserRepository())
111
+
112
+ export const AppLive = Layer.merge(DatabaseLive, UserRepositoryLive)
113
+ ```
114
+
115
+ The core Layer API intentionally stays small:
116
+
117
+ ```ts
118
+ Layer.make(Service, acquire)
119
+ Layer.succeed(Service, instance)
120
+ Layer.scoped(Service, acquire, release)
121
+ Layer.merge(...layers)
122
+ Layer.override(base, ...overrides)
123
+ ```
124
+
125
+ ### Test environments
126
+
127
+ Layers make implementation replacement explicit:
128
+
129
+ ```ts
130
+ const DatabaseTest = Layer.succeed(Database, new InMemoryDatabase())
131
+
132
+ const AppTest = Layer.override(AppLive, DatabaseTest)
133
+ ```
134
+
135
+ ## ITI adapter
136
+
137
+ `better-effect` does not depend on ITI in its core. ITI is just one possible backend.
138
+
139
+ ```ts
140
+ import { buildLayer } from 'better-effect'
141
+
142
+ import { ItiLayerBackend } from 'better-effect/iti'
143
+
144
+ const runtime = await buildLayer(AppLive, new ItiLayerBackend())
145
+
146
+ try {
147
+ await main()
148
+ } finally {
149
+ await runtime.dispose()
150
+ }
151
+ ```
152
+
153
+ This keeps application code independent from the DI container.
154
+
155
+ A different backend can implement the same resolver/backend contracts without changing Services or Layers.
156
+
157
+ ## Resource
158
+
159
+ `Resource.acquireUseRelease()` handles local resource lifecycle while preserving typed `Result` errors.
160
+
161
+ ```ts
162
+ import { Resource } from 'better-effect'
163
+
164
+ const result = await Resource.acquireUseRelease({
165
+ name: 'transaction',
166
+
167
+ acquire: () => database.begin(),
168
+
169
+ use: (transaction) => executeCommand(transaction),
170
+
171
+ release: (transaction) => transaction.close()
172
+ })
173
+ ```
174
+
175
+ When `release` is omitted, `Resource` attempts to use the JavaScript explicit resource management protocol:
176
+
177
+ ```ts
178
+ Symbol.asyncDispose
179
+ Symbol.dispose
180
+ ```
181
+
182
+ ### Error precedence
183
+
184
+ If both `use` and `release` fail, the error produced by `use` is preserved.
185
+
186
+ The precedence is:
187
+
188
+ ```text
189
+ 1. use failure
190
+ 2. release failure
191
+ 3. successful use value
192
+ ```
193
+
194
+ Acquisition exceptions, rejected promises, and unexpected failures are normalized through `better-result`.
195
+
196
+ ## Service vs Layer vs Resource
197
+
198
+ | Primitive | Responsibility |
199
+ | --------------- | ----------------------------------------------------- |
200
+ | `Service` | Request a contextual dependency |
201
+ | `Layer` | Describe the implementations that form an environment |
202
+ | `Resource` | Manage a resource local to one operation |
203
+ | DI backend | Resolve, cache and dispose service instances |
204
+ | `better-result` | Typed failures and generator control flow |
205
+
206
+ ## Complete example
207
+
208
+ The repository contains a TODO API example under:
209
+
210
+ ```text
211
+ examples/todo-api
212
+ ```
213
+
214
+ It demonstrates:
215
+
216
+ - Bun HTTP server
217
+ - SQLite in memory with `Bun.SQL`
218
+ - user login
219
+ - session authentication
220
+ - TODO CRUD
221
+ - `Service` dependency access
222
+ - Layer composition
223
+ - scoped database lifecycle
224
+ - `Resource.acquireUseRelease()`
225
+ - ITI as the DI backend
226
+
227
+ Run it from the repository root:
228
+
229
+ ```bash
230
+ bun examples/todo-api/index.ts
231
+ ```
232
+
233
+ ## Development
234
+
235
+ Install dependencies:
236
+
237
+ ```bash
238
+ bun install
239
+ ```
240
+
241
+ Run tests:
242
+
243
+ ```bash
244
+ bun test
245
+ ```
246
+
247
+ Typecheck:
248
+
249
+ ```bash
250
+ bun run typecheck
251
+ ```
252
+
253
+ Run the full quality gate:
254
+
255
+ ```bash
256
+ bun run check
257
+ ```
258
+
259
+ The project uses:
260
+
261
+ - Bun as package manager and test runner
262
+ - TypeScript
263
+ - tsdown for library builds
264
+ - Oxlint
265
+ - Oxfmt
266
+ - publint
267
+
268
+ ## Design principles
269
+
270
+ ### Keep the core small
271
+
272
+ `better-effect` should not grow into a second Effect runtime.
273
+
274
+ The core intentionally does not implement:
275
+
276
+ - fibers
277
+ - schedules
278
+ - streams
279
+ - queues
280
+ - a dependency graph runtime
281
+ - a custom DI container
282
+ - a custom Context
283
+ - a custom Scope runtime
284
+ - `Effect<A, E, R>`
285
+
286
+ ### Delegate instead of rebuilding
287
+
288
+ Dependency resolution, caching and container lifecycle belong to DI backends.
289
+
290
+ `better-effect` supplies the protocol and composition primitives.
291
+
292
+ ### Preserve inference at public boundaries
293
+
294
+ Type safety is part of the API.
295
+
296
+ For example:
297
+
298
+ ```ts
299
+ const auth = yield * AuthService
300
+ // AuthService
301
+ ```
302
+
303
+ and:
304
+
305
+ ```ts
306
+ const database = await ServiceRuntime.resolve(Database)
307
+ // Database
308
+ ```
309
+
310
+ must preserve exact instance types.
311
+
312
+ ### Type erasure stays internal
313
+
314
+ Layers may store heterogeneous providers internally using erased types.
315
+
316
+ The public constructors (`Layer.make`, `Layer.succeed`, `Layer.scoped`) are responsible for preserving the relationship between a Service token and its instance type.
317
+
318
+ ## Current scope
319
+
320
+ The project is intentionally small and experimental.
321
+
322
+ The initial scope is:
323
+
324
+ ```text
325
+ Service
326
+ Layer
327
+ Resource
328
+ DI adapters
329
+ better-result integration
330
+ ```
331
+
332
+ New abstractions should only be added when they solve a concrete problem without duplicating responsibilities already handled well by another library.
333
+
334
+ ## License
335
+
336
+ MIT
@@ -0,0 +1,2 @@
1
+ import { t as ItiLayerBackend } from "../iti-xXAb9-Qs.mjs";
2
+ export { ItiLayerBackend };
@@ -0,0 +1,30 @@
1
+ import { createContainer } from "iti";
2
+ //#region src/adapters/iti.ts
3
+ var ItiLayerBackend = class {
4
+ container = createContainer();
5
+ keys = /* @__PURE__ */ new WeakMap();
6
+ nextId = 0;
7
+ keyFor(token) {
8
+ const existing = this.keys.get(token);
9
+ if (existing) return existing;
10
+ const key = `better-effect:${token.name || "Service"}:${this.nextId++}`;
11
+ this.keys.set(token, key);
12
+ return key;
13
+ }
14
+ register(provider) {
15
+ const key = this.keyFor(provider.service);
16
+ this.container = this.container.add({ [key]: provider.acquire });
17
+ if (provider.release) this.container = this.container.addDisposer({ [key]: provider.release });
18
+ }
19
+ resolve(token) {
20
+ const key = this.keyFor(token);
21
+ return this.container.get(key);
22
+ }
23
+ async disposeAll() {
24
+ await this.container.disposeAll();
25
+ }
26
+ };
27
+ //#endregion
28
+ export { ItiLayerBackend };
29
+
30
+ //# sourceMappingURL=iti.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iti.mjs","names":[],"sources":["../../src/adapters/iti.ts"],"sourcesContent":["import { createContainer } from 'iti'\n\nimport type { LayerBackend } from '../layer'\n\nimport type { AnyServiceToken, ServiceToken } from '../service'\nimport type { LayerProvider } from '../layer/types'\n\nexport class ItiLayerBackend implements LayerBackend {\n private container: any = createContainer()\n\n private readonly keys = new WeakMap<ServiceToken<any>, string>()\n\n private nextId = 0\n\n private keyFor(token: AnyServiceToken): string {\n const existing = this.keys.get(token)\n\n if (existing) {\n return existing\n }\n\n const name = (token as Function).name || 'Service'\n\n const key = `better-effect:${name}:${this.nextId++}`\n\n this.keys.set(token, key)\n\n return key\n }\n\n register(provider: LayerProvider): void {\n const key = this.keyFor(provider.service)\n\n this.container = this.container.add({\n [key]: provider.acquire\n })\n\n if (provider.release) {\n this.container = this.container.addDisposer({\n [key]: provider.release\n })\n }\n }\n\n resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>> {\n const key = this.keyFor(token)\n\n return this.container.get(key) as InstanceType<T> | PromiseLike<InstanceType<T>>\n }\n\n async disposeAll(): Promise<void> {\n await this.container.disposeAll()\n }\n}\n"],"mappings":";;AAOA,IAAa,kBAAb,MAAqD;CACnD,YAAyB,gBAAgB;CAEzC,uBAAwB,IAAI,QAAmC;CAE/D,SAAiB;CAEjB,OAAe,OAAgC;EAC7C,MAAM,WAAW,KAAK,KAAK,IAAI,KAAK;EAEpC,IAAI,UACF,OAAO;EAKT,MAAM,MAAM,iBAFE,MAAmB,QAAQ,UAEP,GAAG,KAAK;EAE1C,KAAK,KAAK,IAAI,OAAO,GAAG;EAExB,OAAO;CACT;CAEA,SAAS,UAA+B;EACtC,MAAM,MAAM,KAAK,OAAO,SAAS,OAAO;EAExC,KAAK,YAAY,KAAK,UAAU,IAAI,GACjC,MAAM,SAAS,QAClB,CAAC;EAED,IAAI,SAAS,SACX,KAAK,YAAY,KAAK,UAAU,YAAY,GACzC,MAAM,SAAS,QAClB,CAAC;CAEL;CAEA,QAAmC,OAA0D;EAC3F,MAAM,MAAM,KAAK,OAAO,KAAK;EAE7B,OAAO,KAAK,UAAU,IAAI,GAAG;CAC/B;CAEA,MAAM,aAA4B;EAChC,MAAM,KAAK,UAAU,WAAW;CAClC;AACF"}
@@ -0,0 +1,79 @@
1
+ import { a as LayerProvider, c as ServiceClass, i as ServiceRuntime, l as ServiceInstance, n as LayerBackend, o as MaybePromise$1, r as ServiceResolver, s as AnyServiceToken, t as ItiLayerBackend } from "./iti-xXAb9-Qs.mjs";
2
+ import { Result, UnhandledException } from "better-result";
3
+ //#region src/layer/layer.d.ts
4
+ declare class Layer {
5
+ readonly providers: readonly LayerProvider[];
6
+ private constructor();
7
+ static make<S extends ServiceClass<any>>(service: S, acquire: () => MaybePromise$1<InstanceType<S>>): Layer;
8
+ static succeed<S extends ServiceClass<any>>(service: S, instance: InstanceType<S>): Layer;
9
+ static scoped<S extends ServiceClass<any>>(service: S, acquire: () => MaybePromise$1<InstanceType<S>>, release: (instance: InstanceType<S>) => MaybePromise$1<void>): Layer;
10
+ static merge(...layers: readonly Layer[]): Layer;
11
+ static override(base: Layer, ...overrides: readonly Layer[]): Layer;
12
+ }
13
+ //#endregion
14
+ //#region src/layer/runtime.d.ts
15
+ interface BuiltLayer {
16
+ dispose(): Promise<void>;
17
+ }
18
+ declare const buildLayer: (layer: Layer, backend: LayerBackend) => Promise<BuiltLayer>;
19
+ declare class RuntimeLayer {
20
+ static build: typeof buildLayer;
21
+ }
22
+ //#endregion
23
+ //#region src/service/service.d.ts
24
+ declare function Service<Self>(): {
25
+ new (): {};
26
+ [Symbol.asyncIterator](): AsyncGenerator<never, Self, unknown>;
27
+ };
28
+ //#endregion
29
+ //#region src/resource/errors.d.ts
30
+ declare const ResourceReleaseFailure_base: import("better-result").TaggedErrorClass<"ResourceReleaseFailure">;
31
+ declare class ResourceReleaseFailure extends ResourceReleaseFailure_base<{
32
+ readonly resource: string;
33
+ readonly cause: unknown;
34
+ readonly message: string;
35
+ }> {}
36
+ //#endregion
37
+ //#region src/resource/types.d.ts
38
+ type MaybePromise<T> = T | PromiseLike<T>;
39
+ type AsyncResult<T, E> = MaybePromise<Result<T, E>>;
40
+ type ReleaseOutcome = void | Result<void, unknown>;
41
+ type DisposableResource = {
42
+ [Symbol.dispose]?: () => void;
43
+ [Symbol.asyncDispose]?: () => MaybePromise<void>;
44
+ };
45
+ type AcquireUseReleaseOptions<R, A, AcquireError, UseError> = {
46
+ /**
47
+ * Nome utilizado para identificar o recurso
48
+ * em eventuais erros de release.
49
+ */
50
+ readonly name: string;
51
+ /**
52
+ * Adquire o recurso.
53
+ */
54
+ readonly acquire: () => AsyncResult<R, AcquireError>;
55
+ /**
56
+ * Executa a operação usando o recurso.
57
+ */
58
+ readonly use: (resource: R) => AsyncResult<A, UseError>;
59
+ /**
60
+ * Libera o recurso.
61
+ *
62
+ * Se não informado, Resource tentará usar:
63
+ *
64
+ * Symbol.asyncDispose
65
+ * Symbol.dispose
66
+ *
67
+ * nessa ordem.
68
+ */
69
+ readonly release?: (resource: R) => MaybePromise<ReleaseOutcome>;
70
+ };
71
+ //#endregion
72
+ //#region src/resource/resource.d.ts
73
+ declare const acquireUseRelease: <R, A, AcquireError, UseError>({ name, acquire, use, release }: AcquireUseReleaseOptions<R, A, AcquireError, UseError>) => Promise<Result<A, AcquireError | UseError | UnhandledException | ResourceReleaseFailure>>;
74
+ declare const Resource: {
75
+ readonly acquireUseRelease: typeof acquireUseRelease;
76
+ };
77
+ //#endregion
78
+ export { type AcquireUseReleaseOptions, type AnyServiceToken, type AsyncResult, type BuiltLayer, type DisposableResource, ItiLayerBackend, Layer, type LayerBackend, type MaybePromise, type ReleaseOutcome, Resource, ResourceReleaseFailure, RuntimeLayer, Service, type ServiceInstance, type ServiceResolver, ServiceRuntime, buildLayer };
79
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/layer/layer.ts","../src/layer/runtime.ts","../src/service/service.ts","../src/resource/errors.ts","../src/resource/types.ts","../src/resource/resource.ts"],"mappings":";;;cAIa;WACkB,oBAAoB;UAA1C;SAEA,KAAK,UAAU,mBACpB,SAAS,GACT,eAAe,eAAa,aAAa,MACxC;SASI,QAAQ,UAAU,mBAAmB,SAAS,GAAG,UAAU,aAAa,KAAK;SAI7E,OAAO,UAAU,mBACtB,SAAS,GACT,eAAe,eAAa,aAAa,KACzC,UAAU,UAAU,aAAa,OAAO,uBACvC;SAWI,SAAS,iBAAiB,UAAU;SAkBpC,SAAS,MAAM,UAAU,oBAAoB,UAAU;;;;UCnD/C;EACf,WAAW;;cAGA,aAAU,OAAiB,OAAK,SAAW,iBAAe,QAAQ;cAkBlE;SACJ,cAAK;;;;iBCxBE,QAAQ;;GAGoB,OAAA,kBAAA,sBAAsB;;;;;cCLrD,+BAA+B;WACjC;WACA;WACA;;;;KCHC,aAAa,KAAK,IAAI,YAAY;KAElC,YAAY,GAAG,KAAK,aAAa,OAAW,GAAG;KAE/C,wBAAwB;KAExB;GACT,OAAO;GAEP,OAAO,sBAAsB;;KAGpB,yBAAyB,GAAG,GAAG,cAAc;;;;;WAK9C;;;;WAKA,eAAe,YAAY,GAAG;;;;WAK9B,MAAM,UAAU,MAAM,YAAY,GAAG;;;;;;;;;;;WAYrC,WAAW,UAAU,MAAM,aAAa;;;;cCjC7C,oBAAqB,GAAG,GAAG,cAAc,YAAQ,MAAA,SAAA,KAAA,WAKpD,yBAAyB,GAAG,GAAG,cAAc,cAAY,QAC1D,OAAW,GAAG,eAAe,WAAW,qBAAqB;cAmBlD;qCAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,139 @@
1
+ import { ItiLayerBackend } from "./adapters/iti.mjs";
2
+ import { Result, TaggedError } from "better-result";
3
+ //#region src/service/runtime.ts
4
+ let currentResolver;
5
+ var ServiceRuntime = class {
6
+ static configure(resolver) {
7
+ currentResolver = resolver;
8
+ }
9
+ static async resolve(token) {
10
+ if (!currentResolver) throw new Error("ServiceRuntime has not been configured");
11
+ return await currentResolver.resolve(token);
12
+ }
13
+ static reset() {
14
+ currentResolver = void 0;
15
+ }
16
+ };
17
+ //#endregion
18
+ //#region src/service/service.ts
19
+ function Service() {
20
+ return class {
21
+ static async *[Symbol.asyncIterator]() {
22
+ return await ServiceRuntime.resolve(this);
23
+ }
24
+ };
25
+ }
26
+ //#endregion
27
+ //#region src/layer/layer.ts
28
+ var Layer = class Layer {
29
+ providers;
30
+ constructor(providers) {
31
+ this.providers = providers;
32
+ }
33
+ static make(service, acquire) {
34
+ return new Layer([{
35
+ service,
36
+ acquire
37
+ }]);
38
+ }
39
+ static succeed(service, instance) {
40
+ return Layer.make(service, () => instance);
41
+ }
42
+ static scoped(service, acquire, release) {
43
+ return new Layer([{
44
+ service,
45
+ acquire,
46
+ release: (instance) => release(instance)
47
+ }]);
48
+ }
49
+ static merge(...layers) {
50
+ const providers = /* @__PURE__ */ new Map();
51
+ for (const layer of layers) for (const provider of layer.providers) {
52
+ const service = provider.service;
53
+ if (providers.has(service)) throw new Error(`Duplicate service "${service.name}" in Layer.merge()`);
54
+ providers.set(service, provider);
55
+ }
56
+ return new Layer([...providers.values()]);
57
+ }
58
+ static override(base, ...overrides) {
59
+ const providers = /* @__PURE__ */ new Map();
60
+ for (const provider of base.providers) providers.set(provider.service, provider);
61
+ for (const layer of overrides) for (const provider of layer.providers) providers.set(provider.service, provider);
62
+ return new Layer([...providers.values()]);
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/layer/runtime.ts
67
+ const buildLayer = async (layer, backend) => {
68
+ for (const provider of layer.providers) await backend.register(provider);
69
+ ServiceRuntime.configure(backend);
70
+ return { async dispose() {
71
+ try {
72
+ await backend.disposeAll();
73
+ } finally {
74
+ ServiceRuntime.reset();
75
+ }
76
+ } };
77
+ };
78
+ var RuntimeLayer = class {
79
+ static build = buildLayer;
80
+ };
81
+ //#endregion
82
+ //#region src/resource/errors.ts
83
+ var ResourceReleaseFailure = class extends TaggedError("ResourceReleaseFailure") {};
84
+ //#endregion
85
+ //#region src/resource/internal.ts
86
+ const disposeResource = (resource) => {
87
+ const candidate = Object(resource);
88
+ return (candidate[Symbol.asyncDispose] ?? candidate[Symbol.dispose])?.call(candidate);
89
+ };
90
+ const toReleaseFailure = (resource, cause) => new ResourceReleaseFailure({
91
+ resource,
92
+ cause,
93
+ message: `Failed to release resource: ${resource}`
94
+ });
95
+ /**
96
+ * Executa uma operação que retorna Result e converte
97
+ * exceptions/rejections inesperadas para UnhandledException.
98
+ */
99
+ const runResult = async (operation) => {
100
+ return (await Result.tryPromise(() => Promise.resolve(operation()))).andThen((result) => result);
101
+ };
102
+ const normalizeReleaseOutcome = (name, outcome) => {
103
+ if (outcome === void 0) return Result.ok();
104
+ return outcome.mapError((cause) => toReleaseFailure(name, cause));
105
+ };
106
+ const runRelease = async (name, resource, release) => {
107
+ return (await Result.tryPromise({
108
+ try: () => Promise.resolve(release(resource)),
109
+ catch: (cause) => toReleaseFailure(name, cause)
110
+ })).andThen((outcome) => normalizeReleaseOutcome(name, outcome));
111
+ };
112
+ /**
113
+ * Precedência:
114
+ *
115
+ * 1. erro ocorrido durante use
116
+ * 2. erro ocorrido durante release
117
+ * 3. valor produzido por use
118
+ */
119
+ const combineUseAndRelease = (used, released) => used.andThen((value) => released.map(() => value));
120
+ //#endregion
121
+ //#region src/resource/resource.ts
122
+ const acquireUseRelease = ({ name, acquire, use, release = disposeResource }) => Result.gen(async function* () {
123
+ const resource = yield* Result.await(runResult(acquire));
124
+ /**
125
+ * Não usamos yield* aqui.
126
+ *
127
+ * Se use retornar Err, o generator seria
128
+ * encerrado imediatamente e o release
129
+ * nunca seria executado.
130
+ */
131
+ const used = await runResult(() => use(resource));
132
+ const released = await runRelease(name, resource, release);
133
+ return combineUseAndRelease(used, released);
134
+ });
135
+ const Resource = { acquireUseRelease };
136
+ //#endregion
137
+ export { ItiLayerBackend, Layer, Resource, ResourceReleaseFailure, RuntimeLayer, Service, ServiceRuntime, buildLayer };
138
+
139
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/service/runtime.ts","../src/service/service.ts","../src/layer/layer.ts","../src/layer/runtime.ts","../src/resource/errors.ts","../src/resource/internal.ts","../src/resource/resource.ts"],"sourcesContent":["import type { AnyServiceToken } from './types'\n\nexport interface ServiceResolver {\n resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>\n}\n\nlet currentResolver: ServiceResolver | undefined\n\nexport class ServiceRuntime {\n static configure(resolver: ServiceResolver): void {\n currentResolver = resolver\n }\n\n static async resolve<T extends AnyServiceToken>(token: T): Promise<InstanceType<T>> {\n if (!currentResolver) {\n throw new Error('ServiceRuntime has not been configured')\n }\n\n return await currentResolver.resolve(token)\n }\n\n static reset(): void {\n currentResolver = undefined\n }\n}\n","import { ServiceRuntime } from './runtime'\n\nimport type { ServiceToken } from './types'\n\nexport function Service<Self>() {\n return class {\n // oxlint-disable-next-line require-yield\n static async *[Symbol.asyncIterator](): AsyncGenerator<never, Self, unknown> {\n return await ServiceRuntime.resolve(this as unknown as ServiceToken<Self>)\n }\n }\n}\n","import type { ServiceClass } from '../service/types'\n\nimport type { LayerProvider, MaybePromise } from './types'\n\nexport class Layer {\n private constructor(readonly providers: readonly LayerProvider[]) {}\n\n static make<S extends ServiceClass<any>>(\n service: S,\n acquire: () => MaybePromise<InstanceType<S>>\n ): Layer {\n return new Layer([\n {\n service,\n acquire\n }\n ])\n }\n\n static succeed<S extends ServiceClass<any>>(service: S, instance: InstanceType<S>): Layer {\n return Layer.make(service, () => instance)\n }\n\n static scoped<S extends ServiceClass<any>>(\n service: S,\n acquire: () => MaybePromise<InstanceType<S>>,\n release: (instance: InstanceType<S>) => MaybePromise<void>\n ): Layer {\n return new Layer([\n {\n service,\n acquire,\n\n release: (instance) => release(instance as InstanceType<S>)\n }\n ])\n }\n\n static merge(...layers: readonly Layer[]): Layer {\n const providers = new Map<ServiceClass<any>, LayerProvider>()\n\n for (const layer of layers) {\n for (const provider of layer.providers) {\n const service = provider.service\n\n if (providers.has(service)) {\n throw new Error(`Duplicate service \"${service.name}\" in Layer.merge()`)\n }\n\n providers.set(service, provider)\n }\n }\n\n return new Layer([...providers.values()])\n }\n\n static override(base: Layer, ...overrides: readonly Layer[]): Layer {\n const providers = new Map<ServiceClass<any>, LayerProvider>()\n\n for (const provider of base.providers) {\n providers.set(provider.service, provider)\n }\n\n for (const layer of overrides) {\n for (const provider of layer.providers) {\n providers.set(provider.service, provider)\n }\n }\n\n return new Layer([...providers.values()])\n }\n}\n","import { ServiceRuntime } from '../service/runtime'\n\nimport type { LayerBackend } from './backend'\nimport type { Layer } from './layer'\n\nexport interface BuiltLayer {\n dispose(): Promise<void>\n}\n\nexport const buildLayer = async (layer: Layer, backend: LayerBackend): Promise<BuiltLayer> => {\n for (const provider of layer.providers) {\n await backend.register(provider)\n }\n\n ServiceRuntime.configure(backend)\n\n return {\n async dispose() {\n try {\n await backend.disposeAll()\n } finally {\n ServiceRuntime.reset()\n }\n }\n }\n}\n\nexport class RuntimeLayer {\n static build = buildLayer\n}\n","import { TaggedError } from 'better-result'\n\nexport class ResourceReleaseFailure extends TaggedError('ResourceReleaseFailure')<{\n readonly resource: string\n readonly cause: unknown\n readonly message: string\n}> {}\n","import { Result, type Result as ResultType, type UnhandledException } from 'better-result'\n\nimport { ResourceReleaseFailure } from './errors'\n\nimport type { AsyncResult, DisposableResource, MaybePromise, ReleaseOutcome } from './types'\n\nexport const disposeResource = (resource: unknown): MaybePromise<void> => {\n const candidate = Object(resource) as DisposableResource\n\n const dispose = candidate[Symbol.asyncDispose] ?? candidate[Symbol.dispose]\n\n return dispose?.call(candidate)\n}\n\nconst toReleaseFailure = (resource: string, cause: unknown): ResourceReleaseFailure =>\n new ResourceReleaseFailure({\n resource,\n cause,\n message: `Failed to release resource: ${resource}`\n })\n\n/**\n * Executa uma operação que retorna Result e converte\n * exceptions/rejections inesperadas para UnhandledException.\n */\nexport const runResult = async <T, E>(\n operation: () => AsyncResult<T, E>\n): Promise<ResultType<T, E | UnhandledException>> => {\n const execution = await Result.tryPromise(() => Promise.resolve(operation()))\n\n return execution.andThen((result) => result)\n}\n\nconst normalizeReleaseOutcome = (\n name: string,\n outcome: ReleaseOutcome\n): ResultType<void, ResourceReleaseFailure> => {\n if (outcome === undefined) {\n return Result.ok()\n }\n\n return outcome.mapError((cause) => toReleaseFailure(name, cause))\n}\n\nexport const runRelease = async <R>(\n name: string,\n resource: R,\n release: (resource: R) => MaybePromise<ReleaseOutcome>\n): Promise<ResultType<void, ResourceReleaseFailure>> => {\n const execution = await Result.tryPromise({\n try: () => Promise.resolve(release(resource)),\n\n catch: (cause) => toReleaseFailure(name, cause)\n })\n\n return execution.andThen((outcome) => normalizeReleaseOutcome(name, outcome))\n}\n\n/**\n * Precedência:\n *\n * 1. erro ocorrido durante use\n * 2. erro ocorrido durante release\n * 3. valor produzido por use\n */\nexport const combineUseAndRelease = <A, E>(\n used: ResultType<A, E>,\n released: ResultType<void, ResourceReleaseFailure>\n): ResultType<A, E | ResourceReleaseFailure> => used.andThen((value) => released.map(() => value))\n","import { Result, type Result as ResultType, type UnhandledException } from 'better-result'\n\nimport { ResourceReleaseFailure } from './errors'\n\nimport { combineUseAndRelease, disposeResource, runRelease, runResult } from './internal'\n\nimport type { AcquireUseReleaseOptions } from './types'\n\nconst acquireUseRelease = <R, A, AcquireError, UseError>({\n name,\n acquire,\n use,\n release = disposeResource\n}: AcquireUseReleaseOptions<R, A, AcquireError, UseError>): Promise<\n ResultType<A, AcquireError | UseError | UnhandledException | ResourceReleaseFailure>\n> =>\n Result.gen(async function* () {\n const resource = yield* Result.await(runResult(acquire))\n\n /**\n * Não usamos yield* aqui.\n *\n * Se use retornar Err, o generator seria\n * encerrado imediatamente e o release\n * nunca seria executado.\n */\n const used = await runResult(() => use(resource))\n\n const released = await runRelease(name, resource, release)\n\n return combineUseAndRelease(used, released)\n })\n\nexport const Resource = {\n acquireUseRelease\n} as const\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ //#region src/service/types.d.ts
2
+ type ServiceToken<A = unknown> = (abstract new (...args: any[]) => A) & {
3
+ readonly name: string;
4
+ };
5
+ type ServiceClass<A = unknown> = ServiceToken<A>;
6
+ type AnyServiceToken = ServiceToken<any>;
7
+ type ServiceInstance<T extends AnyServiceToken> = InstanceType<T>;
8
+ //#endregion
9
+ //#region src/layer/types.d.ts
10
+ type MaybePromise<T> = T | PromiseLike<T>;
11
+ interface LayerProvider {
12
+ readonly service: ServiceClass<any>;
13
+ readonly acquire: () => MaybePromise<unknown>;
14
+ readonly release?: (instance: unknown) => MaybePromise<void>;
15
+ }
16
+ //#endregion
17
+ //#region src/service/runtime.d.ts
18
+ interface ServiceResolver {
19
+ resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>;
20
+ }
21
+ declare class ServiceRuntime {
22
+ static configure(resolver: ServiceResolver): void;
23
+ static resolve<T extends AnyServiceToken>(token: T): Promise<InstanceType<T>>;
24
+ static reset(): void;
25
+ }
26
+ //#endregion
27
+ //#region src/layer/backend.d.ts
28
+ interface LayerBackend extends ServiceResolver {
29
+ register(provider: LayerProvider): void | PromiseLike<void>;
30
+ disposeAll(): void | PromiseLike<void>;
31
+ }
32
+ //#endregion
33
+ //#region src/adapters/iti.d.ts
34
+ declare class ItiLayerBackend implements LayerBackend {
35
+ private container;
36
+ private readonly keys;
37
+ private nextId;
38
+ private keyFor;
39
+ register(provider: LayerProvider): void;
40
+ resolve<T extends AnyServiceToken>(token: T): InstanceType<T> | PromiseLike<InstanceType<T>>;
41
+ disposeAll(): Promise<void>;
42
+ }
43
+ //#endregion
44
+ export { LayerProvider as a, ServiceClass as c, ServiceRuntime as i, ServiceInstance as l, LayerBackend as n, MaybePromise as o, ServiceResolver as r, AnyServiceToken as s, ItiLayerBackend as t };
45
+ //# sourceMappingURL=iti-xXAb9-Qs.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iti-xXAb9-Qs.d.mts","names":[],"sources":["../src/service/types.ts","../src/layer/types.ts","../src/service/runtime.ts","../src/layer/backend.ts","../src/adapters/iti.ts"],"mappings":";KAAY,aAAa,iCAAiC,gBAAgB;WAC/D;;KAGC,aAAa,eAAe,aAAa;KAEzC,kBAAkB;KAElB,gBAAgB,UAAU,mBAAmB,aAAa;;;KCN1D,aAAa,KAAK,IAAI,YAAY;UAE7B;WACN,SAAS;WAET,eAAe;WAEf,WAAW,sBAAsB;;;;UCP3B;EACf,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;;cAK9E;SACJ,UAAU,UAAU;SAId,QAAQ,UAAU,iBAAiB,OAAO,IAAI,QAAQ,aAAa;SAQzE;;;;UCjBQ,qBAAqB;EACpC,SAAS,UAAU,uBAAuB;EAE1C,qBAAqB;;;;cCAV,2BAA2B;UAC9B;mBAES;UAET;UAEA;EAgBR,SAAS,UAAU;EAcnB,QAAQ,UAAU,iBAAiB,OAAO,IAAI,aAAa,KAAK,YAAY,aAAa;EAMnF,cAAc"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "better-effect",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight Effect-inspired primitives for better-result",
5
+ "keywords": [
6
+ "better-result",
7
+ "dependency-injection",
8
+ "effect",
9
+ "layer",
10
+ "resource",
11
+ "result",
12
+ "typescript"
13
+ ],
14
+ "homepage": "https://github.com/nitoba/better-effect#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/nitoba/better-effect/issues"
17
+ },
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/nitoba/better-effect.git"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "exports": {
29
+ ".": "./dist/index.mjs",
30
+ "./adapters/iti": "./dist/adapters/iti.mjs",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "bun test",
41
+ "test:coverage": "bun test --coverage",
42
+ "lint": "oxlint --type-aware .",
43
+ "lint:fix": "oxlint --type-aware --fix .",
44
+ "format": "oxfmt .",
45
+ "format:check": "oxfmt .",
46
+ "publint": "publint",
47
+ "check": "bun run typecheck && bun test && bun run lint && bun run format:check && bun run build && bun run publint",
48
+ "prepublishOnly": "bun run check",
49
+ "release:dry": "bun publish --dry-run"
50
+ },
51
+ "devDependencies": {
52
+ "@types/bun": "latest",
53
+ "iti": "^0.8.0",
54
+ "lefthook": "^2.1.10",
55
+ "oxfmt": "^0.63.0",
56
+ "oxlint": "^1.78.0",
57
+ "oxlint-tsgolint": "^7.0.2001",
58
+ "publint": "^0.3.23",
59
+ "tsdown": "^0.22.14"
60
+ },
61
+ "peerDependencies": {
62
+ "better-result": "^3.0.0",
63
+ "iti": "^0.8.0",
64
+ "typescript": "^7.0.2"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "iti": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "optionalDependencies": {
72
+ "iti": "^0.8.0"
73
+ }
74
+ }