@shivaedev/effect-contract 0.4.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-10-13
4
+
5
+ ### Breaking
6
+
7
+ - 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`.
8
+
9
+ ## 0.4.1 - 2026-10-12
10
+
11
+ ### Added
12
+
13
+ - `makeLiveServer` in `makeLiveServer.ts` converts local Reactivity invalidations into native `LiveHint` streams for explicit read keys. Independent helpers keep their resync scopes separate. Each subscription starts with `Resync`; `resync` reconciles every active stream, including after a PostgreSQL listener loses its connection.
14
+ - Declare typed streaming operations with Schema payloads and elements, rejections and read keys in native RPC contracts. Bound `run` returns a native Stream, with HTTP tests proving element delivery, declared failures and server subscription release on completion or client interruption.
15
+
16
+ ### Changed
17
+
18
+ - Prefix generic type parameter names in contract, operation and collection declarations with `T` while preserving their types and runtime behavior.
19
+ - Document the package's purpose, native RPC and freshness model, complete usage, design priorities and roadmap; publish `docs/` with the README.
20
+ - Build package JavaScript, declarations and source maps with TypeScript 7.
21
+
3
22
  ## 0.4.0 - 2026-10-05
4
23
 
5
24
  ### Added
package/README.md CHANGED
@@ -1,100 +1,369 @@
1
1
  # @shivaedev/effect-contract
2
2
 
3
- Declare queries and commands once. Each declaration becomes an ordinary native Effect `Rpc`, grouped into a native `RpcGroup`, plus the typed rejection classes and reactivity keys that application code would otherwise repeat by hand. Handlers, middleware, servers and clients stay native.
3
+ Declare a feature's queries, commands, streams, expected failures and refresh dependencies once. The declarations become native Effect RPC definitions, and a client binding connects their dependency keys to native AtomRpc queries.
4
+
5
+ ## Why you want this
6
+
7
+ A feature often repeats its inputs, results and errors on both sides of an RPC boundary, then separately wires which views a write should refresh. Those copies are easy to make disagree. Put the boundary and its dependencies together:
4
8
 
5
9
  ```ts
6
10
  import { Schema } from "effect";
11
+ import { contract } from "@shivaedev/effect-contract/contract.ts";
7
12
  import { collection } from "@shivaedev/effect-contract/keys.ts";
8
13
  import { command, query } from "@shivaedev/effect-contract/operation.ts";
14
+
15
+ const notes = collection("notes", Schema.Number);
16
+ const List = query("list", { reads: () => [notes.list], success: Schema.Array(Schema.String) });
17
+ const Rename = command("rename", {
18
+ payload: { id: Schema.Number, title: Schema.String },
19
+ invalidates: ({ id }) => [notes.item(id)],
20
+ });
21
+ const Notes = contract("notes", { queries: [List], commands: [Rename] });
22
+ ```
23
+
24
+ `Notes` is a native RPC group with `notes.list` and `notes.rename` handlers. When its bound rename call succeeds, the declared item change refreshes the list. An item query for another note keeps its result. Handlers, middleware and transport use Effect's own APIs.
25
+
26
+ ## Using it
27
+
28
+ ### How to think about it
29
+
30
+ An **operation** describes a boundary, not its implementation: its name, payload schema, success schema and expected failures. A **query** also declares the keys it reads; a **command** declares the keys its successful result changes. A **stream** declares an element schema in `success`, typed rejections and the keys it reads. A **rejection** is an expected typed failure, such as a missing note or an invalid title. Middleware failures and RPC client failures remain separate parts of the native client's error type.
31
+
32
+ A **contract** groups operations into a native `RpcGroup`. Its RPC tags are the contract name and operation name joined by a dot. A **binding** connects that declaration to an existing `AtomRpc.Service`: queries get native query atoms and call Effects, while commands get call Effects that invalidate after success. Streams expose a native `Stream` through `run`; the caller owns consumption and interruption.
33
+
34
+ A **collection key** names a dependency. Its item key identifies one item; its list key identifies a list in that collection. Keys do not hold data. Native query atoms hold results, and native Reactivity asks matching queries to run again.
35
+
36
+ Optional live hints use those same keys. A hint asks for another read; it does not supply a replacement value, a journal entry or proof that a query is fresh.
37
+
38
+ ### 1. Once per feature: schemas and operations
39
+
40
+ Keep the shared declaration in a module that both server and client can import:
41
+
42
+ ```ts
43
+ import { Schema } from "effect";
9
44
  import { contract } from "@shivaedev/effect-contract/contract.ts";
45
+ import { collection } from "@shivaedev/effect-contract/keys.ts";
46
+ import { command, query } from "@shivaedev/effect-contract/operation.ts";
10
47
  import { fieldRejection } from "@shivaedev/effect-contract/rejection.ts";
11
48
 
12
- class Order extends Schema.Class<Order>("Order")({ id: Schema.Number, name: Schema.String, quantity: Schema.Number }) {}
13
- class OrderNotFound extends Schema.TaggedError<OrderNotFound>()("OrderNotFound", {}) {}
14
- const OrderDraft = Schema.Struct({ name: Order.fields.name, quantity: Order.fields.quantity });
15
-
16
- export const orders = collection("orders", Order.fields.id);
49
+ export class Note extends Schema.Class<Note>("Note")({
50
+ body: Schema.String,
51
+ id: Schema.Number,
52
+ title: Schema.String,
53
+ }) {}
54
+ export const Draft = Schema.Struct({ body: Schema.String, title: Schema.String });
55
+ export class NoteMissing extends Schema.TaggedError<NoteMissing>()("NoteMissing", { id: Schema.Number }) {}
56
+ export const notes = collection("notes", Note.fields.id);
17
57
 
18
- export const GetOrder = query("get", {
58
+ export const Get = query("get", {
19
59
  payload: { id: Schema.Number },
20
- success: Order,
21
- rejections: { OrderNotFound },
22
- reads: ({ id }) => [orders.item(id)],
60
+ success: Note,
61
+ rejections: { NoteMissing },
62
+ reads: ({ id }) => [notes.item(id)],
63
+ });
64
+ export const List = query("list", {
65
+ success: Schema.Array(Note),
66
+ reads: () => [notes.list],
23
67
  });
24
- export const ListOrders = query("list", { success: Schema.Array(Order), reads: () => [orders.list] });
25
- export const SaveOrder = command("save", {
26
- payload: { id: Schema.Number, ...OrderDraft.fields },
27
- success: Order,
28
- rejections: { OrderNotFound, OrderValidation: fieldRejection(OrderDraft) },
29
- invalidates: ({ id }) => [orders.item(id)],
68
+ export const Rename = command("rename", {
69
+ payload: { id: Schema.Number, title: Schema.String },
70
+ success: Note,
71
+ rejections: { NoteMissing, Invalid: fieldRejection(Draft, ["title"]) },
72
+ invalidates: ({ id }) => [notes.item(id)],
30
73
  });
74
+ export const Create = command("create", {
75
+ payload: Draft,
76
+ success: Note,
77
+ rejections: { Invalid: fieldRejection(Draft) },
78
+ invalidates: (_draft, note) => [notes.item(note.id)],
79
+ });
80
+ export const Notes = contract("notes", { queries: [Get, List], commands: [Rename, Create] });
81
+ ```
82
+
83
+ Save this module as `notes.ts`. The payload can be struct fields, as in `Get`, or a schema, as in `Create`. An omitted payload or success is `Schema.Void`; no declared rejections gives `Schema.Never`. The `reads` callback receives the decoded payload. `invalidates` receives the decoded payload and successful result, so a create call can invalidate the ID the server assigned.
31
84
 
32
- export const Orders = contract("orders", { queries: [GetOrder, ListOrders], commands: [SaveOrder] }).middleware(Authentication);
85
+ The collection's ID schema gives `item` its ID type. Here, `notes.item` takes a number. Query and command callbacks retain their payload and result types, including when the operations are declared inline inside `contract`.
86
+
87
+ Operation names must differ within a contract, including across queries, commands and streams. Literal duplicate declarations fail compilation. If the compiler cannot see the names in an array, contract construction checks them and throws with the duplicated names.
88
+
89
+ ### Expected failures
90
+
91
+ `rejections: { NoteMissing }` reuses the class. The object's key must equal the class's `_tag`. Sharing a class lets a service and several operations use the same expected failure.
92
+
93
+ `rejections: { Invalid: fieldRejection(Draft) }` generates a tagged error class for that operation. `fieldRejection` derives the allowed field names from the struct; an explicit field list narrows them. `Rename` permits only `"title"`, while `Create` permits `"body"` or `"title"`. Their generated `Invalid` classes are distinct.
94
+
95
+ ```ts
96
+ const invalid = new Rename.Rejection.Invalid({ field: "title", message: "Enter a title" });
97
+ const refused = Rename.reject.Invalid({ field: "title", message: "Enter a title" });
98
+ const missing = Get.reject.NoteMissing({ id: 9 });
33
99
  ```
34
100
 
35
- ## API
101
+ `invalid` is the generated class instance. `refused` and `missing` are failed Effects, with the corresponding rejection in their error channel. Each operation also exposes `payload`, `success` and `error`, so another boundary can use its schemas directly. A handler returning an undeclared rejection fails compilation.
36
102
 
37
- | Export | Signature (simplified) | Result |
38
- | --- | --- | --- |
39
- | `query` | `query(name, { payload?, success?, rejections?, reads: (payload) => Key[] })` | `Query<Name, Payload, Success, Specs>` |
40
- | `command` | `command(name, { payload?, success?, rejections?, invalidates: (payload, result) => Key[] })` | `Command<Name, Payload, Success, Specs>` |
41
- | `contract` | `contract(name, { queries?, commands? })` | `Contract<Name, Queries, Commands, Rpcs>`, a native `RpcGroup<Rpcs>` |
42
- | `collection` | `collection(name, idSchema)` | `{ name, list, item(id) }` typed reactivity keys |
43
- | `fieldRejection` | `fieldRejection(struct, keys?)` | fields `{ field: Literals<keys>, message: String }` |
44
- | `bind` | `bind(contract, atomRpcService)` | `{ [query]: { query, run }, [command]: { run } }` |
45
- | `readKeys` / `invalidationKeys` | `(keys: Key[]) => string[]` | native Reactivity keys |
46
- | `Key` / `LiveHint` | Schemas in `keys.ts` / `live.ts` | existing list/item keys and optional Changed/Resync hints |
47
- | `resumeSignal` | `resumeSignal({ window?, native? })` in `resume.ts` | scoped native atom for visible browser resume/online and injected native resume |
48
- | `live` | `live(atomRuntime, { stream, resyncKeys, resume?, retryDelay? })` | native connection atom, writable pause atom and typed status atom |
103
+ ### 2. Once per host: native handlers and transport
49
104
 
50
- Payload accepts struct fields or any schema, as in `Rpc.make`. Omitted payload and success are `Schema.Void`.
105
+ Implement the contract with native `of` and `toLayer`:
51
106
 
52
- Optional [live updates](../../docs/framework/live-updates.md) compose native
53
- Streams, RPC, Reactivity and AtomRegistry without React or a second cache. The
54
- guide distinguishes library behavior, real HTTP/browser fixture evidence and
55
- application adoption, and explains why streaming HTTP uses native NDJSON.
107
+ ```ts
108
+ import { Effect, Ref } from "effect";
109
+ import { Create, Get, Note, Notes, Rename } from "./notes.ts";
110
+
111
+ export const NotesHandlers = Notes.toLayer(Effect.gen(function* () {
112
+ const stored = yield* Ref.make(new Map([
113
+ [1, new Note({ body: "", id: 1, title: "One" })],
114
+ [2, new Note({ body: "", id: 2, title: "Two" })],
115
+ ]));
116
+ const find = (id: number) => Effect.flatMap(Ref.get(stored), (all) => {
117
+ const note = all.get(id);
118
+ return note === undefined ? Get.reject.NoteMissing({ id }) : Effect.succeed(note);
119
+ });
120
+ const save = (note: Note) => Ref.update(stored, (all) => new Map([...all, [note.id, note]])).pipe(Effect.as(note));
121
+ return Notes.of({
122
+ "notes.get": ({ id }) => find(id),
123
+ "notes.list": () => Effect.map(Ref.get(stored), (all) => [...all.values()]),
124
+ "notes.rename": ({ id, title }) => title.trim() === ""
125
+ ? Rename.reject.Invalid({ field: "title", message: "Enter a title" })
126
+ : Effect.flatMap(find(id), (note) => save(new Note({ ...note, title }))),
127
+ "notes.create": (draft) => draft.title === ""
128
+ ? Create.reject.Invalid({ field: "title", message: "Enter a title" })
129
+ : Effect.flatMap(Ref.get(stored), (all) => save(new Note({ id: all.size + 1, ...draft }))),
130
+ });
131
+ }));
132
+ ```
56
133
 
57
- ### Operations and rejections
134
+ Save this module as `handlers.ts`. This in-memory layer is the same shape as the package's native RPC fixture. An application can put repository and service calls in these handlers. The contract checks handler result and rejection types; it does not implement persistence, authorization or transactions.
58
135
 
59
- - Each operation exposes `payload`, `success`, `error` (the rejection union, or `Schema.Never` without rejections), `Rejection.X` (the class) and `reject.X(...)`, an `Effect<never, X>`.
60
- - `rejections: { Tag: fields }` generates a `Schema.TaggedError` class with that `_tag`. `rejections: { Tag: ExistingClass }` reuses a class; its `_tag` must equal the key. Reuse one class when several operations or a shared service raise the same rejection. Generated classes are distinct per operation.
61
- - `fieldRejection(OrderDraft)` derives the `field` literal union from the struct's keys, so a form can attach the rejection to one of its own fields. Pass a key list to narrow it.
136
+ Native `.middleware(M)` returns a contract that retains its declarations. For example, a native middleware service can supply request identity:
62
137
 
63
- ### Contract
138
+ ```ts
139
+ import { Context, Schema } from "effect";
140
+ import { RpcMiddleware } from "effect/rpc";
141
+ import { Notes } from "./notes.ts";
64
142
 
65
- - RPC tags are `` `${contract}.${operation}` ``. Implement handlers with native `Orders.toLayer(Effect.gen(...))` and `Orders.of({ "orders.get": ... })`; `of` rejects wrong payloads, successes and undeclared failures.
66
- - Middleware is the native `.middleware(M)`. On a contract it returns a contract, keeping `declaration` (`{ name, queries, commands }`). Other native group combinators (`add`, `merge`, `prefix`, `omit`) return plain groups.
67
- - Operation names in one contract must all differ, across and within queries and commands. The compiler rejects duplicates in literal arrays; `contract` throws for duplicates it cannot see.
68
- - Operations can be declared inline in `contract(...)` or beforehand; both keep the same types and defaults.
143
+ class Session extends Context.Service<Session, { readonly user: string }>()("app/Session") {}
144
+ class Denied extends Schema.TaggedError<Denied>()("Denied", {}) {}
145
+ class Guard extends RpcMiddleware.Service<Guard, { provides: Session }>()("app/Guard", { error: Denied }) {}
146
+ const SecuredNotes = Notes.middleware(Guard);
147
+ ```
69
148
 
70
- ### Reactivity keys
149
+ The host supplies the middleware implementation and request policy. Native middleware failures join the client's failure type. The [`platform` request-context guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/request-context.md) explains the shared identity and server boundary.
71
150
 
72
- `collection("orders", Order.fields.id)` gives `orders.list` and `orders.item(id)`; ids must be strings or numbers. The binding applies the one rule that is easy to get wrong with native keys:
151
+ For an ordinary HTTP server, compose the contract with native layers:
73
152
 
74
- | Declared | Query registers | Command invalidates |
153
+ ```ts
154
+ import { Layer } from "effect";
155
+ import { RpcSerialization, RpcServer } from "effect/rpc";
156
+ import { Notes } from "./notes.ts";
157
+ import { NotesHandlers } from "./handlers.ts";
158
+
159
+ export const NotesRoutes = RpcServer.layerHttp({ group: Notes, path: "/rpc", protocol: "http" }).pipe(
160
+ Layer.provide([NotesHandlers, RpcSerialization.layerJson]),
161
+ );
162
+ ```
163
+
164
+ The server host provides `NotesRoutes` to its native HTTP router. In a separate client module, use the same contract:
165
+
166
+ ```ts
167
+ import { Layer } from "effect";
168
+ import { FetchHttpClient } from "effect/http";
169
+ import * as AtomRpc from "effect/reactivity/AtomRpc";
170
+ import { RpcClient, RpcSerialization } from "effect/rpc";
171
+ import { bind } from "@shivaedev/effect-contract/bind.ts";
172
+ import { Notes } from "./notes.ts";
173
+
174
+ export class NotesClient extends AtomRpc.Service<NotesClient>()("app/NotesClient", {
175
+ group: Notes,
176
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
177
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]),
178
+ ),
179
+ }) {}
180
+ export const api = bind(Notes, NotesClient);
181
+ ```
182
+
183
+ Save the client module as `client.ts`. The [native RPC guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/native-rpc.md) covers hosting; the [order example](https://github.com/ShivaeDev/platform/blob/main/docs/framework/order-example.md) composes handlers, request identity, SQL and a rendered client.
184
+
185
+ ### 3. Every day: read and change
186
+
187
+ ```ts
188
+ const one = api.get.query({ id: 1 });
189
+ const list = api.list.query();
190
+ const readOnce = api.get.run({ id: 1 });
191
+ const rename = api.rename.run({ id: 1, title: "Uno" });
192
+ ```
193
+
194
+ `one` and `list` are native query atoms. `readOnce` and `rename` are Effects for individual calls. A query's Effect requires `NotesClient`; a command's Effect also requires native `Reactivity`. Commands expose `run`, and have no `query` member.
195
+
196
+ Mount the atoms and run commands through the service's atom runtime in the same registry:
197
+
198
+ ```ts
199
+ import { Effect } from "effect";
200
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
201
+ import { api, NotesClient } from "./client.ts";
202
+
203
+ const registry = AtomRegistry.make();
204
+ const one = api.get.query({ id: 1 });
205
+ const list = api.list.query();
206
+ const releaseOne = registry.mount(one);
207
+ const releaseList = registry.mount(list);
208
+
209
+ const renamed = await Effect.runPromise(
210
+ AtomRegistry.getResult(registry, NotesClient.runtime.atom(api.rename.run({ id: 1, title: "Uno" }))),
211
+ );
212
+
213
+ releaseOne();
214
+ releaseList();
215
+ registry.dispose();
216
+ ```
217
+
218
+ The successful rename invalidates note 1 and the list. A rejected rename invalidates neither. Bound queries and commands take the schema's decoded payload type; native clients take its constructor input, which can differ when a field has a constructor default. Result and failure types preserve operation rejections, middleware errors and `RpcClientError`.
219
+
220
+ The dependency rule is deliberately asymmetric:
221
+
222
+ | Declared key | Query reads | Successful command invalidates |
75
223
  | --- | --- | --- |
76
- | `orders.item(id)` | `` `orders:${id}` `` | `` `orders:${id}` `` and `"orders"` |
77
- | `orders.list` | `"orders"` | `"orders"` |
224
+ | `notes.item(1)` | `"notes:1"` | `"notes:1"` and `"notes"` |
225
+ | `notes.list` | `"notes"` | `"notes"` |
226
+
227
+ `readKeys` and `invalidationKeys` produce these native keys and remove duplicates. An item change refreshes that item and its list, preserving other item queries. A list-only invalidation refreshes list queries and leaves item queries alone. These keys match native Reactivity's record-key hashing of `{ notes: [1] }`.
228
+
229
+ Client invalidation follows RPC success. For server invalidation at the SQL commit boundary, use `transact` from [`effect-sql`](https://github.com/ShivaeDev/platform/tree/main/packages/effect-sql); commit-bound changes belong to [`effect-changes`](https://github.com/ShivaeDev/platform/tree/main/packages/effect-changes).
78
230
 
79
- An item change therefore refreshes that item and every list, but not other items. Invalidating `orders.list` does not refresh item queries. The strings match native Reactivity's hashing of `{ orders: [id] }`.
231
+ ### 4. When needed: live invalidation and resume
80
232
 
81
- ### Client binding
233
+ Declare subscriptions with `stream` in the same contract. Its `success` schema describes each element, and its `rejections` become the native stream error schema. `bind` exposes `run(payload)` as a native `Stream`, preserving declared rejections, middleware errors and RPC client errors. Completion and client interruption release the server subscription in the real HTTP tests. Compiler checks reject a unary client for a streaming operation.
82
234
 
83
- `bind` is browser-safe and uses no React:
235
+ Every query that should reconcile together must read the same explicit scope. This variant of the feature adds `workspace.list` alongside each query's item or list key:
84
236
 
85
237
  ```ts
86
- class OrdersClient extends AtomRpc.Service<OrdersClient>()("app/OrdersClient", { group: Orders, protocol }) {}
87
- const api = bind(Orders, OrdersClient);
238
+ import { Schema } from "effect";
239
+ import { contract } from "@shivaedev/effect-contract/contract.ts";
240
+ import { collection } from "@shivaedev/effect-contract/keys.ts";
241
+ import { LiveHint } from "@shivaedev/effect-contract/live.ts";
242
+ import { query, stream } from "@shivaedev/effect-contract/operation.ts";
243
+ import { Create, Get, List, notes, Rename } from "./notes.ts";
244
+
245
+ export const workspace = collection("workspace", Schema.String);
246
+ const LiveGet = query("get", {
247
+ payload: Get.payload,
248
+ success: Get.success,
249
+ rejections: Get.rejections,
250
+ reads: ({ id }) => [notes.item(id), workspace.list],
251
+ });
252
+ const LiveList = query("list", {
253
+ success: List.success,
254
+ reads: () => [notes.list, workspace.list],
255
+ });
256
+ export const Subscribe = stream("subscribe", { reads: () => [workspace.list], success: LiveHint });
257
+ export const LiveNotes = contract("notes", { queries: [LiveGet, LiveList], commands: [Rename, Create], streams: [Subscribe] });
258
+ ```
259
+
260
+ Save the shared module as `live-notes.ts`. A collection list key alone does not cover its item queries. The common `workspace.list` key expresses that the caller intends to reconcile all queries that read it.
261
+
262
+ Use `LiveNotes` as the server group and implement `notes.subscribe` with a native Stream. For streaming HTTP, the tested fixture uses `RpcSerialization.layerNdjson` on both sides. This client mounts live coordination alongside its queries:
88
263
 
89
- api.get.query({ id }, { timeToLive: "1 minute" }); // native Client.query atom with the declared read keys
90
- api.get.run({ id }); // one call, no registration
91
- api.save.run(input); // Effect: call, then invalidate declared keys
264
+ ```ts
265
+ import { Layer } from "effect";
266
+ import { FetchHttpClient } from "effect/http";
267
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
268
+ import * as AtomRpc from "effect/reactivity/AtomRpc";
269
+ import { RpcClient, RpcSerialization } from "effect/rpc";
270
+ import { bind } from "@shivaedev/effect-contract/bind.ts";
271
+ import { live } from "@shivaedev/effect-contract/live.ts";
272
+ import { resumeSignal } from "@shivaedev/effect-contract/resume.ts";
273
+ import { LiveNotes, Subscribe } from "./live-notes.ts";
274
+
275
+ class Client extends AtomRpc.Service<Client>()("app/LiveNotes", {
276
+ group: LiveNotes,
277
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
278
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]),
279
+ ),
280
+ }) {}
281
+ const api = bind(LiveNotes, Client);
282
+ const registry = AtomRegistry.make();
283
+ const updates = live(Client.runtime, {
284
+ stream: api.subscribe.run(undefined),
285
+ resyncKeys: Subscribe.reads(undefined),
286
+ resume: resumeSignal({ window }),
287
+ retryDelay: "100 millis",
288
+ });
289
+ const releaseQuery = registry.mount(api.get.query({ id: 1 }));
290
+ const releaseConnection = registry.mount(updates.connection);
92
291
  ```
93
292
 
94
- - `query` returns the native `AtomRpc` query atom; `headers`, `timeToLive` and `serializationKey` pass through.
95
- - `run` returns `Effect<Success, Rejection | middleware error | RpcClientError, OrdersClient | Reactivity>` for that single invocation. Run it through the service's atom runtime (for example as a form's submit handler) so invalidation reaches the same registry. Keys, including result-dependent keys, are invalidated only after success.
96
- - There is no shared mutation atom or second cache.
293
+ `LiveNotes` supplies both the declaration for binding and the native RPC group for transport. Stream read keys describe its dependencies; `run` does not mount query atoms or trigger automatic invalidation. Here the caller uses `Subscribe.reads(undefined)` as the explicit live reconciliation scope. `bind` also accepts a larger native client group that preserves the contract RPC types. The [live updates guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/live-updates.md) shows server and client composition, and the [HTTP fixture](https://github.com/ShivaeDev/platform/tree/main/packages/effect-contract/src/test-support/live) supplies a complete working server.
294
+
295
+ | Observation | Invalidation |
296
+ | --- | --- |
297
+ | `Changed` with keys after the first hint | Affected items and their lists. |
298
+ | `Resync` | The exact `resyncKeys` read scope. |
299
+ | First hint on a new or reconnected stream | The full explicit scope, even if the hint is targeted. |
300
+
301
+ `LiveHint` and `Key` are schemas. The package's schema test rejects unknown hint tags and item keys with an invalid ID type.
302
+
303
+ ```ts
304
+ registry.set(updates.paused, true);
305
+ registry.set(updates.paused, false);
306
+ const status = registry.get(updates.status);
307
+ ```
308
+
309
+ While paused, the HTTP consumer keeps observing hints and suppresses automatic refresh. `status.pending` saturates at 256 observations. Unpausing reconciles the explicit scope and resets the count. A resume signal also requests reconciliation; the injected native source and visible browser `online` and `visibilitychange` events are tested, while hidden events do not request refresh.
310
+
311
+ `status` exposes `connection`, `failure`, `needsResync` and `pending`. The real HTTP tests cover typed stream failure, reconnection, missed changes and clearing the stream failure after successful delivery. They also show native query atoms retaining previous successful data during a read failure and rejecting stale HTTP responses after invalidation. Query failures remain in their own `AsyncResult`; stream failures appear in `status.failure`.
312
+
313
+ The resume signal is a scoped native atom. Its browser listeners and injected source cleanup are tested on registry disposal, including a resume source used only through `live`. The HTTP fixture also verifies release of the stream subscription when the registry is disposed:
314
+
315
+ ```ts
316
+ releaseQuery();
317
+ releaseConnection();
318
+ registry.dispose();
319
+ ```
320
+
321
+ Call the release functions and dispose the registry when the application's root no longer owns them.
322
+
323
+ ### API by module
324
+
325
+ Import the module that defines the name; the package has no root entry.
326
+
327
+ | Module | Functions and schemas |
328
+ | --- | --- |
329
+ | `operation.ts` | `query(name, { payload?, success?, rejections?, reads })`; `command(name, { payload?, success?, rejections?, invalidates })`; `stream(name, { payload?, success, rejections?, reads })`. |
330
+ | `contract.ts` | `contract(name, { queries?, commands?, streams? })`, returning a native RPC group with `declaration: { name, queries, commands, streams }`. |
331
+ | `rejection.ts` | `fieldRejection(struct, fields?)`; lower-level `rejectionSet(specs)`, which constructs the rejection classes, union schema and failure helpers used by operations. |
332
+ | `keys.ts` | `collection(name, idSchema)`; `Key` schema; `readKeys(keys)` and `invalidationKeys(keys)`. |
333
+ | `bind.ts` | `bind(contract, atomRpcService)`, exposing `{ query, run }` for queries, `{ run }` Effects for commands and `{ run }` native Streams for streams. |
334
+ | `live.ts` | `LiveHint` schema; `live(runtime, { stream, resyncKeys, resume?, retryDelay? })`, exposing `connection`, `paused` and `status`. |
335
+ | `resume.ts` | `resumeSignal({ window?, native? })`, returning a native numeric signal atom. |
336
+
337
+ `QueryOptions` is the query atom's optional second argument: `headers?: Headers.Input`, `timeToLive?: Duration.Input` and `serializationKey?: string`. For example, `api.get.query({ id: 1 }, { timeToLive: "1 minute" })` uses the native query options interface.
338
+
339
+ | Module | Exported types |
340
+ | --- | --- |
341
+ | `operation.ts` | `Query`, `Command`, `Streaming`, `QueryShape`, `CommandShape`, `StreamShape`, `OperationShape`, `PayloadSchema`. |
342
+ | `contract.ts` | `Contract`, `Declared`, `OperationRpc`, `Tag`. |
343
+ | `rejection.ts` | `FieldRejection`, `MatchingTags`, `Reject`, `RejectedBy`, `RejectionClass`, `RejectionSpecs`, `Rejections`, `RejectionSet`, `RejectionUnion`, `RejectionValue`, `TaggedRejection`. |
344
+ | `keys.ts` | `Collection`, `Identity`, `ItemKey`, `ListKey`, `Key`. |
345
+ | `bind.ts` | `Bound`, `BoundQuery`, `BoundCommand`, `BoundStream`, `Failure`, `RunFailure`, `QueryOptions`. |
346
+ | `live.ts` | `LiveHint`, `LiveOptions`, `LiveStatus`. |
347
+ | `resume.ts` | `ResumeOptions`, `ResumeSource`, `ResumeWindow`. |
348
+
349
+ The shape types describe operation declarations, the bound types describe client members, and the rejection types derive classes and failures from declared specs. A native resume source receives a callback and returns a cleanup function. `ResumeWindow` describes the browser event and visibility methods the signal uses.
350
+
351
+ ### Install, setup and limits
352
+
353
+ ```sh
354
+ pnpm add @shivaedev/effect-contract effect@4.0.4
355
+ ```
356
+
357
+ `effect` is a peer. The repository's dependency catalog uses Effect `4.0.4`, and the package declares Node.js 24 or newer. Choose the native server host separately; this package supplies no server process or HTTP transport implementation.
358
+
359
+ - Put schema declarations where server and browser can share them. Put service, authorization and transaction implementations on the server. React rendering and editable forms belong to `effect-react` and `effect-form`.
360
+ - Run command Effects with the Reactivity instance that owns the queries to refresh, as in the registry example.
361
+ - Choose a common read key for every query in a full reconciliation scope. A list key does not implicitly cover item queries.
362
+ - Supply browser or native resume sources explicitly. Synthetic visibility and native-source tests establish signal behavior; they do not establish physical tab transitions or Capacitor lifecycle behavior.
363
+ - Live hints carry refresh requests, not authoritative data or durable history. Applications own dependency discovery, authenticated subscriptions, delivery across processes, SSR and deployment behavior. Those boundaries need their own tests.
364
+
365
+ The package's design priorities are in [docs/north-star.md](docs/north-star.md), and package work and maintainer questions are in [docs/roadmap.md](docs/roadmap.md).
97
366
 
98
- Authorization and transactions stay in handlers and services. For server-side invalidation after a SQL commit, see `transact` in `@shivaedev/effect-sql`.
367
+ ### Server hint streams
99
368
 
100
- This release targets Effect `4.0.0-rc.112`.
369
+ Import `makeLiveServer` from `@shivaedev/effect-contract/makeLiveServer.ts` and create it once with the server's `Reactivity` service. After application authorization, return `hints.stream([orders.item(id)])` from the native streaming RPC handler. Each subscription starts with `Resync`; invalidating its read keys produces `Changed` with those declared keys. `hints.resync` sends `Resync` to every live stream owned by that helper, including streams for otherwise unrelated keys. Independent helpers keep their resync scopes separate even when they share Reactivity. The native stream owns registration and interruption. Keep this server helper separate from the browser's `live` coordinator.
package/dist/bind.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { Duration } from "effect";
2
- import { Effect } from "effect";
3
- import type { Headers } from "effect/unstable/http";
4
- import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
- import type * as Atom from "effect/unstable/reactivity/Atom";
6
- import type * as AtomRpc from "effect/unstable/reactivity/AtomRpc";
7
- import * as Reactivity from "effect/unstable/reactivity/Reactivity";
8
- import type { Rpc } from "effect/unstable/rpc";
9
- import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
2
+ import { Effect, Stream } from "effect";
3
+ import type { Headers } from "effect/http";
4
+ import type * as AsyncResult from "effect/reactivity/AsyncResult";
5
+ import type * as Atom from "effect/reactivity/Atom";
6
+ import type * as AtomRpc from "effect/reactivity/AtomRpc";
7
+ import * as Reactivity from "effect/reactivity/Reactivity";
8
+ import type { Rpc } from "effect/rpc";
9
+ import type { RpcClientError } from "effect/rpc/RpcClientError";
10
10
  import type { Contract, Tag } from "./contract.ts";
11
- import type { CommandShape, QueryShape } from "./operation.ts";
12
- export type Failure<R extends Rpc.Any> = R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer TError, infer TMiddleware, infer _Requires> ? TError["Type"] | TMiddleware["error"]["Type"] | RpcClientError : never;
11
+ import type { CommandShape, QueryShape, StreamShape } from "./operation.ts";
12
+ export type Failure<R extends Rpc.Any> = R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer _Error, infer TMiddleware, infer _Requires> ? Rpc.ErrorExit<R> | TMiddleware["error"]["Type"] | RpcClientError : never;
13
13
  export type RunFailure<R extends Rpc.Any> = R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer _Error, infer TMiddleware, infer _Requires> ? Failure<R> | TMiddleware["~ClientError"] : never;
14
14
  export interface QueryOptions {
15
15
  readonly headers?: Headers.Input;
@@ -23,16 +23,21 @@ export interface BoundQuery<R extends Rpc.Any, TSelf> {
23
23
  export interface BoundCommand<R extends Rpc.Any, TSelf> {
24
24
  readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>, TSelf | Reactivity.Reactivity>;
25
25
  }
26
- export type Bound<TName extends string, TQueries extends readonly QueryShape[], TCommands extends readonly CommandShape[], TRpcs extends Rpc.Any, TSelf> = {
26
+ export interface BoundStream<R extends Rpc.Any, TSelf> {
27
+ readonly run: (payload: Rpc.Payload<R>) => Stream.Stream<Rpc.SuccessChunk<R>, RunFailure<R>, TSelf | Rpc.ServicesClient<R>>;
28
+ }
29
+ export type Bound<TName extends string, TQueries extends readonly QueryShape[], TCommands extends readonly CommandShape[], TRpcs extends Rpc.Any, TSelf, TStreams extends readonly StreamShape[] = readonly []> = {
27
30
  readonly [TQuery in TQueries[number] as TQuery["name"]]: BoundQuery<Rpc.ExtractTag<TRpcs, Tag<TName, TQuery["name"]>>, TSelf>;
28
31
  } & {
29
32
  readonly [TCommand in TCommands[number] as TCommand["name"]]: BoundCommand<Rpc.ExtractTag<TRpcs, Tag<TName, TCommand["name"]>>, TSelf>;
33
+ } & {
34
+ readonly [TStream in TStreams[number] as TStream["name"]]: BoundStream<Rpc.ExtractTag<TRpcs, Tag<TName, TStream["name"]>>, TSelf>;
30
35
  };
31
36
  type MatchingClient<TRpcs extends Rpc.Any, TClientRpcs extends Rpc.Any> = [TRpcs] extends [Extract<TClientRpcs, {
32
37
  readonly _tag: TRpcs["_tag"];
33
38
  }>] ? [Extract<TClientRpcs, {
34
39
  readonly _tag: TRpcs["_tag"];
35
40
  }>] extends [TRpcs] ? unknown : "Client must preserve the contract RPC types" : "Client must implement the contract RPCs";
36
- export declare function bind<TName extends string, TQueries extends readonly QueryShape[], TCommands extends readonly CommandShape[], TRpcs extends Rpc.Any, TSelf, TId extends string, TClientRpcs extends Rpc.Any>(contract: Contract<TName, TQueries, TCommands, TRpcs>, service: AtomRpc.AtomRpcClient<TSelf, TId, TClientRpcs> & MatchingClient<NoInfer<TRpcs>, TClientRpcs>): Bound<TName, TQueries, TCommands, TRpcs, TSelf>;
41
+ export declare function bind<TName extends string, TQueries extends readonly QueryShape[], TCommands extends readonly CommandShape[], TRpcs extends Rpc.Any, TSelf, TId extends string, TClientRpcs extends Rpc.Any, TStreams extends readonly StreamShape[]>(contract: Contract<TName, TQueries, TCommands, TRpcs, TStreams>, service: AtomRpc.AtomRpcClient<TSelf, TId, TClientRpcs> & MatchingClient<NoInfer<TRpcs>, TClientRpcs>): Bound<TName, TQueries, TCommands, TRpcs, TSelf, TStreams>;
37
42
  export {};
38
43
  //# sourceMappingURL=bind.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"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"}
package/dist/bind.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Effect } from "effect";
2
- import * as Reactivity from "effect/unstable/reactivity/Reactivity";
1
+ import { Effect, Stream } from "effect";
2
+ import * as Reactivity from "effect/reactivity/Reactivity";
3
3
  import { invalidationKeys, readKeys } from "./keys.js";
4
4
  const invalidateAfter = Effect.fn("EffectContract.invalidateAfter")(function* (keys) {
5
5
  yield* Reactivity.invalidate(invalidationKeys(keys));
@@ -18,11 +18,17 @@ function boundCommand(service, tag, command) {
18
18
  run: (payload) => service.use((client) => client(tag, payload)).pipe(Effect.tap((result) => invalidateAfter(command.invalidates(payload, result)))),
19
19
  };
20
20
  }
21
+ function boundStream(service, tag) {
22
+ return {
23
+ run: (payload) => Stream.unwrap(service.use((client) => Effect.succeed(client(tag, payload)))),
24
+ };
25
+ }
21
26
  export function bind(contract, service) {
22
- const { name, queries, commands } = contract.declaration;
27
+ const { name, queries, commands, streams } = contract.declaration;
23
28
  return Object.fromEntries([
24
29
  ...queries.map((query) => [query.name, boundQuery(service, `${name}.${query.name}`, query)]),
25
30
  ...commands.map((command) => [command.name, boundCommand(service, `${name}.${command.name}`, command)]),
31
+ ...streams.map((stream) => [stream.name, boundStream(service, `${name}.${stream.name}`)]),
26
32
  ]);
27
33
  }
28
34
  //# sourceMappingURL=bind.js.map
package/dist/bind.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bind.js","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,OAAO,KAAK,UAAU,MAAM,uCAAuC,CAAC;AAIpE,OAAO,EAAE,gBAAgB,EAAY,QAAQ,EAAE,MAAM,WAAW,CAAC;AAgDjE,MAAM,eAAe,GAAG,MAAM,CAAC,EAAE,CAAC,gCAAgC,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAoB;IAClG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,OAAqB,EAAE,GAAW,EAAE,KAAiB;IACxE,SAAS,KAAK,CAAC,OAAgB;QAC9B,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,OAAO;QACN,KAAK,EAAE,CAAC,OAAgB,EAAE,UAAwB,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACpI,GAAG,EAAE,CAAC,OAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;KACxE,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,OAAqB,EAAE,GAAW,EAAE,OAAqB;IAC9E,OAAO;QACN,GAAG,EAAE,CAAC,OAAgB,EAAE,EAAE,CACzB,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;KAClI,CAAC;AACH,CAAC;AAcD,MAAM,UAAU,IAAI,CACnB,QAAoG,EACpG,OAAqB;IAErB,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,WAAW,CAAC;IACzD,OAAO,MAAM,CAAC,WAAW,CAAC;QACzB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,CAAU,CAAC;QACrG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAU,CAAC;KAChH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"bind.js","sourceRoot":"","sources":["../src/bind.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKxC,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAI3D,OAAO,EAAE,gBAAgB,EAAY,QAAQ,EAAE,MAAM,WAAW,CAAC;AA6DjE,MAAM,eAAe,GAAG,MAAM,CAAC,EAAE,CAAC,gCAAgC,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAoB;IAClG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,OAAqB,EAAE,GAAW,EAAE,KAAiB;IACxE,SAAS,KAAK,CAAC,OAAgB;QAC9B,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,OAAO;QACN,KAAK,EAAE,CAAC,OAAgB,EAAE,OAAO,GAAiB,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACpI,GAAG,EAAE,CAAC,OAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;KACxE,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,OAAqB,EAAE,GAAW,EAAE,OAAqB;IAC9E,OAAO;QACN,GAAG,EAAE,CAAC,OAAgB,EAAE,EAAE,CACzB,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;KAClI,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAA8B,EAAE,GAAW;IAC/D,OAAO;QACN,GAAG,EAAE,CAAC,OAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;KACvG,CAAC;AACH,CAAC;AAeD,MAAM,UAAU,IAAI,CACnB,QAA4H,EAC5H,OAAwD;IAExD,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,WAAW,CAAC;IAClE,OAAO,MAAM,CAAC,WAAW,CAAC;QACzB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,CAAU,CAAC;QACrG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAU,CAAC;QAChH,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAU,CAAC;KAClG,CAAC,CAAC;AACJ,CAAC"}