@shivaedev/service 0.0.0 → 0.5.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/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0 - 2026-10-27
4
+
5
+ ### Breaking
6
+
7
+ - Rename `@shivaedev/effect-service` to `@shivaedev/service`. Change dependency declarations and imports from `@shivaedev/effect-service/...` to `@shivaedev/service/...`; module paths inside the package are unchanged.
8
+
9
+ ## 0.4.1 - 2026-10-24
10
+
11
+ ### Changed
12
+
13
+ - Require Effect and its companion packages at exactly 4.0.5. Generated applications use the same versions.
14
+
15
+ ## 0.4.0 - 2026-10-18
16
+
17
+ ### Breaking
18
+
19
+ - Modules are named after their export: `define-service.ts` becomes `defineService.ts`, `generic-method.ts` becomes `genericMethod.ts`, `initializer-proof.ts` becomes `InitializerProof.ts`, `method-proof.ts` becomes `MethodProof.ts`, `requirement-proof.ts` becomes `RequirementProof.ts`, and `service-requirements.ts` becomes `ServiceRequirements.ts`.
20
+
21
+ ## 0.3.0 - 2026-10-13
22
+
23
+ ### Changed
24
+
25
+ - Prefix generic type parameter names in service definitions and method proofs with `T`, preserving their type relationships and runtime behavior.
26
+ - Document the service definition, private state, dependency binding and caller scope in the README, agent guidance, north star and roadmap; publish `docs/` with the README while keeping agent guidance out of the package.
27
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
28
+
29
+ ### Breaking
30
+
31
+ - Requires stable Effect 4.0.4 in place of 4.0.0-rc.112. Effect 4.0.4 moves the former `effect/unstable/*` modules to `effect/*` and renames constructors such as `Config.string` to `Config.String`.
32
+
33
+ ## 0.2.0 - 2026-10-04
34
+
35
+ ### Changed
36
+
37
+ - Breaking: no root entry and no module that re-exports another. `exports` maps one `"./*.ts"` pattern to every module (`src` under the `source` condition, `dist` otherwise), so you import the module that defines a name, and a bundler sees only the modules you use. Where each name now lives:
38
+ - `@shivaedev/effect-service`: `@shivaedev/effect-service/define-service.ts` (`defineService`), `@shivaedev/effect-service/generic-method.ts` (`genericMethod`), `@shivaedev/effect-service/service-requirements.ts` (`ServiceRequirements`)
39
+
40
+ ## 0.1.1 - 2026-10-04
41
+
42
+ ### Changed
43
+
44
+ - Imports that leave their folder go through `#` aliases declared in the `imports` field of `package.json`: `#*.ts` resolves to `src` under the `source` condition and to `dist` otherwise, and `#test/*` names a test file. The published modules and declarations name the package's own files through these aliases.
45
+
46
+ - Validate packed consumers and executable bins through the shared workspace gate.
47
+
48
+ - Build with the `@shivaedev/quality` tsconfig presets, which target ESNext and allow only erasable TypeScript syntax.
49
+
50
+ - Type-check with TypeScript 7 only. TypeScript 6 still builds the package.
51
+
52
+ - Lint and format with the `@shivaedev/quality` Biome preset through `quality lint` at the repository root, which replaces the package's `check` script.
53
+
54
+ - Run tests through the `@shivaedev/quality` Vitest projects, which take a test's environment from its file name. Type tests are named `*.typecheck.test.ts`.
55
+
56
+ - Keep `package.json` in sort-package-json key order, checked by `quality lint`.
57
+
58
+ ## 0.1.0 - 2026-09-26
59
+
60
+ ### Added
61
+
62
+ - Declare service dependencies once and bind them in scoped initialization and
63
+ ordinary Effect methods.
64
+ - Preserve typed initialization failures, method failures, caller scopes, and
65
+ explicitly marked generic methods.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
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
@@ -1,3 +1,225 @@
1
1
  # @shivaedev/service
2
2
 
3
- Placeholder. Real releases start above 0.0.0.
3
+ Declare an Effect service's dependencies once, initialize its private state, and call its methods without supplying those dependencies again. The service remains a native Context service with a native Layer; its methods remain Effects with their own result, failure and caller scope types.
4
+
5
+ ## Why you want this
6
+
7
+ A service should explain what it needs and what callers can do with it. When every method binds dependencies separately, that explanation is scattered across setup and application code. Put the dependency list, private initialization and public methods in one definition:
8
+
9
+ ```ts
10
+ const Greetings = defineService({
11
+ id: "app/Greetings",
12
+ requires: [Prefix],
13
+ initialize: Effect.succeed({ punctuation: "!" }),
14
+ methods: (state) => ({
15
+ greet: Effect.fn("Greetings.greet")((name: string) =>
16
+ Effect.map(Prefix, (prefix) => `${prefix}${name}${state.punctuation}`),
17
+ ),
18
+ }),
19
+ });
20
+ ```
21
+
22
+ `Prefix` is an Effect service. Provide it to `Greetings.layer` once; callers ask for `Greetings` and call `greet(name)`. The punctuation stays private, the prefix stays bound to the service's dependency, and each caller sees an ordinary Effect instead of dependency plumbing.
23
+
24
+ ## Using it
25
+
26
+ ### How to think about it
27
+
28
+ A **service definition** has four parts: an identity, a list of required services, an initialization Effect, and a function that turns its private state into methods. `defineService` returns the native Context service callers yield and its `.layer`, which applications compose with the dependencies' Layers.
29
+
30
+ A **declared dependency** is a service tag in `requires`. The Layer supplies those services to initialization and binds them to each method. The public method's type removes their requirements. If a caller provides a different value for a declared dependency around a method call, the method still uses the value supplied when the service was initialized.
31
+
32
+ **Private initialization state** is the value returned by `initialize`. The method factory receives it, but a caller receives only the methods. Within one provided Layer's lifetime, initialization and the method factory each run once, and two reads of the service share the same state.
33
+
34
+ **Caller scope** is different from initialization scope. The Layer owns initialization resources: their finalizers run once when its scope closes or its owner is interrupted, including when initialization is still running. A method that acquires a scoped resource keeps `Scope.Scope` in its public requirement type; its caller supplies the scope and releases the resource when that scope closes, even while the service stays live. Interrupting a method call in `Effect.scoped` interrupts the call and releases its resources without ending the service's lifetime.
35
+
36
+ The work has three parts:
37
+
38
+ 1. Once at the application boundary, compose service Layers with their dependencies.
39
+ 2. Once per service, define its dependency list, private state and methods.
40
+ 3. In application code, yield the service and call its methods.
41
+
42
+ ### 1. Define a service and provide its dependencies
43
+
44
+ ```ts
45
+ import { Context, Effect, Layer } from "effect";
46
+ import { defineService } from "@shivaedev/service/defineService.ts";
47
+
48
+ class Prefix extends Context.Service<Prefix, string>()("app/Prefix") {}
49
+
50
+ const Greetings = defineService({
51
+ id: "app/Greetings",
52
+ requires: [Prefix],
53
+ initialize: Effect.succeed({ punctuation: "!" }),
54
+ methods: (state) => ({
55
+ greet: Effect.fn("Greetings.greet")((name: string) =>
56
+ Effect.map(Prefix, (prefix) => `${prefix}${name}${state.punctuation}`),
57
+ ),
58
+ }),
59
+ });
60
+
61
+ const live = Greetings.layer.pipe(
62
+ Layer.provide(Layer.succeed(Prefix, "Hello ")),
63
+ );
64
+
65
+ const greeting = Effect.gen(function* () {
66
+ const greetings = yield* Greetings;
67
+ return yield* greetings.greet("Ada");
68
+ }).pipe(Effect.provide(live));
69
+ ```
70
+
71
+ The Layer needs `Prefix`. After a caller resolves `Greetings`, `greet` needs no `Prefix` from that caller. Its argument and success type are inferred from the method. An initialization failure fails the Layer build with that typed error and runs finalizers registered before the failure. A method failure reaches the caller as that method's typed error, and the same service instance can handle the next call. A method defect remains a defect.
72
+
73
+ `initialize` and ordinary methods may require the listed services and `Scope.Scope`. The compiler rejects a definition that reads another service. Scope is supplied by the Layer or method caller, so `Scope.Scope` itself cannot appear in `requires`.
74
+
75
+ Return functions from the method factory, each producing an Effect. An Effect value placed directly in the method record is rejected. Keep the private state behind those functions; it is absent from the emitted public service declaration.
76
+
77
+ ### 2. Annotate generators when inference needs help
78
+
79
+ For an explicitly annotated `Effect.fn` generator, `ServiceRequirements` expresses the declared services without repeating their union:
80
+
81
+ ```ts
82
+ import { Context, Effect, type Scope } from "effect";
83
+ import { defineService } from "@shivaedev/service/defineService.ts";
84
+ import type { ServiceRequirements } from "@shivaedev/service/ServiceRequirements.ts";
85
+
86
+ class Declared extends Context.Service<Declared, { readonly identity: object }>()(
87
+ "app/Declared",
88
+ ) {}
89
+
90
+ const requirements = [Declared] as const;
91
+
92
+ type Requirements<Success, Failure = never, CallerScope extends Scope.Scope = never> =
93
+ ServiceRequirements<typeof requirements, Success, Failure, CallerScope>;
94
+
95
+ const Identity = defineService({
96
+ id: "app/Identity",
97
+ requires: requirements,
98
+ initialize: Effect.fn("Identity.initialize")(function* (): Requirements<{
99
+ readonly identity: object;
100
+ }> {
101
+ return { identity: (yield* Declared).identity };
102
+ })(),
103
+ methods: (state) => ({
104
+ sameIdentity: Effect.fn("Identity.sameIdentity")(function* (): Requirements<boolean> {
105
+ return (yield* Declared).identity === state.identity;
106
+ }),
107
+ }),
108
+ });
109
+ ```
110
+
111
+ The alias is `Effect.fn.Return<Success, Failure, declared services | CallerScope>`. The last parameter can be `Scope.Scope` when the generator acquires a scoped resource; it cannot add another undeclared service. The annotation describes the implementation's requirements. `defineService` removes declared dependencies from the public methods' requirements.
112
+
113
+ ### 3. Give method resources a caller scope
114
+
115
+ ```ts
116
+ import { Effect, Ref } from "effect";
117
+ import { defineService } from "@shivaedev/service/defineService.ts";
118
+
119
+ const Resources = defineService({
120
+ id: "app/Resources",
121
+ requires: [],
122
+ initialize: Ref.make(0),
123
+ methods: (released) => ({
124
+ open: Effect.fn("Resources.open")(() =>
125
+ Effect.acquireRelease(
126
+ Effect.succeed("resource"),
127
+ () => Ref.update(released, (count) => count + 1),
128
+ ),
129
+ ),
130
+ released: Effect.fn("Resources.released")(() => Ref.get(released)),
131
+ }),
132
+ });
133
+
134
+ const useResource = Effect.gen(function* () {
135
+ const resources = yield* Resources;
136
+ yield* Effect.scoped(resources.open());
137
+ return yield* resources.released();
138
+ }).pipe(Effect.provide(Resources.layer));
139
+ ```
140
+
141
+ `open()` requires a caller scope. `Effect.scoped` supplies it and closes it before the next method call. The finalizer updates the private counter while the service is still available. An initializer that registers a finalizer instead ties that finalizer to the service Layer's scope.
142
+
143
+ ### Replace a service at an external test boundary
144
+
145
+ A native Layer can provide the method interface directly:
146
+
147
+ ```ts
148
+ const testGreetings = Layer.succeed(Greetings, {
149
+ greet: Effect.fn("TestGreetings.greet")((name: string) => Effect.succeed(name)),
150
+ });
151
+ ```
152
+
153
+ The replacement provides the public method interface, rather than private initialization state. The compiler rejects a replacement that leaves a required method out. Keep the real service in tests of its behavior; use a replacement where the service represents an external boundary.
154
+
155
+ ### Preserve a generic method's signature
156
+
157
+ Ordinary methods have one concrete signature. For a service with an empty `requires` list, `genericMethod` marks a generic function so its success, failure and caller requirements remain related at each call:
158
+
159
+ ```ts
160
+ import { Context, Effect } from "effect";
161
+ import { defineService } from "@shivaedev/service/defineService.ts";
162
+ import { genericMethod } from "@shivaedev/service/genericMethod.ts";
163
+
164
+ class Caller extends Context.Service<Caller, { readonly value: number }>()(
165
+ "app/Caller",
166
+ ) {}
167
+
168
+ const preserve = Effect.fn("Generic.preserve")(
169
+ <Success, Failure, Requirements>(
170
+ effect: Effect.Effect<Success, Failure, Requirements>,
171
+ ): Effect.Effect<{ readonly value: Success }, Failure, Requirements> =>
172
+ Effect.map(effect, (value) => ({ value })),
173
+ );
174
+
175
+ const Generic = defineService({
176
+ id: "app/Generic",
177
+ requires: [],
178
+ initialize: Effect.void,
179
+ methods: () => ({ preserve: genericMethod(preserve) }),
180
+ });
181
+
182
+ const preserved = Effect.gen(function* () {
183
+ const service = yield* Generic;
184
+ return yield* service.preserve(Effect.map(Caller, ({ value }) => value));
185
+ }).pipe(
186
+ Effect.provide(Generic.layer),
187
+ Effect.provideService(Caller, { value: 42 }),
188
+ );
189
+ ```
190
+
191
+ The wrapped Effect still requires `Caller`, which the caller supplies separately. The generic service's Layer has no declared dependencies. The emitted declarations preserve the generic success and failure types as well as caller requirements.
192
+
193
+ Marked generic methods require a service with no declared dependencies. The compiler rejects an unmarked generic that correlates its input and returned Effect, and a structurally distinct two-signature overload; it may not reject other generic or overloaded signatures. Use ordinary methods with concrete signatures when the service binds dependencies.
194
+
195
+ Dependency binding wraps the method's outer returned Effect. A Stream returned as its value retains its own service requirements; provide them when running the Stream.
196
+
197
+ ### API
198
+
199
+ Import the module that defines the name:
200
+
201
+ | Module | Name | Use |
202
+ | --- | --- | --- |
203
+ | `@shivaedev/service/defineService.ts` | `defineService(definition)` | Create a native service with its `.layer`. |
204
+ | `@shivaedev/service/genericMethod.ts` | `genericMethod(method)` | Mark a generic Effect-returning function in a service with no declared dependencies. |
205
+ | `@shivaedev/service/ServiceRequirements.ts` | `ServiceRequirements<Requirements, Success, Failure = never, CallerScope = never>` | Annotate an `Effect.fn` generator with declared dependencies and optional caller scope. |
206
+
207
+ The definition's fields are:
208
+
209
+ | Field | Meaning |
210
+ | --- | --- |
211
+ | `id` | The Context service identity, also retained in the Layer's output type. |
212
+ | `requires` | A readonly list of service tags available to initialization and ordinary methods. |
213
+ | `initialize` | The Effect producing private state; its failure type becomes the Layer's failure type. |
214
+ | `methods(state)` | A function returning the public method record, with declared dependencies removed from ordinary methods' requirement types. |
215
+
216
+ ### Install and limits
217
+
218
+ ```sh
219
+ pnpm add @shivaedev/service effect@4.0.5
220
+ ```
221
+
222
+ `effect` is a peer dependency, and the package needs Node.js 24 or later.
223
+
224
+ - Keep request identity at the request boundary when it changes per call. Pass it as a method argument rather than binding it into a service intended to live across requests.
225
+ - Choose service Layer lifetimes at the application's composition boundary; keep initialization resource ownership separate from a method caller's scope.
@@ -0,0 +1,7 @@
1
+ import type { Effect, Scope } from "effect";
2
+ interface InitializerHasUndeclaredServiceRequirements {
3
+ readonly _serviceDefinitionError: "initializer has service requirements absent from the service declaration";
4
+ }
5
+ export type InitializerProof<TInitializer, TRequirements> = TInitializer extends Effect.Effect<unknown, unknown, infer TResidual> ? [Exclude<TResidual, TRequirements | Scope.Scope>] extends [never] ? TInitializer : InitializerHasUndeclaredServiceRequirements : never;
6
+ export {};
7
+ //# sourceMappingURL=InitializerProof.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InitializerProof.d.ts","sourceRoot":"","sources":["../src/InitializerProof.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE5C,UAAU,2CAA2C;IACpD,QAAQ,CAAC,uBAAuB,EAAE,0EAA0E,CAAC;CAC7G;AAED,MAAM,MAAM,gBAAgB,CAAC,YAAY,EAAE,aAAa,IACvD,YAAY,SAAS,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC,GAClE,CAAC,OAAO,CAAC,SAAS,EAAE,aAAa,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAChE,YAAY,GACZ,2CAA2C,GAC5C,KAAK,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=InitializerProof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InitializerProof.js","sourceRoot":"","sources":["../src/InitializerProof.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import type { Effect, Scope } from "effect";
2
+ import type { AnyMethod, GenericMethodDescriptor, GenericOrStructurallyOverloadedMethodsAreUnsupported, HasDistinctCallSignatures } from "./genericMethod.ts";
3
+ import type { RequirementRecord, RequirementsOf } from "./ServiceRequirements.ts";
4
+ export type MethodEntry = AnyMethod | GenericMethodDescriptor<AnyMethod>;
5
+ export type MethodRecord = Readonly<Record<string, AnyMethod>>;
6
+ export type MethodInventory = Readonly<Record<string, unknown>>;
7
+ export type RuntimeMethodInventory = Readonly<Record<string, MethodEntry>>;
8
+ interface GenericMethodWithDeclaredRequirementsIsUnsupported {
9
+ readonly _serviceDefinitionError: "generic methods cannot subtract declared service requirements";
10
+ }
11
+ type MethodRequirements<TMethod> = TMethod extends (...arguments_: readonly never[]) => Effect.Effect<unknown, unknown, infer TRequirements> ? TRequirements : never;
12
+ interface MethodHasUndeclaredServiceRequirements {
13
+ readonly _serviceDefinitionError: "method has service requirements absent from the service declaration";
14
+ }
15
+ interface ServiceMembersMustBeMethods {
16
+ readonly _serviceDefinitionError: "service members must be methods";
17
+ }
18
+ type OrdinaryMethodProof<TMethod, TRequirements extends RequirementRecord> = [
19
+ Exclude<MethodRequirements<TMethod>, RequirementsOf<TRequirements> | Scope.Scope>
20
+ ] extends [never] ? TMethod : MethodHasUndeclaredServiceRequirements;
21
+ type SupportedMethod<TMethod, TRequirements extends RequirementRecord> = TMethod extends GenericMethodDescriptor<AnyMethod> ? TRequirements extends readonly [] ? TMethod : GenericMethodWithDeclaredRequirementsIsUnsupported : TMethod extends AnyMethod ? HasDistinctCallSignatures<TMethod> extends true ? GenericOrStructurallyOverloadedMethodsAreUnsupported : TMethod extends (...arguments_: infer TArguments) => infer TResult ? ((...arguments_: TArguments) => TResult) extends TMethod ? OrdinaryMethodProof<TMethod, TRequirements> : GenericOrStructurallyOverloadedMethodsAreUnsupported : GenericOrStructurallyOverloadedMethodsAreUnsupported : ServiceMembersMustBeMethods;
22
+ export type MethodProof<TMethods extends MethodInventory, TRequirements extends RequirementRecord> = {
23
+ readonly [TName in keyof TMethods]: SupportedMethod<TMethods[TName], TRequirements>;
24
+ };
25
+ export {};
26
+ //# sourceMappingURL=MethodProof.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MethodProof.d.ts","sourceRoot":"","sources":["../src/MethodProof.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EACX,SAAS,EACT,uBAAuB,EACvB,oDAAoD,EACpD,yBAAyB,EACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAElF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;AAEzE,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAE/D,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEhE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAE3E,UAAU,kDAAkD;IAC3D,QAAQ,CAAC,uBAAuB,EAAE,+DAA+D,CAAC;CAClG;AAED,KAAK,kBAAkB,CAAC,OAAO,IAAI,OAAO,SAAS,CAAC,GAAG,UAAU,EAAE,SAAS,KAAK,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,GACzI,aAAa,GACb,KAAK,CAAC;AAET,UAAU,sCAAsC;IAC/C,QAAQ,CAAC,uBAAuB,EAAE,qEAAqE,CAAC;CACxG;AAED,UAAU,2BAA2B;IACpC,QAAQ,CAAC,uBAAuB,EAAE,iCAAiC,CAAC;CACpE;AAED,KAAK,mBAAmB,CAAC,OAAO,EAAE,aAAa,SAAS,iBAAiB,IAAI;IAC5E,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;CACjF,SAAS,CAAC,KAAK,CAAC,GACd,OAAO,GACP,sCAAsC,CAAC;AAE1C,KAAK,eAAe,CAAC,OAAO,EAAE,aAAa,SAAS,iBAAiB,IACpE,OAAO,SAAS,uBAAuB,CAAC,SAAS,CAAC,GAC/C,aAAa,SAAS,SAAS,EAAE,GAChC,OAAO,GACP,kDAAkD,GACnD,OAAO,SAAS,SAAS,GACxB,yBAAyB,CAAC,OAAO,CAAC,SAAS,IAAI,GAC9C,oDAAoD,GACpD,OAAO,SAAS,CAAC,GAAG,UAAU,EAAE,MAAM,UAAU,KAAK,MAAM,OAAO,GACjE,CAAC,CAAC,GAAG,UAAU,EAAE,UAAU,KAAK,OAAO,CAAC,SAAS,OAAO,GACvD,mBAAmB,CAAC,OAAO,EAAE,aAAa,CAAC,GAC3C,oDAAoD,GACrD,oDAAoD,GACtD,2BAA2B,CAAC;AAEjC,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,eAAe,EAAE,aAAa,SAAS,iBAAiB,IAAI;IACpG,QAAQ,EAAE,KAAK,IAAI,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CACnF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=MethodProof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MethodProof.js","sourceRoot":"","sources":["../src/MethodProof.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import type { Context, Scope } from "effect";
2
+ import type { RequirementRecord } from "./ServiceRequirements.ts";
3
+ interface ScopeCannotBeDeclaredAsServiceRequirement {
4
+ readonly _serviceDefinitionError: "Scope.Scope is caller-owned and cannot be declared as a service requirement";
5
+ }
6
+ type ProveRequirement<TRequirement> = TRequirement extends Context.Service.Any ? Context.Service.Identifier<TRequirement> extends Scope.Scope ? ScopeCannotBeDeclaredAsServiceRequirement : TRequirement : never;
7
+ export type RequirementProof<TRequirements extends RequirementRecord> = {
8
+ readonly [TIndex in keyof TRequirements]: ProveRequirement<TRequirements[TIndex]>;
9
+ };
10
+ export {};
11
+ //# sourceMappingURL=RequirementProof.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequirementProof.d.ts","sourceRoot":"","sources":["../src/RequirementProof.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,UAAU,yCAAyC;IAClD,QAAQ,CAAC,uBAAuB,EAAE,6EAA6E,CAAC;CAChH;AAED,KAAK,gBAAgB,CAAC,YAAY,IAAI,YAAY,SAAS,OAAO,CAAC,OAAO,CAAC,GAAG,GAC3E,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,SAAS,KAAK,CAAC,KAAK,GAC3D,yCAAyC,GACzC,YAAY,GACb,KAAK,CAAC;AAET,MAAM,MAAM,gBAAgB,CAAC,aAAa,SAAS,iBAAiB,IAAI;IACvE,QAAQ,EAAE,MAAM,IAAI,MAAM,aAAa,GAAG,gBAAgB,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;CACjF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=RequirementProof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequirementProof.js","sourceRoot":"","sources":["../src/RequirementProof.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import type { Context, Effect, Scope } from "effect";
2
+ export type RequirementRecord = readonly Context.Service.Any[];
3
+ export type RequirementsOf<TRequirements extends RequirementRecord> = Context.Service.Identifier<TRequirements[number]>;
4
+ export type ServiceRequirements<TRequirements extends RequirementRecord, TSuccess, TFailure = never, TCallerScope extends Scope.Scope = never> = Effect.fn.Return<TSuccess, TFailure, RequirementsOf<TRequirements> | TCallerScope>;
5
+ //# sourceMappingURL=ServiceRequirements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceRequirements.d.ts","sourceRoot":"","sources":["../src/ServiceRequirements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,SAAS,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;AAE/D,MAAM,MAAM,cAAc,CAAC,aAAa,SAAS,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAExH,MAAM,MAAM,mBAAmB,CAC9B,aAAa,SAAS,iBAAiB,EACvC,QAAQ,EACR,QAAQ,GAAG,KAAK,EAChB,YAAY,SAAS,KAAK,CAAC,KAAK,GAAG,KAAK,IACrC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,CAAC,aAAa,CAAC,GAAG,YAAY,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ServiceRequirements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ServiceRequirements.js","sourceRoot":"","sources":["../src/ServiceRequirements.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import type { GenericMethodDescriptor } from "./genericMethod.ts";
3
+ import type { InitializerProof } from "./InitializerProof.ts";
4
+ import type { MethodInventory, MethodProof } from "./MethodProof.ts";
5
+ import type { RequirementProof } from "./RequirementProof.ts";
6
+ import type { RequirementRecord, RequirementsOf } from "./ServiceRequirements.ts";
7
+ type BoundMethod<TMethod, TRequirements> = TMethod extends (...arguments_: infer TArguments) => Effect.Effect<infer TSuccess, infer TFailure, infer TResidual> ? (...arguments_: TArguments) => Effect.Effect<TSuccess, TFailure, Exclude<TResidual, TRequirements>> : never;
8
+ type PublicMethod<TEntry, TRequirements> = TEntry extends GenericMethodDescriptor<infer TMethod> ? TMethod : BoundMethod<TEntry, TRequirements>;
9
+ type ServiceShape<TMethods extends MethodInventory, TRequirements extends RequirementRecord> = Readonly<{
10
+ [TName in keyof TMethods]: PublicMethod<TMethods[TName], RequirementsOf<TRequirements>>;
11
+ }>;
12
+ interface ServiceDefinition<TIdentifier extends string, TRequirements extends RequirementRecord, TInitializer extends Effect.Effect<unknown, unknown, unknown>, TMethods extends MethodInventory> {
13
+ readonly id: TIdentifier;
14
+ readonly initialize: TInitializer & InitializerProof<TInitializer, RequirementsOf<TRequirements>>;
15
+ readonly methods: (state: Effect.Success<TInitializer>) => TMethods & MethodProof<NoInfer<TMethods>, TRequirements>;
16
+ readonly requires: TRequirements & RequirementProof<TRequirements>;
17
+ }
18
+ type DefinedService<TIdentifier extends string, TShape, TFailure, TRequirements> = Context.Service<TIdentifier, TShape> & {
19
+ readonly layer: Layer.Layer<TIdentifier, TFailure, TRequirements>;
20
+ };
21
+ export declare function defineService<const TIdentifier extends string, const TRequirements extends RequirementRecord, const TInitializer extends Effect.Effect<unknown, unknown, unknown>, const TMethods extends MethodInventory>(definition: ServiceDefinition<TIdentifier, TRequirements, TInitializer, TMethods>): DefinedService<TIdentifier, ServiceShape<TMethods, TRequirements>, Effect.Error<TInitializer>, RequirementsOf<TRequirements>>;
22
+ export {};
23
+ //# sourceMappingURL=defineService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defineService.d.ts","sourceRoot":"","sources":["../src/defineService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,EAAa,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAe,eAAe,EAAE,WAAW,EAAwC,MAAM,kBAAkB,CAAC;AACxH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAElF,KAAK,WAAW,CAAC,OAAO,EAAE,aAAa,IAAI,OAAO,SAAS,CAC1D,GAAG,UAAU,EAAE,MAAM,UAAU,KAC3B,MAAM,CAAC,MAAM,CAAC,MAAM,QAAQ,EAAE,MAAM,QAAQ,EAAE,MAAM,SAAS,CAAC,GAChE,CAAC,GAAG,UAAU,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,GACnG,KAAK,CAAC;AAET,KAAK,YAAY,CAAC,MAAM,EAAE,aAAa,IAAI,MAAM,SAAS,uBAAuB,CAAC,MAAM,OAAO,CAAC,GAAG,OAAO,GAAG,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAEhJ,KAAK,YAAY,CAAC,QAAQ,SAAS,eAAe,EAAE,aAAa,SAAS,iBAAiB,IAAI,QAAQ,CAAC;KACtG,KAAK,IAAI,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;CACvF,CAAC,CAAC;AAEH,UAAU,iBAAiB,CAC1B,WAAW,SAAS,MAAM,EAC1B,aAAa,SAAS,iBAAiB,EACvC,YAAY,SAAS,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAC7D,QAAQ,SAAS,eAAe;IAEhC,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,YAAY,GAAG,gBAAgB,CAAC,YAAY,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC;IAClG,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC;IACpH,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,gBAAgB,CAAC,aAAa,CAAC,CAAC;CACnE;AASD,KAAK,cAAc,CAAC,WAAW,SAAS,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;IACzH,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;CAClE,CAAC;AAEF,wBAAgB,aAAa,CAC5B,KAAK,CAAC,WAAW,SAAS,MAAM,EAChC,KAAK,CAAC,aAAa,SAAS,iBAAiB,EAC7C,KAAK,CAAC,YAAY,SAAS,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EACnE,KAAK,CAAC,QAAQ,SAAS,eAAe,EAEtC,UAAU,EAAE,iBAAiB,CAAC,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,CAAC,GAC/E,cAAc,CAAC,WAAW,EAAE,YAAY,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { Context, Effect, Layer, Record } from "effect";
2
+ export function defineService(definition) {
3
+ const service = Context.Service(definition.id);
4
+ const layer = Layer.effect(service)(Effect.gen(function* () {
5
+ const ambient = yield* Effect.context();
6
+ const declared = Context.pick(...definition.requires)(ambient);
7
+ const state = yield* Effect.provide(definition.initialize, declared);
8
+ return Record.map(definition.methods(state), (entry) => {
9
+ const method = typeof entry === "function" ? entry : entry.method;
10
+ return (...arguments_) => Effect.provide(method(...arguments_), declared);
11
+ });
12
+ }));
13
+ return Object.assign(service, { layer });
14
+ }
15
+ //# sourceMappingURL=defineService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defineService.js","sourceRoot":"","sources":["../src/defineService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAkDxD,MAAM,UAAU,aAAa,CAAS,UAAqC;IAC1E,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAuB,UAAU,CAAC,EAAE,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAClC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAU,CAAC;QAChD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC;QAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAkB,EAAE,EAAE;YACnE,MAAM,MAAM,GAAc,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;YAC7E,OAAO,CAAC,GAAG,UAA4B,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7F,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CACF,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Effect } from "effect";
2
+ export type AnyMethod = (...arguments_: readonly never[]) => Effect.Effect<unknown, unknown, unknown>;
3
+ export interface GenericMethodDescriptor<TMethod extends AnyMethod> {
4
+ readonly _tag: "GenericMethod";
5
+ readonly method: TMethod;
6
+ }
7
+ type Same<TLeft, TRight> = (<TValue>() => TValue extends TLeft ? 1 : 2) extends <TValue>() => (TValue extends TRight ? 1 : 2) ? true : false;
8
+ export type HasDistinctCallSignatures<TMethod> = TMethod extends {
9
+ (...arguments_: infer TFirstArguments): infer TFirstResult;
10
+ (...arguments_: infer TLastArguments): infer TLastResult;
11
+ } ? Same<TFirstArguments, TLastArguments> extends true ? Same<TFirstResult, TLastResult> extends true ? false : true : true : false;
12
+ export interface GenericOrStructurallyOverloadedMethodsAreUnsupported {
13
+ readonly _serviceDefinitionError: "generic and structurally overloaded methods are unsupported";
14
+ }
15
+ type MarkedMethod<TMethod extends AnyMethod> = HasDistinctCallSignatures<TMethod> extends true ? GenericOrStructurallyOverloadedMethodsAreUnsupported : GenericMethodDescriptor<TMethod>;
16
+ export declare function genericMethod<TMethod extends AnyMethod>(method: TMethod): MarkedMethod<TMethod>;
17
+ export {};
18
+ //# sourceMappingURL=genericMethod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genericMethod.d.ts","sourceRoot":"","sources":["../src/genericMethod.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,UAAU,EAAE,SAAS,KAAK,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAEtG,MAAM,WAAW,uBAAuB,CAAC,OAAO,SAAS,SAAS;IACjE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAED,KAAK,IAAI,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,CAAC,MAAM,OAAO,MAAM,SAAS,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,QAAO,MAAM,SAAS,MAAM,GAAG,CAAC,GAAG,CAAC,CAAA,GAAG,IAAI,GAAG,KAAK,CAAC;AAE3I,MAAM,MAAM,yBAAyB,CAAC,OAAO,IAAI,OAAO,SAAS;IAChE,CAAC,GAAG,UAAU,EAAE,MAAM,eAAe,GAAG,MAAM,YAAY,CAAC;IAC3D,CAAC,GAAG,UAAU,EAAE,MAAM,cAAc,GAAG,MAAM,WAAW,CAAC;CACzD,GACE,IAAI,CAAC,eAAe,EAAE,cAAc,CAAC,SAAS,IAAI,GACjD,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,SAAS,IAAI,GAC3C,KAAK,GACL,IAAI,GACL,IAAI,GACL,KAAK,CAAC;AAET,MAAM,WAAW,oDAAoD;IACpE,QAAQ,CAAC,uBAAuB,EAAE,6DAA6D,CAAC;CAChG;AAED,KAAK,YAAY,CAAC,OAAO,SAAS,SAAS,IAC1C,yBAAyB,CAAC,OAAO,CAAC,SAAS,IAAI,GAAG,oDAAoD,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAE3I,wBAAgB,aAAa,CAAC,OAAO,SAAS,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export function genericMethod(method) {
2
+ return { _tag: "GenericMethod", method };
3
+ }
4
+ //# sourceMappingURL=genericMethod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genericMethod.js","sourceRoot":"","sources":["../src/genericMethod.ts"],"names":[],"mappings":"AA8BA,MAAM,UAAU,aAAa,CAAC,MAAiB;IAC9C,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;AAC1C,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,71 @@
1
1
  {
2
2
  "name": "@shivaedev/service",
3
- "version": "0.0.0",
4
- "description": "Placeholder for the ShivaeDev service package.",
5
- "license": "MIT",
3
+ "version": "0.5.0",
4
+ "description": "Declared Effect services with scoped initialization and bound dependencies",
5
+ "keywords": [
6
+ "effect"
7
+ ],
8
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/service#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/ShivaeDev/platform/issues"
11
+ },
6
12
  "repository": {
7
13
  "type": "git",
8
- "url": "git+https://github.com/ShivaeDev/platform.git"
14
+ "url": "git+https://github.com/ShivaeDev/platform.git",
15
+ "directory": "packages/service"
16
+ },
17
+ "license": "MIT",
18
+ "sideEffects": false,
19
+ "type": "module",
20
+ "imports": {
21
+ "#test/*": "./src/test-support/*",
22
+ "#*.ts": {
23
+ "source": "./src/*.ts",
24
+ "types": "./dist/*.d.ts",
25
+ "default": "./dist/*.js"
26
+ }
27
+ },
28
+ "exports": {
29
+ "./*.ts": {
30
+ "source": "./src/*.ts",
31
+ "types": "./dist/*.d.ts",
32
+ "default": "./dist/*.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "!src/**/*.test.ts",
40
+ "!src/**/*.spec.ts",
41
+ "!src/**/test-support",
42
+ "CHANGELOG.md",
43
+ "README.md"
44
+ ],
45
+ "devDependencies": {
46
+ "@effect/vitest": "4.0.5",
47
+ "@shivaedev/quality": "0.42.0",
48
+ "@shivaedev/test": "0.10.0",
49
+ "@shivaedev/test-story": "0.21.0",
50
+ "@types/node": "24.19.2",
51
+ "effect": "4.0.5",
52
+ "typescript": "7.0.2",
53
+ "vitest": "5.0.5"
54
+ },
55
+ "peerDependencies": {
56
+ "effect": "4.0.5"
57
+ },
58
+ "engines": {
59
+ "node": ">=24"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public",
63
+ "provenance": true
64
+ },
65
+ "scripts": {
66
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
67
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
68
+ "test": "vitest run",
69
+ "typecheck": "tsc --noEmit"
9
70
  }
10
71
  }
@@ -0,0 +1,12 @@
1
+ import type { Effect, Scope } from "effect";
2
+
3
+ interface InitializerHasUndeclaredServiceRequirements {
4
+ readonly _serviceDefinitionError: "initializer has service requirements absent from the service declaration";
5
+ }
6
+
7
+ export type InitializerProof<TInitializer, TRequirements> =
8
+ TInitializer extends Effect.Effect<unknown, unknown, infer TResidual>
9
+ ? [Exclude<TResidual, TRequirements | Scope.Scope>] extends [never]
10
+ ? TInitializer
11
+ : InitializerHasUndeclaredServiceRequirements
12
+ : never;
@@ -0,0 +1,57 @@
1
+ import type { Effect, Scope } from "effect";
2
+ import type {
3
+ AnyMethod,
4
+ GenericMethodDescriptor,
5
+ GenericOrStructurallyOverloadedMethodsAreUnsupported,
6
+ HasDistinctCallSignatures,
7
+ } from "./genericMethod.ts";
8
+ import type { RequirementRecord, RequirementsOf } from "./ServiceRequirements.ts";
9
+
10
+ export type MethodEntry = AnyMethod | GenericMethodDescriptor<AnyMethod>;
11
+
12
+ export type MethodRecord = Readonly<Record<string, AnyMethod>>;
13
+
14
+ export type MethodInventory = Readonly<Record<string, unknown>>;
15
+
16
+ export type RuntimeMethodInventory = Readonly<Record<string, MethodEntry>>;
17
+
18
+ interface GenericMethodWithDeclaredRequirementsIsUnsupported {
19
+ readonly _serviceDefinitionError: "generic methods cannot subtract declared service requirements";
20
+ }
21
+
22
+ type MethodRequirements<TMethod> = TMethod extends (...arguments_: readonly never[]) => Effect.Effect<unknown, unknown, infer TRequirements>
23
+ ? TRequirements
24
+ : never;
25
+
26
+ interface MethodHasUndeclaredServiceRequirements {
27
+ readonly _serviceDefinitionError: "method has service requirements absent from the service declaration";
28
+ }
29
+
30
+ interface ServiceMembersMustBeMethods {
31
+ readonly _serviceDefinitionError: "service members must be methods";
32
+ }
33
+
34
+ type OrdinaryMethodProof<TMethod, TRequirements extends RequirementRecord> = [
35
+ Exclude<MethodRequirements<TMethod>, RequirementsOf<TRequirements> | Scope.Scope>,
36
+ ] extends [never]
37
+ ? TMethod
38
+ : MethodHasUndeclaredServiceRequirements;
39
+
40
+ type SupportedMethod<TMethod, TRequirements extends RequirementRecord> =
41
+ TMethod extends GenericMethodDescriptor<AnyMethod>
42
+ ? TRequirements extends readonly []
43
+ ? TMethod
44
+ : GenericMethodWithDeclaredRequirementsIsUnsupported
45
+ : TMethod extends AnyMethod
46
+ ? HasDistinctCallSignatures<TMethod> extends true
47
+ ? GenericOrStructurallyOverloadedMethodsAreUnsupported
48
+ : TMethod extends (...arguments_: infer TArguments) => infer TResult
49
+ ? ((...arguments_: TArguments) => TResult) extends TMethod
50
+ ? OrdinaryMethodProof<TMethod, TRequirements>
51
+ : GenericOrStructurallyOverloadedMethodsAreUnsupported
52
+ : GenericOrStructurallyOverloadedMethodsAreUnsupported
53
+ : ServiceMembersMustBeMethods;
54
+
55
+ export type MethodProof<TMethods extends MethodInventory, TRequirements extends RequirementRecord> = {
56
+ readonly [TName in keyof TMethods]: SupportedMethod<TMethods[TName], TRequirements>;
57
+ };
@@ -0,0 +1,16 @@
1
+ import type { Context, Scope } from "effect";
2
+ import type { RequirementRecord } from "./ServiceRequirements.ts";
3
+
4
+ interface ScopeCannotBeDeclaredAsServiceRequirement {
5
+ readonly _serviceDefinitionError: "Scope.Scope is caller-owned and cannot be declared as a service requirement";
6
+ }
7
+
8
+ type ProveRequirement<TRequirement> = TRequirement extends Context.Service.Any
9
+ ? Context.Service.Identifier<TRequirement> extends Scope.Scope
10
+ ? ScopeCannotBeDeclaredAsServiceRequirement
11
+ : TRequirement
12
+ : never;
13
+
14
+ export type RequirementProof<TRequirements extends RequirementRecord> = {
15
+ readonly [TIndex in keyof TRequirements]: ProveRequirement<TRequirements[TIndex]>;
16
+ };
@@ -0,0 +1,12 @@
1
+ import type { Context, Effect, Scope } from "effect";
2
+
3
+ export type RequirementRecord = readonly Context.Service.Any[];
4
+
5
+ export type RequirementsOf<TRequirements extends RequirementRecord> = Context.Service.Identifier<TRequirements[number]>;
6
+
7
+ export type ServiceRequirements<
8
+ TRequirements extends RequirementRecord,
9
+ TSuccess,
10
+ TFailure = never,
11
+ TCallerScope extends Scope.Scope = never,
12
+ > = Effect.fn.Return<TSuccess, TFailure, RequirementsOf<TRequirements> | TCallerScope>;
@@ -0,0 +1,65 @@
1
+ import { Context, Effect, Layer, Record } from "effect";
2
+ import type { AnyMethod, GenericMethodDescriptor } from "./genericMethod.ts";
3
+ import type { InitializerProof } from "./InitializerProof.ts";
4
+ import type { MethodEntry, MethodInventory, MethodProof, MethodRecord, RuntimeMethodInventory } from "./MethodProof.ts";
5
+ import type { RequirementProof } from "./RequirementProof.ts";
6
+ import type { RequirementRecord, RequirementsOf } from "./ServiceRequirements.ts";
7
+
8
+ type BoundMethod<TMethod, TRequirements> = TMethod extends (
9
+ ...arguments_: infer TArguments
10
+ ) => Effect.Effect<infer TSuccess, infer TFailure, infer TResidual>
11
+ ? (...arguments_: TArguments) => Effect.Effect<TSuccess, TFailure, Exclude<TResidual, TRequirements>>
12
+ : never;
13
+
14
+ type PublicMethod<TEntry, TRequirements> = TEntry extends GenericMethodDescriptor<infer TMethod> ? TMethod : BoundMethod<TEntry, TRequirements>;
15
+
16
+ type ServiceShape<TMethods extends MethodInventory, TRequirements extends RequirementRecord> = Readonly<{
17
+ [TName in keyof TMethods]: PublicMethod<TMethods[TName], RequirementsOf<TRequirements>>;
18
+ }>;
19
+
20
+ interface ServiceDefinition<
21
+ TIdentifier extends string,
22
+ TRequirements extends RequirementRecord,
23
+ TInitializer extends Effect.Effect<unknown, unknown, unknown>,
24
+ TMethods extends MethodInventory,
25
+ > {
26
+ readonly id: TIdentifier;
27
+ readonly initialize: TInitializer & InitializerProof<TInitializer, RequirementsOf<TRequirements>>;
28
+ readonly methods: (state: Effect.Success<TInitializer>) => TMethods & MethodProof<NoInfer<TMethods>, TRequirements>;
29
+ readonly requires: TRequirements & RequirementProof<TRequirements>;
30
+ }
31
+
32
+ interface RuntimeDefinition<TState> {
33
+ readonly id: string;
34
+ readonly initialize: Effect.Effect<TState, unknown, unknown>;
35
+ readonly methods: (state: TState) => RuntimeMethodInventory;
36
+ readonly requires: RequirementRecord;
37
+ }
38
+
39
+ type DefinedService<TIdentifier extends string, TShape, TFailure, TRequirements> = Context.Service<TIdentifier, TShape> & {
40
+ readonly layer: Layer.Layer<TIdentifier, TFailure, TRequirements>;
41
+ };
42
+
43
+ export function defineService<
44
+ const TIdentifier extends string,
45
+ const TRequirements extends RequirementRecord,
46
+ const TInitializer extends Effect.Effect<unknown, unknown, unknown>,
47
+ const TMethods extends MethodInventory,
48
+ >(
49
+ definition: ServiceDefinition<TIdentifier, TRequirements, TInitializer, TMethods>,
50
+ ): DefinedService<TIdentifier, ServiceShape<TMethods, TRequirements>, Effect.Error<TInitializer>, RequirementsOf<TRequirements>>;
51
+ export function defineService<TState>(definition: RuntimeDefinition<TState>): unknown {
52
+ const service = Context.Service<string, MethodRecord>(definition.id);
53
+ const layer = Layer.effect(service)(
54
+ Effect.gen(function* () {
55
+ const ambient = yield* Effect.context<string>();
56
+ const declared = Context.pick(...definition.requires)(ambient);
57
+ const state = yield* Effect.provide(definition.initialize, declared);
58
+ return Record.map(definition.methods(state), (entry: MethodEntry) => {
59
+ const method: AnyMethod = typeof entry === "function" ? entry : entry.method;
60
+ return (...arguments_: readonly never[]) => Effect.provide(method(...arguments_), declared);
61
+ });
62
+ }),
63
+ );
64
+ return Object.assign(service, { layer });
65
+ }
@@ -0,0 +1,33 @@
1
+ import type { Effect } from "effect";
2
+
3
+ export type AnyMethod = (...arguments_: readonly never[]) => Effect.Effect<unknown, unknown, unknown>;
4
+
5
+ export interface GenericMethodDescriptor<TMethod extends AnyMethod> {
6
+ readonly _tag: "GenericMethod";
7
+ readonly method: TMethod;
8
+ }
9
+
10
+ type Same<TLeft, TRight> = (<TValue>() => TValue extends TLeft ? 1 : 2) extends <TValue>() => TValue extends TRight ? 1 : 2 ? true : false;
11
+
12
+ export type HasDistinctCallSignatures<TMethod> = TMethod extends {
13
+ (...arguments_: infer TFirstArguments): infer TFirstResult;
14
+ (...arguments_: infer TLastArguments): infer TLastResult;
15
+ }
16
+ ? Same<TFirstArguments, TLastArguments> extends true
17
+ ? Same<TFirstResult, TLastResult> extends true
18
+ ? false
19
+ : true
20
+ : true
21
+ : false;
22
+
23
+ export interface GenericOrStructurallyOverloadedMethodsAreUnsupported {
24
+ readonly _serviceDefinitionError: "generic and structurally overloaded methods are unsupported";
25
+ }
26
+
27
+ type MarkedMethod<TMethod extends AnyMethod> =
28
+ HasDistinctCallSignatures<TMethod> extends true ? GenericOrStructurallyOverloadedMethodsAreUnsupported : GenericMethodDescriptor<TMethod>;
29
+
30
+ export function genericMethod<TMethod extends AnyMethod>(method: TMethod): MarkedMethod<TMethod>;
31
+ export function genericMethod(method: AnyMethod): unknown {
32
+ return { _tag: "GenericMethod", method };
33
+ }