@shivaedev/service 0.5.0 → 0.5.1
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 +12 -0
- package/README.md +66 -83
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 - 2026-10-30
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Require Effect 4.0.6 and align the Effect companion package peers and generated starter catalog to the same exact release. Install the 4.0.6 Effect stack together.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Register hosted RPC cache-control handling through Effect 4.0.6's request-scoped pre-response API so responses keep `private, no-store`.
|
|
12
|
+
- Accept native RPC's `application/x-ndjson` media type while preserving `application/ndjson` callers at the transport guard.
|
|
13
|
+
- Keep subscriptions open after their HTTP request body completes; closing the socket still cancels them.
|
|
14
|
+
|
|
3
15
|
## 0.5.0 - 2026-10-27
|
|
4
16
|
|
|
5
17
|
### Breaking
|
package/README.md
CHANGED
|
@@ -8,14 +8,12 @@ A service should explain what it needs and what callers can do with it. When eve
|
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
const Greetings = defineService({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
),
|
|
18
|
-
}),
|
|
11
|
+
id: "app/Greetings",
|
|
12
|
+
requires: [Prefix],
|
|
13
|
+
initialize: Effect.succeed({ punctuation: "!" }),
|
|
14
|
+
methods: (state) => ({
|
|
15
|
+
greet: Effect.fn("Greetings.greet")((name: string) => Effect.map(Prefix, (prefix) => `${prefix}${name}${state.punctuation}`)),
|
|
16
|
+
}),
|
|
19
17
|
});
|
|
20
18
|
```
|
|
21
19
|
|
|
@@ -34,7 +32,6 @@ A **declared dependency** is a service tag in `requires`. The Layer supplies tho
|
|
|
34
32
|
**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
33
|
|
|
36
34
|
The work has three parts:
|
|
37
|
-
|
|
38
35
|
1. Once at the application boundary, compose service Layers with their dependencies.
|
|
39
36
|
2. Once per service, define its dependency list, private state and methods.
|
|
40
37
|
3. In application code, yield the service and call its methods.
|
|
@@ -48,23 +45,19 @@ import { defineService } from "@shivaedev/service/defineService.ts";
|
|
|
48
45
|
class Prefix extends Context.Service<Prefix, string>()("app/Prefix") {}
|
|
49
46
|
|
|
50
47
|
const Greetings = defineService({
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
),
|
|
58
|
-
}),
|
|
48
|
+
id: "app/Greetings",
|
|
49
|
+
requires: [Prefix],
|
|
50
|
+
initialize: Effect.succeed({ punctuation: "!" }),
|
|
51
|
+
methods: (state) => ({
|
|
52
|
+
greet: Effect.fn("Greetings.greet")((name: string) => Effect.map(Prefix, (prefix) => `${prefix}${name}${state.punctuation}`)),
|
|
53
|
+
}),
|
|
59
54
|
});
|
|
60
55
|
|
|
61
|
-
const live = Greetings.layer.pipe(
|
|
62
|
-
Layer.provide(Layer.succeed(Prefix, "Hello ")),
|
|
63
|
-
);
|
|
56
|
+
const live = Greetings.layer.pipe(Layer.provide(Layer.succeed(Prefix, "Hello ")));
|
|
64
57
|
|
|
65
58
|
const greeting = Effect.gen(function* () {
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const greetings = yield* Greetings;
|
|
60
|
+
return yield* greetings.greet("Ada");
|
|
68
61
|
}).pipe(Effect.provide(live));
|
|
69
62
|
```
|
|
70
63
|
|
|
@@ -83,28 +76,30 @@ import { Context, Effect, type Scope } from "effect";
|
|
|
83
76
|
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
84
77
|
import type { ServiceRequirements } from "@shivaedev/service/ServiceRequirements.ts";
|
|
85
78
|
|
|
86
|
-
class Declared extends Context.Service<Declared, { readonly identity: object }>()(
|
|
87
|
-
"app/Declared",
|
|
88
|
-
) {}
|
|
79
|
+
class Declared extends Context.Service<Declared, { readonly identity: object }>()("app/Declared") {}
|
|
89
80
|
|
|
90
81
|
const requirements = [Declared] as const;
|
|
91
82
|
|
|
92
|
-
type Requirements<Success, Failure = never, CallerScope extends Scope.Scope = never> =
|
|
93
|
-
|
|
83
|
+
type Requirements<Success, Failure = never, CallerScope extends Scope.Scope = never> = ServiceRequirements<
|
|
84
|
+
typeof requirements,
|
|
85
|
+
Success,
|
|
86
|
+
Failure,
|
|
87
|
+
CallerScope
|
|
88
|
+
>;
|
|
94
89
|
|
|
95
90
|
const Identity = defineService({
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
id: "app/Identity",
|
|
92
|
+
requires: requirements,
|
|
93
|
+
initialize: Effect.fn("Identity.initialize")(function* (): Requirements<{
|
|
94
|
+
readonly identity: object;
|
|
95
|
+
}> {
|
|
96
|
+
return { identity: (yield* Declared).identity };
|
|
97
|
+
})(),
|
|
98
|
+
methods: (state) => ({
|
|
99
|
+
sameIdentity: Effect.fn("Identity.sameIdentity")(function* (): Requirements<boolean> {
|
|
100
|
+
return (yield* Declared).identity === state.identity;
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
108
103
|
});
|
|
109
104
|
```
|
|
110
105
|
|
|
@@ -117,24 +112,19 @@ import { Effect, Ref } from "effect";
|
|
|
117
112
|
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
118
113
|
|
|
119
114
|
const Resources = defineService({
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
() => Ref.update(released, (count) => count + 1),
|
|
128
|
-
),
|
|
129
|
-
),
|
|
130
|
-
released: Effect.fn("Resources.released")(() => Ref.get(released)),
|
|
131
|
-
}),
|
|
115
|
+
id: "app/Resources",
|
|
116
|
+
requires: [],
|
|
117
|
+
initialize: Ref.make(0),
|
|
118
|
+
methods: (released) => ({
|
|
119
|
+
open: Effect.fn("Resources.open")(() => Effect.acquireRelease(Effect.succeed("resource"), () => Ref.update(released, (count) => count + 1))),
|
|
120
|
+
released: Effect.fn("Resources.released")(() => Ref.get(released)),
|
|
121
|
+
}),
|
|
132
122
|
});
|
|
133
123
|
|
|
134
124
|
const useResource = Effect.gen(function* () {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
125
|
+
const resources = yield* Resources;
|
|
126
|
+
yield* Effect.scoped(resources.open());
|
|
127
|
+
return yield* resources.released();
|
|
138
128
|
}).pipe(Effect.provide(Resources.layer));
|
|
139
129
|
```
|
|
140
130
|
|
|
@@ -146,7 +136,7 @@ A native Layer can provide the method interface directly:
|
|
|
146
136
|
|
|
147
137
|
```ts
|
|
148
138
|
const testGreetings = Layer.succeed(Greetings, {
|
|
149
|
-
|
|
139
|
+
greet: Effect.fn("TestGreetings.greet")((name: string) => Effect.succeed(name)),
|
|
150
140
|
});
|
|
151
141
|
```
|
|
152
142
|
|
|
@@ -161,31 +151,25 @@ import { Context, Effect } from "effect";
|
|
|
161
151
|
import { defineService } from "@shivaedev/service/defineService.ts";
|
|
162
152
|
import { genericMethod } from "@shivaedev/service/genericMethod.ts";
|
|
163
153
|
|
|
164
|
-
class Caller extends Context.Service<Caller, { readonly value: number }>()(
|
|
165
|
-
"app/Caller",
|
|
166
|
-
) {}
|
|
154
|
+
class Caller extends Context.Service<Caller, { readonly value: number }>()("app/Caller") {}
|
|
167
155
|
|
|
168
156
|
const preserve = Effect.fn("Generic.preserve")(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Effect.map(effect, (value) => ({ value })),
|
|
157
|
+
<Success, Failure, Requirements>(
|
|
158
|
+
effect: Effect.Effect<Success, Failure, Requirements>,
|
|
159
|
+
): Effect.Effect<{ readonly value: Success }, Failure, Requirements> => Effect.map(effect, (value) => ({ value })),
|
|
173
160
|
);
|
|
174
161
|
|
|
175
162
|
const Generic = defineService({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
163
|
+
id: "app/Generic",
|
|
164
|
+
requires: [],
|
|
165
|
+
initialize: Effect.void,
|
|
166
|
+
methods: () => ({ preserve: genericMethod(preserve) }),
|
|
180
167
|
});
|
|
181
168
|
|
|
182
169
|
const preserved = Effect.gen(function* () {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}).pipe(
|
|
186
|
-
Effect.provide(Generic.layer),
|
|
187
|
-
Effect.provideService(Caller, { value: 42 }),
|
|
188
|
-
);
|
|
170
|
+
const service = yield* Generic;
|
|
171
|
+
return yield* service.preserve(Effect.map(Caller, ({ value }) => value));
|
|
172
|
+
}).pipe(Effect.provide(Generic.layer), Effect.provideService(Caller, { value: 42 }));
|
|
189
173
|
```
|
|
190
174
|
|
|
191
175
|
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.
|
|
@@ -198,28 +182,27 @@ Dependency binding wraps the method's outer returned Effect. A Stream returned a
|
|
|
198
182
|
|
|
199
183
|
Import the module that defines the name:
|
|
200
184
|
|
|
201
|
-
| Module
|
|
202
|
-
|
|
|
203
|
-
| `@shivaedev/service/defineService.ts`
|
|
204
|
-
| `@shivaedev/service/genericMethod.ts`
|
|
185
|
+
| Module | Name | Use |
|
|
186
|
+
| ------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
187
|
+
| `@shivaedev/service/defineService.ts` | `defineService(definition)` | Create a native service with its `.layer`. |
|
|
188
|
+
| `@shivaedev/service/genericMethod.ts` | `genericMethod(method)` | Mark a generic Effect-returning function in a service with no declared dependencies. |
|
|
205
189
|
| `@shivaedev/service/ServiceRequirements.ts` | `ServiceRequirements<Requirements, Success, Failure = never, CallerScope = never>` | Annotate an `Effect.fn` generator with declared dependencies and optional caller scope. |
|
|
206
190
|
|
|
207
191
|
The definition's fields are:
|
|
208
192
|
|
|
209
|
-
| Field
|
|
210
|
-
|
|
|
211
|
-
| `id`
|
|
212
|
-
| `requires`
|
|
213
|
-
| `initialize`
|
|
193
|
+
| Field | Meaning |
|
|
194
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| `id` | The Context service identity, also retained in the Layer's output type. |
|
|
196
|
+
| `requires` | A readonly list of service tags available to initialization and ordinary methods. |
|
|
197
|
+
| `initialize` | The Effect producing private state; its failure type becomes the Layer's failure type. |
|
|
214
198
|
| `methods(state)` | A function returning the public method record, with declared dependencies removed from ordinary methods' requirement types. |
|
|
215
199
|
|
|
216
200
|
### Install and limits
|
|
217
201
|
|
|
218
202
|
```sh
|
|
219
|
-
pnpm add @shivaedev/service effect
|
|
203
|
+
pnpm add @shivaedev/service effect
|
|
220
204
|
```
|
|
221
205
|
|
|
222
206
|
`effect` is a peer dependency, and the package needs Node.js 24 or later.
|
|
223
|
-
|
|
224
207
|
- 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
208
|
- Choose service Layer lifetimes at the application's composition boundary; keep initialization resource ownership separate from a method caller's scope.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/service",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Declared Effect services with scoped initialization and bound dependencies",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -43,17 +43,17 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@effect/vitest": "4.0.
|
|
47
|
-
"@shivaedev/quality": "0.
|
|
48
|
-
"@shivaedev/test": "0.10.
|
|
49
|
-
"@shivaedev/test-story": "0.
|
|
46
|
+
"@effect/vitest": "4.0.6",
|
|
47
|
+
"@shivaedev/quality": "0.45.0",
|
|
48
|
+
"@shivaedev/test": "0.10.1",
|
|
49
|
+
"@shivaedev/test-story": "0.24.0",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
|
-
"effect": "4.0.
|
|
51
|
+
"effect": "4.0.6",
|
|
52
52
|
"typescript": "7.0.2",
|
|
53
53
|
"vitest": "5.0.5"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"effect": "4.0.
|
|
56
|
+
"effect": "4.0.6"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=24"
|