@shivaedev/contract 0.19.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.19.0 - 2026-10-28
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -15,8 +15,8 @@ import { command, query } from "@shivaedev/contract/operation.ts";
15
15
  const notes = collection("notes", Schema.Number);
16
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
  ```
@@ -35,28 +35,6 @@ A **collection key** names a dependency. Its item key identifies one item; its l
35
35
 
36
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
37
 
38
- ### Build an HTTP client
39
-
40
- Use `makeClient` once per session to bind a contract to an HTTP endpoint:
41
-
42
- ```ts
43
- import { HttpClient, HttpClientRequest } from "effect/http";
44
- import { makeClient } from "@shivaedev/contract/bind.ts";
45
- import { Notes } from "./notes.ts";
46
-
47
- const { api, runtime } = makeClient(Notes, {
48
- url: "/rpc",
49
- transformClient: (client) => HttpClient.mapRequest(
50
- client,
51
- HttpClientRequest.setHeader("authorization", "Bearer session-token"),
52
- ),
53
- });
54
- const note = api.get.query({ id: 1 });
55
- const rename = runtime.fn(api.rename.run);
56
- ```
57
-
58
- 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.
59
-
60
38
  ### 1. Once per feature: schemas and operations
61
39
 
62
40
  Keep the shared declaration in a module that both server and client can import:
@@ -69,37 +47,37 @@ import { command, query } from "@shivaedev/contract/operation.ts";
69
47
  import { fieldRejection } from "@shivaedev/contract/rejection.ts";
70
48
 
71
49
  export class Note extends Schema.Class<Note>("Note")({
72
- body: Schema.String,
73
- id: Schema.Number,
74
- title: Schema.String,
50
+ body: Schema.String,
51
+ id: Schema.Number,
52
+ title: Schema.String,
75
53
  }) {}
76
54
  export const Draft = Schema.Struct({ body: Schema.String, title: Schema.String });
77
55
  export class NoteMissing extends Schema.TaggedError<NoteMissing>()("NoteMissing", { id: Schema.Number }) {}
78
56
  export const notes = collection("notes", Note.fields.id);
79
57
 
80
58
  export const Get = query("get", {
81
- readPolicy: () => Effect.succeed(true),
82
- payload: { id: Schema.Number },
83
- success: Note,
84
- rejections: { NoteMissing },
85
- 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)],
86
64
  });
87
65
  export const List = query("list", {
88
- readPolicy: () => Effect.succeed(true),
89
- success: Schema.Array(Note),
90
- reads: () => [notes.list],
66
+ readPolicy: () => Effect.succeed(true),
67
+ success: Schema.Array(Note),
68
+ reads: () => [notes.list],
91
69
  });
92
70
  export const Rename = command("rename", {
93
- payload: { id: Schema.Number, title: Schema.String },
94
- success: Note,
95
- rejections: { NoteMissing, Invalid: fieldRejection(Draft, ["title"]) },
96
- 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)],
97
75
  });
98
76
  export const Create = command("create", {
99
- payload: Draft,
100
- success: Note,
101
- rejections: { Invalid: fieldRejection(Draft) },
102
- 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)],
103
81
  });
104
82
  export const Notes = contract("notes", { queries: [Get, List], commands: [Rename, Create] });
105
83
  ```
@@ -120,9 +98,9 @@ import { Rpc } from "effect/rpc";
120
98
  import { BadRequest, Conflict } from "@shivaedev/errors/taxonomy.ts";
121
99
 
122
100
  export const Rename = Rpc.make("Rename", {
123
- error: Schema.Union([BadRequest, Conflict]),
124
- payload: { name: Schema.String },
125
- success: Schema.String,
101
+ error: Schema.Union([BadRequest, Conflict]),
102
+ payload: { name: Schema.String },
103
+ success: Schema.String,
126
104
  });
127
105
  ```
128
106
 
@@ -148,28 +126,35 @@ Implement the contract with native `of` and `toLayer`:
148
126
  import { Effect, Ref, Stream } from "effect";
149
127
  import { Create, Get, Note, Notes, Rename } from "./notes.ts";
150
128
 
151
- export const NotesHandlers = Notes.toLayer(Effect.gen(function* () {
152
- const stored = yield* Ref.make(new Map([
153
- [1, new Note({ body: "", id: 1, title: "One" })],
154
- [2, new Note({ body: "", id: 2, title: "Two" })],
155
- ]));
156
- const find = (id: number) => Effect.flatMap(Ref.get(stored), (all) => {
157
- const note = all.get(id);
158
- return note === undefined ? Get.reject.NoteMissing({ id }) : Effect.succeed(note);
159
- });
160
- const save = (note: Note) => Ref.update(stored, (all) => new Map([...all, [note.id, note]])).pipe(Effect.as(note));
161
- return Notes.of({
162
- "notes.$changes": () => Stream.never,
163
- "notes.get": ({ id }) => find(id),
164
- "notes.list": () => Effect.map(Ref.get(stored), (all) => [...all.values()]),
165
- "notes.rename": ({ id, title }) => title.trim() === ""
166
- ? Rename.reject.Invalid({ field: "title", message: "Enter a title" })
167
- : Effect.flatMap(find(id), (note) => save(new Note({ ...note, title }))),
168
- "notes.create": (draft) => draft.title === ""
169
- ? Create.reject.Invalid({ field: "title", message: "Enter a title" })
170
- : Effect.flatMap(Ref.get(stored), (all) => save(new Note({ id: all.size + 1, ...draft }))),
171
- });
172
- }));
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
+ );
173
158
  ```
174
159
 
175
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.
@@ -198,7 +183,7 @@ import { Notes } from "./notes.ts";
198
183
  import { NotesHandlers } from "./handlers.ts";
199
184
 
200
185
  export const NotesRoutes = RpcServer.layerHttp({ group: Notes, path: "/rpc", protocol: "http" }).pipe(
201
- Layer.provide([NotesHandlers, RpcSerialization.layerJson]),
186
+ Layer.provide([NotesHandlers, RpcSerialization.layerJson]),
202
187
  );
203
188
  ```
204
189
 
@@ -213,16 +198,35 @@ import { bind } from "@shivaedev/contract/bind.ts";
213
198
  import { Notes } from "./notes.ts";
214
199
 
215
200
  export class NotesClient extends AtomRpc.Service<NotesClient>()("app/NotesClient", {
216
- group: Notes,
217
- protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
218
- Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]),
219
- ),
201
+ group: Notes,
202
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
203
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]),
204
+ ),
220
205
  }) {}
221
206
  export const api = bind(Notes, NotesClient);
222
207
  ```
223
208
 
224
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.
225
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
+
226
230
  ### 3. Every day: read and change
227
231
 
228
232
  ```ts
@@ -232,8 +236,6 @@ const readOnce = api.get.run({ id: 1 });
232
236
  const rename = api.rename.run({ id: 1, title: "Uno" });
233
237
  ```
234
238
 
235
- `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" })`.
236
-
237
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.
238
240
 
239
241
  Mount the atoms and run commands through the service's atom runtime in the same registry:
@@ -249,9 +251,7 @@ const list = api.list.query();
249
251
  const releaseOne = registry.mount(one);
250
252
  const releaseList = registry.mount(list);
251
253
 
252
- const renamed = await Effect.runPromise(
253
- AtomRegistry.getResult(registry, NotesClient.runtime.atom(api.rename.run({ id: 1, title: "Uno" }))),
254
- );
254
+ const renamed = await Effect.runPromise(AtomRegistry.getResult(registry, NotesClient.runtime.atom(api.rename.run({ id: 1, title: "Uno" }))));
255
255
 
256
256
  releaseOne();
257
257
  releaseList();
@@ -262,54 +262,36 @@ The successful rename invalidates note 1 and the list. A rejected rename invalid
262
262
 
263
263
  The dependency rule is deliberately asymmetric:
264
264
 
265
- | Declared key | Query reads | Successful command invalidates |
266
- | --- | --- | --- |
267
- | `notes.item(1)` | `"notes:1"` | `"notes:1"` and `"notes"` |
268
- | `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"` |
269
269
 
270
- `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] }`.
271
271
 
272
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).
273
273
 
274
274
  ### Session-owned keys
275
275
 
276
- Before migration, an owner field exists only to give client reads and invalidations a key. The server already chooses the account from `Identity`:
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:
277
277
 
278
278
  ```ts
279
279
  const preferences = collection("preferences", Schema.String);
280
280
  const Preferences = contract("preferences", {
281
- queries: [query("self", {
282
- payload: { ownerId: Schema.String },
283
- reads: ({ ownerId }) => [preferences.item(ownerId)],
284
- success: Schema.String,
285
- })],
286
- commands: [command("save", {
287
- payload: { ownerId: Schema.String, title: Schema.String },
288
- invalidates: ({ ownerId }) => [preferences.item(ownerId)],
289
- success: Schema.String,
290
- })],
291
- }).middleware(Authenticated);
292
-
293
- const api = bind(Preferences, Client);
294
- const self = api.self.query({ ownerId: session.userId });
295
- const save = api.save.run({ ownerId: session.userId, title: "My preferences" });
296
- ```
297
-
298
- 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:
299
-
300
- ```ts
301
- const preferences = collection("preferences", Schema.String);
302
- const Preferences = contract("preferences", {
303
- keyScope: "session",
304
- queries: [query("self", {
305
- reads: () => [preferences.item("self")],
306
- success: Schema.String,
307
- })],
308
- commands: [command("save", {
309
- payload: { title: Schema.String },
310
- invalidates: () => [preferences.item("self")],
311
- success: Schema.String,
312
- })],
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
+ ],
313
295
  }).middleware(Authenticated);
314
296
 
315
297
  const api = bind(Preferences, Client, { registry });
@@ -317,30 +299,14 @@ const self = api.self.query(undefined);
317
299
  const save = api.save.run({ title: "My preferences" });
318
300
  ```
319
301
 
320
- 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. In React, obtain `registry` from the second argument to `SessionBoundary.connect`:
321
-
322
- ```tsx
323
- import { SessionBoundary } from "@shivaedev/react/client/sessionBoundary.ts";
324
-
325
- <SessionBoundary
326
- session={session}
327
- identify={(current) => current.generation}
328
- connect={(current, registry) => bind(Preferences, makePreferencesClient(current.token), { registry })}
329
- recheck={recheck}
330
- signedOut={<p>Signed out</p>}
331
- >
332
- {(api) => <PreferencesScreen api={api} />}
333
- </SessionBoundary>
334
- ```
335
-
336
- `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.
337
303
 
338
- 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`:
339
305
 
340
306
  ```ts
341
307
  Preferences.application.toLayer({
342
- "preferences.self": () => Effect.flatMap(Identity, ({ id }) => store.read(id)),
343
- "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)),
344
310
  });
345
311
  ```
346
312
 
@@ -364,9 +330,12 @@ import type { ResumeSource } from "@shivaedev/contract/resume.ts";
364
330
  import { onAppResume, onNetworkReturn } from "./native-lifecycle.ts";
365
331
 
366
332
  const resume: ResumeSource = (notify) => {
367
- const stopApp = onAppResume(notify);
368
- const stopNetwork = onNetworkReturn(notify);
369
- return () => { stopApp(); stopNetwork(); };
333
+ const stopApp = onAppResume(notify);
334
+ const stopNetwork = onNetworkReturn(notify);
335
+ return () => {
336
+ stopApp();
337
+ stopNetwork();
338
+ };
370
339
  };
371
340
  const api = bind(Notes, NotesClient, { resume });
372
341
  ```
@@ -379,14 +348,14 @@ For a direct subscription, configure the server and `NotesClient` with `RpcSeria
379
348
  import { Effect, Stream } from "effect";
380
349
  import { NotesClient } from "./client.ts";
381
350
 
382
- const changes = Stream.unwrap(NotesClient.use((client) =>
383
- Effect.succeed(client("notes.$changes", { queries: [{ query: "get", payload: { id: 1 } }] })),
384
- ));
351
+ const changes = Stream.unwrap(
352
+ NotesClient.use((client) => Effect.succeed(client("notes.$changes", { queries: [{ query: "get", payload: { id: 1 } }] }))),
353
+ );
385
354
  ```
386
355
 
387
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.
388
357
 
389
- 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.
390
359
 
391
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.
392
361
 
@@ -419,18 +388,18 @@ import { LiveNotes } from "./live-notes.ts";
419
388
  import { notes } from "./notes.ts";
420
389
 
421
390
  class Client extends AtomRpc.Service<Client>()("app/LiveNotes", {
422
- group: LiveNotes,
423
- protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
424
- Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]),
425
- ),
391
+ group: LiveNotes,
392
+ protocol: RpcClient.layerProtocolHttp({ url: "http://127.0.0.1:3000/rpc" }).pipe(
393
+ Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]),
394
+ ),
426
395
  }) {}
427
396
  const api = bind(LiveNotes, Client);
428
397
  const registry = AtomRegistry.make();
429
398
  const updates = live(Client.runtime, {
430
- stream: api.subscribe.run(undefined),
431
- resyncKeys: [notes.item(1), notes.item(2), notes.list],
432
- resume: resumeSignal({ window }),
433
- 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",
434
403
  });
435
404
  const releaseQuery = registry.mount(api.get.query({ id: 1 }));
436
405
  const releaseConnection = registry.mount(updates.connection);
@@ -438,14 +407,16 @@ const releaseConnection = registry.mount(updates.connection);
438
407
 
439
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.
440
409
 
441
- | Observation | Invalidation |
442
- | --- | --- |
443
- | `Changed` with keys after the first hint | Affected items and their lists. |
444
- | `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. |
445
414
  | First hint on a new or reconnected stream | The full explicit scope, even if the hint is targeted. |
446
415
 
447
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.
448
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
+
449
420
  `LiveHint` and `Key` are schemas. Decoding rejects unknown hint tags and item keys with an invalid ID type.
450
421
 
451
422
  ```ts
@@ -483,12 +454,10 @@ import { query } from "@shivaedev/contract/operation.ts";
483
454
 
484
455
  const notes = collection("notes", Schema.Number, { userId: Schema.String });
485
456
  const Mine = query("mine", {
486
- payload: { title: Schema.String },
487
- success: Schema.Array(Schema.String),
488
- readPolicy: (_input, viewer) => Effect.succeed(Option.isSome(viewer)),
489
- reads: (_input, viewer) => Option.isSome(viewer)
490
- ? [notes.bucket.userId(viewer.value.id)]
491
- : [],
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)] : []),
492
461
  });
493
462
  ```
494
463
 
@@ -500,7 +469,7 @@ The session middleware supplies the viewer and runs `authorizeReads(effect, requ
500
469
 
501
470
  ### Live hints in stories
502
471
 
503
- 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.
504
473
 
505
474
  ```ts
506
475
  const alice = yield* watchHints(aliceChanges);
@@ -516,31 +485,31 @@ expect(yield* heard(bob, [notes.item(1)], { within: "50 millis" })).toBe(false);
516
485
 
517
486
  Import the module that defines the name; the package has no root entry.
518
487
 
519
- | Module | Functions and schemas |
520
- | --- | --- |
521
- | `operation.ts` | `query(name, { payload?, success?, rejections?, readPolicy?, reads })`; `command(name, { payload?, success?, rejections?, invalidates })`; `stream(name, { payload?, success, rejections?, reads })`. |
522
- | `contract.ts` | `contract(name, { queries?, commands?, streams?, keyScope? })`, returning a native RPC group with `declaration: { name, queries, commands, streams, keyScope }`. |
523
- | `rejection.ts` | `fieldRejection(struct, fields?)`; lower-level `rejectionSet(specs)`, which constructs the rejection classes, union schema and failure helpers used by operations. |
524
- | `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. |
525
- | `live.ts` | `LiveHint` schema; `live(runtime, { stream, resyncKeys, coalesceWindow?, keyContext?, resume?, retryDelay? })`, exposing `connection`, `coalesceWindow`, `paused` and `status`. |
526
- | `querySubscription.ts` | `querySubscription(requests, viewer)` checks each referenced query and captures its scopes and ongoing authorization. |
527
- | `makeLiveServer.ts` | `makeLiveServer`, an Effect requiring `Reactivity` that creates scoped server hint streams and a `resync` Effect. |
528
- | `readPolicy.ts` | `QueryReadPolicy`, `Viewer`, `authorizeReads(effect, request, viewer)`, `CurrentViewer`. |
529
- | `readPolicyErrors.ts` | `MissingReadPolicy` and `QueryReadDenied`. |
530
- | `serverMetadata.ts` | `KeyScope`, `QueryDefinition.definition`, `CommandInvalidation.invalidates`, `ChangesQueries.resolve(references)`, the Effect `queryKeys(queries, references)` and `changesRpc(name, queries)` for server adapters. |
531
- | `resume.ts` | `resumeSignal({ window?, native? })`, returning a native numeric signal atom. |
532
- | `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. |
533
503
 
534
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.
535
505
 
536
506
  ### Install, setup and limits
537
507
 
538
508
  ```sh
539
- pnpm add @shivaedev/contract effect@4.0.5
509
+ pnpm add @shivaedev/contract effect
540
510
  ```
541
511
 
542
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.
543
-
544
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`.
545
514
  - Run command Effects with the Reactivity instance that owns the queries to refresh, as in the registry example.
546
515
  - Choose a common read key for every query in a full reconciliation scope. A list key does not implicitly cover item queries.
@@ -1 +1 @@
1
- {"version":3,"file":"clientOperations.d.ts","sourceRoot":"","sources":["../../src/internal/clientOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE7D,OAAO,KAAK,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAE1D,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD,KAAK,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAKhH,KAAK,qBAAqB,GAAG,OAAO,CAAC,aAAa,CACjD,OAAO,EACP,MAAM,EACN,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CACnF,CAAC;AAMF,wBAAgB,UAAU,CACzB,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,OAAO,CAAC,EAChD,KAAK,CAAC,EAAE,YAAY;IAInB,KAAK,YAAY,OAAO,YAAW,YAAY;IA4B/C,GAAG,YAAY,OAAO;EAOvB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,YAAY;IAS1G,GAAG,YAAY,OAAO;EAGvB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM;IAErE,GAAG,YAAY,OAAO;EAEvB"}
1
+ {"version":3,"file":"clientOperations.d.ts","sourceRoot":"","sources":["../../src/internal/clientOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA0B,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE7E,OAAO,KAAK,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAE1D,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD,KAAK,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAMhH,KAAK,qBAAqB,GAAG,OAAO,CAAC,aAAa,CACjD,OAAO,EACP,MAAM,EACN,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CACnF,CAAC;AAMF,wBAAgB,UAAU,CACzB,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,OAAO,CAAC,EAChD,KAAK,CAAC,EAAE,YAAY;IAInB,KAAK,YAAY,OAAO,YAAW,YAAY;IA6B/C,GAAG,YAAY,OAAO;EAOvB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,YAAY;IAS1G,GAAG,YAAY,OAAO;EAGvB;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM;IAErE,GAAG,YAAY,OAAO;EAEvB"}
@@ -1,10 +1,11 @@
1
- import { Effect, Option, Stream } from "effect";
1
+ import { Effect, MutableHashMap, Option, Stream } from "effect";
2
2
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
  import * as Reactivity from "effect/reactivity/Reactivity";
4
4
  import { invalidationKeys, readKeys } from "@shivaedev/changes/keys.ts";
5
5
  import { MissingReadPolicy } from "#readPolicyErrors.ts";
6
6
  import { queryReactivity } from "./queryReactivity.js";
7
- const queryIds = new WeakMap();
7
+ // AtomRpc holds query atoms weakly, so an id per atom object would change after a collection and make live tracking resubscribe.
8
+ const queryIds = MutableHashMap.empty();
8
9
  let nextQueryId = 0;
9
10
  function run(service, effect, owner) {
10
11
  return owner === undefined ? effect : Effect.suspend(() => AtomRegistry.getResult(owner.registry, service.runtime.atom(effect)));
@@ -17,11 +18,12 @@ export function boundQuery(service, tag, query, track, owner) {
17
18
  return service.runtime.atom(Effect.fail(new MissingReadPolicy({ message: `Declare a readPolicy on query ${query.name} before running it live or subscribing` })));
18
19
  }
19
20
  const atom = service.query(tag, payload, options);
20
- let id = queryIds.get(atom);
21
+ const identity = { payload, tag };
22
+ let id = Option.getOrUndefined(MutableHashMap.get(queryIds, identity));
21
23
  if (id === undefined) {
22
24
  id = nextQueryId;
23
25
  nextQueryId += 1;
24
- queryIds.set(atom, id);
26
+ MutableHashMap.set(queryIds, identity, id);
25
27
  }
26
28
  const local = query.reads(payload, Option.none());
27
29
  const keys = [...local, { _tag: "List", collection: `@query/${tag}/${id}` }];
@@ -1 +1 @@
1
- {"version":3,"file":"clientOperations.js","sourceRoot":"","sources":["../../src/internal/clientOperations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"clientOperations.js","sourceRoot":"","sources":["../../src/internal/clientOperations.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/contract",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Typed query and command contracts over native Effect RPC and reactivity",
5
5
  "keywords": [
6
6
  "effect"
@@ -50,7 +50,7 @@
50
50
  "devDependencies": {
51
51
  "@effect/platform-node": "4.0.5",
52
52
  "@effect/vitest": "4.0.5",
53
- "@shivaedev/quality": "0.43.0",
53
+ "@shivaedev/quality": "0.44.0",
54
54
  "@shivaedev/test": "0.10.0",
55
55
  "@shivaedev/test-sql": "0.10.2",
56
56
  "@shivaedev/test-story": "0.22.0",
@@ -1,4 +1,4 @@
1
- import { Effect, Option, type Schema, Stream } from "effect";
1
+ import { Effect, MutableHashMap, Option, type Schema, Stream } from "effect";
2
2
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
  import type * as AtomRpc from "effect/reactivity/AtomRpc";
4
4
  import * as Reactivity from "effect/reactivity/Reactivity";
@@ -13,7 +13,8 @@ import { queryReactivity } from "./queryReactivity.ts";
13
13
 
14
14
  type ErasedClient = AtomRpc.AtomRpcClient<unknown, string, Rpc.Rpc<string, Schema.Top, Schema.Top, Schema.Top>>;
15
15
 
16
- const queryIds = new WeakMap<object, number>();
16
+ // AtomRpc holds query atoms weakly, so an id per atom object would change after a collection and make live tracking resubscribe.
17
+ const queryIds = MutableHashMap.empty<{ readonly payload: unknown; readonly tag: string }, number>();
17
18
  let nextQueryId = 0;
18
19
 
19
20
  type ErasedStreamingClient = AtomRpc.AtomRpcClient<
@@ -42,11 +43,12 @@ export function boundQuery(
42
43
  );
43
44
  }
44
45
  const atom = service.query(tag, payload, options);
45
- let id = queryIds.get(atom);
46
+ const identity = { payload, tag };
47
+ let id = Option.getOrUndefined(MutableHashMap.get(queryIds, identity));
46
48
  if (id === undefined) {
47
49
  id = nextQueryId;
48
50
  nextQueryId += 1;
49
- queryIds.set(atom, id);
51
+ MutableHashMap.set(queryIds, identity, id);
50
52
  }
51
53
  const local = query.reads(payload, Option.none());
52
54
  const keys = [...local, { _tag: "List" as const, collection: `@query/${tag}/${id}` }];