@shivaedev/contract 0.0.0 → 0.16.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +210 -0
  2. package/LICENSE +21 -0
  3. package/README.md +510 -1
  4. package/dist/automaticLive.d.ts +28 -0
  5. package/dist/automaticLive.d.ts.map +1 -0
  6. package/dist/automaticLive.js +63 -0
  7. package/dist/automaticLive.js.map +1 -0
  8. package/dist/bind.d.ts +51 -0
  9. package/dist/bind.d.ts.map +1 -0
  10. package/dist/bind.js +14 -0
  11. package/dist/bind.js.map +1 -0
  12. package/dist/contract.d.ts +33 -0
  13. package/dist/contract.d.ts.map +1 -0
  14. package/dist/contract.js +44 -0
  15. package/dist/contract.js.map +1 -0
  16. package/dist/internal/bindingOwner.d.ts +8 -0
  17. package/dist/internal/bindingOwner.d.ts.map +1 -0
  18. package/dist/internal/bindingOwner.js +16 -0
  19. package/dist/internal/bindingOwner.js.map +1 -0
  20. package/dist/internal/clientOperations.d.ts +21 -0
  21. package/dist/internal/clientOperations.d.ts.map +1 -0
  22. package/dist/internal/clientOperations.js +36 -0
  23. package/dist/internal/clientOperations.js.map +1 -0
  24. package/dist/internal/liveInvalidation.d.ts +12 -0
  25. package/dist/internal/liveInvalidation.d.ts.map +1 -0
  26. package/dist/internal/liveInvalidation.js +14 -0
  27. package/dist/internal/liveInvalidation.js.map +1 -0
  28. package/dist/internal/makeHintPolicy.d.ts +4 -0
  29. package/dist/internal/makeHintPolicy.d.ts.map +1 -0
  30. package/dist/internal/makeHintPolicy.js +25 -0
  31. package/dist/internal/makeHintPolicy.js.map +1 -0
  32. package/dist/internal/queryReactivity.d.ts +5 -0
  33. package/dist/internal/queryReactivity.d.ts.map +1 -0
  34. package/dist/internal/queryReactivity.js +96 -0
  35. package/dist/internal/queryReactivity.js.map +1 -0
  36. package/dist/keys.d.ts +34 -0
  37. package/dist/keys.d.ts.map +1 -0
  38. package/dist/keys.js +25 -0
  39. package/dist/keys.js.map +1 -0
  40. package/dist/live.d.ts +39 -0
  41. package/dist/live.d.ts.map +1 -0
  42. package/dist/live.js +98 -0
  43. package/dist/live.js.map +1 -0
  44. package/dist/makeLiveServer.d.ts +20 -0
  45. package/dist/makeLiveServer.d.ts.map +1 -0
  46. package/dist/makeLiveServer.js +62 -0
  47. package/dist/makeLiveServer.js.map +1 -0
  48. package/dist/operation.d.ts +66 -0
  49. package/dist/operation.d.ts.map +1 -0
  50. package/dist/operation.js +29 -0
  51. package/dist/operation.js.map +1 -0
  52. package/dist/publicConfig.d.ts +20 -0
  53. package/dist/publicConfig.d.ts.map +1 -0
  54. package/dist/publicConfig.js +8 -0
  55. package/dist/publicConfig.js.map +1 -0
  56. package/dist/queryCoalesceWindow.d.ts +4 -0
  57. package/dist/queryCoalesceWindow.d.ts.map +1 -0
  58. package/dist/queryCoalesceWindow.js +11 -0
  59. package/dist/queryCoalesceWindow.js.map +1 -0
  60. package/dist/readPolicy.d.ts +10 -0
  61. package/dist/readPolicy.d.ts.map +1 -0
  62. package/dist/readPolicy.js +24 -0
  63. package/dist/readPolicy.js.map +1 -0
  64. package/dist/rejection.d.ts +41 -0
  65. package/dist/rejection.d.ts.map +1 -0
  66. package/dist/rejection.js +16 -0
  67. package/dist/rejection.js.map +1 -0
  68. package/dist/resume.d.ts +17 -0
  69. package/dist/resume.d.ts.map +1 -0
  70. package/dist/resume.js +28 -0
  71. package/dist/resume.js.map +1 -0
  72. package/dist/serverMetadata.d.ts +42 -0
  73. package/dist/serverMetadata.d.ts.map +1 -0
  74. package/dist/serverMetadata.js +27 -0
  75. package/dist/serverMetadata.js.map +1 -0
  76. package/package.json +73 -4
  77. package/src/automaticLive.ts +93 -0
  78. package/src/bind.ts +103 -0
  79. package/src/contract.ts +140 -0
  80. package/src/internal/bindingOwner.ts +24 -0
  81. package/src/internal/clientOperations.ts +74 -0
  82. package/src/internal/liveInvalidation.ts +21 -0
  83. package/src/internal/makeHintPolicy.ts +40 -0
  84. package/src/internal/queryReactivity.ts +109 -0
  85. package/src/keys.ts +62 -0
  86. package/src/live.ts +122 -0
  87. package/src/makeLiveServer.ts +76 -0
  88. package/src/operation.ts +145 -0
  89. package/src/publicConfig.ts +9 -0
  90. package/src/queryCoalesceWindow.ts +13 -0
  91. package/src/readPolicy.ts +33 -0
  92. package/src/rejection.ts +91 -0
  93. package/src/resume.ts +44 -0
  94. package/src/serverMetadata.ts +50 -0
package/README.md CHANGED
@@ -1,3 +1,512 @@
1
1
  # @shivaedev/contract
2
2
 
3
- Placeholder. Real releases start above 0.0.0.
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:
8
+
9
+ ```ts
10
+ import { Schema } from "effect";
11
+ import { contract } from "@shivaedev/contract/contract.ts";
12
+ import { collection } from "@shivaedev/contract/keys.ts";
13
+ import { command, query } from "@shivaedev/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";
44
+ import { contract } from "@shivaedev/contract/contract.ts";
45
+ import { collection } from "@shivaedev/contract/keys.ts";
46
+ import { command, query } from "@shivaedev/contract/operation.ts";
47
+ import { fieldRejection } from "@shivaedev/contract/rejection.ts";
48
+
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);
57
+
58
+ export const Get = query("get", {
59
+ payload: { id: Schema.Number },
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],
67
+ });
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)],
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.
84
+
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
+ Import the shared expected failures from `@shivaedev/errors/taxonomy.ts`: `NotFound`, `Unauthorized`, `Forbidden`, `BadRequest`, `Conflict`, `PreconditionFailed`, `TooManyRequests`, `AuthUnavailable`, `InternalServerError` and `ServiceUnavailable`. Each class is a tagged error schema with a string `message`; `BadRequest` and `Conflict` also accept an optional string `field`. Declare only the failures an operation can return:
92
+
93
+ ```ts
94
+ import { Schema } from "effect";
95
+ import { Rpc } from "effect/rpc";
96
+ import { BadRequest, Conflict } from "@shivaedev/errors/taxonomy.ts";
97
+
98
+ export const Rename = Rpc.make("Rename", {
99
+ error: Schema.Union([BadRequest, Conflict]),
100
+ payload: { name: Schema.String },
101
+ success: Schema.String,
102
+ });
103
+ ```
104
+
105
+ A handler fails with `new Conflict({ field: "name", message: "Name is taken" })`. Native RPC JSON decodes the rejection back to the error instance with its field. Taxonomy errors can be declared on an RPC with no authentication middleware. [`@shivaedev/errors`](https://github.com/ShivaeDev/platform/tree/main/packages/errors#readme) also exports `rejectedField`, which reads the field and message of a rejection on the client.
106
+
107
+ `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.
108
+
109
+ `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.
110
+
111
+ ```ts
112
+ const invalid = new Rename.Rejection.Invalid({ field: "title", message: "Enter a title" });
113
+ const refused = Rename.reject.Invalid({ field: "title", message: "Enter a title" });
114
+ const missing = Get.reject.NoteMissing({ id: 9 });
115
+ ```
116
+
117
+ `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.
118
+
119
+ ### 2. Once per host: native handlers and transport
120
+
121
+ Implement the contract with native `of` and `toLayer`:
122
+
123
+ ```ts
124
+ import { Effect, Ref, Stream } from "effect";
125
+ import { Create, Get, Note, Notes, Rename } from "./notes.ts";
126
+
127
+ export const NotesHandlers = Notes.toLayer(Effect.gen(function* () {
128
+ const stored = yield* Ref.make(new Map([
129
+ [1, new Note({ body: "", id: 1, title: "One" })],
130
+ [2, new Note({ body: "", id: 2, title: "Two" })],
131
+ ]));
132
+ const find = (id: number) => Effect.flatMap(Ref.get(stored), (all) => {
133
+ const note = all.get(id);
134
+ return note === undefined ? Get.reject.NoteMissing({ id }) : Effect.succeed(note);
135
+ });
136
+ const save = (note: Note) => Ref.update(stored, (all) => new Map([...all, [note.id, note]])).pipe(Effect.as(note));
137
+ return Notes.of({
138
+ "notes.$changes": () => Stream.never,
139
+ "notes.get": ({ id }) => find(id),
140
+ "notes.list": () => Effect.map(Ref.get(stored), (all) => [...all.values()]),
141
+ "notes.rename": ({ id, title }) => title.trim() === ""
142
+ ? Rename.reject.Invalid({ field: "title", message: "Enter a title" })
143
+ : Effect.flatMap(find(id), (note) => save(new Note({ ...note, title }))),
144
+ "notes.create": (draft) => draft.title === ""
145
+ ? Create.reject.Invalid({ field: "title", message: "Enter a title" })
146
+ : Effect.flatMap(Ref.get(stored), (all) => save(new Note({ id: all.size + 1, ...draft }))),
147
+ });
148
+ }));
149
+ ```
150
+
151
+ Save this module as `handlers.ts`. 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. This in-memory server keeps the reserved change stream open without publishing hints. When a Platform host supplies that handler, build application handlers from `Notes.application` and keep `Notes` as the host and client group.
152
+
153
+ Native `.middleware(M)` returns a contract that retains its declarations. For example, a native middleware service can supply request identity:
154
+
155
+ ```ts
156
+ import { Context, Schema } from "effect";
157
+ import { RpcMiddleware } from "effect/rpc";
158
+ import { Notes } from "./notes.ts";
159
+
160
+ class Session extends Context.Service<Session, { readonly user: string }>()("app/Session") {}
161
+ class Denied extends Schema.TaggedError<Denied>()("Denied", {}) {}
162
+ class Guard extends RpcMiddleware.Service<Guard, { provides: Session }>()("app/Guard", { error: Denied }) {}
163
+ const SecuredNotes = Notes.middleware(Guard);
164
+ ```
165
+
166
+ 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.
167
+
168
+ For an ordinary HTTP server, compose the contract with native layers:
169
+
170
+ ```ts
171
+ import { Layer } from "effect";
172
+ import { RpcSerialization, RpcServer } from "effect/rpc";
173
+ import { Notes } from "./notes.ts";
174
+ import { NotesHandlers } from "./handlers.ts";
175
+
176
+ export const NotesRoutes = RpcServer.layerHttp({ group: Notes, path: "/rpc", protocol: "http" }).pipe(
177
+ Layer.provide([NotesHandlers, RpcSerialization.layerJson]),
178
+ );
179
+ ```
180
+
181
+ The server host provides `NotesRoutes` to its native HTTP router. In a separate client module, use the same contract:
182
+
183
+ ```ts
184
+ import { Layer } from "effect";
185
+ import { FetchHttpClient } from "effect/http";
186
+ import * as AtomRpc from "effect/reactivity/AtomRpc";
187
+ import { RpcClient, RpcSerialization } from "effect/rpc";
188
+ import { bind } from "@shivaedev/contract/bind.ts";
189
+ import { Notes } from "./notes.ts";
190
+
191
+ export class NotesClient extends AtomRpc.Service<NotesClient>()("app/NotesClient", {
192
+ group: Notes,
193
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
194
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]),
195
+ ),
196
+ }) {}
197
+ export const api = bind(Notes, NotesClient);
198
+ ```
199
+
200
+ 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 [reference composition](https://github.com/ShivaeDev/platform/blob/main/docs/framework/composed-example.md) composes handlers, request identity, SQL and a rendered client.
201
+
202
+ ### 3. Every day: read and change
203
+
204
+ ```ts
205
+ const one = api.get.query({ id: 1 });
206
+ const list = api.list.query();
207
+ const readOnce = api.get.run({ id: 1 });
208
+ const rename = api.rename.run({ id: 1, title: "Uno" });
209
+ ```
210
+
211
+ `queryCoalesceWindow(query)` from `@shivaedev/contract/queryCoalesceWindow.ts` returns a writable native atom for that bound query's live changed-hint window. Set it through the query's registry to a Duration, or `undefined` to use the live connection's default. Changing it does not refetch or reconnect; the next window uses the new value. React consumers can set it with `useQuery(query, { coalesceWindow: "2 seconds" })`.
212
+
213
+ `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.
214
+
215
+ Mount the atoms and run commands through the service's atom runtime in the same registry:
216
+
217
+ ```ts
218
+ import { Effect } from "effect";
219
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
220
+ import { api, NotesClient } from "./client.ts";
221
+
222
+ const registry = AtomRegistry.make();
223
+ const one = api.get.query({ id: 1 });
224
+ const list = api.list.query();
225
+ const releaseOne = registry.mount(one);
226
+ const releaseList = registry.mount(list);
227
+
228
+ const renamed = await Effect.runPromise(
229
+ AtomRegistry.getResult(registry, NotesClient.runtime.atom(api.rename.run({ id: 1, title: "Uno" }))),
230
+ );
231
+
232
+ releaseOne();
233
+ releaseList();
234
+ registry.dispose();
235
+ ```
236
+
237
+ 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`.
238
+
239
+ The dependency rule is deliberately asymmetric:
240
+
241
+ | Declared key | Query reads | Successful command invalidates |
242
+ | --- | --- | --- |
243
+ | `notes.item(1)` | `"notes:1"` | `"notes:1"` and `"notes"` |
244
+ | `notes.list` | `"notes"` | `"notes"` |
245
+
246
+ `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] }`.
247
+
248
+ Client invalidation follows RPC success. Database hosts from `@shivaedev/server` also publish declared command invalidations automatically after the handler succeeds, excluding keys already announced by repository commits, using `transact` from [`sql`](https://github.com/ShivaeDev/platform/tree/main/packages/sql); commit-bound changes belong to [`changes`](https://github.com/ShivaeDev/platform/tree/main/packages/changes).
249
+
250
+ ### Session-owned keys
251
+
252
+ Before migration, an owner field exists only to give client reads and invalidations a key. The server already chooses the account from `Identity`:
253
+
254
+ ```ts
255
+ const preferences = collection("preferences", Schema.String);
256
+ const Preferences = contract("preferences", {
257
+ queries: [query("self", {
258
+ payload: { ownerId: Schema.String },
259
+ reads: ({ ownerId }) => [preferences.item(ownerId)],
260
+ success: Schema.String,
261
+ })],
262
+ commands: [command("save", {
263
+ payload: { ownerId: Schema.String, title: Schema.String },
264
+ invalidates: ({ ownerId }) => [preferences.item(ownerId)],
265
+ success: Schema.String,
266
+ })],
267
+ }).middleware(Authenticated);
268
+
269
+ const api = bind(Preferences, Client);
270
+ const self = api.self.query({ ownerId: session.userId });
271
+ const save = api.save.run({ ownerId: session.userId, title: "My preferences" });
272
+ ```
273
+
274
+ After migration, remove `ownerId` from both payload schemas and every call site. Add `keyScope: "session"` and keep a logical item key for the self-only record:
275
+
276
+ ```ts
277
+ const preferences = collection("preferences", Schema.String);
278
+ const Preferences = contract("preferences", {
279
+ keyScope: "session",
280
+ queries: [query("self", {
281
+ reads: () => [preferences.item("self")],
282
+ success: Schema.String,
283
+ })],
284
+ commands: [command("save", {
285
+ payload: { title: Schema.String },
286
+ invalidates: () => [preferences.item("self")],
287
+ success: Schema.String,
288
+ })],
289
+ }).middleware(Authenticated);
290
+
291
+ const api = bind(Preferences, Client, { registry });
292
+ const self = api.self.query(undefined);
293
+ const save = api.save.run({ title: "My preferences" });
294
+ ```
295
+
296
+ The examples use `Schema` and `Effect` from `effect`, `contract`, `query`, `command`, `collection` and `bind` from contract's defining modules, and `Authenticated` from `@shivaedev/identity/middleware.ts`. `Client` is the application's native AtomRpc service. In React, obtain `registry` from the second argument to `SessionBoundary.connect`:
297
+
298
+ ```tsx
299
+ import { SessionBoundary } from "@shivaedev/react/client/sessionBoundary.ts";
300
+
301
+ <SessionBoundary
302
+ session={session}
303
+ identify={(current) => current.generation}
304
+ connect={(current, registry) => bind(Preferences, makePreferencesClient(current.token), { registry })}
305
+ recheck={recheck}
306
+ signedOut={<p>Signed out</p>}
307
+ >
308
+ {(api) => <PreferencesScreen api={api} />}
309
+ </SessionBoundary>
310
+ ```
311
+
312
+ `makePreferencesClient(token)` creates the application's native AtomRpc service and configures its transport credentials. Existing one-argument `connect` callbacks continue to work. The boundary allocates the registry before calling `connect`; an unchanged generation keeps its binding and state. Account replacement, sign-out and same-account re-entry dispose the old registry, private query tracking, live registrations and pending refreshes. Choose a generation that changes whenever the auth owner starts a new session.
313
+
314
+ Handlers continue reading the account from request-local `Identity`, imported from `@shivaedev/identity/identity.ts`, before and after migration:
315
+
316
+ ```ts
317
+ Preferences.application.toLayer({
318
+ "preferences.self": () => Effect.flatMap(Identity, ({ id }) => store.read(id)),
319
+ "preferences.save": ({ title }) => Effect.flatMap(Identity, ({ id }) => store.save(id, title)),
320
+ });
321
+ ```
322
+
323
+ `store` is the application's persistence service. No owner or client key scope is accepted from a request. The database app host of `@shivaedev/server` resolves the server key scope from that same request-local identity.
324
+
325
+ The registry is required for session-scoped bindings. Query atoms and live connections belong in it; command and one-off query Effects execute through it. A successful command invalidates its owner's keys, while a rejected command declares no invalidation. Ordinary contracts keep unscoped keys and the two-argument `bind` call.
326
+
327
+ `List` and `Item` are still the key shapes. `readKeys(keys, { scope })` and `invalidationKeys(keys, { scope })` resolve the extra dimension while preserving item, list and wildcard semantics. An item invalidation refreshes that scope's item and list; a wildcard reaches only that scope's items. The client's opaque scope comes from its registry and is never sent as an owner id.
328
+
329
+ Server adapters read the RPC's `KeyScope` annotation. For `"session"`, they must obtain scope from request-local authenticated identity, then pass the same `{ scope }` to dependency registration, invalidation and `makeLiveServer.stream(keys, context)`. A shared server Layer must not capture identity, accept a client-selected scope or fall back to unscoped keys. This package supplies metadata and contextual keys; it has no server or authentication dependency.
330
+
331
+ ### 4. Live invalidation and resume
332
+
333
+ Database hosts from `@shivaedev/server` serve the contract's reserved `<contract>.$changes` stream automatically. With NDJSON transport, `bind` and a React `SessionBoundary` connect query atoms without an application subscribe operation or `live()` call. The boundary finds bound APIs returned directly by `connect` or as properties such as `client.api`. See [automatic changes](https://github.com/ShivaeDev/platform/blob/main/docs/framework/changes.md) for defaults.
334
+
335
+ A native shell supplies its lifecycle events through `bind`'s optional `resume` source:
336
+
337
+ ```ts
338
+ import { bind } from "@shivaedev/contract/bind.ts";
339
+ import type { ResumeSource } from "@shivaedev/contract/resume.ts";
340
+ import { onAppResume, onNetworkReturn } from "./native-lifecycle.ts";
341
+
342
+ const resume: ResumeSource = (notify) => {
343
+ const stopApp = onAppResume(notify);
344
+ const stopNetwork = onNetworkReturn(notify);
345
+ return () => { stopApp(); stopNetwork(); };
346
+ };
347
+ const api = bind(Notes, NotesClient, { resume });
348
+ ```
349
+
350
+ The application owns `onAppResume` and `onNetworkReturn`; each subscribes a callback and returns an unsubscribe function. The source joins the visible browser visibility and online defaults. Each notification reopens the live subscription and reconciles all reads tracked by the connection. The owning registry releases the source on disposal. Omitting `resume` keeps the browser defaults. Pass the same binding from React's `SessionBoundary.connect` to use it with automatic session subscriptions.
351
+
352
+ For a direct subscription, configure the server and `NotesClient` with `RpcSerialization.layerNdjson` and use the client already typed from `Notes`:
353
+
354
+ ```ts
355
+ import { Effect, Stream } from "effect";
356
+ import { NotesClient } from "./client.ts";
357
+
358
+ const changes = Stream.unwrap(NotesClient.use((client) =>
359
+ Effect.succeed(client("notes.$changes", { queries: [{ query: "get", payload: { id: 1 } }] })),
360
+ ));
361
+ ```
362
+
363
+ The contract includes the reserved stream's payload, `LiveHint` elements and middleware failures in its native RPC type. The subscription uses the contract's middleware and needs no separate RPC group or cast. The caller owns stream consumption and interruption. References name declared queries and reuse their payload schemas; payloadless queries use `{ query: "list", payload: undefined }`. Subscription handlers resolve keys with `queryKeys(Notes.declaration.queries, queries)` from `serverMetadata.ts`, or the stream RPC's `ChangesReads` annotation, before calling `makeLiveServer.stream`. Session-scoped bindings track mounted queries per registry and drop their references on unmount or disposal. Ordinary bindings track query references when atoms are created.
364
+
365
+ For a custom transport or explicit pause/status controls, the native live APIs remain available:
366
+
367
+ 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. The compiler rejects a unary client for a streaming operation.
368
+
369
+ Each item query reads its own item key, and each list query reads its list key. Commands invalidate the keys they change; an item invalidation also refreshes its collection list. Reuse the feature's `Get`, `List`, `Rename` and `Create` declarations:
370
+
371
+ ```ts
372
+ import { contract } from "@shivaedev/contract/contract.ts";
373
+ import { LiveHint } from "@shivaedev/contract/live.ts";
374
+ import { stream } from "@shivaedev/contract/operation.ts";
375
+ import { Create, Get, List, notes, Rename } from "./notes.ts";
376
+
377
+ export const Subscribe = stream("subscribe", { reads: () => [notes.list], success: LiveHint });
378
+ export const LiveNotes = contract("notes", { queries: [Get, List], commands: [Rename, Create], streams: [Subscribe] });
379
+ ```
380
+
381
+ Save the shared module as `live-notes.ts`. Keep full reconciliation in the session's explicit `resyncKeys`: enumerate the item keys the session reads and its list keys. A list key alone does not cover separately registered item queries. Having every query read a broad shared key makes every invalidation of that key refetch them all.
382
+
383
+ Use `LiveNotes` as the server group and implement `notes.subscribe` with a native Stream. For streaming HTTP, use `RpcSerialization.layerNdjson` on both sides. This client mounts live coordination alongside its queries:
384
+
385
+ ```ts
386
+ import { Layer } from "effect";
387
+ import { FetchHttpClient } from "effect/http";
388
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
389
+ import * as AtomRpc from "effect/reactivity/AtomRpc";
390
+ import { RpcClient, RpcSerialization } from "effect/rpc";
391
+ import { bind } from "@shivaedev/contract/bind.ts";
392
+ import { live } from "@shivaedev/contract/live.ts";
393
+ import { resumeSignal } from "@shivaedev/contract/resume.ts";
394
+ import { LiveNotes } from "./live-notes.ts";
395
+ import { notes } from "./notes.ts";
396
+
397
+ class Client extends AtomRpc.Service<Client>()("app/LiveNotes", {
398
+ group: LiveNotes,
399
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
400
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]),
401
+ ),
402
+ }) {}
403
+ const api = bind(LiveNotes, Client);
404
+ const registry = AtomRegistry.make();
405
+ const updates = live(Client.runtime, {
406
+ stream: api.subscribe.run(undefined),
407
+ resyncKeys: [notes.item(1), notes.item(2), notes.list],
408
+ resume: resumeSignal({ window }),
409
+ retryDelay: "100 millis",
410
+ });
411
+ const releaseQuery = registry.mount(api.get.query({ id: 1 }));
412
+ const releaseConnection = registry.mount(updates.connection);
413
+ ```
414
+
415
+ `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 explicitly reconciles two items and their list. `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/contract/src/test-support/live) supplies a complete working server.
416
+
417
+ | Observation | Invalidation |
418
+ | --- | --- |
419
+ | `Changed` with keys after the first hint | Affected items and their lists. |
420
+ | `Resync` | The exact `resyncKeys` read scope. |
421
+ | First hint on a new or reconnected stream | The full explicit scope, even if the hint is targeted. |
422
+
423
+ Changed hints share a fixed 20 ms window by default. Their keys merge, and each affected bound query refetches once per window. A bound query already fetching keeps its request and refetches once more after it settles if further hints arrive. Set `coalesceWindow` in `live()` to a native Duration, or set `SessionBoundary`'s `liveCoalesceWindow` for automatic session connections. Zero restores immediate per-hint invalidation, including interruption of in-flight reads. Full reconciliation on first hint, reconnect, resume and unpause stays immediate. Registry teardown cancels a pending window.
424
+
425
+ `LiveHint` and `Key` are schemas. Decoding rejects unknown hint tags and item keys with an invalid ID type.
426
+
427
+ ```ts
428
+ registry.set(updates.paused, true);
429
+ registry.set(updates.paused, false);
430
+ const status = registry.get(updates.status);
431
+ ```
432
+
433
+ While paused, live coordination 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: an injected native source and visible browser `online` and `visibilitychange` events request it, while hidden events do not.
434
+
435
+ `status` exposes `connection`, `failure`, `needsResync` and `pending`. A reconnection reconciles changes missed while disconnected, and successful delivery clears a typed stream failure. Native query atoms keep previous successful data during a read failure and reject stale responses after invalidation. Query failures remain in their own `AsyncResult`; stream failures appear in `status.failure`.
436
+
437
+ The resume signal is a scoped native atom. Disposing the registry removes its browser listeners, cleans up an injected source, including one used only through `live`, and releases the stream subscription:
438
+
439
+ ```ts
440
+ releaseQuery();
441
+ releaseConnection();
442
+ registry.dispose();
443
+ ```
444
+
445
+ Call the release functions and dispose the registry when the application's root no longer owns them.
446
+
447
+ ### Server hint streams
448
+
449
+ Import `makeLiveServer` from `@shivaedev/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.
450
+
451
+ For viewer-scoped hints, call `authorizeReads` once in the application's native authorization middleware, after resolving the session:
452
+
453
+ ```ts
454
+ import { authorizeReads } from "@shivaedev/contract/readPolicy.ts";
455
+
456
+ const guard = Layer.succeed(Guard, (effect, request) =>
457
+ Effect.flatMap(resolveSession(request.headers), (session) =>
458
+ authorizeReads(effect, request, (key) => canRead(session, key), Effect.fail(new Denied())).pipe(
459
+ Effect.provideService(Session, session),
460
+ ),
461
+ ),
462
+ );
463
+ ```
464
+
465
+ The application supplies `resolveSession`, `canRead` and its middleware's `Denied` failure. Apply this middleware to the contract, including its reserved change stream. The policy returns `Effect<boolean, PolicyError, PolicyServices>` for a key. `authorizeReads` checks every declared query read key before running its handler and supplies the same policy to `makeLiveServer` streams, with the request's Effect context. Queries need no additional authorization declaration. A `false` returns the supplied denial; a failed policy returns its original typed error. Declare policy errors alongside the denial in the native middleware's error schema so they reach the client, for example `error: Schema.Union([Denied, GitHubUnavailable])`.
466
+
467
+ Each changed hint checks its keys against that viewer's policy before delivery and omits denied keys. An empty hint is suppressed. Item and list keys are checked independently: allow a list key when the viewer may read that list, even when a changed item is private to another viewer. The list query still selects its visible rows. Without a supplied `ReadPolicy`, streams keep the default of allowing their subscribed keys; applications own the policy.
468
+
469
+ A failed hint check omits that key without ending the stream. The subscription rechecks failed keys after one second and repeats while checks fail. Once allowed, the key reaches the existing consumer even without another change or a reconnect. A `false` does not schedule a recheck. Interruption of the subscription stops its pending rechecks.
470
+
471
+ Successful boolean results are memoised per subscription stream and key for one second by default, including `false`; failures are never memoised. Queries always run their policy afresh. The memo holds at most the subscription's key count. It can delay filtering a revoked key from hints by at most that window; hints contain keys, and the subsequent query still checks current access before returning data. Set `ReadPolicyMemoWindow` around the request effect to shorten the window or use `0` to turn memoisation off:
472
+
473
+ ```ts
474
+ import { ReadPolicyMemoWindow } from "@shivaedev/contract/readPolicy.ts";
475
+
476
+ authorizeReads(effect, request, canRead, Effect.fail(new Denied())).pipe(
477
+ Effect.provideService(ReadPolicyMemoWindow, "100 millis"),
478
+ );
479
+ ```
480
+
481
+ ### API by module
482
+
483
+ Import the module that defines the name; the package has no root entry.
484
+
485
+ | Module | Functions and schemas |
486
+ | --- | --- |
487
+ | `operation.ts` | `query(name, { payload?, success?, rejections?, reads })`; `command(name, { payload?, success?, rejections?, invalidates })`; `stream(name, { payload?, success, rejections?, reads })`. |
488
+ | `contract.ts` | `contract(name, { queries?, commands?, streams?, keyScope? })`, returning a native RPC group with `declaration: { name, queries, commands, streams, keyScope }`. |
489
+ | `rejection.ts` | `fieldRejection(struct, fields?)`; lower-level `rejectionSet(specs)`, which constructs the rejection classes, union schema and failure helpers used by operations. |
490
+ | `keys.ts` | `collection(name, idSchema)`; `Key` schema; `readKeys(keys, context?)` and `invalidationKeys(keys, context?)`. |
491
+ | `bind.ts` | `bind(contract, atomRpcService, { registry }?)`, requiring the registry for session-scoped contracts and exposing `{ query, run }` for queries, `{ run }` Effects for commands and `{ run }` native Streams for streams. |
492
+ | `live.ts` | `LiveHint` schema; `live(runtime, { stream, resyncKeys, coalesceWindow?, keyContext?, resume?, retryDelay? })`, exposing `connection`, `coalesceWindow`, `paused` and `status`. |
493
+ | `makeLiveServer.ts` | `makeLiveServer`, an Effect requiring `Reactivity` that creates scoped server hint streams and a `resync` Effect. |
494
+ | `readPolicy.ts` | `authorizeReads(effect, request, policy, denied)`; `ReadPolicy`, the request-local fallible key policy used by server hint streams; `ReadPolicyMemoWindow`, a `Context.Reference<Duration.Input>` controlling each stream's memo duration. |
495
+ | `serverMetadata.ts` | `KeyScope`, `QueryReads`, `CommandInvalidation`, `ChangesReads`, `queryKeys(queries, references)` and `changesRpc(name, queries)` for server adapters. |
496
+ | `resume.ts` | `resumeSignal({ window?, native? })`, returning a native numeric signal atom. |
497
+
498
+ A query atom takes native query options as its optional second argument, for example `api.get.query({ id: 1 }, { timeToLive: "1 minute" })`. A native resume source receives a callback and returns a cleanup function.
499
+
500
+ ### Install, setup and limits
501
+
502
+ ```sh
503
+ pnpm add @shivaedev/contract effect@4.0.5
504
+ ```
505
+
506
+ `effect` is a peer, and the package needs Node.js 24 or newer. Choose the native server host separately; this package supplies no server process or HTTP transport implementation.
507
+
508
+ - 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 `react` and `form`.
509
+ - Run command Effects with the Reactivity instance that owns the queries to refresh, as in the registry example.
510
+ - Choose a common read key for every query in a full reconciliation scope. A list key does not implicitly cover item queries.
511
+ - Supply browser or native resume sources explicitly.
512
+ - Live hints carry refresh requests, not authoritative data or durable history. Applications own dependency discovery, authenticated subscriptions, delivery across processes, SSR and deployment behavior.
@@ -0,0 +1,28 @@
1
+ import { type Duration } from "effect";
2
+ import * as Atom from "effect/reactivity/Atom";
3
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
+ import type * as AtomRpc from "effect/reactivity/AtomRpc";
5
+ import type { BindingOwner } from "#internal/bindingOwner.ts";
6
+ import { type Key } from "#keys.ts";
7
+ import { type LiveStatus } from "#live.ts";
8
+ import { type ResumeSource } from "#resume.ts";
9
+ import type { changesRpc, QueryReference } from "#serverMetadata.ts";
10
+ export declare const LiveConnection: unique symbol;
11
+ export interface LiveBound {
12
+ readonly [LiveConnection]: {
13
+ readonly coalesceWindow: Atom.Writable<Duration.Input>;
14
+ readonly connection: Atom.Atom<unknown>;
15
+ readonly status: Atom.Atom<LiveStatus<unknown>>;
16
+ };
17
+ }
18
+ export type ChangesClient = AtomRpc.AtomRpcClient<unknown, string, ReturnType<typeof changesRpc>>;
19
+ export declare function automaticLive(service: ChangesClient, name: string, owner?: BindingOwner, resume?: ResumeSource): {
20
+ subscription: {
21
+ coalesceWindow: Atom.Writable<Duration.Input, Duration.Input>;
22
+ paused: Atom.Writable<boolean, boolean>;
23
+ status: Atom.Writable<LiveStatus<import("effect/rpc/RpcClientError").RpcClientError>, LiveStatus<import("effect/rpc/RpcClientError").RpcClientError>>;
24
+ connection: Atom.Atom<import("effect/reactivity/AsyncResult").AsyncResult<void, never>>;
25
+ };
26
+ track: (registry: AtomRegistry.AtomRegistry | undefined, reference: QueryReference, keys: readonly Key[]) => () => void;
27
+ };
28
+ //# sourceMappingURL=automaticLive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automaticLive.d.ts","sourceRoot":"","sources":["../src/automaticLive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAmC,MAAM,QAAQ,CAAC;AACxE,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,KAAK,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,KAAK,UAAU,EAAQ,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,KAAK,YAAY,EAAmC,MAAM,YAAY,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAIrE,eAAO,MAAM,cAAc,eAAmD,CAAC;AAC/E,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE;QAC1B,QAAQ,CAAC,cAAc,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACvD,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;KAChD,CAAC;CACF;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC;AAOlG,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,YAAY;;;;;;;sBAIrF,YAAY,CAAC,YAAY,GAAG,SAAS,aAAa,cAAc,QAAQ,SAAS,GAAG,EAAE;EA4D/G"}
@@ -0,0 +1,63 @@
1
+ import { Effect, Stream, SubscriptionRef } from "effect";
2
+ import * as Atom from "effect/reactivity/Atom";
3
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
+ import { readKeys } from "#keys.ts";
5
+ import { live } from "#live.ts";
6
+ import { resumeSignal } from "#resume.ts";
7
+ export const LiveConnection = Symbol.for("@shivaedev/contract/LiveConnection");
8
+ export function automaticLive(service, name, owner, resume) {
9
+ const eager = owner === undefined ? Effect.runSync(SubscriptionRef.make(new Map())) : undefined;
10
+ const known = new Set();
11
+ const tracked = Atom.keepAlive(Atom.make(() => eager ?? Effect.runSync(SubscriptionRef.make(new Map()))));
12
+ function track(registry, reference, keys) {
13
+ const fresh = eager === undefined
14
+ ? keys
15
+ : keys.filter((key) => {
16
+ const id = readKeys([key])[0] ?? "";
17
+ if (known.has(id)) {
18
+ return false;
19
+ }
20
+ known.add(id);
21
+ return true;
22
+ });
23
+ if (fresh.length === 0) {
24
+ return () => undefined;
25
+ }
26
+ const token = {};
27
+ const state = eager ?? registry?.get(tracked);
28
+ if (state === undefined) {
29
+ throw new Error("Session query tracking requires its owning registry");
30
+ }
31
+ Effect.runSync(SubscriptionRef.update(state, (current) => new Map([...current, [token, { keys: fresh, reference }]])));
32
+ if (eager !== undefined) {
33
+ return () => undefined;
34
+ }
35
+ return () => Effect.runSync(SubscriptionRef.update(state, (current) => {
36
+ const next = new Map(current);
37
+ next.delete(token);
38
+ return next;
39
+ }));
40
+ }
41
+ function subscribe(current) {
42
+ if (current.size === 0) {
43
+ return Stream.never;
44
+ }
45
+ const queries = [...current.values()].map(({ reference }) => reference);
46
+ return Stream.unwrap(service.use((client) => Effect.succeed(client(`${name}.$changes`, { queries }))));
47
+ }
48
+ const signal = resumeSignal({ ...(typeof window === "undefined" ? {} : { window }), ...(resume === undefined ? {} : { native: resume }) });
49
+ const subscription = live(service.runtime, {
50
+ keyContext: owner?.context,
51
+ resume: signal,
52
+ resyncKeys: (registry) => [...Effect.runSync(SubscriptionRef.get(registry.get(tracked))).values()].flatMap(({ keys }) => keys),
53
+ stream: Stream.unwrap(Effect.map(AtomRegistry.AtomRegistry, (registry) => Stream.zipLatest(SubscriptionRef.changes(registry.get(tracked)), AtomRegistry.toStream(registry, signal)).pipe(Stream.switchMap(([current]) => subscribe(current))))),
54
+ });
55
+ const connection = Atom.make((get) => {
56
+ if (owner !== undefined && owner.registry !== get.registry) {
57
+ throw new Error("A session-scoped live connection belongs to its binding registry");
58
+ }
59
+ return get(subscription.connection);
60
+ }).pipe(Atom.setIdleTTL(0));
61
+ return { subscription: { ...subscription, connection }, track };
62
+ }
63
+ //# sourceMappingURL=automaticLive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automaticLive.js","sourceRoot":"","sources":["../src/automaticLive.ts"],"names":[],"mappings":"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"}