@swimmesberger/elarion-jsonrpc-client-generator 0.2.22-preview.190.1 → 0.3.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.
package/README.md CHANGED
@@ -87,7 +87,9 @@ When the schema carries an `events` block (the host registers Elarion client-eve
87
87
  additionally emits `events-client.ts`: `createElarionEvents(...)` multiplexes topic-typed subscriptions over one
88
88
  `EventSource`, validates incoming payloads with the generated Zod schemas (`rpcEventPayloadSchemas`), and exposes
89
89
  `$client.onConnected(...)` — fired on connect and every reconnect — as the "you may have missed events, re-query"
90
- signal. Event-free schemas produce byte-identical output with no events file.
90
+ signal. The platform `EventSource` stops retrying when an answer is not a 200 event stream (a 401, a login page, a
91
+ 502 during a restart), so the client reopens such a connection itself, backing off from 1 s to 30 s
92
+ (`reconnectDelay`). Event-free schemas produce byte-identical output with no events file.
91
93
 
92
94
  ```ts
93
95
  import { createElarionEvents } from './generated/events-client'
@@ -23,6 +23,8 @@ export function generateEventsClientSource(options) {
23
23
  'export interface ElarionEventSourceLike {',
24
24
  ' addEventListener(type: string, listener: (event: { readonly data?: unknown }) => void): void',
25
25
  ' close(): void',
26
+ ' // 2 (CLOSED) once the source gave up for good; a source without it is never reconnected by the client.',
27
+ ' readonly readyState?: number',
26
28
  '}',
27
29
  '',
28
30
  'export type ElarionEventSourceFactory = (url: string) => ElarionEventSourceLike',
@@ -35,6 +37,9 @@ export function generateEventsClientSource(options) {
35
37
  ' readonly validateEvents?: boolean',
36
38
  ' // Called when an incoming event cannot be parsed or validated; the event is dropped either way.',
37
39
  ' readonly onEventError?: (topic: RpcEventTopic, error: unknown) => void',
40
+ ' // Milliseconds to wait before reopening a connection the platform EventSource gave up on, by attempt',
41
+ ' // (0-based, reset by every elarion.connected). Defaults to 1 s doubling up to 30 s.',
42
+ ' readonly reconnectDelay?: (attempt: number) => number',
38
43
  '}',
39
44
  '',
40
45
  'export interface ElarionEventSubscribeOptions {',
@@ -76,6 +81,8 @@ export function generateEventsClientSource(options) {
76
81
  '] as const satisfies readonly RpcEventTopic[]',
77
82
  '',
78
83
  "const CONNECTED_EVENT_TYPE = 'elarion.connected'",
84
+ '// EventSource.CLOSED — a literal, so the client needs no EventSource global to compare against.',
85
+ 'const EVENT_SOURCE_CLOSED = 2',
79
86
  '',
80
87
  'interface EventSubscriptionEntry {',
81
88
  ' readonly topic: RpcEventTopic',
@@ -102,11 +109,14 @@ export function generateEventsClientSource(options) {
102
109
  'export function createElarionEventsClient(options: ElarionEventsOptions): ElarionEventsClient {',
103
110
  ' const createSource = options.eventSource ?? defaultEventSourceFactory',
104
111
  ' const validateEvents = options.validateEvents ?? true',
112
+ ' const reconnectDelay = options.reconnectDelay ?? defaultReconnectDelay',
105
113
  '',
106
114
  ' const subscriptions = new Map<string, EventSubscriptionEntry>()',
107
115
  ' const connectedHandlers = new Set<() => void>()',
108
116
  ' let source: ElarionEventSourceLike | undefined',
109
117
  ' let reconnectScheduled = false',
118
+ ' let retryTimer: ReturnType<typeof setTimeout> | undefined',
119
+ ' let failedAttempts = 0',
110
120
  ' let closed = false',
111
121
  '',
112
122
  ' // Subscription changes coalesce onto a microtask, so subscribing N topics during setup opens one',
@@ -120,7 +130,25 @@ export function generateEventsClientSource(options) {
120
130
  ' })',
121
131
  ' }',
122
132
  '',
133
+ ' // The platform EventSource retries a dropped connection by itself, but fails it for good (readyState',
134
+ ' // CLOSED, no further retries) when the answer is not a 200 event stream: an expired session\'s 401 or',
135
+ ' // login page, a 502 while the server restarts. Without a retry here the client would stay silent until',
136
+ ' // the page reloads.',
137
+ ' function scheduleRetry(): void {',
138
+ ' if (retryTimer !== undefined || closed) return',
139
+ ' const delay = reconnectDelay(failedAttempts)',
140
+ ' failedAttempts += 1',
141
+ ' retryTimer = setTimeout(() => {',
142
+ ' retryTimer = undefined',
143
+ ' if (!closed) connect()',
144
+ ' }, delay)',
145
+ ' }',
146
+ '',
123
147
  ' function connect(): void {',
148
+ ' if (retryTimer !== undefined) {',
149
+ ' clearTimeout(retryTimer)',
150
+ ' retryTimer = undefined',
151
+ ' }',
124
152
  ' source?.close()',
125
153
  ' source = undefined',
126
154
  ' if (subscriptions.size === 0) return',
@@ -134,8 +162,12 @@ export function generateEventsClientSource(options) {
134
162
  ' next.addEventListener(topic, (event) => dispatch(topic, event))',
135
163
  ' }',
136
164
  ' next.addEventListener(CONNECTED_EVENT_TYPE, () => {',
165
+ ' failedAttempts = 0',
137
166
  ' for (const handler of [...connectedHandlers]) handler()',
138
167
  ' })',
168
+ " next.addEventListener('error', () => {",
169
+ ' if (source === next && next.readyState === EVENT_SOURCE_CLOSED) scheduleRetry()',
170
+ ' })',
139
171
  ' source = next',
140
172
  ' }',
141
173
  '',
@@ -208,6 +240,8 @@ export function generateEventsClientSource(options) {
208
240
  ' },',
209
241
  ' close(): void {',
210
242
  ' closed = true',
243
+ ' if (retryTimer !== undefined) clearTimeout(retryTimer)',
244
+ ' retryTimer = undefined',
211
245
  ' source?.close()',
212
246
  ' source = undefined',
213
247
  ' subscriptions.clear()',
@@ -216,6 +250,10 @@ export function generateEventsClientSource(options) {
216
250
  ' }',
217
251
  '}',
218
252
  '',
253
+ 'function defaultReconnectDelay(attempt: number): number {',
254
+ ' return Math.min(30_000, 1_000 * 2 ** attempt)',
255
+ '}',
256
+ '',
219
257
  'function defaultEventSourceFactory(url: string): ElarionEventSourceLike {',
220
258
  ' const ctor = (globalThis as { EventSource?: new (url: string) => ElarionEventSourceLike }).EventSource',
221
259
  ' if (!ctor) {',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-jsonrpc-client-generator",
3
- "version": "0.2.22-preview.190.1",
3
+ "version": "0.3.0",
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",