@procedures/client 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +62 -7
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +2 -1
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +9 -2
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/server/transport.d.ts +3 -1
  9. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  10. package/dist/adapters/http/server/transport.js +8 -6
  11. package/dist/adapters/http/server/transport.js.map +1 -1
  12. package/dist/adapters/local/client/transport.d.ts +2 -1
  13. package/dist/adapters/local/client/transport.d.ts.map +1 -1
  14. package/dist/adapters/local/client/transport.js +12 -4
  15. package/dist/adapters/local/client/transport.js.map +1 -1
  16. package/dist/adapters/local/client/types.d.ts +6 -0
  17. package/dist/adapters/local/client/types.d.ts.map +1 -1
  18. package/dist/adapters/local/client/types.js.map +1 -1
  19. package/dist/adapters/sequence.d.ts +36 -0
  20. package/dist/adapters/sequence.d.ts.map +1 -0
  21. package/dist/adapters/sequence.js +55 -0
  22. package/dist/adapters/sequence.js.map +1 -0
  23. package/dist/adapters/websocket/client/node.d.ts +30 -0
  24. package/dist/adapters/websocket/client/node.d.ts.map +1 -0
  25. package/dist/adapters/websocket/client/node.js +32 -0
  26. package/dist/adapters/websocket/client/node.js.map +1 -0
  27. package/dist/adapters/websocket/client/transport.d.ts +39 -1
  28. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  29. package/dist/adapters/websocket/client/transport.js +119 -16
  30. package/dist/adapters/websocket/client/transport.js.map +1 -1
  31. package/dist/adapters/websocket/client/types.d.ts +15 -5
  32. package/dist/adapters/websocket/client/types.d.ts.map +1 -1
  33. package/dist/adapters/websocket/client/types.js.map +1 -1
  34. package/dist/adapters/websocket/server/transport.d.ts +22 -1
  35. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  36. package/dist/adapters/websocket/server/transport.js +29 -1
  37. package/dist/adapters/websocket/server/transport.js.map +1 -1
  38. package/dist/adapters/websocket/server/types.d.ts +6 -0
  39. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  40. package/dist/adapters/websocket/shared/rpc.d.ts +4 -3
  41. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  42. package/dist/adapters/websocket/shared/rpc.js +18 -9
  43. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  44. package/dist/browser.d.ts +3 -0
  45. package/dist/browser.d.ts.map +1 -1
  46. package/dist/browser.js +4 -0
  47. package/dist/browser.js.map +1 -1
  48. package/dist/client/errors/http-errors.d.ts.map +1 -1
  49. package/dist/client/errors/http-errors.js +6 -0
  50. package/dist/client/errors/http-errors.js.map +1 -1
  51. package/dist/client/errors/index.d.ts +1 -1
  52. package/dist/client/errors/index.js +1 -1
  53. package/dist/client/errors/registry.d.ts +2 -1
  54. package/dist/client/errors/registry.d.ts.map +1 -1
  55. package/dist/client/errors/registry.js +73 -22
  56. package/dist/client/errors/registry.js.map +1 -1
  57. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  58. package/dist/client/middleware/rate-limit.js +0 -1
  59. package/dist/client/middleware/rate-limit.js.map +1 -1
  60. package/dist/client/middleware/timeout.d.ts.map +1 -1
  61. package/dist/client/middleware/timeout.js +2 -9
  62. package/dist/client/middleware/timeout.js.map +1 -1
  63. package/dist/events/index.d.ts +4 -0
  64. package/dist/events/index.d.ts.map +1 -1
  65. package/dist/events/index.js +3 -0
  66. package/dist/events/index.js.map +1 -1
  67. package/dist/events/publisher.d.ts +95 -0
  68. package/dist/events/publisher.d.ts.map +1 -0
  69. package/dist/events/publisher.js +268 -0
  70. package/dist/events/publisher.js.map +1 -0
  71. package/dist/events/remote.d.ts +164 -0
  72. package/dist/events/remote.d.ts.map +1 -0
  73. package/dist/events/remote.js +250 -0
  74. package/dist/events/remote.js.map +1 -0
  75. package/dist/index.d.ts +4 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +4 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/node.d.ts +11 -0
  80. package/dist/node.d.ts.map +1 -0
  81. package/dist/node.js +10 -0
  82. package/dist/node.js.map +1 -0
  83. package/dist/procedures/collection/index.d.ts +1 -1
  84. package/dist/procedures/collection/index.d.ts.map +1 -1
  85. package/dist/procedures/collection/index.js.map +1 -1
  86. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  87. package/dist/procedures/collection/procedures.js +6 -1
  88. package/dist/procedures/collection/procedures.js.map +1 -1
  89. package/dist/procedures/collection/schemas.d.ts +16 -1
  90. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  91. package/dist/procedures/collection/schemas.js +13 -2
  92. package/dist/procedures/collection/schemas.js.map +1 -1
  93. package/dist/procedures/collection/storage.d.ts +14 -2
  94. package/dist/procedures/collection/storage.d.ts.map +1 -1
  95. package/dist/procedures/core/array.d.ts +1 -1
  96. package/dist/procedures/core/array.d.ts.map +1 -1
  97. package/dist/procedures/core/array.js +56 -15
  98. package/dist/procedures/core/array.js.map +1 -1
  99. package/dist/procedures/core/index.d.ts.map +1 -1
  100. package/dist/procedures/core/index.js +9 -2
  101. package/dist/procedures/core/index.js.map +1 -1
  102. package/dist/procedures/core/logic.js +1 -1
  103. package/dist/procedures/core/logic.js.map +1 -1
  104. package/dist/procedures/core/math.d.ts.map +1 -1
  105. package/dist/procedures/core/math.js +5 -1
  106. package/dist/procedures/core/math.js.map +1 -1
  107. package/dist/procedures/core/meta.d.ts.map +1 -1
  108. package/dist/procedures/core/meta.js +41 -7
  109. package/dist/procedures/core/meta.js.map +1 -1
  110. package/dist/procedures/core/type.d.ts.map +1 -1
  111. package/dist/procedures/core/type.js +4 -2
  112. package/dist/procedures/core/type.js.map +1 -1
  113. package/dist/procedures/discovery.d.ts +4 -6
  114. package/dist/procedures/discovery.d.ts.map +1 -1
  115. package/dist/procedures/discovery.js +4 -6
  116. package/dist/procedures/discovery.js.map +1 -1
  117. package/dist/procedures/invoke.d.ts +4 -2
  118. package/dist/procedures/invoke.d.ts.map +1 -1
  119. package/dist/procedures/invoke.js +1 -1
  120. package/dist/procedures/invoke.js.map +1 -1
  121. package/dist/procedures/ref.d.ts +4 -2
  122. package/dist/procedures/ref.d.ts.map +1 -1
  123. package/dist/procedures/ref.js +2 -1
  124. package/dist/procedures/ref.js.map +1 -1
  125. package/dist/protocol.d.ts +10 -3
  126. package/dist/protocol.d.ts.map +1 -1
  127. package/dist/protocol.js +13 -2
  128. package/dist/protocol.js.map +1 -1
  129. package/dist/server/host-policy.d.ts +11 -0
  130. package/dist/server/host-policy.d.ts.map +1 -1
  131. package/dist/server/host-policy.js +5 -0
  132. package/dist/server/host-policy.js.map +1 -1
  133. package/dist/server/procedure-server.d.ts +26 -3
  134. package/dist/server/procedure-server.d.ts.map +1 -1
  135. package/dist/server/procedure-server.js +38 -6
  136. package/dist/server/procedure-server.js.map +1 -1
  137. package/package.json +7 -6
  138. package/src/adapters/http/client/transport.ts +9 -2
  139. package/src/adapters/http/server/transport.ts +8 -6
  140. package/src/adapters/local/client/transport.ts +12 -4
  141. package/src/adapters/local/client/types.ts +7 -0
  142. package/src/adapters/sequence.ts +55 -0
  143. package/src/adapters/websocket/client/node.ts +34 -0
  144. package/src/adapters/websocket/client/transport.ts +128 -16
  145. package/src/adapters/websocket/client/types.ts +16 -5
  146. package/src/adapters/websocket/server/transport.ts +43 -1
  147. package/src/adapters/websocket/server/types.ts +7 -0
  148. package/src/adapters/websocket/shared/rpc.ts +19 -9
  149. package/src/browser.ts +15 -0
  150. package/src/client/errors/README.md +2 -0
  151. package/src/client/errors/http-errors.ts +5 -0
  152. package/src/client/errors/index.ts +1 -1
  153. package/src/client/errors/registry.ts +80 -22
  154. package/src/client/middleware/rate-limit.ts +0 -2
  155. package/src/client/middleware/timeout.ts +2 -9
  156. package/src/events/index.ts +16 -0
  157. package/src/events/publisher.ts +314 -0
  158. package/src/events/remote.ts +369 -0
  159. package/src/index.ts +4 -0
  160. package/src/node.ts +11 -0
  161. package/src/procedures/collection/index.ts +1 -0
  162. package/src/procedures/collection/procedures.ts +6 -1
  163. package/src/procedures/collection/schemas.ts +28 -2
  164. package/src/procedures/collection/storage.ts +14 -2
  165. package/src/procedures/core/array.ts +53 -16
  166. package/src/procedures/core/index.ts +9 -2
  167. package/src/procedures/core/logic.ts +1 -1
  168. package/src/procedures/core/math.ts +5 -1
  169. package/src/procedures/core/meta.ts +43 -7
  170. package/src/procedures/core/type.ts +4 -2
  171. package/src/procedures/discovery.ts +4 -6
  172. package/src/procedures/invoke.ts +4 -2
  173. package/src/procedures/ref.ts +4 -2
  174. package/src/protocol.ts +16 -3
  175. package/src/server/host-policy.ts +14 -0
  176. package/src/server/procedure-server.ts +45 -8
  177. package/dist/procedures/cli/announce.d.ts +0 -67
  178. package/dist/procedures/cli/announce.d.ts.map +0 -1
  179. package/dist/procedures/cli/announce.js +0 -392
  180. package/dist/procedures/cli/announce.js.map +0 -1
  181. package/dist/procedures/cli/discover.d.ts +0 -66
  182. package/dist/procedures/cli/discover.d.ts.map +0 -1
  183. package/dist/procedures/cli/discover.js +0 -310
  184. package/dist/procedures/cli/discover.js.map +0 -1
  185. package/dist/procedures/cli/index.d.ts +0 -13
  186. package/dist/procedures/cli/index.d.ts.map +0 -1
  187. package/dist/procedures/cli/index.js +0 -203
  188. package/dist/procedures/cli/index.js.map +0 -1
  189. package/dist/procedures/cli/init.d.ts +0 -17
  190. package/dist/procedures/cli/init.d.ts.map +0 -1
  191. package/dist/procedures/cli/init.js +0 -336
  192. package/dist/procedures/cli/init.js.map +0 -1
  193. package/src/procedures/cli/announce.ts +0 -470
  194. package/src/procedures/cli/discover.ts +0 -390
  195. package/src/procedures/cli/index.ts +0 -231
  196. package/src/procedures/cli/init.ts +0 -370
@@ -14,6 +14,7 @@ import {
14
14
  parseProcedureJson,
15
15
  stringifyProcedureJson,
16
16
  isAnyProcedureRef,
17
+ isProcedureRef,
17
18
  isOutputRef,
18
19
  isPlainObject,
19
20
  resolveOutputRef,
@@ -58,18 +59,37 @@ const ImportOutputSchema = z.looseObject({
58
59
 
59
60
  type ImportProcedure = Procedure<ImportInput, ImportOutput, { description: string; tags: string[] }>;
60
61
 
62
+ /**
63
+ * This function gives true for the runtime form of a procedure reference whose path is a list of
64
+ * texts. `client.import` accepts only such a value at the top level.
65
+ */
66
+ function isImportableRef(value: unknown): boolean {
67
+ if (!isProcedureRef(value)) return false;
68
+ const path = (value as { path?: unknown }).path;
69
+ return Array.isArray(path) && path.length > 0 && path.every((segment) => typeof segment === "string");
70
+ }
71
+
61
72
  const importProcedure: ImportProcedure = defineProcedure({
62
73
  path: ["import"],
63
74
  input: ImportInputSchema,
64
75
  output: ImportOutputSchema,
65
76
  metadata: {
66
- description: "Parse procedure aggregation from JSON string",
77
+ description:
78
+ "Parse a procedure reference ({ $proc, input }) from JSON text. Other JSON, for example a number or a list, gives success false.",
67
79
  tags: ["core", "meta"],
68
80
  },
69
81
  handler: async (input: ImportInput): Promise<ImportOutput> => {
70
82
  try {
71
83
  if (input.json) {
72
84
  const procedure = parseProcedureJson(input.json);
85
+ // Before, each JSON value gave success true, for example 42 (catalog test, roadmap R29)
86
+ if (!isImportableRef(procedure)) {
87
+ return {
88
+ procedure: null,
89
+ success: false,
90
+ error: "The JSON is not a procedure reference. A reference is an object whose $proc is a list of texts.",
91
+ };
92
+ }
73
93
  return { procedure, success: true };
74
94
  }
75
95
  if (input.path) {
@@ -107,8 +127,19 @@ interface ExportInput {
107
127
  pretty?: boolean | undefined;
108
128
  }
109
129
 
130
+ /**
131
+ * This function gives a schema of a required value: undefined has no JSON text, so the input
132
+ * schema refuses it with `VALIDATION_ERROR`. Before, the call failed with
133
+ * `OUTPUT_VALIDATION_ERROR` (catalog test, roadmap R29).
134
+ */
135
+ const definedValue = (description: string) =>
136
+ z
137
+ .unknown()
138
+ .refine((value) => value !== undefined, { message: "The value is missing: undefined has no JSON text" })
139
+ .describe(description);
140
+
110
141
  const ExportInputSchema = z.object({
111
- procedure: z.unknown().optional().describe("Procedure reference to serialize"),
142
+ procedure: definedValue("The procedure reference to serialize. The procedure refuses a missing value (undefined) with VALIDATION_ERROR, because undefined has no JSON text."),
112
143
  pretty: z.boolean().optional().describe("Pretty print JSON output"),
113
144
  });
114
145
 
@@ -134,13 +165,15 @@ const exportProcedure: ExportProcedure = defineProcedure({
134
165
  input: ExportInputSchema,
135
166
  output: ExportOutputSchema,
136
167
  metadata: {
137
- description: "Serialize procedure aggregation to JSON string",
168
+ description: "Serialize a procedure reference to JSON text. It refuses a missing procedure (undefined) with VALIDATION_ERROR.",
138
169
  // The refs of the input are data: exec() must not run them (deep dive CORE-5)
139
170
  tags: ["core", "meta", RAW_INPUT_TAG],
140
171
  },
141
172
  handler: async (input: ExportInput): Promise<ExportOutput> => {
142
173
  try {
143
- const json = stringifyProcedureJson(input.procedure, input.pretty ? 2 : undefined);
174
+ const json = stringifyProcedureJson(input.procedure, input.pretty ? 2 : undefined) as string | undefined;
175
+ // A function or a symbol has no JSON text either: an error result, not an invalid output
176
+ if (json === undefined) return { json: "", success: false, error: "The procedure has no JSON text" };
144
177
  return { json, success: true };
145
178
  } catch (error) {
146
179
  return {
@@ -268,7 +301,7 @@ interface StringifyJsonInput {
268
301
  }
269
302
 
270
303
  const StringifyJsonInputSchema = z.object({
271
- value: z.unknown().optional().describe("Value to stringify"),
304
+ value: definedValue("The value to write as JSON. The procedure refuses a missing value (undefined) with VALIDATION_ERROR, because undefined has no JSON text."),
272
305
  pretty: z.boolean().optional().describe("Pretty print with indentation"),
273
306
  indent: z.union([z.number(), z.string()]).optional().describe("Custom indentation (number of spaces or string)"),
274
307
  });
@@ -280,13 +313,16 @@ const stringifyJsonProcedure: StringifyJsonProcedure = defineProcedure({
280
313
  input: StringifyJsonInputSchema,
281
314
  output: stringOutput("The JSON text"),
282
315
  metadata: {
283
- description: "Stringify value to JSON (with procedure ref support)",
316
+ description: "Stringify value to JSON (with procedure ref support). It refuses a missing value (undefined) with VALIDATION_ERROR.",
284
317
  // The refs of the input are data: exec() must not run them (deep dive CORE-5)
285
318
  tags: ["core", "meta", RAW_INPUT_TAG],
286
319
  },
287
320
  handler: async (input: StringifyJsonInput): Promise<string> => {
288
321
  const indent = input.pretty ? (input.indent ?? 2) : input.indent;
289
- return stringifyProcedureJson(input.value, indent as number);
322
+ const json = stringifyProcedureJson(input.value, indent as number) as string | undefined;
323
+ // A function or a symbol has no JSON text either: a clear error, not an invalid output
324
+ if (json === undefined) throw new Error("The value has no JSON text");
325
+ return json;
290
326
  },
291
327
  });
292
328
 
@@ -10,6 +10,7 @@ import { anyOutput, booleanOutput, stringOutput } from "./outputs.js";
10
10
  import { defineProcedure, namespace } from "../define.js";
11
11
  import type { AnyProcedure, Procedure } from "../types.js";
12
12
  import { PURE_EFFECTS, withEffects } from "../effects.js";
13
+ import { isPlainObject } from "../ref.js";
13
14
 
14
15
  // =============================================================================
15
16
  // Typeof Procedure
@@ -168,11 +169,12 @@ const isObjectProcedure: IsObjectProcedure = defineProcedure({
168
169
  input: IsObjectInputSchema,
169
170
  output: booleanOutput("True for a plain object"),
170
171
  metadata: {
171
- description: "Check if value is a plain object (not array, not null)",
172
+ description: "Check if value is a plain object (not array, not null, not a Date or another class instance)",
172
173
  tags: ["core", "type"],
173
174
  },
174
175
  handler: async (input: IsObjectInput): Promise<boolean> => {
175
- return typeof input.value === "object" && input.value !== null && !Array.isArray(input.value);
176
+ // Before, a Date or a Map gave true (catalog test, roadmap R29)
177
+ return isPlainObject(input.value);
176
178
  },
177
179
  });
178
180
 
@@ -1,13 +1,11 @@
1
1
  /**
2
2
  * Procedure Registration Helpers
3
3
  *
4
- * Manual registration helpers for procedures. For compile-time auto-discovery,
5
- * use the CLI: `npx client-registry discover`
4
+ * These helpers add procedures to the global registry.
6
5
  *
7
- * Libraries that depend on client should:
8
- * 1. Add `"client": { "procedures": "./dist/register.js" }` to package.json
9
- * 2. Create a register.ts that uses createAndRegister()
10
- * 3. Consumers run `npx client-registry discover` to generate imports
6
+ * A package with procedures declares `"client": { "procedures": "./dist/register.js" }` in its
7
+ * `package.json`. Its `src/register.ts` registers its module. `mark` finds the packages by the
8
+ * `client.procedures` field, and an app imports the `/register` entry of each package that it uses.
11
9
  */
12
10
 
13
11
  import { PROCEDURE_REGISTRY } from "./registry.js";
@@ -68,8 +68,10 @@ export type InvocationErrorCode =
68
68
  * of `@procedures/fs`).
69
69
  * - `host-not-allowed`: the input has a URL whose host the host does not allow, or a redirect goes
70
70
  * to such a host (`createHttpModule({ allow })` of `@procedures/http`).
71
+ * - `topic-not-allowed`: the `allow` function of an `EventPublisher` refuses a topic of a
72
+ * subscription (`documentation/EVENTS.md`).
71
73
  */
72
- export type ForbiddenReason = CapabilityRefusalReason | "outside-root" | "host-not-allowed";
74
+ export type ForbiddenReason = CapabilityRefusalReason | "outside-root" | "host-not-allowed" | "topic-not-allowed";
73
75
 
74
76
  /** An error of the invocation itself. An error that the handler throws passes through unchanged. */
75
77
  export class InvocationError extends Error {
@@ -79,7 +81,7 @@ export class InvocationError extends Error {
79
81
  readonly retryable: boolean = false;
80
82
  /**
81
83
  * The reason of a `FORBIDDEN` error: `effects-undeclared`, `effects-exceeded`, `clearance`,
82
- * `deadline`, `outside-root` or `host-not-allowed`.
84
+ * `deadline`, `outside-root`, `host-not-allowed` or `topic-not-allowed`.
83
85
  */
84
86
  readonly reason: ForbiddenReason | undefined;
85
87
 
@@ -22,7 +22,8 @@
22
22
  * - `"$never"`: Never execute during hydration, pass as pure data (with its whole subtree)
23
23
  * - `"$parent"`: Defer to parent procedure (pass as data)
24
24
  * - `"someName"`: Defer to the runner that gives this name in its `contextStack`
25
- * (`dag.traverse` gives "dag.traverse", `core.catch` gives "catch")
25
+ * (`dag.traverse` gives "dag.traverse". `client.tryCatch` gives no name: it is control flow,
26
+ * and it runs its `try` and `catch` operands itself.)
26
27
  *
27
28
  * A deferred ref is data at every depth: hydration of the input that holds it does not run it.
28
29
  * The procedure that receives it runs it later (deep dive CORE-11).
@@ -681,7 +682,8 @@ export interface HydrateOptions {
681
682
 
682
683
  /**
683
684
  * The context names of the runner. A ref with `$when: "<name>"` runs only when this list has
684
- * the name. A runner such as `core.catch` gives its own name here when it hydrates its operand.
685
+ * the name. A runner such as `dag.traverse` gives its own name here when it hydrates its operand.
686
+ * (Control flow such as `client.tryCatch` runs its operands itself and gives no name.)
685
687
  */
686
688
  contextStack?: string[] | undefined;
687
689
 
package/src/protocol.ts CHANGED
@@ -46,12 +46,14 @@ export type ProtocolErrorCode =
46
46
  | "TIMEOUT"
47
47
  | "UNAVAILABLE"
48
48
  | "CONNECTION_CLOSED"
49
- | "CONNECTION_NOT_FOUND";
49
+ | "CONNECTION_NOT_FOUND"
50
+ | "TOPIC_NOT_FOUND";
50
51
 
51
52
  /**
52
53
  * The error codes of the protocol, on each face (HTTP, WebSocket, MCP, local). An error that a
53
- * handler throws with its own `code` keeps it. Such a code is not in this list, and HTTP gives it
54
- * the status 500.
54
+ * handler throws with its own `code` keeps it, for a value and for a stream (`errorCodeOf`). The
55
+ * HTTP status follows the code: the status of this list, the status of `ERROR_REGISTRY` for
56
+ * another known code, or 500.
55
57
  */
56
58
  export const ERROR_CODES: Readonly<Record<ProtocolErrorCode, ErrorCodeInfo>> = {
57
59
  NOT_FOUND: { http: 404, retryable: false, meaning: "No procedure has the path, or the host does not expose it" },
@@ -75,6 +77,7 @@ export const ERROR_CODES: Readonly<Record<ProtocolErrorCode, ErrorCodeInfo>> = {
75
77
  UNAVAILABLE: { http: 503, retryable: true, meaning: "The server cannot take the call now: it stops, or it is busy" },
76
78
  CONNECTION_CLOSED: { http: 503, retryable: true, meaning: "The connection closed before the call left" },
77
79
  CONNECTION_NOT_FOUND: { http: 404, retryable: false, meaning: "The server has no connection with this id" },
80
+ TOPIC_NOT_FOUND: { http: 404, retryable: false, meaning: "The topic rule of the host has no topic with this name" },
78
81
  };
79
82
 
80
83
  /** True when a code is an error code of the protocol. */
@@ -82,6 +85,16 @@ export function isProtocolErrorCode(code: unknown): code is ProtocolErrorCode {
82
85
  return typeof code === "string" && Object.prototype.hasOwnProperty.call(ERROR_CODES, code);
83
86
  }
84
87
 
88
+ /**
89
+ * The code of an error that a handler or an invocation throws, on each face: its own code when
90
+ * it is a string, or `HANDLER_ERROR`. An invocation error has a code of this list. Before, a
91
+ * value lost the own code of a handler error, and a stream kept it (roadmap R30).
92
+ */
93
+ export function errorCodeOf(error: unknown): string {
94
+ const code = (error as { code?: unknown } | null | undefined)?.code;
95
+ return typeof code === "string" ? code : "HANDLER_ERROR";
96
+ }
97
+
85
98
  /** The HTTP status of an error code, or undefined for a code that is not in the list. */
86
99
  export function httpStatusOf(code: unknown): number | undefined {
87
100
  return isProtocolErrorCode(code) ? ERROR_CODES[code].http : undefined;
@@ -14,6 +14,8 @@
14
14
  * - `token`, `browser`: the checks of the network transports (`client-server` applies them).
15
15
  * - `capability`, `readOnly`, `clearanceOrder`, `resources`, `admitItem`: the capability of each
16
16
  * call and what the host gives for it (CAPABILITIES-2026-10).
17
+ * - `events`: the remote events of the host (`documentation/EVENTS.md`). The server pins
18
+ * `events.subscribe` with this topic rule, and its procedures get the bus as `ctx.bus`.
17
19
  */
18
20
 
19
21
  import type { Capability, ClearanceOrder, ProcedurePath, ResourcesOf } from "../procedures/types.js";
@@ -24,6 +26,7 @@ import { timeoutInterceptor } from "../procedures/interceptors.js";
24
26
  import type { BrowserGuardOptions } from "./browser-guard.js";
25
27
  import type { ProcedureRegistry } from "../procedures/registry.js";
26
28
  import type { ProcedureServerOptions } from "./procedure-server.js";
29
+ import type { EventPublisherOptions } from "../events/publisher.js";
27
30
  import { toMs, type Duration } from "../duration.js";
28
31
 
29
32
  /** The procedures of a host that are tools: namespaces (first path segments), less some keys. */
@@ -86,6 +89,12 @@ export interface HostPolicyOptions {
86
89
  resources?: ((invocation: Invocation) => ResourcesOf) | undefined;
87
90
  /** The filter of the output that leaves the host. */
88
91
  admitItem?: AdmitItem | undefined;
92
+ /**
93
+ * The remote events of the host (`documentation/EVENTS.md`): the topic rule, with an optional
94
+ * bus, `allow` function and buffer. The default bus is the global bus, which is also the default
95
+ * `ctx.bus`. Without this option, the host does not serve `events.subscribe`.
96
+ */
97
+ events?: EventPublisherOptions | undefined;
89
98
  }
90
99
 
91
100
  /** What a host lets its callers do. */
@@ -100,6 +109,8 @@ export interface HostPolicy {
100
109
  readonly clearanceOrder: ClearanceOrder | undefined;
101
110
  readonly resources: ((invocation: Invocation) => ResourcesOf) | undefined;
102
111
  readonly admitItem: AdmitItem | undefined;
112
+ /** The remote events: each server of the policy makes its own publisher from these options. */
113
+ readonly events: EventPublisherOptions | undefined;
103
114
  }
104
115
 
105
116
  /** The capability function of a policy: the capability of the options, narrowed by `readOnly`. */
@@ -145,6 +156,7 @@ export function hostPolicy(options: HostPolicyOptions = {}): HostPolicy {
145
156
  clearanceOrder: options.clearanceOrder,
146
157
  resources: options.resources,
147
158
  admitItem: options.admitItem,
159
+ events: options.events,
148
160
  };
149
161
  }
150
162
 
@@ -162,5 +174,7 @@ export function procedureServerOptions(policy: HostPolicy, registry?: ProcedureR
162
174
  ...(policy.clearanceOrder ? { clearanceOrder: policy.clearanceOrder } : {}),
163
175
  ...(policy.resources ? { resources: policy.resources } : {}),
164
176
  ...(policy.admitItem ? { admitItem: policy.admitItem } : {}),
177
+ // The remote events: the server makes its publisher and pins events.subscribe (EVENTS.md)
178
+ ...(policy.events ? { events: policy.events } : {}),
165
179
  };
166
180
  }
@@ -24,6 +24,9 @@ import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptio
24
24
  import { optionalMs, type Duration } from "../duration.js";
25
25
  import type { CollectionStorage } from "../procedures/collection/storage.js";
26
26
  import { TIMER_DEFAULTS } from "../timers.js";
27
+ import { errorCodeOf } from "../protocol.js";
28
+ import { EventPublisher, type EventPublisherOptions } from "../events/publisher.js";
29
+ import { getGlobalEventBus } from "../events/bus.js";
27
30
 
28
31
  // =============================================================================
29
32
  // Types
@@ -118,6 +121,14 @@ export interface ProcedureServerOptions extends ServerOptions {
118
121
  * `request.principal`.
119
122
  */
120
123
  authenticate?: ((request: ServerRequest<unknown>) => Principal | undefined | Promise<Principal | undefined>) | undefined;
124
+
125
+ /**
126
+ * The remote events of the server (`documentation/EVENTS.md`): a bus and its topic rule, or an
127
+ * `EventPublisher`. The server pins the procedure `events.subscribe`, outside its registry, so a
128
+ * peer can subscribe to the topics. The procedures of the server get this bus as `ctx.bus`.
129
+ * `stop()` ends each subscription first, with `UNAVAILABLE`.
130
+ */
131
+ events?: EventPublisherOptions | EventPublisher | undefined;
121
132
  }
122
133
 
123
134
  /**
@@ -198,7 +209,8 @@ function withRequestTimeout(
198
209
 
199
210
  /**
200
211
  * The error response of a failed invocation. An invocation error keeps its code
201
- * (VALIDATION_ERROR, NOT_EXPOSED, ABORTED and so on). An error of the handler is HANDLER_ERROR.
212
+ * (VALIDATION_ERROR, NOT_EXPOSED, ABORTED and so on). An error of the handler keeps its own code
213
+ * when it is a string, as for a stream (`WIRE-PROTOCOL.md`). Without a code, it is HANDLER_ERROR.
202
214
  */
203
215
  export function errorResponse(id: string, error: unknown): ServerResponse<unknown> {
204
216
  // An error that says it is retryable stays retryable: before, every error lost the flag
@@ -214,7 +226,7 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
214
226
  id,
215
227
  status: {
216
228
  type: "error",
217
- code: error instanceof InvocationError ? error.code : "HANDLER_ERROR",
229
+ code: errorCodeOf(error),
218
230
  message: error instanceof Error ? error.message : String(error),
219
231
  retryable: typeof retryable === "boolean" ? retryable : false,
220
232
  },
@@ -286,6 +298,8 @@ export class ProcedureServer extends Server implements RepositoryProvider {
286
298
  private readonly authenticate: ProcedureServerOptions["authenticate"];
287
299
  /** The functions that `stop()` waits on: each one runs when no call is in flight */
288
300
  private idleWaiters: Array<() => void> = [];
301
+ /** The publisher of the remote events, from the option `events`, or undefined. */
302
+ readonly publisher: EventPublisher | undefined;
289
303
 
290
304
  constructor(options: ProcedureServerOptions = {}) {
291
305
  super(options);
@@ -303,6 +317,9 @@ export class ProcedureServer extends Server implements RepositoryProvider {
303
317
  this.idempotency = options.idempotency === false ? undefined : new IdempotencyCache(options.idempotency ?? {});
304
318
  this.token = options.token;
305
319
  this.authenticate = options.authenticate;
320
+ // The remote events: the server pins events.subscribe, as it pins host.health (EVENTS.md)
321
+ this.publisher = options.events instanceof EventPublisher ? options.events : options.events ? new EventPublisher(options.events) : undefined;
322
+ if (this.publisher) this.registerProcedure(this.publisher.procedure);
306
323
 
307
324
  // Configure collections
308
325
  if (options.collections) {
@@ -643,6 +660,8 @@ export class ProcedureServer extends Server implements RepositoryProvider {
643
660
  clearanceOrder,
644
661
  resources,
645
662
  admitItem,
663
+ // The bus of the remote events is the bus of the procedures (EVENTS.md)
664
+ ...(this.publisher ? { bus: this.publisher.bus } : {}),
646
665
  });
647
666
  if (output.kind === "stream") {
648
667
  // The faces that collect a stream read this mark (roadmap R15)
@@ -721,23 +740,41 @@ export class ProcedureServer extends Server implements RepositoryProvider {
721
740
  }
722
741
 
723
742
  /**
724
- * Close all storage backends.
743
+ * This function publishes an event on the bus of the server: the bus of the option `events`, or
744
+ * the global bus. Each subscription of the topic gets it (`documentation/EVENTS.md`).
745
+ *
746
+ * @param topic - The topic: a channel of the bus
747
+ * @param data - The data of the event. It crosses the wire as JSON.
748
+ */
749
+ publish<T>(topic: string, data: T): void {
750
+ (this.publisher?.bus ?? getGlobalEventBus()).emit(topic, data);
751
+ }
752
+
753
+ /**
754
+ * This method closes each storage one time, also a storage of two collections, and removes the
755
+ * storages from the server. A closed storage rejects each operation, so the server does not keep
756
+ * it. Before a new `start()`, give the storages again with `useStorage()`.
725
757
  */
726
758
  async closeStorages(): Promise<void> {
727
- const closePromises = Array.from(this.storages.values()).map((s) =>
728
- s.close()
729
- );
730
- await Promise.all(closePromises);
759
+ const storages = new Set(this.storages.values());
760
+ this.storages.clear();
761
+ const results = await Promise.allSettled(Array.from(storages, (storage) => storage.close()));
762
+ const failed = results.find((result): result is PromiseRejectedResult => result.status === "rejected");
763
+ if (failed) throw failed.reason;
731
764
  }
732
765
 
733
766
  /**
734
- * Stop server and close storages.
767
+ * This method stops the server, then closes the storages and removes them (`closeStorages()`).
768
+ * The server can start again, with new storages from `useStorage()`. Before, a server that
769
+ * started again used the closed storages.
735
770
  */
736
771
  override async stop(options: StopOptions = {}): Promise<void> {
737
772
  const graceMs = optionalMs(options.grace, "grace") ?? TIMER_DEFAULTS.server.stopGraceMs;
738
773
  // A graceful stop: new calls get UNAVAILABLE, and the calls in flight have the grace time
739
774
  // to end (roadmap R14). Before, the transports aborted every call at once.
740
775
  this.draining = true;
776
+ // A subscription has no end: it ends now, with a retryable UNAVAILABLE (EVENTS.md)
777
+ this.publisher?.end();
741
778
  if (graceMs > 0 && this.inFlight > 0) {
742
779
  await new Promise<void>((resolve) => {
743
780
  const timer = setTimeout(resolve, graceMs);
@@ -1,67 +0,0 @@
1
- /**
2
- * Procedure Announcement (Self-Registration)
3
- *
4
- * When a package with procedures is installed, it "announces" itself
5
- * by writing to a registry file at the root of the project.
6
- *
7
- * This enables true bottom-up propagation:
8
- * - Each package with procedures has `"postinstall": "client-registry announce"`
9
- * - On install, it walks up to find the root and registers itself
10
- * - `client-registry discover` reads this registry instead of scanning
11
- *
12
- * The registry file (.client-registry.json) accumulates entries from
13
- * all packages as they're installed, making discovery instant.
14
- */
15
- export interface AnnounceOptions {
16
- /** Be verbose about what's happening */
17
- verbose: boolean;
18
- /** The folder to start from (default: the current folder) */
19
- cwd?: string | undefined;
20
- /**
21
- * Add `client-registry announce` to the postinstall script of the package that depends on this one.
22
- * This edits a package.json that belongs to the consumer, so it happens only when asked
23
- * (`client-registry announce --propagate`). Default: false.
24
- */
25
- propagate?: boolean | undefined;
26
- }
27
- interface RegistryEntry {
28
- /** Path to procedures file relative to package */
29
- proceduresPath: string;
30
- /** When this entry was added */
31
- registeredAt: string;
32
- /** Package version */
33
- version?: string;
34
- }
35
- interface Registry {
36
- /** Schema version for future compatibility */
37
- schemaVersion: 1;
38
- /** Map of package name to registry entry */
39
- packages: Record<string, RegistryEntry>;
40
- }
41
- declare const REGISTRY_FILENAME = ".client-registry.json";
42
- /**
43
- * Announce this package's procedures to the project registry.
44
- *
45
- * Called from a package's postinstall script to register its procedures.
46
- *
47
- * With `propagate: true` (`client-registry announce --propagate`), it also adds the postinstall script
48
- * to the package that depends on this one, for transitive discovery:
49
- * - When client is installed in package D, client's postinstall adds postinstall to D
50
- * - When D is installed in C, D's postinstall adds postinstall to C
51
- * - When C is installed in B, C's postinstall adds postinstall to B
52
- *
53
- * The propagation edits the package.json of the consumer, so it happens only when asked.
54
- * Without it, each package that has its own postinstall script still registers itself.
55
- */
56
- export declare function announce(options: AnnounceOptions): Promise<void>;
57
- /**
58
- * Remove this package from the registry (for uninstall).
59
- */
60
- export declare function unannounce(options: AnnounceOptions): Promise<void>;
61
- /**
62
- * Get the registry for use by discover command.
63
- */
64
- export declare function getRegistry(rootDir: string): Promise<Registry>;
65
- export { REGISTRY_FILENAME };
66
- export type { Registry, RegistryEntry };
67
- //# sourceMappingURL=announce.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"announce.d.ts","sourceRoot":"","sources":["../../../src/procedures/cli/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC;AAED,UAAU,aAAa;IACrB,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,gCAAgC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,QAAQ;IAChB,8CAA8C;IAC9C,aAAa,EAAE,CAAC,CAAC;IACjB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACzC;AAkFD,QAAA,MAAM,iBAAiB,0BAA0B,CAAC;AA0GlD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtE;AAiKD;;GAEG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBxE;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEpE;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC"}