@shivaedev/contract 0.18.0 → 0.19.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +168 -178
  3. package/dist/automaticLive.d.ts +2 -2
  4. package/dist/automaticLive.d.ts.map +1 -1
  5. package/dist/automaticLive.js +4 -3
  6. package/dist/automaticLive.js.map +1 -1
  7. package/dist/bind.d.ts +11 -2
  8. package/dist/bind.d.ts.map +1 -1
  9. package/dist/bind.js +9 -0
  10. package/dist/bind.js.map +1 -1
  11. package/dist/clientOptions.d.ts +23 -0
  12. package/dist/clientOptions.d.ts.map +1 -0
  13. package/dist/clientOptions.js +2 -0
  14. package/dist/clientOptions.js.map +1 -0
  15. package/dist/contract.d.ts +4 -2
  16. package/dist/contract.d.ts.map +1 -1
  17. package/dist/contract.js +6 -5
  18. package/dist/contract.js.map +1 -1
  19. package/dist/internal/bindingOwner.d.ts +1 -1
  20. package/dist/internal/bindingOwner.d.ts.map +1 -1
  21. package/dist/internal/clientOperations.d.ts.map +1 -1
  22. package/dist/internal/clientOperations.js +28 -7
  23. package/dist/internal/clientOperations.js.map +1 -1
  24. package/dist/internal/httpClient.d.ts +21 -0
  25. package/dist/internal/httpClient.d.ts.map +1 -0
  26. package/dist/internal/httpClient.js +13 -0
  27. package/dist/internal/httpClient.js.map +1 -0
  28. package/dist/internal/operationDeclaration.d.ts +19 -0
  29. package/dist/internal/operationDeclaration.d.ts.map +1 -0
  30. package/dist/internal/operationDeclaration.js +20 -0
  31. package/dist/internal/operationDeclaration.js.map +1 -0
  32. package/dist/internal/queryHints.d.ts +8 -0
  33. package/dist/internal/queryHints.d.ts.map +1 -0
  34. package/dist/internal/queryHints.js +34 -0
  35. package/dist/internal/queryHints.js.map +1 -0
  36. package/dist/live.d.ts +7 -1
  37. package/dist/live.d.ts.map +1 -1
  38. package/dist/live.js +2 -2
  39. package/dist/live.js.map +1 -1
  40. package/dist/makeLiveServer.d.ts +27 -1
  41. package/dist/makeLiveServer.d.ts.map +1 -1
  42. package/dist/makeLiveServer.js +15 -8
  43. package/dist/makeLiveServer.js.map +1 -1
  44. package/dist/operation.d.ts +13 -9
  45. package/dist/operation.d.ts.map +1 -1
  46. package/dist/operation.js +5 -19
  47. package/dist/operation.js.map +1 -1
  48. package/dist/publicConfig.d.ts +9 -3
  49. package/dist/publicConfig.d.ts.map +1 -1
  50. package/dist/publicConfig.js +2 -1
  51. package/dist/publicConfig.js.map +1 -1
  52. package/dist/querySubscription.d.ts +15 -0
  53. package/dist/querySubscription.d.ts.map +1 -0
  54. package/dist/querySubscription.js +10 -0
  55. package/dist/querySubscription.js.map +1 -0
  56. package/dist/readPolicy.d.ts +11 -6
  57. package/dist/readPolicy.d.ts.map +1 -1
  58. package/dist/readPolicy.js +16 -19
  59. package/dist/readPolicy.js.map +1 -1
  60. package/dist/readPolicyErrors.d.ts +13 -0
  61. package/dist/readPolicyErrors.d.ts.map +1 -0
  62. package/dist/readPolicyErrors.js +6 -0
  63. package/dist/readPolicyErrors.js.map +1 -0
  64. package/dist/serverMetadata.d.ts +29 -11
  65. package/dist/serverMetadata.d.ts.map +1 -1
  66. package/dist/serverMetadata.js +19 -12
  67. package/dist/serverMetadata.js.map +1 -1
  68. package/dist/testing.d.ts +13 -1
  69. package/dist/testing.d.ts.map +1 -1
  70. package/dist/testing.js +1 -4
  71. package/dist/testing.js.map +1 -1
  72. package/package.json +6 -5
  73. package/src/automaticLive.ts +8 -3
  74. package/src/bind.ts +45 -3
  75. package/src/clientOptions.ts +35 -0
  76. package/src/contract.ts +11 -6
  77. package/src/internal/bindingOwner.ts +1 -1
  78. package/src/internal/clientOperations.ts +32 -11
  79. package/src/internal/httpClient.ts +21 -0
  80. package/src/internal/operationDeclaration.ts +28 -0
  81. package/src/internal/queryHints.ts +38 -0
  82. package/src/live.ts +2 -2
  83. package/src/makeLiveServer.ts +18 -12
  84. package/src/operation.ts +27 -37
  85. package/src/publicConfig.ts +2 -2
  86. package/src/querySubscription.ts +21 -0
  87. package/src/readPolicy.ts +23 -23
  88. package/src/readPolicyErrors.ts +4 -0
  89. package/src/serverMetadata.ts +35 -19
  90. package/src/testing.ts +2 -12
  91. package/dist/internal/makeHintPolicy.d.ts +0 -4
  92. package/dist/internal/makeHintPolicy.d.ts.map +0 -1
  93. package/dist/internal/makeHintPolicy.js +0 -25
  94. package/dist/internal/makeHintPolicy.js.map +0 -1
  95. package/dist/keys.d.ts +0 -34
  96. package/dist/keys.d.ts.map +0 -1
  97. package/dist/keys.js +0 -25
  98. package/dist/keys.js.map +0 -1
  99. package/src/internal/makeHintPolicy.ts +0 -40
  100. package/src/keys.ts +0 -62
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.1 - 2026-10-29
4
+
5
+ ### Fixed
6
+
7
+ - Automatic live tracking no longer resubscribes, and refetches every tracked query, when a query is read again after the garbage collector reclaimed its atom. A query's tracking id now follows its tag and payload instead of the atom object AtomRpc holds weakly.
8
+
9
+ ## 0.19.0 - 2026-10-28
10
+
11
+ ### Added
12
+
13
+ - `@shivaedev/contract/bind.ts` builds a bound HTTP RPC client and atom runtime from a contract and URL, with an inferred request transform for cookies or bearer tokens.
14
+ - `@shivaedev/react/client/useNow.ts` exports `useNow(interval, runtime?)` to render the runtime's Effect clock and refresh on the requested interval, including with TestClock.
15
+ - Declare collection policies beside module contracts with `ModuleReadPolicies` and combine them with `mergeReadPolicies`. Unclaimed collections are denied. Each policy receives the request's optional viewer.
16
+ - The application host accepts a merged `readPolicy` and checks queries and live hints inside native session middleware, using its resolved identity without another session lookup. Policies can use application services and return `Forbidden` or `AuthUnavailable`.
17
+ - Query authorization and live-hint filtering check keys concurrently so native `Effect.request` resolvers can batch membership checks. Plain Effect policies remain supported.
18
+
19
+ ### Fixed
20
+
21
+ - Live queries accept inputs JSON cannot encode, such as bigint ids.
22
+ - Enforce declared query read policies at the hosted RPC handler boundary, including contracts without identity middleware, whose viewer is anonymous. Query RPC error schemas include `QueryReadDenied` so native clients receive typed refusals before handlers run.
23
+ - Keep collection methods and untyped schema internals from making data parameters appear to carry callable services. Direct service implementations, nested dependency fields and service arrays remain subject to `effect/service-parameters`.
24
+
25
+ ### Breaking
26
+
27
+ - Move key imports from `@shivaedev/contract/keys.ts` to `@shivaedev/changes/keys.ts`. Declare bucket fields with `collection(name, idSchema, fields)` and publish affected buckets alongside record keys.
28
+ - Replace collection policy maps, `mergeReadPolicies`, `ReadPolicy` and `ReadPolicyMemoWindow` with each query's `readPolicy(payload, viewer)`. Declare the same query's live scope with `reads(payload, viewer)`. Bound query atoms and subscriptions refuse missing policies with a declaration error; one-off query `run` calls remain available. Denied queries refuse the entire subscription. Server adapters replace `QueryReads` and `ChangesReads` with `QueryDefinition` and `ChangesQueries`. Custom middleware calls `authorizeReads(effect, request, viewer)` and declares `MissingReadPolicy` and `QueryReadDenied` errors.
29
+ - Custom subscription adapters yield `ChangesQueries.resolve(references)`, call `querySubscription(requests, viewer)` and pass the authorized result to `makeLiveServer.subscribe(subscription, context)`. Metadata values expose `QueryDefinition.definition` and `CommandInvalidation.invalidates` instead of bare definitions and callbacks. Yield the Effect returned by `queryKeys(queries, references)` when projecting keys for a custom stream. `resolveChanges` supplies the authorized subscription in hosted applications. Changed hints can carry query positions so bindings can rerun session-scoped reads without deriving session scopes in the browser.
30
+ - Remove `appHost`'s `readPolicy` option and module policy registration. Session and permission middleware apply query policies using the current session viewer. Handle the new typed policy errors in native clients, including `QueryReadDenied` on query RPCs without identity middleware; widen explicit query error annotations accordingly.
31
+ - Replace `sessionCommandChanges` with `commandChanges` for custom session middleware. Command invalidation callbacks can receive the session viewer as their third argument. Announce viewer buckets there; background writers must explicitly publish those bucket keys after commit.
32
+ - Built-in admin queries declare their permission policies alongside their schemas and read scopes. Consumers keep supplying the existing session and permission middleware layers.
33
+ - Local Work Board and Fleet queries declare their existing loopback read access explicitly so their generated live subscriptions remain available.
34
+ - `authorizeReads` now takes `(effect, request, policy, viewer, denied)`, and policies take `(key, viewer)`. Pass `Option.some(resolvedIdentity)` or `Option.none()` before the denial effect. Existing unary policy functions may ignore the new viewer argument. Apps using `appHost` can replace custom read middleware with its `readPolicy` option and merge their modules' policy maps.
35
+
3
36
  ## 0.18.0 - 2026-10-27
4
37
 
5
38
  ### Added
package/README.md CHANGED
@@ -7,16 +7,16 @@ Declare a feature's queries, commands, streams, expected failures and refresh de
7
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
8
 
9
9
  ```ts
10
- import { Schema } from "effect";
10
+ import { Effect, Schema } from "effect";
11
11
  import { contract } from "@shivaedev/contract/contract.ts";
12
- import { collection } from "@shivaedev/contract/keys.ts";
12
+ import { collection } from "@shivaedev/changes/keys.ts";
13
13
  import { command, query } from "@shivaedev/contract/operation.ts";
14
14
 
15
15
  const notes = collection("notes", Schema.Number);
16
- const List = query("list", { reads: () => [notes.list], success: Schema.Array(Schema.String) });
16
+ const List = query("list", { readPolicy: () => Effect.succeed(true), reads: () => [notes.list], success: Schema.Array(Schema.String) });
17
17
  const Rename = command("rename", {
18
- payload: { id: Schema.Number, title: Schema.String },
19
- invalidates: ({ id }) => [notes.item(id)],
18
+ payload: { id: Schema.Number, title: Schema.String },
19
+ invalidates: ({ id }) => [notes.item(id)],
20
20
  });
21
21
  const Notes = contract("notes", { queries: [List], commands: [Rename] });
22
22
  ```
@@ -40,47 +40,49 @@ Optional live hints use those same keys. A hint asks for another read; it does n
40
40
  Keep the shared declaration in a module that both server and client can import:
41
41
 
42
42
  ```ts
43
- import { Schema } from "effect";
43
+ import { Effect, Schema } from "effect";
44
44
  import { contract } from "@shivaedev/contract/contract.ts";
45
- import { collection } from "@shivaedev/contract/keys.ts";
45
+ import { collection } from "@shivaedev/changes/keys.ts";
46
46
  import { command, query } from "@shivaedev/contract/operation.ts";
47
47
  import { fieldRejection } from "@shivaedev/contract/rejection.ts";
48
48
 
49
49
  export class Note extends Schema.Class<Note>("Note")({
50
- body: Schema.String,
51
- id: Schema.Number,
52
- title: Schema.String,
50
+ body: Schema.String,
51
+ id: Schema.Number,
52
+ title: Schema.String,
53
53
  }) {}
54
54
  export const Draft = Schema.Struct({ body: Schema.String, title: Schema.String });
55
55
  export class NoteMissing extends Schema.TaggedError<NoteMissing>()("NoteMissing", { id: Schema.Number }) {}
56
56
  export const notes = collection("notes", Note.fields.id);
57
57
 
58
58
  export const Get = query("get", {
59
- payload: { id: Schema.Number },
60
- success: Note,
61
- rejections: { NoteMissing },
62
- reads: ({ id }) => [notes.item(id)],
59
+ readPolicy: () => Effect.succeed(true),
60
+ payload: { id: Schema.Number },
61
+ success: Note,
62
+ rejections: { NoteMissing },
63
+ reads: ({ id }) => [notes.item(id)],
63
64
  });
64
65
  export const List = query("list", {
65
- success: Schema.Array(Note),
66
- reads: () => [notes.list],
66
+ readPolicy: () => Effect.succeed(true),
67
+ success: Schema.Array(Note),
68
+ reads: () => [notes.list],
67
69
  });
68
70
  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)],
71
+ payload: { id: Schema.Number, title: Schema.String },
72
+ success: Note,
73
+ rejections: { NoteMissing, Invalid: fieldRejection(Draft, ["title"]) },
74
+ invalidates: ({ id }) => [notes.item(id)],
73
75
  });
74
76
  export const Create = command("create", {
75
- payload: Draft,
76
- success: Note,
77
- rejections: { Invalid: fieldRejection(Draft) },
78
- invalidates: (_draft, note) => [notes.item(note.id)],
77
+ payload: Draft,
78
+ success: Note,
79
+ rejections: { Invalid: fieldRejection(Draft) },
80
+ invalidates: (_draft, note) => [notes.item(note.id)],
79
81
  });
80
82
  export const Notes = contract("notes", { queries: [Get, List], commands: [Rename, Create] });
81
83
  ```
82
84
 
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.
85
+ 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 server resolves `reads` with the decoded payload and session viewer. For local command dependencies, the browser also evaluates it with `Option.none()`; return no viewer keys for that case. Authorized subscription scopes always come from the server. `invalidates` receives the decoded payload, successful result and optional session viewer, so a create call can invalidate the ID the server assigned.
84
86
 
85
87
  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
88
 
@@ -91,14 +93,14 @@ Operation names must differ within a contract, including across queries, command
91
93
  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
94
 
93
95
  ```ts
94
- import { Schema } from "effect";
96
+ import { Effect, Schema } from "effect";
95
97
  import { Rpc } from "effect/rpc";
96
98
  import { BadRequest, Conflict } from "@shivaedev/errors/taxonomy.ts";
97
99
 
98
100
  export const Rename = Rpc.make("Rename", {
99
- error: Schema.Union([BadRequest, Conflict]),
100
- payload: { name: Schema.String },
101
- success: Schema.String,
101
+ error: Schema.Union([BadRequest, Conflict]),
102
+ payload: { name: Schema.String },
103
+ success: Schema.String,
102
104
  });
103
105
  ```
104
106
 
@@ -124,28 +126,35 @@ Implement the contract with native `of` and `toLayer`:
124
126
  import { Effect, Ref, Stream } from "effect";
125
127
  import { Create, Get, Note, Notes, Rename } from "./notes.ts";
126
128
 
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
- }));
129
+ export const NotesHandlers = Notes.toLayer(
130
+ Effect.gen(function* () {
131
+ const stored = yield* Ref.make(
132
+ new Map([
133
+ [1, new Note({ body: "", id: 1, title: "One" })],
134
+ [2, new Note({ body: "", id: 2, title: "Two" })],
135
+ ]),
136
+ );
137
+ const find = (id: number) =>
138
+ Effect.flatMap(Ref.get(stored), (all) => {
139
+ const note = all.get(id);
140
+ return note === undefined ? Get.reject.NoteMissing({ id }) : Effect.succeed(note);
141
+ });
142
+ const save = (note: Note) => Ref.update(stored, (all) => new Map([...all, [note.id, note]])).pipe(Effect.as(note));
143
+ return Notes.of({
144
+ "notes.$changes": () => Stream.never,
145
+ "notes.get": ({ id }) => find(id),
146
+ "notes.list": () => Effect.map(Ref.get(stored), (all) => [...all.values()]),
147
+ "notes.rename": ({ id, title }) =>
148
+ title.trim() === ""
149
+ ? Rename.reject.Invalid({ field: "title", message: "Enter a title" })
150
+ : Effect.flatMap(find(id), (note) => save(new Note({ ...note, title }))),
151
+ "notes.create": (draft) =>
152
+ draft.title === ""
153
+ ? Create.reject.Invalid({ field: "title", message: "Enter a title" })
154
+ : Effect.flatMap(Ref.get(stored), (all) => save(new Note({ id: all.size + 1, ...draft }))),
155
+ });
156
+ }),
157
+ );
149
158
  ```
150
159
 
151
160
  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.
@@ -174,7 +183,7 @@ import { Notes } from "./notes.ts";
174
183
  import { NotesHandlers } from "./handlers.ts";
175
184
 
176
185
  export const NotesRoutes = RpcServer.layerHttp({ group: Notes, path: "/rpc", protocol: "http" }).pipe(
177
- Layer.provide([NotesHandlers, RpcSerialization.layerJson]),
186
+ Layer.provide([NotesHandlers, RpcSerialization.layerJson]),
178
187
  );
179
188
  ```
180
189
 
@@ -189,16 +198,35 @@ import { bind } from "@shivaedev/contract/bind.ts";
189
198
  import { Notes } from "./notes.ts";
190
199
 
191
200
  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
- ),
201
+ group: Notes,
202
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
203
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]),
204
+ ),
196
205
  }) {}
197
206
  export const api = bind(Notes, NotesClient);
198
207
  ```
199
208
 
200
209
  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
210
 
211
+ ### Build an HTTP client in one call
212
+
213
+ Instead of declaring the `AtomRpc.Service` yourself, use `makeClient` once per session to build the HTTP client and bind the contract to it:
214
+
215
+ ```ts
216
+ import { HttpClient, HttpClientRequest } from "effect/http";
217
+ import { makeClient } from "@shivaedev/contract/bind.ts";
218
+ import { Notes } from "./notes.ts";
219
+
220
+ const { api, runtime } = makeClient(Notes, {
221
+ url: "/rpc",
222
+ transformClient: (client) => HttpClient.mapRequest(client, HttpClientRequest.setHeader("authorization", "Bearer session-token")),
223
+ });
224
+ const note = api.get.query({ id: 1 });
225
+ const rename = runtime.fn(api.rename.run);
226
+ ```
227
+
228
+ The request transform is optional and its native HTTP client type is inferred. Tests can set cookie and Origin headers through the same option. The default transport uses Fetch and NDJSON; `httpClient` and `serialization` accept native Layers for another HTTP client or codec. `group` can supply a larger native RPC group containing the contract, such as an admin registry. The result is a native client Layer that a host can pass to `ManagedRuntime.make`. It also exposes the native `Client` service and a scoped `client` Effect for callers outside React. Session-scoped contracts require `registry` in the options, just as `bind` does.
229
+
202
230
  ### 3. Every day: read and change
203
231
 
204
232
  ```ts
@@ -208,8 +236,6 @@ const readOnce = api.get.run({ id: 1 });
208
236
  const rename = api.rename.run({ id: 1, title: "Uno" });
209
237
  ```
210
238
 
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
239
  `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
240
 
215
241
  Mount the atoms and run commands through the service's atom runtime in the same registry:
@@ -225,9 +251,7 @@ const list = api.list.query();
225
251
  const releaseOne = registry.mount(one);
226
252
  const releaseList = registry.mount(list);
227
253
 
228
- const renamed = await Effect.runPromise(
229
- AtomRegistry.getResult(registry, NotesClient.runtime.atom(api.rename.run({ id: 1, title: "Uno" }))),
230
- );
254
+ const renamed = await Effect.runPromise(AtomRegistry.getResult(registry, NotesClient.runtime.atom(api.rename.run({ id: 1, title: "Uno" }))));
231
255
 
232
256
  releaseOne();
233
257
  releaseList();
@@ -238,54 +262,36 @@ The successful rename invalidates note 1 and the list. A rejected rename invalid
238
262
 
239
263
  The dependency rule is deliberately asymmetric:
240
264
 
241
- | Declared key | Query reads | Successful command invalidates |
242
- | --- | --- | --- |
243
- | `notes.item(1)` | `"notes:1"` | `"notes:1"` and `"notes"` |
244
- | `notes.list` | `"notes"` | `"notes"` |
265
+ | Declared key | Query reads | Successful command invalidates |
266
+ | --------------- | ----------- | ------------------------------ |
267
+ | `notes.item(1)` | `"notes:1"` | `"notes:1"` and `"notes"` |
268
+ | `notes.list` | `"notes"` | `"notes"` |
245
269
 
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] }`.
270
+ `readKeys` and `invalidationKeys` from `@shivaedev/changes/keys.ts` 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
271
 
248
272
  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
273
 
250
274
  ### Session-owned keys
251
275
 
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:
276
+ A self-only feature, such as a user's preferences, reads and changes only the signed-in account's records. Declare its contract with `keyScope: "session"`. The server chooses the account from request-local identity, so payloads carry no owner field, and reads and invalidations name a logical item key for the record:
275
277
 
276
278
  ```ts
277
279
  const preferences = collection("preferences", Schema.String);
278
280
  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
- })],
281
+ keyScope: "session",
282
+ queries: [
283
+ query("self", {
284
+ reads: () => [preferences.item("self")],
285
+ success: Schema.String,
286
+ }),
287
+ ],
288
+ commands: [
289
+ command("save", {
290
+ payload: { title: Schema.String },
291
+ invalidates: () => [preferences.item("self")],
292
+ success: Schema.String,
293
+ }),
294
+ ],
289
295
  }).middleware(Authenticated);
290
296
 
291
297
  const api = bind(Preferences, Client, { registry });
@@ -293,30 +299,14 @@ const self = api.self.query(undefined);
293
299
  const save = api.save.run({ title: "My preferences" });
294
300
  ```
295
301
 
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.
302
+ The examples use `Schema` and `Effect` from `effect`, `contract`, `query`, `command` and `bind` from contract's defining modules, `collection` from `@shivaedev/changes/keys.ts`, and `Authenticated` from `@shivaedev/identity/middleware.ts`. `Client` is the application's native AtomRpc service, and `registry` is the atom registry that owns the session's queries. In React, `SessionBoundary` supplies that registry to `connect`, as [react's session-scoped bindings](https://github.com/ShivaeDev/platform/blob/main/packages/react/README.md#session-scoped-bindings) show.
313
303
 
314
- Handlers continue reading the account from request-local `Identity`, imported from `@shivaedev/identity/identity.ts`, before and after migration:
304
+ Handlers read the account from request-local `Identity`, imported from `@shivaedev/identity/identity.ts`:
315
305
 
316
306
  ```ts
317
307
  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)),
308
+ "preferences.self": () => Effect.flatMap(Identity, ({ id }) => store.read(id)),
309
+ "preferences.save": ({ title }) => Effect.flatMap(Identity, ({ id }) => store.save(id, title)),
320
310
  });
321
311
  ```
322
312
 
@@ -340,9 +330,12 @@ import type { ResumeSource } from "@shivaedev/contract/resume.ts";
340
330
  import { onAppResume, onNetworkReturn } from "./native-lifecycle.ts";
341
331
 
342
332
  const resume: ResumeSource = (notify) => {
343
- const stopApp = onAppResume(notify);
344
- const stopNetwork = onNetworkReturn(notify);
345
- return () => { stopApp(); stopNetwork(); };
333
+ const stopApp = onAppResume(notify);
334
+ const stopNetwork = onNetworkReturn(notify);
335
+ return () => {
336
+ stopApp();
337
+ stopNetwork();
338
+ };
346
339
  };
347
340
  const api = bind(Notes, NotesClient, { resume });
348
341
  ```
@@ -355,14 +348,14 @@ For a direct subscription, configure the server and `NotesClient` with `RpcSeria
355
348
  import { Effect, Stream } from "effect";
356
349
  import { NotesClient } from "./client.ts";
357
350
 
358
- const changes = Stream.unwrap(NotesClient.use((client) =>
359
- Effect.succeed(client("notes.$changes", { queries: [{ query: "get", payload: { id: 1 } }] })),
360
- ));
351
+ const changes = Stream.unwrap(
352
+ NotesClient.use((client) => Effect.succeed(client("notes.$changes", { queries: [{ query: "get", payload: { id: 1 } }] }))),
353
+ );
361
354
  ```
362
355
 
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.
356
+ 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 }`. Custom subscription handlers yield the Effect from the `ChangesQueries` annotation's `resolve(references)` method, call `querySubscription(requests, viewer)` and pass its authorized result to `makeLiveServer.subscribe(subscription, context)`. Hosted applications use `resolveChanges` from identity to supply that subscription. 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
357
 
365
- For a custom transport or explicit pause/status controls, the native live APIs remain available:
358
+ For a custom transport or explicit pause and status controls, use the native live APIs.
366
359
 
367
360
  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
361
 
@@ -395,18 +388,18 @@ import { LiveNotes } from "./live-notes.ts";
395
388
  import { notes } from "./notes.ts";
396
389
 
397
390
  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
- ),
391
+ group: LiveNotes,
392
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
393
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]),
394
+ ),
402
395
  }) {}
403
396
  const api = bind(LiveNotes, Client);
404
397
  const registry = AtomRegistry.make();
405
398
  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",
399
+ stream: api.subscribe.run(undefined),
400
+ resyncKeys: [notes.item(1), notes.item(2), notes.list],
401
+ resume: resumeSignal({ window }),
402
+ retryDelay: "100 millis",
410
403
  });
411
404
  const releaseQuery = registry.mount(api.get.query({ id: 1 }));
412
405
  const releaseConnection = registry.mount(updates.connection);
@@ -414,14 +407,16 @@ const releaseConnection = registry.mount(updates.connection);
414
407
 
415
408
  `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
409
 
417
- | Observation | Invalidation |
418
- | --- | --- |
419
- | `Changed` with keys after the first hint | Affected items and their lists. |
420
- | `Resync` | The exact `resyncKeys` read scope. |
410
+ | Observation | Invalidation |
411
+ | ----------------------------------------- | ------------------------------------------------------ |
412
+ | `Changed` with keys after the first hint | Affected items and their lists. |
413
+ | `Resync` | The exact `resyncKeys` read scope. |
421
414
  | First hint on a new or reconnected stream | The full explicit scope, even if the hint is targeted. |
422
415
 
423
416
  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
417
 
418
+ `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" })`.
419
+
425
420
  `LiveHint` and `Key` are schemas. Decoding rejects unknown hint tags and item keys with an invalid ID type.
426
421
 
427
422
  ```ts
@@ -448,39 +443,33 @@ Call the release functions and dispose the registry when the application's root
448
443
 
449
444
  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
445
 
451
- For viewer-scoped hints, call `authorizeReads` once in the application's native authorization middleware, after resolving the session:
446
+ ### Query policies, record keys and buckets
447
+
448
+ A query declares its decoded input, result, `readPolicy` and `reads` together. `readPolicy(payload, viewer)` returns an Effect boolean. `viewer` is the session's `Option<{ id: string }>`; the browser does not send the identity. Policies may use browser-safe Effect service tags whose implementations the server supplies.
452
449
 
453
450
  ```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
- );
451
+ import { Effect, Option, Schema } from "effect";
452
+ import { collection } from "@shivaedev/changes/keys.ts";
453
+ import { query } from "@shivaedev/contract/operation.ts";
454
+
455
+ const notes = collection("notes", Schema.Number, { userId: Schema.String });
456
+ const Mine = query("mine", {
457
+ payload: { title: Schema.String },
458
+ success: Schema.Array(Schema.String),
459
+ readPolicy: (_input, viewer) => Effect.succeed(Option.isSome(viewer)),
460
+ reads: (_input, viewer) => (Option.isSome(viewer) ? [notes.bucket.userId(viewer.value.id)] : []),
461
+ });
463
462
  ```
464
463
 
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.
464
+ A record key, `notes.item(id)`, names one record. A bucket, `notes.bucket.userId(id)`, names every row with that field value. Declare bucket fields on the collection model with their schemas. A list may add title, state or other filters to its input and handler, but those filters never narrow its bucket. It reruns whenever any row in that bucket changes, including a row excluded by the filter. Writers explicitly announce the record and affected buckets with the same constructors from `@shivaedev/changes/keys.ts`.
468
465
 
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.
466
+ The same query policy guards running the query and subscribing to it. A subscription names query references, never arbitrary record or bucket keys. The entire subscription is refused with `QueryReadDenied` if any referenced query is denied. Without `readPolicy`, a query has no authorized live subscription: `MissingReadPolicy` tells its author to declare one. Bound query atoms also refuse a missing policy before their first read. Ordinary one-off `run` calls remain available. Reads outside a declared query acquire no automatic subscription.
470
467
 
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
- ```
468
+ The session middleware supplies the viewer and runs `authorizeReads(effect, request, viewer)`. Custom middleware calls that function after resolving its session and declares `MissingReadPolicy` and `QueryReadDenied` in its error schema. Before delivering changed keys, the stream rechecks the owning query's policy without memoization. A client refresh runs the same authorized query that produced its first value; hints carry no row values.
480
469
 
481
470
  ### Live hints in stories
482
471
 
483
- Use `watchHints` and `heard` from `@shivaedev/contract/testing.ts` in an application's story kit. Pass each viewer's authenticated live stream to `watchHints`; it starts a scoped subscription and waits for its first hint before returning the viewer handle. The contract's viewer story kit exposes these as `listen(viewer, queries)` and `heard(viewer, keys)`.
472
+ Use `watchHints` and `heard` from `@shivaedev/contract/testing.ts` in an application's story kit. Pass each viewer's authenticated live stream to `watchHints`; it starts a scoped subscription and waits for its first hint before returning the viewer handle.
484
473
 
485
474
  ```ts
486
475
  const alice = yield* watchHints(aliceChanges);
@@ -496,30 +485,31 @@ expect(yield* heard(bob, [notes.item(1)], { within: "50 millis" })).toBe(false);
496
485
 
497
486
  Import the module that defines the name; the package has no root entry.
498
487
 
499
- | Module | Functions and schemas |
500
- | --- | --- |
501
- | `operation.ts` | `query(name, { payload?, success?, rejections?, reads })`; `command(name, { payload?, success?, rejections?, invalidates })`; `stream(name, { payload?, success, rejections?, reads })`. |
502
- | `contract.ts` | `contract(name, { queries?, commands?, streams?, keyScope? })`, returning a native RPC group with `declaration: { name, queries, commands, streams, keyScope }`. |
503
- | `rejection.ts` | `fieldRejection(struct, fields?)`; lower-level `rejectionSet(specs)`, which constructs the rejection classes, union schema and failure helpers used by operations. |
504
- | `keys.ts` | `collection(name, idSchema)`; `Key` schema; `readKeys(keys, context?)` and `invalidationKeys(keys, context?)`. |
505
- | `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. |
506
- | `live.ts` | `LiveHint` schema; `live(runtime, { stream, resyncKeys, coalesceWindow?, keyContext?, resume?, retryDelay? })`, exposing `connection`, `coalesceWindow`, `paused` and `status`. |
507
- | `makeLiveServer.ts` | `makeLiveServer`, an Effect requiring `Reactivity` that creates scoped server hint streams and a `resync` Effect. |
508
- | `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. |
509
- | `serverMetadata.ts` | `KeyScope`, `QueryReads`, `CommandInvalidation`, `ChangesReads`, `queryKeys(queries, references)` and `changesRpc(name, queries)` for server adapters. |
510
- | `resume.ts` | `resumeSignal({ window?, native? })`, returning a native numeric signal atom. |
511
- | `testing.ts` | `watchHints(stream)` starts a scoped viewer subscription; `heard(viewer, keys, { within? }?)` waits for matching changed keys. |
488
+ | Module | Functions and schemas |
489
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
490
+ | `operation.ts` | `query(name, { payload?, success?, rejections?, readPolicy?, reads })`; `command(name, { payload?, success?, rejections?, invalidates })`; `stream(name, { payload?, success, rejections?, reads })`. |
491
+ | `contract.ts` | `contract(name, { queries?, commands?, streams?, keyScope? })`, returning a native RPC group with `declaration: { name, queries, commands, streams, keyScope }`. |
492
+ | `rejection.ts` | `fieldRejection(struct, fields?)`; lower-level `rejectionSet(specs)`, which constructs the rejection classes, union schema and failure helpers used by operations. |
493
+ | `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; `makeClient(contract, { url, transformClient?, httpClient?, serialization?, group?, registry? })`, which builds the HTTP client and binds it. |
494
+ | `queryCoalesceWindow.ts` | `queryCoalesceWindow(query)`, a writable atom for a bound query's live changed-hint window. |
495
+ | `live.ts` | `LiveHint` schema; `live(runtime, { stream, resyncKeys, coalesceWindow?, keyContext?, resume?, retryDelay? })`, exposing `connection`, `coalesceWindow`, `paused` and `status`. |
496
+ | `querySubscription.ts` | `querySubscription(requests, viewer)` checks each referenced query and captures its scopes and ongoing authorization. |
497
+ | `makeLiveServer.ts` | `makeLiveServer`, an Effect requiring `Reactivity` that creates scoped server hint streams and a `resync` Effect. |
498
+ | `readPolicy.ts` | `QueryReadPolicy`, `Viewer`, `authorizeReads(effect, request, viewer)`, `CurrentViewer`. |
499
+ | `readPolicyErrors.ts` | `MissingReadPolicy` and `QueryReadDenied`. |
500
+ | `serverMetadata.ts` | `KeyScope`, `QueryDefinition.definition`, `CommandInvalidation.invalidates`, `ChangesQueries.resolve(references)`, the Effect `queryKeys(queries, references)` and `changesRpc(name, queries)` for server adapters. |
501
+ | `resume.ts` | `resumeSignal({ window?, native? })`, returning a native numeric signal atom. |
502
+ | `testing.ts` | `watchHints(stream)` starts a scoped viewer subscription; `heard(viewer, keys, { within? }?)` waits for matching changed keys. |
512
503
 
513
504
  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.
514
505
 
515
506
  ### Install, setup and limits
516
507
 
517
508
  ```sh
518
- pnpm add @shivaedev/contract effect@4.0.5
509
+ pnpm add @shivaedev/contract effect
519
510
  ```
520
511
 
521
512
  `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.
522
-
523
513
  - 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`.
524
514
  - Run command Effects with the Reactivity instance that owns the queries to refresh, as in the registry example.
525
515
  - Choose a common read key for every query in a full reconciliation scope. A list key does not implicitly cover item queries.