@swimmesberger/elarion-jsonrpc-client-generator 0.2.3-preview.40.1 → 0.2.3-preview.41.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/dist/generate.js CHANGED
@@ -49,12 +49,14 @@ export function generateRpcClientFiles(schema, options = {}) {
49
49
  schemasLines.push('');
50
50
  schemasLines.push('export type RpcResultSchemas = typeof rpcResultSchemas');
51
51
  schemasLines.push('');
52
+ const idempotentMethods = methods.filter((method) => schema.methods[method].idempotent === true);
52
53
  const clientSource = generateRpcClientSource({
53
54
  generatedBy,
54
55
  sourceLabel,
55
56
  typesFileName,
56
57
  schemasFileName,
57
58
  methods,
59
+ idempotentMethods,
58
60
  });
59
61
  return {
60
62
  methodCount: methods.length,
@@ -4,6 +4,7 @@ interface GenerateRpcClientSourceOptions {
4
4
  typesFileName: string;
5
5
  schemasFileName: string;
6
6
  methods: readonly string[];
7
+ idempotentMethods: readonly string[];
7
8
  }
8
9
  export declare function generateRpcClientSource(options: GenerateRpcClientSourceOptions): string;
9
10
  export {};
@@ -1,3 +1,6 @@
1
+ // The framework-owned params._meta key that carries the idempotency key (matches the server's
2
+ // IdempotencyKeyNames.MetaKey).
3
+ const IDEMPOTENCY_META_KEY = 'dev.wimmesberger.elarion/idempotencyKey';
1
4
  export function generateRpcClientSource(options) {
2
5
  const typesImport = moduleSpecifier(options.typesFileName);
3
6
  const schemasImport = moduleSpecifier(options.schemasFileName);
@@ -42,6 +45,17 @@ export function generateRpcClientSource(options) {
42
45
  ' startSpan(context: RpcRequestContext): RpcClientSpan | undefined',
43
46
  '}',
44
47
  '',
48
+ '// Automatic idempotency-key attachment. For operations the server marked [Idempotent], the client puts a',
49
+ '// generated key in `params._meta` so a retry is deduplicated server-side. On by default; note that any',
50
+ '// retry layer above this client (e.g. TanStack Query) must own key stability — generate the key once at the',
51
+ '// operation boundary and pass it via `RpcRequestOptions.idempotencyKey`, so all retries reuse the same key.',
52
+ 'export interface RpcIdempotencyOptions {',
53
+ ' // Attach a key by default to idempotent operations. Defaults to true.',
54
+ ' readonly enabled?: boolean',
55
+ ' // Key factory (defaults to crypto.randomUUID with a fallback). Called once per logical call.',
56
+ ' readonly generateKey?: () => string',
57
+ '}',
58
+ '',
45
59
  'export interface RpcClientOptions {',
46
60
  ' readonly url: string | URL',
47
61
  ' readonly fetch?: RpcFetch',
@@ -50,16 +64,22 @@ export function generateRpcClientSource(options) {
50
64
  ' readonly validateResults?: boolean',
51
65
  ' readonly transformResult?: <M extends RpcMethod>(method: M, result: unknown) => unknown',
52
66
  ' readonly instrumentation?: RpcInstrumentation',
67
+ ' readonly idempotency?: RpcIdempotencyOptions',
53
68
  '}',
54
69
  '',
55
70
  'export interface RpcRequestOptions {',
56
71
  ' readonly signal?: AbortSignal',
57
72
  ' readonly headers?: RpcHeadersInit',
73
+ ' // Override the idempotency key for this call: a fixed key (reuse across your own retries), or `false` to',
74
+ ' // send no key. When omitted, an idempotent operation gets an auto-generated key.',
75
+ ' readonly idempotencyKey?: string | false',
58
76
  '}',
59
77
  '',
60
78
  'export interface BatchItem<M extends RpcMethod = RpcMethod> {',
61
79
  ' readonly method: M',
62
80
  ' readonly params: RpcParams<M>',
81
+ ' // Per-item idempotency key override (see RpcRequestOptions.idempotencyKey).',
82
+ ' readonly idempotencyKey?: string | false',
63
83
  '}',
64
84
  '',
65
85
  'export type BatchItemResult<M extends RpcMethod = RpcMethod> =',
@@ -85,6 +105,8 @@ export function generateRpcClientSource(options) {
85
105
  '}',
86
106
  '',
87
107
  ...generateMethodNameLines(options.methods),
108
+ `const rpcIdempotentMethods: ReadonlySet<RpcMethod> = new Set(${JSON.stringify(options.idempotentMethods)})`,
109
+ '',
88
110
  'type JsonRpcId = string | number',
89
111
  '',
90
112
  'interface JsonRpcRequestEnvelope<M extends RpcMethod = RpcMethod> {',
@@ -190,6 +212,17 @@ export function generateRpcClientSource(options) {
190
212
  ' let nextId = 1',
191
213
  ' const createId = options.idGenerator ?? (() => globalThis.crypto?.randomUUID?.() ?? String(nextId++))',
192
214
  ' const validateResults = options.validateResults ?? true',
215
+ ' const idempotencyEnabled = options.idempotency?.enabled ?? true',
216
+ ' const generateIdempotencyKey = options.idempotency?.generateKey ?? defaultIdempotencyKey',
217
+ '',
218
+ ' function resolveIdempotencyKey<M extends RpcMethod>(',
219
+ ' method: M,',
220
+ ' override: string | false | undefined',
221
+ ' ): string | undefined {',
222
+ ' if (override === false) return undefined',
223
+ " if (typeof override === 'string') return override",
224
+ ' return idempotencyEnabled && rpcIdempotentMethods.has(method) ? generateIdempotencyKey() : undefined',
225
+ ' }',
193
226
  '',
194
227
  ' async function post(',
195
228
  ' payload: unknown,',
@@ -226,7 +259,8 @@ export function generateRpcClientSource(options) {
226
259
  ' params: RpcParams<M>,',
227
260
  ' requestOptions?: RpcRequestOptions',
228
261
  ' ): Promise<RpcResult<M>> {',
229
- ' const request = createRequest(method, params, createId())',
262
+ ' const idempotencyKey = resolveIdempotencyKey(method, requestOptions?.idempotencyKey)',
263
+ ' const request = createRequest(method, withIdempotencyKey(params, idempotencyKey), createId())',
230
264
  ' const context: RpcRequestContext = { methods: [method], batch: false }',
231
265
  ' const span = options.instrumentation?.startSpan(context)',
232
266
  ' try {',
@@ -257,7 +291,13 @@ export function generateRpcClientSource(options) {
257
291
  ' return [] as unknown as BatchResult<T>',
258
292
  ' }',
259
293
  '',
260
- ' const requests = items.map((item) => createRequest(item.method, item.params, createId()))',
294
+ ' const requests = items.map((item) =>',
295
+ ' createRequest(',
296
+ ' item.method,',
297
+ ' withIdempotencyKey(item.params, resolveIdempotencyKey(item.method, item.idempotencyKey)),',
298
+ ' createId()',
299
+ ' )',
300
+ ' )',
261
301
  ' const context: RpcRequestContext = { methods: items.map((item) => item.method), batch: true }',
262
302
  ' const span = options.instrumentation?.startSpan(context)',
263
303
  ' try {',
@@ -309,10 +349,28 @@ export function generateRpcClientSource(options) {
309
349
  '',
310
350
  'function createRequest<M extends RpcMethod>(',
311
351
  ' method: M,',
312
- ' params: RpcParams<M>,',
352
+ ' params: unknown,',
313
353
  ' id: JsonRpcId',
314
354
  '): JsonRpcRequestEnvelope<M> {',
315
- " return { jsonrpc: '2.0', method, params, id }",
355
+ " return { jsonrpc: '2.0', method, params: params as RpcParams<M>, id }",
356
+ '}',
357
+ '',
358
+ 'function defaultIdempotencyKey(): string {',
359
+ ' const uuid = globalThis.crypto?.randomUUID?.()',
360
+ ' if (uuid) return uuid',
361
+ ' return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`',
362
+ '}',
363
+ '',
364
+ '// Merges the idempotency key into params._meta without mutating the input object; no-op when no key.',
365
+ 'function withIdempotencyKey(params: unknown, key: string | undefined): unknown {',
366
+ ' if (key === undefined) return params',
367
+ " const base = params && typeof params === 'object' && !Array.isArray(params)",
368
+ ' ? (params as Record<string, unknown>)',
369
+ ' : {}',
370
+ " const existingMeta = base['_meta'] && typeof base['_meta'] === 'object' && !Array.isArray(base['_meta'])",
371
+ " ? (base['_meta'] as Record<string, unknown>)",
372
+ ' : {}',
373
+ ` return { ...base, _meta: { ...existingMeta, [${JSON.stringify(IDEMPOTENCY_META_KEY)}]: key } }`,
316
374
  '}',
317
375
  '',
318
376
  'function setApiPath(root: Record<string, unknown>, method: string, value: unknown): void {',
package/dist/schema.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface JsonSchema {
14
14
  export interface RpcMethodSchema {
15
15
  params: JsonSchema;
16
16
  result: JsonSchema;
17
+ idempotent?: boolean;
17
18
  }
18
19
  export interface RpcSchema {
19
20
  methods: Record<string, RpcMethodSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-jsonrpc-client-generator",
3
- "version": "0.2.3-preview.40.1",
3
+ "version": "0.2.3-preview.41.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",