@procedures/client 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +63 -7
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +5 -0
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +10 -4
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/server/ndjson.d.ts +73 -0
  9. package/dist/adapters/http/server/ndjson.d.ts.map +1 -0
  10. package/dist/adapters/http/server/ndjson.js +228 -0
  11. package/dist/adapters/http/server/ndjson.js.map +1 -0
  12. package/dist/adapters/http/server/transport.d.ts +15 -4
  13. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  14. package/dist/adapters/http/server/transport.js +29 -69
  15. package/dist/adapters/http/server/transport.js.map +1 -1
  16. package/dist/adapters/http/server/types.d.ts +15 -0
  17. package/dist/adapters/http/server/types.d.ts.map +1 -1
  18. package/dist/adapters/sequence.d.ts +14 -0
  19. package/dist/adapters/sequence.d.ts.map +1 -1
  20. package/dist/adapters/sequence.js +16 -0
  21. package/dist/adapters/sequence.js.map +1 -1
  22. package/dist/adapters/websocket/client/transport.d.ts +7 -3
  23. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  24. package/dist/adapters/websocket/client/transport.js +9 -3
  25. package/dist/adapters/websocket/client/transport.js.map +1 -1
  26. package/dist/adapters/websocket/server/transport.d.ts +10 -0
  27. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  28. package/dist/adapters/websocket/server/transport.js +22 -2
  29. package/dist/adapters/websocket/server/transport.js.map +1 -1
  30. package/dist/browser.d.ts +5 -5
  31. package/dist/browser.d.ts.map +1 -1
  32. package/dist/browser.js +4 -3
  33. package/dist/browser.js.map +1 -1
  34. package/dist/client/errors/registry.d.ts.map +1 -1
  35. package/dist/client/errors/registry.js +1 -0
  36. package/dist/client/errors/registry.js.map +1 -1
  37. package/dist/client/middleware/retry.d.ts +4 -0
  38. package/dist/client/middleware/retry.d.ts.map +1 -1
  39. package/dist/client/middleware/retry.js +86 -78
  40. package/dist/client/middleware/retry.js.map +1 -1
  41. package/dist/events/bus.d.ts +32 -14
  42. package/dist/events/bus.d.ts.map +1 -1
  43. package/dist/events/bus.js +119 -65
  44. package/dist/events/bus.js.map +1 -1
  45. package/dist/events/index.d.ts +3 -2
  46. package/dist/events/index.d.ts.map +1 -1
  47. package/dist/events/index.js.map +1 -1
  48. package/dist/events/publisher.d.ts.map +1 -1
  49. package/dist/events/publisher.js +20 -6
  50. package/dist/events/publisher.js.map +1 -1
  51. package/dist/events/remote.d.ts +80 -18
  52. package/dist/events/remote.d.ts.map +1 -1
  53. package/dist/events/remote.js +448 -58
  54. package/dist/events/remote.js.map +1 -1
  55. package/dist/events/types.d.ts +13 -2
  56. package/dist/events/types.d.ts.map +1 -1
  57. package/dist/node.d.ts +6 -0
  58. package/dist/node.d.ts.map +1 -1
  59. package/dist/node.js +5 -0
  60. package/dist/node.js.map +1 -1
  61. package/dist/procedures/caller.d.ts +33 -0
  62. package/dist/procedures/caller.d.ts.map +1 -0
  63. package/dist/procedures/caller.js +27 -0
  64. package/dist/procedures/caller.js.map +1 -0
  65. package/dist/procedures/collection/index.d.ts +2 -2
  66. package/dist/procedures/collection/index.d.ts.map +1 -1
  67. package/dist/procedures/collection/index.js +1 -1
  68. package/dist/procedures/collection/index.js.map +1 -1
  69. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  70. package/dist/procedures/collection/procedures.js +21 -1
  71. package/dist/procedures/collection/procedures.js.map +1 -1
  72. package/dist/procedures/collection/schemas.d.ts +9 -0
  73. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  74. package/dist/procedures/collection/schemas.js +28 -0
  75. package/dist/procedures/collection/schemas.js.map +1 -1
  76. package/dist/procedures/collection/storage.d.ts +6 -0
  77. package/dist/procedures/collection/storage.d.ts.map +1 -1
  78. package/dist/procedures/index.d.ts +3 -1
  79. package/dist/procedures/index.d.ts.map +1 -1
  80. package/dist/procedures/index.js +2 -0
  81. package/dist/procedures/index.js.map +1 -1
  82. package/dist/procedures/invoke.d.ts.map +1 -1
  83. package/dist/procedures/invoke.js +3 -0
  84. package/dist/procedures/invoke.js.map +1 -1
  85. package/dist/procedures/types.d.ts +50 -0
  86. package/dist/procedures/types.d.ts.map +1 -1
  87. package/dist/procedures/types.js.map +1 -1
  88. package/dist/protocol.d.ts +1 -1
  89. package/dist/protocol.d.ts.map +1 -1
  90. package/dist/protocol.js +1 -0
  91. package/dist/protocol.js.map +1 -1
  92. package/dist/server/procedure-server.d.ts +41 -5
  93. package/dist/server/procedure-server.d.ts.map +1 -1
  94. package/dist/server/procedure-server.js +94 -9
  95. package/dist/server/procedure-server.js.map +1 -1
  96. package/dist/timers.d.ts +9 -0
  97. package/dist/timers.d.ts.map +1 -1
  98. package/dist/timers.js +6 -1
  99. package/dist/timers.js.map +1 -1
  100. package/package.json +1 -1
  101. package/src/adapters/http/client/transport.ts +10 -4
  102. package/src/adapters/http/server/ndjson.ts +259 -0
  103. package/src/adapters/http/server/transport.ts +31 -69
  104. package/src/adapters/http/server/types.ts +17 -0
  105. package/src/adapters/sequence.ts +18 -0
  106. package/src/adapters/websocket/client/transport.ts +9 -3
  107. package/src/adapters/websocket/server/transport.ts +23 -2
  108. package/src/browser.ts +9 -2
  109. package/src/client/errors/registry.ts +1 -0
  110. package/src/client/middleware/retry.ts +89 -78
  111. package/src/events/bus.ts +141 -73
  112. package/src/events/index.ts +3 -0
  113. package/src/events/publisher.ts +23 -6
  114. package/src/events/remote.ts +575 -73
  115. package/src/events/types.ts +15 -2
  116. package/src/node.ts +6 -0
  117. package/src/procedures/caller.ts +45 -0
  118. package/src/procedures/collection/index.ts +3 -0
  119. package/src/procedures/collection/procedures.ts +22 -0
  120. package/src/procedures/collection/schemas.ts +37 -0
  121. package/src/procedures/collection/storage.ts +6 -0
  122. package/src/procedures/index.ts +4 -0
  123. package/src/procedures/invoke.ts +2 -0
  124. package/src/procedures/types.ts +53 -0
  125. package/src/protocol.ts +3 -1
  126. package/src/server/procedure-server.ts +111 -11
  127. package/src/timers.ts +15 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # @procedures/client
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4b09a03: `debounceChannel()` and `throttleChannel()` take a `Duration`, as every other time of the client does since 0.3.0. A plain number of milliseconds throws a `TypeError`. A new option `clock` gives the clock of their timers, for example a fake clock in a test. MINOR: a breaking change of a signature (the API is before 1.0.0).
8
+
9
+ Migration: `debounceChannel(bus, "in", "out", 300)` becomes `debounceChannel(bus, "in", "out", ms(300))`.
10
+ - 698e24d: Three gaps of the remote events (`documentation/EVENTS.md`) are closed. The option `backoff` of a subscription changes, so this release is breaking for a caller that gives it.
11
+
12
+ - **Shared subscriptions.** The subscribers of the same topics with the same options on one transport share one call: one `request` on the wire, with a count of its subscribers. Only the last unsubscribe sends the `cancel`. Before, each `subscribeRemote()`, `remoteEvents()` or `transport.subscribe()` made its own call. Each subscriber keeps its own id, its own `ready` and its own queue of at most 1024 events. A late subscriber gets no replay: its `ready` resolves at once, and its `seq` starts at 1. An error or an end of the shared call goes to each subscriber. `WebSocketServerTransport` shares the subscriptions of one connection.
13
+ - **The end at `clear()`.** `clear()` and `clearAll()` of the bus of the publisher end each subscription of a cleared topic. The subscriber first gets the events of the queue, then the error `TOPIC_CLEARED` (new protocol code, HTTP 410, not retryable) through `onError`, `ready` or the iterator. Before, the listeners went, and the subscriber saw no end. The new option `onClear` of `EventBus.on(channel, handler, { onClear })` (type `EventListenOptions`) gives the same notice to a local handler.
14
+ - **The limit of the resubscribe.** The new option `maxAttempts` (default 10) is the number of resubscribe tries without a confirmation. When the tries run out, the subscription ends with a `SubscriptionError` that has the code of the last failure. Before, a subscription over HTTP resubscribed forever. The WebSocket transport uses `reconnect.maxAttempts` (default `Infinity`), so its behavior stays. The new option `clock` is the clock of the backoff, for a fake clock in a test. `SubscriptionHooks` has the new fields `maxAttempts` and `clock`.
15
+
16
+ Migration: the option `backoff` is an object of durations (type `ResubscribeBackoff`), with the names of the `reconnect` options of `WebSocketTransport`: `initialDelay` (default `ms(250)`), `maxDelay` (default `seconds(30)`), `backoffMultiplier` (default 2) and `jitter` (default 1). Before, it was a function of milliseconds. A function or a plain number throws a `TypeError` at the call. For the old behavior over HTTP, give `maxAttempts: Infinity`.
17
+
18
+ ```ts
19
+ // Before
20
+ subscribeRemote(transport, "orders.created", handler, { backoff: (attempt) => Math.min(30_000, 500 * 2 ** (attempt - 1)) });
21
+ // Now
22
+ subscribeRemote(transport, "orders.created", handler, { backoff: { initialDelay: ms(500), maxDelay: seconds(30), jitter: 0 }, maxAttempts: Infinity });
23
+ ```
24
+ - d25805a: `ProcedureServer.useStorage` with `close` waits for the calls on the old storage before it closes it. Before, it closed the old storage at once, and a call that started on it could fail as closed.
25
+
26
+ MINOR: the call of `useStorage` with `close` can now take up to 30 seconds to resolve. That is a change of behaviour that a caller can see. Give `grace: ms(0)` for the old behaviour.
27
+
28
+ - The new calls use the new storage at once. The server waits only for the calls that took the old storage. A stream holds its storage until its end.
29
+ - The new option `grace` of `UseStorageOptions` is the longest wait, a `Duration`. A plain number throws a `TypeError`. The default is 30 seconds, `TIMER_DEFAULTS.server.storageGraceMs`. 0 closes at once.
30
+ - When the time ends, the server closes the old storage. A call that still uses it then fails with the error of a closed storage.
31
+ - The new option `clock` of `ProcedureServerOptions` is the clock of this wait. A test gives a fake clock.
32
+
33
+ ### Patch Changes
34
+
35
+ - 4b09a03: New `callerOf(...modules)` in the root and the browser entry. It gives a function that runs the procedures of the modules and gives one value. It makes the registry of the modules at the first call and does not use the global registry. An app wrote the three steps itself before: `createRegistry`, `invokePath` and `outputValue`. PATCH: a new export (the API is before 1.0.0).
36
+ - aa6e3aa: `ApiStorage` has `getIds()`. A full pull through it does not read each entry for the ids.
37
+
38
+ - The collections have the procedure `collections.<name>.ids`. It gives the ids in the order of the entries, without the values. Its effects read the collection and write nothing. A storage without `getIds()` gives the ids of its entries.
39
+ - The `CollectionStorage` of the core has the optional method `getIds()`, as the `CollectionStorage` of `@procedures/collections` has.
40
+ - `ApiStorage.getIds()` uses the procedure `ids`. An older server without it answers `NOT_FOUND`. Then the storage reads the ids from the entries, and it does not ask for `ids` again.
41
+ - `ApiStorageOptions.operations` takes `ids`, the name of the operation. The default is `ids`.
42
+ - 4b09a03: `bus.once()` rejects with an `AbortError` when `clear()` or `clearAll()` removes its channel. Before, its promise never settled.
43
+ - 600c497: A server on port 0 tells its real port.
44
+
45
+ - New `HttpServerTransport.port()` returns the port that the server listens on. With the option `port` 0, the system gives a free port, and the function returns that port.
46
+ - `peer.getEndpoints()` gives the real port of each HTTP and WebSocket endpoint. Before, a peer on port 0 gave the address `:0`.
47
+ - The tests of the body and the stop of the peer listen on port 0. Before, a helper found a free port and closed it before the peer took it, so one CI run failed with `EADDRINUSE`.
48
+ - 7c2e7f7: The `onAfterRetry` hook of `createRetryMiddleware` hears of a failure when the reader stops at the error item. A `Client` with `throwOnError` (the default) stops there. Before, the hook ran after the yield of that item, so it ran only for a success. Now it runs when the call ends: after the last item, or when the reader stops. A thrown error and an abort still do not call it. PATCH: a bug fix.
49
+ - 90ea741: A procedure can declare its rules under a root of the files in `metadata.fsRoot` (the new type `FsRootRules`): the inputs that name a path, or the reason that a root cannot confine it. A handler gets `ctx.caller`, the procedure whose handler made the nested call. The framework sets it from the invocation, so an input cannot change it (roadmap R33).
50
+ - c4cfa34: `ProcedureServer.useStorage` with `close` keeps an old storage open while another collection still uses it. The server closes it when the last collection leaves it. Before, the close broke the calls of the other collection.
51
+
52
+ ## 0.7.0
53
+
54
+ ### Minor Changes
55
+
56
+ - 841d083: An NDJSON stream over HTTP gets keep-alive lines. Before, a proxy or a client with an idle timeout (for example 300 s) closed a long procedure that was quiet for that time.
57
+
58
+ - `HttpServerTransport` writes a keep-alive line, a blank line, after each quiet time of the new option `keepAlive` (default `seconds(15)`, `TIMER_DEFAULTS.httpServer.keepAliveMs`). Each line starts the time again. No keep-alive line comes after the `done` line, an `error` line, an abort or a closed connection. `keepAlive: false` turns it off.
59
+ - The new option `clock` of `HttpServerTransport` is the clock of the keep-alive timers. A test gives the `fakeClock` of `@mark1russell7/service/testing`.
60
+ - A keep-alive line has no `seq`, so the sequence check of the reader sees no gap. `WIRE-PROTOCOL.md` specifies the line: it is an additive change of protocol v1, because each reader skipped a blank line before.
61
+ - New `writeNdjsonStream(res, items, options?)` in `@procedures/client/node`. It writes the NDJSON stream response of protocol v1 for a `ServerResponse` of `node:http`, without Express. It writes the headers, the `item`, `done` and `error` lines with `seq`, and the keep-alive lines. It waits for `drain` when the buffer is full, and it stops when the connection closes or `signal` aborts. Its options are `signal`, `keepAlive`, `errorOf` and `clock`. `HttpServerTransport` uses it, so the two write the same bytes.
62
+ - `HttpTransport` gives each item of an NDJSON stream the metadata key `stream` with the value `true` (the new constant `STREAM_ITEM_METADATA_KEY`). The error item that ends the stream has it too. The key does not cross the wire, and the client removes a key `stream` that the peer sent.
63
+ - When the client goes away while the handler is quiet, the writer of the stream stops at once. Before, it waited for the next item of a handler that ignores its signal.
64
+
65
+ ### Patch Changes
66
+
67
+ - 389b054: A token check no longer tells the length of the token through its time. The peer compares the SHA-256 digests of the two tokens with `timingSafeEqual`. The `tokenMatches` of the client, which runs in a browser too, compares each character of the expected token for each presented token. Before, a token of another length returned at once. The re-audit of jarvis found it.
68
+
3
69
  ## 0.6.0
4
70
 
5
71
  No changes in this release.
package/README.md CHANGED
@@ -179,6 +179,18 @@ const getUserProcedure = defineProcedure({
179
179
  registerProcedures([getUserProcedure]);
180
180
  ```
181
181
 
182
+ ### Procedures of a package in the same process
183
+
184
+ `callerOf(...modules)` gives a function that runs the procedures of the modules. It makes the registry of the modules at the first call, and it does not use the global registry. Each call gives one value: the output, or the last item of a stream.
185
+
186
+ ```typescript
187
+ import { callerOf } from "@procedures/client";
188
+ import { ghModule } from "@procedures/gh";
189
+
190
+ const gh = callerOf(ghModule);
191
+ const runs = await gh<RunList>(["gh", "run", "list"], { repo: "owner/name" });
192
+ ```
193
+
182
194
  ## API Reference
183
195
 
184
196
  ### Client
@@ -289,7 +301,7 @@ await toClient.call({ service: "ui", operation: "notify" }, { text: "Build done"
289
301
  // function gives the unsubscribe. After a drop, the transport resubscribes on the new connection.
290
302
  const unsubscribe = transport.subscribe("orders.created", (order, event) => console.log(event.seq, order), {
291
303
  onSubscribed: (topics) => console.log("subscribed", topics),
292
- onError: (error) => console.error(error.code), // TOPIC_NOT_FOUND, FORBIDDEN, NOT_FOUND
304
+ onError: (error) => console.error(error.code), // TOPIC_NOT_FOUND, FORBIDDEN, NOT_FOUND, TOPIC_CLEARED
293
305
  });
294
306
  await unsubscribe.ready;
295
307
  unsubscribe();
@@ -531,7 +543,10 @@ const server = new ProcedureServer({
531
543
  server.publish("orders.created", { order: 1 }) // the same as bus.emit()
532
544
 
533
545
  // The subscriber, over each transport
534
- const unsubscribe = subscribeRemote(transport, "orders.created", (order, event) => console.log(order, event.seq))
546
+ const unsubscribe = subscribeRemote(transport, "orders.created", (order, event) => console.log(order, event.seq), {
547
+ maxAttempts: 10, // the resubscribe tries without a confirmation (default 10; WebSocket: reconnect.maxAttempts)
548
+ backoff: { initialDelay: ms(250), maxDelay: seconds(30), backoffMultiplier: 2, jitter: 1 }, // Durations: a plain number throws a TypeError
549
+ })
535
550
  for await (const event of remoteEvents(transport, ["orders.created"], { signal })) console.log(event.data)
536
551
 
537
552
  // The server subscribes to the bus of a client: the client serves it with `serve`
@@ -541,7 +556,9 @@ wsServer.subscribe(connectionId, "ui.clicked", (click) => console.log(click))
541
556
 
542
557
  - The delivery is at most once, in the order of `emit()`. `seq` numbers the events of a subscription, and `dropped` tells the events that the publisher dropped for a slow subscriber.
543
558
  - A topic outside the topic rule gives `TOPIC_NOT_FOUND`. A topic that `allow` refuses gives `FORBIDDEN` with the reason `topic-not-allowed`. The procedure reads the resource `events`, so the capability of the host applies to it.
544
- - `ProcedureServer.stop()` ends each subscription with `UNAVAILABLE`, and the subscriber resubscribes later.
559
+ - `ProcedureServer.stop()` ends each subscription with `UNAVAILABLE`, and the subscriber resubscribes later, at most `maxAttempts` times without a confirmation.
560
+ - `clear()` or `clearAll()` of the bus ends each subscription of a cleared topic with `TOPIC_CLEARED`. The subscriber does not resubscribe. `bus.on(channel, handler, { onClear })` gives the same notice to a local handler.
561
+ - The subscribers of the same topics with the same options on one transport share one call. The last unsubscribe sends the cancel. A late subscriber gets no replay: its `ready` resolves at once, and its `seq` starts at 1.
545
562
  - `events.subscribe` is not in a registry: it is not an MCP tool.
546
563
 
547
564
  ### Server
@@ -567,13 +584,52 @@ await server.stop()
567
584
  new HttpServerTransport(server: Server, options: HttpServerTransportOptions)
568
585
 
569
586
  interface HttpServerTransportOptions {
570
- port: number;
571
- hostname?: string;
587
+ app: Express; // required
588
+ httpServer?: HttpServer; // an existing server: the transport does not listen
589
+ port?: number; // default: 3000
590
+ host?: string; // default: "127.0.0.1"
591
+ basePath?: string; // default: "/api"
572
592
  urlStrategy?: HttpUrlStrategy;
573
- cors?: boolean | CorsOptions;
593
+ cors?: boolean;
594
+ corsOptions?: { origin?: string | string[]; methods?: string[]; allowedHeaders?: string[]; credentials?: boolean };
595
+ bodyLimit?: number; // default: 1 MiB
596
+ keepAlive?: Duration | false; // default: seconds(15)
597
+ clock?: Clock; // the clock of the keep-alive timers
574
598
  }
575
599
  ```
576
600
 
601
+ A client that accepts `application/x-ndjson` gets a stream as NDJSON lines ([WIRE-PROTOCOL.md](../documentation/WIRE-PROTOCOL.md)).
602
+
603
+ - After each quiet time of `keepAlive`, the server writes a keep-alive line: a blank line. Thus a proxy or a client with an idle timeout does not close a long, quiet stream.
604
+ - Each line starts the quiet time again. No keep-alive line comes after the end, an error, an abort or a closed connection.
605
+ - `keepAlive: false` turns the keep-alive lines off.
606
+ - The HTTP client skips each blank line. Each item that it reads from an NDJSON stream has the metadata key `stream: true` (`STREAM_ITEM_METADATA_KEY`).
607
+
608
+ A server without Express writes the same lines with `writeNdjsonStream` of `@procedures/client/node`:
609
+
610
+ ```typescript
611
+ import { createServer } from "node:http";
612
+ import { seconds } from "@procedures/client";
613
+ import { writeNdjsonStream } from "@procedures/client/node";
614
+
615
+ async function* progress() {
616
+ for (let step = 1; step <= 3; step++) {
617
+ await new Promise((resolve) => setTimeout(resolve, 60_000)); // a quiet minute: 4 keep-alive lines
618
+ yield { step };
619
+ }
620
+ }
621
+
622
+ createServer((req, res) => {
623
+ // The headers, the item, done and error lines with seq, the keep-alive lines and the backpressure
624
+ void writeNdjsonStream(res, progress(), {
625
+ keepAlive: seconds(15),
626
+ errorOf: (error) => ({ code: "UNAVAILABLE", message: String(error), retryable: true }),
627
+ });
628
+ }).listen(3000);
629
+ ```
630
+
631
+ The promise of `writeNdjsonStream` resolves when the response ends. A closed connection or an abort of `signal` stops the stream, and the iterator of the items gets `return()`.
632
+
577
633
  #### WebSocket Server Transport
578
634
  ```typescript
579
635
  new WebSocketServerTransport(server: Server, options: WebSocketServerTransportOptions)
@@ -642,7 +698,7 @@ import { ... } from "@procedures/client/events";
642
698
  |---|---|
643
699
  | `@procedures/client` | Node. The browser entry and the server transports. Its `WebSocketTransport` uses the global `WebSocket`. |
644
700
  | `@procedures/client/browser` | A browser bundle. It does not import `http` or `ws`. |
645
- | `@procedures/client/node` | Node. A `WebSocketTransport` whose sockets are of the `ws` package: a drop ends a socket at once. |
701
+ | `@procedures/client/node` | Node. A `WebSocketTransport` whose sockets are of the `ws` package: a drop ends a socket at once. `writeNdjsonStream`: the NDJSON stream response of `HttpServerTransport`, for a `ServerResponse` of `node:http`. |
646
702
  | `@procedures/client/register` | It registers the core procedures in the global registry. |
647
703
 
648
704
  ## Procedure Discovery