@swimmesberger/elarion-jsonrpc-client-generator 0.2.3-preview.83.1 → 0.2.3-preview.84.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/README.md CHANGED
@@ -65,6 +65,18 @@ Result validation is enabled by default through `rpcResultSchemas`. Use `transfo
65
65
 
66
66
  Params validation is also enabled by default through `rpcParamsSchemas`: single calls and batch items are checked against the exported schema's constraints (lengths, ranges, patterns, formats) before the request is sent, and a failure throws `RpcParamsValidationError` locally — naming the method, with the underlying Zod error as `cause` — so tier-1 contract violations never reach the wire. Set `validateParams: false` to opt out.
67
67
 
68
+ ## Client-event subscriptions
69
+
70
+ When the schema carries an `events` block (the host registers Elarion client-event topics, ADR-0043), the generator additionally emits `events-client.ts`: `createElarionEvents(...)` multiplexes topic-typed subscriptions over one `EventSource`, validates incoming payloads with the generated Zod schemas (`rpcEventPayloadSchemas`), and exposes `$client.onConnected(...)` — fired on connect and every reconnect — as the "you may have missed events, re-query" signal. Event-free schemas produce byte-identical output with no events file.
71
+
72
+ ```ts
73
+ import { createElarionEvents } from './generated/events-client'
74
+
75
+ const events = createElarionEvents({ url: '/events' })
76
+ events.invoicing.invoiceChanged.subscribe((evt) => refetch(evt.invoiceId))
77
+ events.$client.onConnected(() => refetchAll())
78
+ ```
79
+
68
80
  ## Error handling
69
81
 
70
82
  A JSON-RPC error from the server is thrown as a typed `RpcError` (`code`, `message`, optional `data`). Elarion maps its `AppError` kinds onto the JSON-RPC server-reserved range, so the generated `RpcError` exposes a getter per kind — branch on the kind directly instead of re-wrapping:
package/dist/cli.js CHANGED
@@ -56,6 +56,11 @@ function parseArgs(argv) {
56
56
  index += 1;
57
57
  continue;
58
58
  }
59
+ if (arg === '--events-client') {
60
+ options.eventsClientFileName = next;
61
+ index += 1;
62
+ continue;
63
+ }
59
64
  if (arg === '--framework') {
60
65
  if (!SUPPORTED_FRAMEWORKS.includes(next)) {
61
66
  throw new Error(`Unsupported framework ${next}. Supported: ${SUPPORTED_FRAMEWORKS.join(', ')}`);
@@ -89,6 +94,8 @@ Options:
89
94
  --client <file> Fetch client filename (default: rpc-client.ts)
90
95
  --session-client <file> Client-capability snapshot client + OpenFeature provider (default: session-client.ts;
91
96
  emitted only when the schema exposes the elarion.session operation)
97
+ --events-client <file> Typed client-event subscription client (default: events-client.ts;
98
+ emitted only when the schema declares an events block)
92
99
  --framework <name> Emit an opt-in framework adapter alongside the neutral core client.
93
100
  Supported: tanstack-start (needs the @tanstack/react-start peer dependency)
94
101
  --framework-adapter <file> Framework adapter filename (default: start-adapter.ts)
@@ -127,6 +134,7 @@ function generateOnce(options) {
127
134
  schemasFileName: options.schemasFileName,
128
135
  clientFileName: options.clientFileName,
129
136
  sessionClientFileName: options.sessionClientFileName,
137
+ eventsClientFileName: options.eventsClientFileName,
130
138
  framework: options.framework,
131
139
  frameworkAdapterFileName: options.frameworkAdapterFileName,
132
140
  });
@@ -139,6 +147,10 @@ function generateOnce(options) {
139
147
  writeFileSync(resolve(outDir, generated.sessionClientFileName), generated.sessionClientSource, 'utf-8');
140
148
  extraNotes.push(generated.sessionClientFileName);
141
149
  }
150
+ if (generated.eventsClientFileName !== undefined && generated.eventsClientSource !== undefined) {
151
+ writeFileSync(resolve(outDir, generated.eventsClientFileName), generated.eventsClientSource, 'utf-8');
152
+ extraNotes.push(generated.eventsClientFileName);
153
+ }
142
154
  if (generated.frameworkAdapterFileName !== undefined && generated.frameworkAdapterSource !== undefined) {
143
155
  writeFileSync(resolve(outDir, generated.frameworkAdapterFileName), generated.frameworkAdapterSource, 'utf-8');
144
156
  extraNotes.push(generated.frameworkAdapterFileName);
@@ -0,0 +1,15 @@
1
+ interface GenerateEventsClientSourceOptions {
2
+ generatedBy: string;
3
+ sourceLabel: string;
4
+ schemasFileName: string;
5
+ /** Sorted topic names from the schema's `events` block. */
6
+ topics: readonly string[];
7
+ }
8
+ /**
9
+ * Generates the typed client-event subscription client (ADR-0043) from the schema's `events` block: topic
10
+ * literal types, `z.infer` payload types over the Zod payload schemas (emitted into the schemas file), and
11
+ * an `EventSource` wrapper that multiplexes the authorized subscriptions over one connection. Emitted only
12
+ * when the schema declares events, so event-free schemas produce byte-identical output.
13
+ */
14
+ export declare function generateEventsClientSource(options: GenerateEventsClientSourceOptions): string;
15
+ export {};
@@ -0,0 +1,314 @@
1
+ import { moduleSpecifier } from './rpc-client-source.js';
2
+ /**
3
+ * Generates the typed client-event subscription client (ADR-0043) from the schema's `events` block: topic
4
+ * literal types, `z.infer` payload types over the Zod payload schemas (emitted into the schemas file), and
5
+ * an `EventSource` wrapper that multiplexes the authorized subscriptions over one connection. Emitted only
6
+ * when the schema declares events, so event-free schemas produce byte-identical output.
7
+ */
8
+ export function generateEventsClientSource(options) {
9
+ const schemasImport = moduleSpecifier(options.schemasFileName);
10
+ const topicTree = buildTopicTree(options.topics);
11
+ return [
12
+ `// Auto-generated by ${options.generatedBy} — DO NOT EDIT`,
13
+ `// Source: ${options.sourceLabel}`,
14
+ '',
15
+ "import type { z } from 'zod'",
16
+ `import { rpcEventPayloadSchemas } from '${schemasImport}'`,
17
+ '',
18
+ 'export type RpcEventTopic = keyof typeof rpcEventPayloadSchemas & string',
19
+ 'export type RpcEventPayload<T extends RpcEventTopic> = z.infer<(typeof rpcEventPayloadSchemas)[T]>',
20
+ '',
21
+ '// The structural surface of the platform EventSource this client needs — injectable for tests,',
22
+ '// polyfills, or a withCredentials/auth wrapper.',
23
+ 'export interface ElarionEventSourceLike {',
24
+ ' addEventListener(type: string, listener: (event: { readonly data?: unknown }) => void): void',
25
+ ' close(): void',
26
+ '}',
27
+ '',
28
+ 'export type ElarionEventSourceFactory = (url: string) => ElarionEventSourceLike',
29
+ '',
30
+ 'export interface ElarionEventsOptions {',
31
+ ' readonly url: string | URL',
32
+ ' // Factory over the platform EventSource (defaults to globalThis.EventSource).',
33
+ ' readonly eventSource?: ElarionEventSourceFactory',
34
+ ' // Validate incoming payloads against the generated Zod payload schemas. Defaults to true.',
35
+ ' readonly validateEvents?: boolean',
36
+ ' // Called when an incoming event cannot be parsed or validated; the event is dropped either way.',
37
+ ' readonly onEventError?: (topic: RpcEventTopic, error: unknown) => void',
38
+ '}',
39
+ '',
40
+ 'export interface ElarionEventSubscribeOptions {',
41
+ ' // Subscribe the topic within an application-defined resource scope (e.g. "customer:42"); the server',
42
+ ' // authorizes it fail-closed. Omitted, the subscription observes the global scope plus your own user scope.',
43
+ ' readonly resource?: string',
44
+ '}',
45
+ '',
46
+ 'export type ElarionEventHandler<T extends RpcEventTopic> = (payload: RpcEventPayload<T>) => void',
47
+ 'export type ElarionUnsubscribe = () => void',
48
+ '',
49
+ '// Client events are at-most-once hints: payloads carry ids/refs, and the client converges by re-running',
50
+ '// queries. Treat every onConnected callback (initial connect and every automatic reconnect) as "you may',
51
+ '// have missed events" and re-query — with TanStack Query, invalidate.',
52
+ 'export interface ElarionEventsClient {',
53
+ ' subscribe<T extends RpcEventTopic>(topic: T, handler: ElarionEventHandler<T>): ElarionUnsubscribe',
54
+ ' subscribe<T extends RpcEventTopic>(',
55
+ ' topic: T,',
56
+ ' options: ElarionEventSubscribeOptions,',
57
+ ' handler: ElarionEventHandler<T>',
58
+ ' ): ElarionUnsubscribe',
59
+ ' // Fires on the server\'s elarion.connected control event — once per (re)connect.',
60
+ ' onConnected(handler: () => void): ElarionUnsubscribe',
61
+ ' close(): void',
62
+ '}',
63
+ '',
64
+ 'export interface ElarionEventTopicApi<T extends RpcEventTopic> {',
65
+ ' subscribe(handler: ElarionEventHandler<T>): ElarionUnsubscribe',
66
+ ' subscribe(options: ElarionEventSubscribeOptions, handler: ElarionEventHandler<T>): ElarionUnsubscribe',
67
+ '}',
68
+ '',
69
+ 'export interface ElarionEventsApi {',
70
+ ' readonly $client: ElarionEventsClient',
71
+ ...emitProperties(topicTree, 1),
72
+ '}',
73
+ '',
74
+ 'const rpcEventTopicNames = [',
75
+ ...options.topics.map((topic) => ` ${JSON.stringify(topic)},`),
76
+ '] as const satisfies readonly RpcEventTopic[]',
77
+ '',
78
+ "const CONNECTED_EVENT_TYPE = 'elarion.connected'",
79
+ '',
80
+ 'interface EventSubscriptionEntry {',
81
+ ' readonly topic: RpcEventTopic',
82
+ ' readonly resource?: string',
83
+ ' readonly handlers: Set<(payload: unknown) => void>',
84
+ '}',
85
+ '',
86
+ 'export function createElarionEvents(options: ElarionEventsOptions): ElarionEventsApi {',
87
+ ' const client = createElarionEventsClient(options)',
88
+ ' const topics: Record<string, unknown> = {}',
89
+ '',
90
+ ' for (const topic of rpcEventTopicNames) {',
91
+ ' setTopicPath(topics, topic, {',
92
+ ' subscribe: (',
93
+ ' optionsOrHandler: ElarionEventSubscribeOptions | ElarionEventHandler<typeof topic>,',
94
+ ' maybeHandler?: ElarionEventHandler<typeof topic>',
95
+ ' ) => client.subscribe(topic, optionsOrHandler as ElarionEventSubscribeOptions, maybeHandler as ElarionEventHandler<typeof topic>),',
96
+ ' })',
97
+ ' }',
98
+ '',
99
+ ' return { ...topics, $client: client } as unknown as ElarionEventsApi',
100
+ '}',
101
+ '',
102
+ 'export function createElarionEventsClient(options: ElarionEventsOptions): ElarionEventsClient {',
103
+ ' const createSource = options.eventSource ?? defaultEventSourceFactory',
104
+ ' const validateEvents = options.validateEvents ?? true',
105
+ '',
106
+ ' const subscriptions = new Map<string, EventSubscriptionEntry>()',
107
+ ' const connectedHandlers = new Set<() => void>()',
108
+ ' let source: ElarionEventSourceLike | undefined',
109
+ ' let reconnectScheduled = false',
110
+ ' let closed = false',
111
+ '',
112
+ ' // Subscription changes coalesce onto a microtask, so subscribing N topics during setup opens one',
113
+ ' // connection, not N. EventSource cannot change its URL, so a changed subscription set reconnects.',
114
+ ' function scheduleReconnect(): void {',
115
+ ' if (reconnectScheduled || closed) return',
116
+ ' reconnectScheduled = true',
117
+ ' queueMicrotask(() => {',
118
+ ' reconnectScheduled = false',
119
+ ' if (!closed) connect()',
120
+ ' })',
121
+ ' }',
122
+ '',
123
+ ' function connect(): void {',
124
+ ' source?.close()',
125
+ ' source = undefined',
126
+ ' if (subscriptions.size === 0) return',
127
+ '',
128
+ ' const requested = Array.from(subscriptions.values(), (entry) =>',
129
+ ' entry.resource === undefined ? { topic: entry.topic } : { topic: entry.topic, resource: entry.resource }',
130
+ ' ).sort(compareSubscriptions)',
131
+ '',
132
+ ' const next = createSource(appendSubscriptions(String(options.url), requested))',
133
+ ' for (const topic of new Set(Array.from(subscriptions.values(), (entry) => entry.topic))) {',
134
+ ' next.addEventListener(topic, (event) => dispatch(topic, event))',
135
+ ' }',
136
+ ' next.addEventListener(CONNECTED_EVENT_TYPE, () => {',
137
+ ' for (const handler of [...connectedHandlers]) handler()',
138
+ ' })',
139
+ ' source = next',
140
+ ' }',
141
+ '',
142
+ ' function dispatch(topic: RpcEventTopic, event: { readonly data?: unknown }): void {',
143
+ ' let payload: unknown',
144
+ ' try {',
145
+ ' payload = JSON.parse(String(event.data))',
146
+ ' if (validateEvents) {',
147
+ ' payload = rpcEventPayloadSchemas[topic].parse(payload)',
148
+ ' }',
149
+ ' } catch (error) {',
150
+ ' options.onEventError?.(topic, error)',
151
+ ' return',
152
+ ' }',
153
+ '',
154
+ ' // Handlers match by topic only: the server already filters by the connection\'s subscribed scopes,',
155
+ ' // but the wire frame carries no scope — handlers on the same topic see every matching event.',
156
+ ' for (const entry of subscriptions.values()) {',
157
+ ' if (entry.topic !== topic) continue',
158
+ ' for (const handler of [...entry.handlers]) handler(payload)',
159
+ ' }',
160
+ ' }',
161
+ '',
162
+ ' function subscribe<T extends RpcEventTopic>(',
163
+ ' topic: T,',
164
+ ' optionsOrHandler: ElarionEventSubscribeOptions | ElarionEventHandler<T>,',
165
+ ' maybeHandler?: ElarionEventHandler<T>',
166
+ ' ): ElarionUnsubscribe {',
167
+ " const subscribeOptions = typeof optionsOrHandler === 'function' ? undefined : optionsOrHandler",
168
+ " const handler = typeof optionsOrHandler === 'function' ? optionsOrHandler : maybeHandler",
169
+ " if (typeof handler !== 'function') {",
170
+ " throw new Error('subscribe requires a handler function.')",
171
+ ' }',
172
+ ' if (closed) {',
173
+ " throw new Error('The events client is closed.')",
174
+ ' }',
175
+ '',
176
+ ' const key = subscriptionKey(topic, subscribeOptions?.resource)',
177
+ ' let entry = subscriptions.get(key)',
178
+ ' if (entry === undefined) {',
179
+ ' entry = { topic, resource: subscribeOptions?.resource, handlers: new Set() }',
180
+ ' subscriptions.set(key, entry)',
181
+ ' scheduleReconnect()',
182
+ ' }',
183
+ '',
184
+ ' // A fresh wrapper per call, so subscribing the same function twice yields two live subscriptions.',
185
+ ' const wrapped = (payload: unknown) => handler(payload as RpcEventPayload<T>)',
186
+ ' entry.handlers.add(wrapped)',
187
+ ' const current = entry',
188
+ '',
189
+ ' let active = true',
190
+ ' return () => {',
191
+ ' if (!active) return',
192
+ ' active = false',
193
+ ' current.handlers.delete(wrapped)',
194
+ ' if (current.handlers.size === 0 && subscriptions.get(key) === current) {',
195
+ ' subscriptions.delete(key)',
196
+ ' scheduleReconnect()',
197
+ ' }',
198
+ ' }',
199
+ ' }',
200
+ '',
201
+ ' return {',
202
+ " subscribe: subscribe as ElarionEventsClient['subscribe'],",
203
+ ' onConnected(handler: () => void): ElarionUnsubscribe {',
204
+ ' connectedHandlers.add(handler)',
205
+ ' return () => {',
206
+ ' connectedHandlers.delete(handler)',
207
+ ' }',
208
+ ' },',
209
+ ' close(): void {',
210
+ ' closed = true',
211
+ ' source?.close()',
212
+ ' source = undefined',
213
+ ' subscriptions.clear()',
214
+ ' connectedHandlers.clear()',
215
+ ' },',
216
+ ' }',
217
+ '}',
218
+ '',
219
+ 'function defaultEventSourceFactory(url: string): ElarionEventSourceLike {',
220
+ ' const ctor = (globalThis as { EventSource?: new (url: string) => ElarionEventSourceLike }).EventSource',
221
+ ' if (!ctor) {',
222
+ " throw new Error('No EventSource implementation is available. Pass eventSource in createElarionEvents options.')",
223
+ ' }',
224
+ ' return new ctor(url)',
225
+ '}',
226
+ '',
227
+ '// The subscription set travels as one URL-encoded JSON query parameter (EventSource cannot POST), in a',
228
+ '// deterministic order so equal sets produce equal URLs.',
229
+ 'function appendSubscriptions(',
230
+ ' url: string,',
231
+ ' requested: readonly { readonly topic: string; readonly resource?: string }[]',
232
+ '): string {',
233
+ " const separator = url.includes('?') ? '&' : '?'",
234
+ ' return `${url}${separator}subscriptions=${encodeURIComponent(JSON.stringify(requested))}`',
235
+ '}',
236
+ '',
237
+ 'function compareSubscriptions(',
238
+ ' left: { readonly topic: string; readonly resource?: string },',
239
+ ' right: { readonly topic: string; readonly resource?: string }',
240
+ '): number {',
241
+ ' if (left.topic !== right.topic) {',
242
+ ' return left.topic < right.topic ? -1 : 1',
243
+ ' }',
244
+ " const leftResource = left.resource ?? ''",
245
+ " const rightResource = right.resource ?? ''",
246
+ ' if (leftResource === rightResource) return 0',
247
+ ' return leftResource < rightResource ? -1 : 1',
248
+ '}',
249
+ '',
250
+ 'function subscriptionKey(topic: string, resource: string | undefined): string {',
251
+ ' return resource === undefined ? topic : `${topic}\\u0000${resource}`',
252
+ '}',
253
+ '',
254
+ 'function setTopicPath(root: Record<string, unknown>, topic: string, value: unknown): void {',
255
+ " const segments = topic.split('.')",
256
+ ' let node = root',
257
+ '',
258
+ ' for (let index = 0; index < segments.length - 1; index += 1) {',
259
+ ' const segment = segments[index]',
260
+ ' const existing = node[segment]',
261
+ " if (typeof existing === 'object' && existing !== null) {",
262
+ ' node = existing as Record<string, unknown>',
263
+ ' continue',
264
+ ' }',
265
+ '',
266
+ ' const next: Record<string, unknown> = {}',
267
+ ' node[segment] = next',
268
+ ' node = next',
269
+ ' }',
270
+ '',
271
+ ' node[segments[segments.length - 1] ?? topic] = value',
272
+ '}',
273
+ '',
274
+ ].join('\n');
275
+ }
276
+ function buildTopicTree(topics) {
277
+ const root = createTopicTreeNode();
278
+ for (const topic of topics) {
279
+ const segments = topic.split('.');
280
+ let node = root;
281
+ for (const segment of segments) {
282
+ let child = node.children.get(segment);
283
+ if (!child) {
284
+ child = createTopicTreeNode();
285
+ node.children.set(segment, child);
286
+ }
287
+ node = child;
288
+ }
289
+ node.topic = topic;
290
+ }
291
+ return root;
292
+ }
293
+ function createTopicTreeNode() {
294
+ return { children: new Map() };
295
+ }
296
+ function emitProperties(node, indent) {
297
+ const pad = ' '.repeat(indent);
298
+ return Array.from(node.children.entries())
299
+ .sort(([left], [right]) => left.localeCompare(right))
300
+ .map(([segment, child]) => `${pad}readonly ${JSON.stringify(segment)}: ${emitNodeType(child, indent)}`);
301
+ }
302
+ function emitNodeType(node, indent) {
303
+ const childLines = emitProperties(node, indent + 1);
304
+ const childObject = childLines.length === 0
305
+ ? '{}'
306
+ : `{\n${childLines.join('\n')}\n${' '.repeat(indent)}}`;
307
+ if (node.topic && childLines.length > 0) {
308
+ return `ElarionEventTopicApi<${JSON.stringify(node.topic)}> & ${childObject}`;
309
+ }
310
+ if (node.topic) {
311
+ return `ElarionEventTopicApi<${JSON.stringify(node.topic)}>`;
312
+ }
313
+ return childObject;
314
+ }
package/dist/generate.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { generateEventsClientSource } from './events-client-source.js';
1
2
  import { jsonSchemaToTypeScript } from './json-schema-to-ts.js';
2
3
  import { jsonSchemaToZod } from './json-schema-to-zod.js';
3
4
  import { collectFilePaths, stripNullable } from './json-schema.js';
@@ -12,6 +13,7 @@ const DEFAULT_CLIENT_FILE = 'rpc-client.ts';
12
13
  const DEFAULT_SESSION_CLIENT_FILE = 'session-client.ts';
13
14
  const DEFAULT_SESSION_OPERATION = 'elarion.session';
14
15
  const DEFAULT_START_ADAPTER_FILE = 'start-adapter.ts';
16
+ const DEFAULT_EVENTS_CLIENT_FILE = 'events-client.ts';
15
17
  export { UnsupportedJsonSchemaError } from './json-schema.js';
16
18
  export function generateRpcClientFiles(schema, options = {}) {
17
19
  const generatedBy = options.generatedBy ?? DEFAULT_GENERATED_BY;
@@ -64,6 +66,21 @@ export function generateRpcClientFiles(schema, options = {}) {
64
66
  schemasLines.push('');
65
67
  schemasLines.push('export type RpcResultSchemas = typeof rpcResultSchemas');
66
68
  schemasLines.push('');
69
+ // Client-event payload schemas (ADR-0043), emitted only when the schema declares events so event-free
70
+ // schemas keep producing byte-identical output.
71
+ const eventTopics = Object.keys(schema.events ?? {}).sort();
72
+ if (eventTopics.length > 0) {
73
+ schemasLines.push('export const rpcEventPayloadSchemas = {');
74
+ for (const topic of eventTopics) {
75
+ const definition = schema.events[topic];
76
+ const zodSchema = jsonSchemaToZod(stripNullable(definition.payload), createContext(definition.payload, `events.${topic}.payload`), 1);
77
+ schemasLines.push(` ${JSON.stringify(topic)}: ${zodSchema},`);
78
+ }
79
+ schemasLines.push('} as const');
80
+ schemasLines.push('');
81
+ schemasLines.push('export type RpcEventPayloadSchemas = typeof rpcEventPayloadSchemas');
82
+ schemasLines.push('');
83
+ }
67
84
  const idempotentMethods = methods.filter((method) => schema.methods[method].idempotent === true);
68
85
  // Where each method carries file payloads (x-elarion-file nodes), so the client can convert between
69
86
  // native File values and the base64 wire envelope. Methods without files are omitted entirely.
@@ -104,6 +121,17 @@ export function generateRpcClientFiles(schema, options = {}) {
104
121
  capabilities: schema.capabilities,
105
122
  })
106
123
  : undefined;
124
+ // The typed client-event subscription client (ADR-0043) is emitted only when the schema declares an
125
+ // `events` block, so event-free schemas produce byte-identical output.
126
+ const eventsClientFileName = options.eventsClientFileName ?? DEFAULT_EVENTS_CLIENT_FILE;
127
+ const eventsClientSource = eventTopics.length > 0
128
+ ? generateEventsClientSource({
129
+ generatedBy,
130
+ sourceLabel,
131
+ schemasFileName,
132
+ topics: eventTopics,
133
+ })
134
+ : undefined;
107
135
  // The framework adapter (ADR-boundary: kept opt-in so the core client stays framework-neutral) is emitted only
108
136
  // when a framework is requested, so an unopted schema produces byte-identical output.
109
137
  const frameworkAdapterFileName = options.frameworkAdapterFileName ?? DEFAULT_START_ADAPTER_FILE;
@@ -122,6 +150,8 @@ export function generateRpcClientFiles(schema, options = {}) {
122
150
  sessionClientSource,
123
151
  frameworkAdapterFileName: frameworkAdapterSource === undefined ? undefined : frameworkAdapterFileName,
124
152
  frameworkAdapterSource,
153
+ eventsClientFileName: eventsClientSource === undefined ? undefined : eventsClientFileName,
154
+ eventsClientSource,
125
155
  };
126
156
  }
127
157
  function createContext(root, path) {
package/dist/schema.d.ts CHANGED
@@ -49,9 +49,18 @@ export interface RpcSchemaCapabilities {
49
49
  permissions?: RpcCapabilityPermission[];
50
50
  roles?: string[];
51
51
  }
52
+ /**
53
+ * One client-event topic from the schema's `events` block (ADR-0043): the payload contract published on the
54
+ * topic. Delivery is at-most-once (SSE) — payloads are hints carrying ids/refs, not state.
55
+ */
56
+ export interface RpcEventSchema {
57
+ payload: JsonSchema;
58
+ }
52
59
  export interface RpcSchema {
53
60
  methods: Record<string, RpcMethodSchema>;
54
61
  capabilities?: RpcSchemaCapabilities;
62
+ /** Client-event topics by name, emitted by the server exporter when the host declares any (ADR-0043). */
63
+ events?: Record<string, RpcEventSchema>;
55
64
  }
56
65
  export interface GenerateRpcClientOptions {
57
66
  generatedBy?: string;
@@ -63,6 +72,8 @@ export interface GenerateRpcClientOptions {
63
72
  sessionClientFileName?: string;
64
73
  /** The operation name the client-capability snapshot is served under (default `elarion.session`). */
65
74
  sessionOperationName?: string;
75
+ /** Output filename for the client-event subscription client (default `events-client.ts`; emitted only when the schema declares events). */
76
+ eventsClientFileName?: string;
66
77
  /**
67
78
  * Emit an opt-in framework adapter alongside the neutral core client. `tanstack-start` produces
68
79
  * `start-adapter.ts` (request-scoped cookie forwarding via `@tanstack/react-start`). Omitted → no adapter,
@@ -92,4 +103,10 @@ export interface GeneratedRpcClientFiles {
92
103
  */
93
104
  frameworkAdapterFileName?: string;
94
105
  frameworkAdapterSource?: string;
106
+ /**
107
+ * The typed client-event subscription client (ADR-0043). Present only when the schema declares an `events`
108
+ * block; otherwise both fields are `undefined` and every other output stays byte-identical.
109
+ */
110
+ eventsClientFileName?: string;
111
+ eventsClientSource?: string;
95
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-jsonrpc-client-generator",
3
- "version": "0.2.3-preview.83.1",
3
+ "version": "0.2.3-preview.84.1",
4
4
  "description": "Generate TypeScript RPC contracts, Zod schemas, and a fetch client from Elarion JSON-RPC schema exports.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",