@vishalsharma7nov/react-native-proto 0.2.2

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 (190) hide show
  1. package/android/src/main/java/com/vishalsharma7nov/reactnativeproto/ReactNativeProtoNativeGrpcModule.java +142 -0
  2. package/app.plugin.js +63 -0
  3. package/bin/react-native-proto.js +956 -0
  4. package/ios/ReactNativeProtoNativeGrpc.m +129 -0
  5. package/lib/commonjs/auth.js +72 -0
  6. package/lib/commonjs/auth.js.map +1 -0
  7. package/lib/commonjs/client-factory.js +81 -0
  8. package/lib/commonjs/client-factory.js.map +1 -0
  9. package/lib/commonjs/codecs.js +41 -0
  10. package/lib/commonjs/codecs.js.map +1 -0
  11. package/lib/commonjs/connect/envelope.js +100 -0
  12. package/lib/commonjs/connect/envelope.js.map +1 -0
  13. package/lib/commonjs/connect/errors.js +71 -0
  14. package/lib/commonjs/connect/errors.js.map +1 -0
  15. package/lib/commonjs/errors.js +54 -0
  16. package/lib/commonjs/errors.js.map +1 -0
  17. package/lib/commonjs/generated/create-api.js +29 -0
  18. package/lib/commonjs/generated/create-api.js.map +1 -0
  19. package/lib/commonjs/generated/message-types.js +2 -0
  20. package/lib/commonjs/generated/message-types.js.map +1 -0
  21. package/lib/commonjs/generated/messages.js +47 -0
  22. package/lib/commonjs/generated/messages.js.map +1 -0
  23. package/lib/commonjs/generated/messages.pb.js +2787 -0
  24. package/lib/commonjs/generated/messages.pb.js.map +1 -0
  25. package/lib/commonjs/generated/method-map.js +68 -0
  26. package/lib/commonjs/generated/method-map.js.map +1 -0
  27. package/lib/commonjs/index.js +283 -0
  28. package/lib/commonjs/index.js.map +1 -0
  29. package/lib/commonjs/interceptors.js +19 -0
  30. package/lib/commonjs/interceptors.js.map +1 -0
  31. package/lib/commonjs/logging.js +54 -0
  32. package/lib/commonjs/logging.js.map +1 -0
  33. package/lib/commonjs/metro/index.js +35 -0
  34. package/lib/commonjs/metro/index.js.map +1 -0
  35. package/lib/commonjs/metro/transformer.js +33 -0
  36. package/lib/commonjs/metro/transformer.js.map +1 -0
  37. package/lib/commonjs/mock-server/index.js +125 -0
  38. package/lib/commonjs/mock-server/index.js.map +1 -0
  39. package/lib/commonjs/native-grpc/index.js +242 -0
  40. package/lib/commonjs/native-grpc/index.js.map +1 -0
  41. package/lib/commonjs/offline-queue.js +137 -0
  42. package/lib/commonjs/offline-queue.js.map +1 -0
  43. package/lib/commonjs/package.json +1 -0
  44. package/lib/commonjs/path-templates.js +44 -0
  45. package/lib/commonjs/path-templates.js.map +1 -0
  46. package/lib/commonjs/react-query/index.js +30 -0
  47. package/lib/commonjs/react-query/index.js.map +1 -0
  48. package/lib/commonjs/streaming.js +251 -0
  49. package/lib/commonjs/streaming.js.map +1 -0
  50. package/lib/commonjs/transport.js +323 -0
  51. package/lib/commonjs/transport.js.map +1 -0
  52. package/lib/commonjs/types.js +6 -0
  53. package/lib/commonjs/types.js.map +1 -0
  54. package/lib/commonjs/validation/zod.js +48 -0
  55. package/lib/commonjs/validation/zod.js.map +1 -0
  56. package/lib/commonjs/version-check.js +102 -0
  57. package/lib/commonjs/version-check.js.map +1 -0
  58. package/lib/module/auth.js +67 -0
  59. package/lib/module/auth.js.map +1 -0
  60. package/lib/module/client-factory.js +77 -0
  61. package/lib/module/client-factory.js.map +1 -0
  62. package/lib/module/codecs.js +36 -0
  63. package/lib/module/codecs.js.map +1 -0
  64. package/lib/module/connect/envelope.js +93 -0
  65. package/lib/module/connect/envelope.js.map +1 -0
  66. package/lib/module/connect/errors.js +65 -0
  67. package/lib/module/connect/errors.js.map +1 -0
  68. package/lib/module/errors.js +46 -0
  69. package/lib/module/errors.js.map +1 -0
  70. package/lib/module/generated/create-api.js +13 -0
  71. package/lib/module/generated/create-api.js.map +1 -0
  72. package/lib/module/generated/message-types.js +2 -0
  73. package/lib/module/generated/message-types.js.map +1 -0
  74. package/lib/module/generated/messages.js +43 -0
  75. package/lib/module/generated/messages.js.map +1 -0
  76. package/lib/module/generated/messages.pb.js +2787 -0
  77. package/lib/module/generated/messages.pb.js.map +1 -0
  78. package/lib/module/generated/method-map.js +64 -0
  79. package/lib/module/generated/method-map.js.map +1 -0
  80. package/lib/module/index.js +53 -0
  81. package/lib/module/index.js.map +1 -0
  82. package/lib/module/interceptors.js +15 -0
  83. package/lib/module/interceptors.js.map +1 -0
  84. package/lib/module/logging.js +50 -0
  85. package/lib/module/logging.js.map +1 -0
  86. package/lib/module/metro/index.js +25 -0
  87. package/lib/module/metro/index.js.map +1 -0
  88. package/lib/module/metro/transformer.js +29 -0
  89. package/lib/module/metro/transformer.js.map +1 -0
  90. package/lib/module/mock-server/index.js +120 -0
  91. package/lib/module/mock-server/index.js.map +1 -0
  92. package/lib/module/native-grpc/index.js +233 -0
  93. package/lib/module/native-grpc/index.js.map +1 -0
  94. package/lib/module/offline-queue.js +132 -0
  95. package/lib/module/offline-queue.js.map +1 -0
  96. package/lib/module/package.json +1 -0
  97. package/lib/module/path-templates.js +39 -0
  98. package/lib/module/path-templates.js.map +1 -0
  99. package/lib/module/react-query/index.js +25 -0
  100. package/lib/module/react-query/index.js.map +1 -0
  101. package/lib/module/streaming.js +246 -0
  102. package/lib/module/streaming.js.map +1 -0
  103. package/lib/module/transport.js +316 -0
  104. package/lib/module/transport.js.map +1 -0
  105. package/lib/module/types.js +4 -0
  106. package/lib/module/types.js.map +1 -0
  107. package/lib/module/validation/zod.js +43 -0
  108. package/lib/module/validation/zod.js.map +1 -0
  109. package/lib/module/version-check.js +93 -0
  110. package/lib/module/version-check.js.map +1 -0
  111. package/lib/typescript/src/auth.d.ts +26 -0
  112. package/lib/typescript/src/auth.d.ts.map +1 -0
  113. package/lib/typescript/src/client-factory.d.ts +7 -0
  114. package/lib/typescript/src/client-factory.d.ts.map +1 -0
  115. package/lib/typescript/src/codecs.d.ts +5 -0
  116. package/lib/typescript/src/codecs.d.ts.map +1 -0
  117. package/lib/typescript/src/connect/envelope.d.ts +18 -0
  118. package/lib/typescript/src/connect/envelope.d.ts.map +1 -0
  119. package/lib/typescript/src/connect/errors.d.ts +11 -0
  120. package/lib/typescript/src/connect/errors.d.ts.map +1 -0
  121. package/lib/typescript/src/errors.d.ts +21 -0
  122. package/lib/typescript/src/errors.d.ts.map +1 -0
  123. package/lib/typescript/src/generated/create-api.d.ts +34 -0
  124. package/lib/typescript/src/generated/create-api.d.ts.map +1 -0
  125. package/lib/typescript/src/generated/message-types.d.ts +54 -0
  126. package/lib/typescript/src/generated/message-types.d.ts.map +1 -0
  127. package/lib/typescript/src/generated/messages.d.ts +17 -0
  128. package/lib/typescript/src/generated/messages.d.ts.map +1 -0
  129. package/lib/typescript/src/generated/method-map.d.ts +7 -0
  130. package/lib/typescript/src/generated/method-map.d.ts.map +1 -0
  131. package/lib/typescript/src/index.d.ts +53 -0
  132. package/lib/typescript/src/index.d.ts.map +1 -0
  133. package/lib/typescript/src/interceptors.d.ts +17 -0
  134. package/lib/typescript/src/interceptors.d.ts.map +1 -0
  135. package/lib/typescript/src/logging.d.ts +16 -0
  136. package/lib/typescript/src/logging.d.ts.map +1 -0
  137. package/lib/typescript/src/metro/index.d.ts +15 -0
  138. package/lib/typescript/src/metro/index.d.ts.map +1 -0
  139. package/lib/typescript/src/metro/transformer.d.ts +16 -0
  140. package/lib/typescript/src/metro/transformer.d.ts.map +1 -0
  141. package/lib/typescript/src/mock-server/index.d.ts +27 -0
  142. package/lib/typescript/src/mock-server/index.d.ts.map +1 -0
  143. package/lib/typescript/src/native-grpc/index.d.ts +36 -0
  144. package/lib/typescript/src/native-grpc/index.d.ts.map +1 -0
  145. package/lib/typescript/src/offline-queue.d.ts +47 -0
  146. package/lib/typescript/src/offline-queue.d.ts.map +1 -0
  147. package/lib/typescript/src/path-templates.d.ts +17 -0
  148. package/lib/typescript/src/path-templates.d.ts.map +1 -0
  149. package/lib/typescript/src/react-query/index.d.ts +14 -0
  150. package/lib/typescript/src/react-query/index.d.ts.map +1 -0
  151. package/lib/typescript/src/streaming.d.ts +10 -0
  152. package/lib/typescript/src/streaming.d.ts.map +1 -0
  153. package/lib/typescript/src/transport.d.ts +19 -0
  154. package/lib/typescript/src/transport.d.ts.map +1 -0
  155. package/lib/typescript/src/types.d.ts +78 -0
  156. package/lib/typescript/src/types.d.ts.map +1 -0
  157. package/lib/typescript/src/validation/zod.d.ts +33 -0
  158. package/lib/typescript/src/validation/zod.d.ts.map +1 -0
  159. package/lib/typescript/src/version-check.d.ts +40 -0
  160. package/lib/typescript/src/version-check.d.ts.map +1 -0
  161. package/metro/index.js +23 -0
  162. package/metro/transformer.js +21 -0
  163. package/package.json +144 -0
  164. package/react-native-proto.podspec +15 -0
  165. package/src/auth.ts +97 -0
  166. package/src/client-factory.ts +109 -0
  167. package/src/codecs.ts +42 -0
  168. package/src/connect/envelope.ts +112 -0
  169. package/src/connect/errors.ts +86 -0
  170. package/src/errors.ts +81 -0
  171. package/src/generated/create-api.ts +40 -0
  172. package/src/generated/message-types.ts +63 -0
  173. package/src/generated/messages.pb.js +3207 -0
  174. package/src/generated/messages.ts +45 -0
  175. package/src/generated/method-map.ts +71 -0
  176. package/src/index.ts +113 -0
  177. package/src/interceptors.ts +32 -0
  178. package/src/logging.ts +67 -0
  179. package/src/metro/index.ts +27 -0
  180. package/src/metro/transformer.ts +38 -0
  181. package/src/mock-server/index.ts +164 -0
  182. package/src/native-grpc/index.ts +302 -0
  183. package/src/offline-queue.ts +160 -0
  184. package/src/path-templates.ts +65 -0
  185. package/src/react-query/index.ts +40 -0
  186. package/src/streaming.ts +326 -0
  187. package/src/transport.ts +427 -0
  188. package/src/types.ts +94 -0
  189. package/src/validation/zod.ts +75 -0
  190. package/src/version-check.ts +121 -0
@@ -0,0 +1,427 @@
1
+ import { ProtoClientError, wrapUnknownError } from "./errors";
2
+ import { tryParseConnectError } from "./connect/errors";
3
+ import {
4
+ composeInterceptors,
5
+ type CallContext,
6
+ type Interceptor,
7
+ } from "./interceptors";
8
+ import { createPathForMethod } from "./path-templates";
9
+ import type {
10
+ HeaderMap,
11
+ MethodMapEntry,
12
+ ProtoClientConfig,
13
+ UnaryCallInput,
14
+ } from "./types";
15
+ import type { CodecRegistry } from "./types";
16
+
17
+ const DEFAULT_TIMEOUT_MS = 30_000;
18
+ const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
19
+ const MAX_HEADER_ENTRIES = 64;
20
+
21
+ export type ResolvedTransportConfig = {
22
+ baseUrl: string;
23
+ timeoutMs: number;
24
+ maxResponseBytes: number;
25
+ transport: NonNullable<ProtoClientConfig["transport"]>;
26
+ getHeaders?: ProtoClientConfig["getHeaders"];
27
+ fetchImpl: typeof fetch;
28
+ onError?: ProtoClientConfig["onError"];
29
+ pathForMethod: (entry: MethodMapEntry) => string;
30
+ interceptors: Interceptor[];
31
+ retryOnUnauthorized: boolean;
32
+ };
33
+
34
+ export function resolveConfig(
35
+ config: ProtoClientConfig,
36
+ ): ResolvedTransportConfig {
37
+ if (!config || typeof config !== "object") {
38
+ throw new ProtoClientError({
39
+ code: "INVALID_CONFIG",
40
+ message: "createClient requires a config object",
41
+ });
42
+ }
43
+
44
+ const baseUrl =
45
+ typeof config.baseUrl === "string" ? config.baseUrl.trim() : "";
46
+ if (!baseUrl) {
47
+ throw new ProtoClientError({
48
+ code: "INVALID_CONFIG",
49
+ message: "baseUrl is required and must be a non-empty string",
50
+ });
51
+ }
52
+
53
+ try {
54
+ // Throws if baseUrl is not absolute
55
+ // eslint-disable-next-line no-new
56
+ new URL(baseUrl);
57
+ } catch (cause) {
58
+ throw new ProtoClientError({
59
+ code: "INVALID_CONFIG",
60
+ message: `baseUrl must be an absolute URL, received: ${baseUrl}`,
61
+ cause,
62
+ });
63
+ }
64
+
65
+ const timeoutMs =
66
+ typeof config.timeoutMs === "number" && config.timeoutMs > 0
67
+ ? Math.min(config.timeoutMs, 600_000)
68
+ : DEFAULT_TIMEOUT_MS;
69
+
70
+ const maxResponseBytes =
71
+ typeof config.maxResponseBytes === "number" && config.maxResponseBytes > 0
72
+ ? Math.min(config.maxResponseBytes, 50 * 1024 * 1024)
73
+ : DEFAULT_MAX_RESPONSE_BYTES;
74
+
75
+ const transport = config.transport ?? "http";
76
+ if (
77
+ transport !== "http" &&
78
+ transport !== "connect" &&
79
+ transport !== "native-grpc"
80
+ ) {
81
+ throw new ProtoClientError({
82
+ code: "INVALID_CONFIG",
83
+ message: `Unsupported transport: ${String(transport)}`,
84
+ });
85
+ }
86
+
87
+ const fetchImpl = config.fetch ?? globalThis.fetch;
88
+ if (typeof fetchImpl !== "function") {
89
+ throw new ProtoClientError({
90
+ code: "INVALID_CONFIG",
91
+ message: "fetch is not available; pass config.fetch",
92
+ });
93
+ }
94
+
95
+ const pathForMethod =
96
+ config.pathForMethod ?? createPathForMethod(config.pathPreset ?? "connect");
97
+
98
+ return {
99
+ baseUrl: baseUrl.replace(/\/$/, ""),
100
+ timeoutMs,
101
+ maxResponseBytes,
102
+ transport,
103
+ getHeaders: config.getHeaders,
104
+ fetchImpl,
105
+ onError: config.onError,
106
+ pathForMethod,
107
+ interceptors: config.interceptors ?? [],
108
+ retryOnUnauthorized: config.retryOnUnauthorized !== false,
109
+ };
110
+ }
111
+
112
+ export async function buildHeaders(
113
+ config: ResolvedTransportConfig,
114
+ contentType: string,
115
+ rpcPath: string,
116
+ extra?: HeaderMap,
117
+ ): Promise<Record<string, string>> {
118
+ const headers: Record<string, string> = {
119
+ "Content-Type": contentType,
120
+ Accept: contentType,
121
+ };
122
+
123
+ const merge = (source: HeaderMap | undefined) => {
124
+ if (!source || typeof source !== "object") return;
125
+ const keys = Object.keys(source);
126
+ if (keys.length > MAX_HEADER_ENTRIES) {
127
+ throw new ProtoClientError({
128
+ code: "INVALID_ARGUMENT",
129
+ message: `Too many headers (max ${MAX_HEADER_ENTRIES})`,
130
+ rpcPath,
131
+ });
132
+ }
133
+ for (let i = 0; i < keys.length; i += 1) {
134
+ const key = keys[i];
135
+ if (!key) continue;
136
+ const value = source[key];
137
+ if (typeof value === "string") {
138
+ headers[key] = value;
139
+ }
140
+ }
141
+ };
142
+
143
+ if (config.getHeaders) {
144
+ let fromConfig: HeaderMap;
145
+ try {
146
+ fromConfig = await config.getHeaders();
147
+ } catch (cause) {
148
+ throw new ProtoClientError({
149
+ code: "INVALID_ARGUMENT",
150
+ message: "getHeaders() failed",
151
+ rpcPath,
152
+ cause,
153
+ });
154
+ }
155
+ merge(fromConfig);
156
+ }
157
+
158
+ merge(extra);
159
+ return headers;
160
+ }
161
+
162
+ function mapHttpStatus(
163
+ status: number,
164
+ rpcPath: string,
165
+ bodyText: string,
166
+ ): ProtoClientError {
167
+ if (bodyText) {
168
+ const connectErr = tryParseConnectError(bodyText, rpcPath, status);
169
+ if (connectErr) {
170
+ return connectErr;
171
+ }
172
+ }
173
+ const retryable = status === 408 || status === 429 || status >= 500;
174
+ return new ProtoClientError({
175
+ code: "HTTP",
176
+ message: `HTTP ${status} for ${rpcPath}${bodyText ? `: ${bodyText.slice(0, 200)}` : ""}`,
177
+ status,
178
+ rpcPath,
179
+ retryable,
180
+ });
181
+ }
182
+
183
+ async function readBodyBytes(
184
+ response: Response,
185
+ maxResponseBytes: number,
186
+ rpcPath: string,
187
+ ): Promise<Uint8Array> {
188
+ const lengthHeader = response.headers.get("content-length");
189
+ if (lengthHeader) {
190
+ const declared = Number(lengthHeader);
191
+ if (Number.isFinite(declared) && declared > maxResponseBytes) {
192
+ throw new ProtoClientError({
193
+ code: "PAYLOAD_TOO_LARGE",
194
+ message: `Response Content-Length ${declared} exceeds maxResponseBytes ${maxResponseBytes}`,
195
+ rpcPath,
196
+ status: response.status,
197
+ });
198
+ }
199
+ }
200
+
201
+ const buffer = await response.arrayBuffer();
202
+ if (buffer.byteLength > maxResponseBytes) {
203
+ throw new ProtoClientError({
204
+ code: "PAYLOAD_TOO_LARGE",
205
+ message: `Response size ${buffer.byteLength} exceeds maxResponseBytes ${maxResponseBytes}`,
206
+ rpcPath,
207
+ status: response.status,
208
+ });
209
+ }
210
+
211
+ return new Uint8Array(buffer);
212
+ }
213
+
214
+ async function unaryCallCore(
215
+ config: ResolvedTransportConfig,
216
+ codecs: CodecRegistry,
217
+ input: UnaryCallInput,
218
+ ): Promise<Record<string, unknown>> {
219
+ const { entry, request, signal } = input;
220
+ const rpcPath = entry.rpcPath;
221
+
222
+ if (config.transport === "native-grpc") {
223
+ const { nativeGrpcUnary } = await import("./native-grpc");
224
+ return nativeGrpcUnary(config, codecs, input);
225
+ }
226
+
227
+ const requestCodec = codecs.get(entry.requestType);
228
+ const responseCodec = codecs.get(entry.responseType);
229
+ if (!requestCodec || !responseCodec) {
230
+ throw new ProtoClientError({
231
+ code: "INVALID_ARGUMENT",
232
+ message: `Missing codec for ${entry.requestType} or ${entry.responseType}`,
233
+ rpcPath,
234
+ });
235
+ }
236
+
237
+ let body: Uint8Array;
238
+ try {
239
+ body = requestCodec.encode(request ?? {});
240
+ } catch (cause) {
241
+ throw new ProtoClientError({
242
+ code: "INVALID_ARGUMENT",
243
+ message: `Failed to encode request for ${rpcPath}`,
244
+ rpcPath,
245
+ cause,
246
+ });
247
+ }
248
+
249
+ const path = config.pathForMethod(entry);
250
+ const url = `${config.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
251
+ const contentType =
252
+ config.transport === "connect"
253
+ ? "application/proto"
254
+ : "application/x-protobuf";
255
+
256
+ const controller = new AbortController();
257
+ const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
258
+
259
+ const onAbort = () => controller.abort();
260
+ if (signal) {
261
+ if (signal.aborted) {
262
+ clearTimeout(timeoutId);
263
+ throw new ProtoClientError({
264
+ code: "ABORTED",
265
+ message: "Request was aborted before send",
266
+ rpcPath,
267
+ });
268
+ }
269
+ signal.addEventListener("abort", onAbort, { once: true });
270
+ }
271
+
272
+ try {
273
+ const headers = await buildHeaders(
274
+ config,
275
+ contentType,
276
+ rpcPath,
277
+ input.headers,
278
+ );
279
+ if (config.transport === "connect") {
280
+ headers["Connect-Protocol-Version"] = "1";
281
+ }
282
+
283
+ let response: Response;
284
+ try {
285
+ response = await config.fetchImpl(url, {
286
+ method: "POST",
287
+ headers,
288
+ body: body as unknown as BodyInit,
289
+ signal: controller.signal,
290
+ });
291
+ } catch (cause) {
292
+ if (controller.signal.aborted) {
293
+ const abortedByCaller = Boolean(signal?.aborted);
294
+ throw new ProtoClientError({
295
+ code: abortedByCaller ? "ABORTED" : "TIMEOUT",
296
+ message: abortedByCaller
297
+ ? "Request was aborted"
298
+ : `Request timed out after ${config.timeoutMs}ms`,
299
+ rpcPath,
300
+ cause,
301
+ retryable: !abortedByCaller,
302
+ });
303
+ }
304
+
305
+ throw new ProtoClientError({
306
+ code: "NETWORK",
307
+ message: `Network error calling ${rpcPath}`,
308
+ rpcPath,
309
+ cause,
310
+ retryable: true,
311
+ });
312
+ }
313
+
314
+ if (!response.ok) {
315
+ let text = "";
316
+ try {
317
+ text = await response.text();
318
+ } catch {
319
+ text = "";
320
+ }
321
+ throw mapHttpStatus(response.status, rpcPath, text);
322
+ }
323
+
324
+ const bytes = await readBodyBytes(
325
+ response,
326
+ config.maxResponseBytes,
327
+ rpcPath,
328
+ );
329
+
330
+ // Connect unary may still return raw proto; if JSON end-stream sneaks in, map it.
331
+ const contentTypeHeader = response.headers.get("content-type") || "";
332
+ if (
333
+ config.transport === "connect" &&
334
+ contentTypeHeader.includes("json") &&
335
+ bytes.byteLength > 0
336
+ ) {
337
+ const text = new TextDecoder().decode(bytes);
338
+ const connectErr = tryParseConnectError(text, rpcPath, response.status);
339
+ if (connectErr) {
340
+ throw connectErr;
341
+ }
342
+ }
343
+
344
+ try {
345
+ return responseCodec.decode(bytes);
346
+ } catch (cause) {
347
+ throw new ProtoClientError({
348
+ code: "DECODE",
349
+ message: `Failed to decode response for ${rpcPath}`,
350
+ rpcPath,
351
+ cause,
352
+ status: response.status,
353
+ });
354
+ }
355
+ } finally {
356
+ clearTimeout(timeoutId);
357
+ if (signal) {
358
+ signal.removeEventListener("abort", onAbort);
359
+ }
360
+ }
361
+ }
362
+
363
+ function notifyOnError(
364
+ config: ResolvedTransportConfig,
365
+ error: ReturnType<typeof wrapUnknownError>,
366
+ ): void {
367
+ if (!config.onError) {
368
+ return;
369
+ }
370
+ try {
371
+ config.onError(error);
372
+ } catch {
373
+ // Logging hooks must not replace throw behavior
374
+ }
375
+ }
376
+
377
+ export async function unaryCall(
378
+ config: ResolvedTransportConfig,
379
+ codecs: CodecRegistry,
380
+ input: UnaryCallInput,
381
+ ): Promise<Record<string, unknown>> {
382
+ const { entry, request, signal } = input;
383
+ const rpcPath = entry.rpcPath;
384
+
385
+ const core = async (ctx: CallContext) =>
386
+ unaryCallCore(config, codecs, {
387
+ entry: ctx.entry,
388
+ request: ctx.request,
389
+ signal: ctx.signal,
390
+ headers: ctx.headers,
391
+ });
392
+
393
+ const pipeline =
394
+ config.interceptors.length > 0
395
+ ? composeInterceptors(config.interceptors, core)
396
+ : core;
397
+
398
+ try {
399
+ return await pipeline({
400
+ entry,
401
+ request: request ?? {},
402
+ headers: input.headers ?? {},
403
+ signal,
404
+ });
405
+ } catch (error) {
406
+ const wrapped = wrapUnknownError(error, rpcPath);
407
+
408
+ // Optional transport-level single retry on 401 when no auth interceptor handled it
409
+ if (
410
+ config.retryOnUnauthorized &&
411
+ config.interceptors.length === 0 &&
412
+ wrapped.status === 401 &&
413
+ config.getHeaders
414
+ ) {
415
+ try {
416
+ return await unaryCallCore(config, codecs, input);
417
+ } catch (retryError) {
418
+ const retryWrapped = wrapUnknownError(retryError, rpcPath);
419
+ notifyOnError(config, retryWrapped);
420
+ throw retryWrapped;
421
+ }
422
+ }
423
+
424
+ notifyOnError(config, wrapped);
425
+ throw wrapped;
426
+ }
427
+ }
package/src/types.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type { Type } from 'protobufjs';
2
+ import type { Interceptor } from './interceptors';
3
+ import type { PathTemplatePreset } from './path-templates';
4
+
5
+ export type TransportKind = 'http' | 'connect' | 'native-grpc';
6
+
7
+ export type HeaderMap = Record<string, string>;
8
+
9
+ export type ProtoClientConfig = {
10
+ /** API origin, for example https://api.example.com */
11
+ baseUrl: string;
12
+ /** Optional per-request headers (auth tokens, app version, etc.) */
13
+ getHeaders?: () => HeaderMap | Promise<HeaderMap>;
14
+ /** Request timeout in milliseconds (default 30000) */
15
+ timeoutMs?: number;
16
+ /** Maximum accepted response body size in bytes (default 2 MiB) */
17
+ maxResponseBytes?: number;
18
+ /** How calls are sent to the server */
19
+ transport?: TransportKind;
20
+ /** Custom fetch implementation (tests / polyfills) */
21
+ fetch?: typeof fetch;
22
+ /** Optional logging hook — errors are still thrown */
23
+ onError?: (error: import('./errors').ProtoClientError) => void;
24
+ /**
25
+ * Builds the HTTP path for a service method.
26
+ * Default: /{package}.{Service}/{Method}
27
+ */
28
+ pathForMethod?: (entry: MethodMapEntry) => string;
29
+ /** Shortcut for built-in path presets (ignored if pathForMethod is set) */
30
+ pathPreset?: PathTemplatePreset;
31
+ /** Unary interceptor pipeline (auth, logging, etc.) */
32
+ interceptors?: Interceptor[];
33
+ /**
34
+ * When true (default), retry a unary call once after auth interceptor refresh
35
+ * is handled inside createAuthInterceptor — this flag reserved for transport-level 401.
36
+ */
37
+ retryOnUnauthorized?: boolean;
38
+ };
39
+
40
+ export type MethodKind = 'unary' | 'serverStreaming' | 'clientStreaming' | 'bidi';
41
+
42
+ export type MethodMapEntry = {
43
+ serviceName: string;
44
+ methodName: string;
45
+ packageName: string;
46
+ rpcPath: string;
47
+ requestType: string;
48
+ responseType: string;
49
+ kind: MethodKind;
50
+ };
51
+
52
+ export type MethodMap = ReadonlyArray<MethodMapEntry>;
53
+
54
+ export type MessageCodec = {
55
+ encode: (message: Record<string, unknown>) => Uint8Array;
56
+ decode: (bytes: Uint8Array) => Record<string, unknown>;
57
+ };
58
+
59
+ export type CodecRegistry = {
60
+ get(typeName: string): MessageCodec | undefined;
61
+ has(typeName: string): boolean;
62
+ };
63
+
64
+ export type UnaryCallInput = {
65
+ entry: MethodMapEntry;
66
+ request: Record<string, unknown>;
67
+ signal?: AbortSignal;
68
+ /** Extra headers merged for this call (from interceptors) */
69
+ headers?: HeaderMap;
70
+ };
71
+
72
+ export type StreamCallInput = UnaryCallInput & {
73
+ onMessage?: (message: Record<string, unknown>) => void;
74
+ /** Client-streaming / bidi: async iterable of request messages */
75
+ requests?: AsyncIterable<Record<string, unknown>> | Iterable<Record<string, unknown>>;
76
+ };
77
+
78
+ export type ClientServices = {
79
+ [serviceName: string]: {
80
+ [methodName: string]: (
81
+ request?: Record<string, unknown>,
82
+ options?: { signal?: AbortSignal }
83
+ ) => Promise<Record<string, unknown>> | AsyncIterable<Record<string, unknown>>;
84
+ };
85
+ };
86
+
87
+ export type ProtoClient = ClientServices & {
88
+ readonly config: Readonly<Required<
89
+ Pick<ProtoClientConfig, 'baseUrl' | 'timeoutMs' | 'maxResponseBytes' | 'transport'>
90
+ >> &
91
+ ProtoClientConfig;
92
+ };
93
+
94
+ export type ProtobufTypeLike = Pick<Type, 'encode' | 'decode' | 'create' | 'fromObject'>;
@@ -0,0 +1,75 @@
1
+ import type { MessageCodec } from "../types";
2
+
3
+ export type ZodLikeSchema<T = unknown> = {
4
+ parse: (data: unknown) => T;
5
+ safeParse?: (
6
+ data: unknown,
7
+ ) => { success: true; data: T } | { success: false; error: unknown };
8
+ };
9
+
10
+ /**
11
+ * Wrap a protobuf codec with optional Zod (or Zod-compatible) request/response
12
+ * validation. Peer dependency: install `zod` in your app.
13
+ */
14
+ export function withZodValidation(
15
+ codec: MessageCodec,
16
+ options: {
17
+ request?: ZodLikeSchema<Record<string, unknown>>;
18
+ response?: ZodLikeSchema<Record<string, unknown>>;
19
+ },
20
+ ): MessageCodec {
21
+ return {
22
+ encode(message) {
23
+ const value = options.request ? options.request.parse(message) : message;
24
+ return codec.encode(value as Record<string, unknown>);
25
+ },
26
+ decode(bytes) {
27
+ const decoded = codec.decode(bytes);
28
+ return options.response
29
+ ? (options.response.parse(decoded) as Record<string, unknown>)
30
+ : decoded;
31
+ },
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Apply Zod schemas to selected type names in a codec registry.
37
+ */
38
+ export function wrapCodecRegistryWithZod(
39
+ registry: {
40
+ get(typeName: string): MessageCodec | undefined;
41
+ has(typeName: string): boolean;
42
+ },
43
+ schemas: Record<
44
+ string,
45
+ { request?: ZodLikeSchema; response?: ZodLikeSchema } | ZodLikeSchema
46
+ >,
47
+ ): {
48
+ get(typeName: string): MessageCodec | undefined;
49
+ has(typeName: string): boolean;
50
+ } {
51
+ return {
52
+ has(typeName) {
53
+ return registry.has(typeName);
54
+ },
55
+ get(typeName) {
56
+ const base = registry.get(typeName);
57
+ if (!base) return undefined;
58
+ const entry = schemas[typeName];
59
+ if (!entry) return base;
60
+ if ("parse" in entry && typeof entry.parse === "function") {
61
+ return withZodValidation(base, {
62
+ request: entry as ZodLikeSchema<Record<string, unknown>>,
63
+ response: entry as ZodLikeSchema<Record<string, unknown>>,
64
+ });
65
+ }
66
+ return withZodValidation(
67
+ base,
68
+ entry as {
69
+ request?: ZodLikeSchema<Record<string, unknown>>;
70
+ response?: ZodLikeSchema<Record<string, unknown>>;
71
+ },
72
+ );
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,121 @@
1
+ import { ProtoClientError } from './errors';
2
+ import type { HeaderMap } from './types';
3
+ import type { Interceptor } from './interceptors';
4
+
5
+ export const CLIENT_VERSION_HEADER = 'X-React-Native-Proto-Client-Version';
6
+ export const SERVER_MIN_CLIENT_HEADER = 'X-React-Native-Proto-Min-Client-Version';
7
+ export const METHOD_MAP_HASH_HEADER = 'X-React-Native-Proto-Method-Map-Hash';
8
+
9
+ export type VersionCheckOptions = {
10
+ /** Semver or opaque version string sent on every request */
11
+ clientVersion: string;
12
+ /** Optional hash/fingerprint of the generated method map */
13
+ methodMapHash?: string;
14
+ /**
15
+ * If the response includes SERVER_MIN_CLIENT_HEADER and client is older,
16
+ * throw (default) or only warn via onMismatch.
17
+ */
18
+ mode?: 'throw' | 'warn';
19
+ onMismatch?: (info: {
20
+ clientVersion: string;
21
+ serverMinClientVersion: string;
22
+ }) => void;
23
+ compareVersions?: (client: string, serverMin: string) => number;
24
+ };
25
+
26
+ /** Simple dotted numeric compare: 1.2.0 vs 1.10.0. Non-numeric falls back to string cmp. */
27
+ export function compareDottedVersions(a: string, b: string): number {
28
+ const pa = a.split('.').map((p) => Number.parseInt(p, 10));
29
+ const pb = b.split('.').map((p) => Number.parseInt(p, 10));
30
+ if (pa.some((n) => Number.isNaN(n)) || pb.some((n) => Number.isNaN(n))) {
31
+ return a === b ? 0 : a < b ? -1 : 1;
32
+ }
33
+ const len = Math.max(pa.length, pb.length);
34
+ for (let i = 0; i < len; i += 1) {
35
+ const av = pa[i] ?? 0;
36
+ const bv = pb[i] ?? 0;
37
+ if (av < bv) return -1;
38
+ if (av > bv) return 1;
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ export function versionHeaders(options: {
44
+ clientVersion: string;
45
+ methodMapHash?: string;
46
+ }): HeaderMap {
47
+ const headers: HeaderMap = {
48
+ [CLIENT_VERSION_HEADER]: options.clientVersion,
49
+ };
50
+ if (options.methodMapHash) {
51
+ headers[METHOD_MAP_HASH_HEADER] = options.methodMapHash;
52
+ }
53
+ return headers;
54
+ }
55
+
56
+ /**
57
+ * Attaches client version headers. Pair with `assertServerMinClient` after responses
58
+ * if you read response headers in a custom transport; for the default HTTP path,
59
+ * use `createVersionInterceptor` which only sends headers (response check needs
60
+ * fetch wrapper — see createVersionAwareFetch).
61
+ */
62
+ export function createVersionInterceptor(
63
+ options: VersionCheckOptions
64
+ ): Interceptor {
65
+ return async (ctx, next) => {
66
+ const headers = {
67
+ ...ctx.headers,
68
+ ...versionHeaders({
69
+ clientVersion: options.clientVersion,
70
+ methodMapHash: options.methodMapHash,
71
+ }),
72
+ };
73
+ return next({ ...ctx, headers });
74
+ };
75
+ }
76
+
77
+ export function assertServerMinClient(
78
+ serverMinClientVersion: string | null | undefined,
79
+ options: VersionCheckOptions
80
+ ): void {
81
+ if (!serverMinClientVersion) return;
82
+ const compare = options.compareVersions ?? compareDottedVersions;
83
+ if (compare(options.clientVersion, serverMinClientVersion) < 0) {
84
+ const info = {
85
+ clientVersion: options.clientVersion,
86
+ serverMinClientVersion,
87
+ };
88
+ if (options.onMismatch) {
89
+ options.onMismatch(info);
90
+ }
91
+ if (options.mode === 'warn') {
92
+ return;
93
+ }
94
+ throw new ProtoClientError({
95
+ code: 'INVALID_CONFIG',
96
+ message: `Client version ${options.clientVersion} is older than server minimum ${serverMinClientVersion}`,
97
+ });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Wraps fetch to validate SERVER_MIN_CLIENT_HEADER on each response.
103
+ */
104
+ export function createVersionAwareFetch(
105
+ baseFetch: typeof fetch,
106
+ options: VersionCheckOptions
107
+ ): typeof fetch {
108
+ return (async (input: RequestInfo | URL, init?: RequestInit) => {
109
+ const headers = new Headers(init?.headers);
110
+ headers.set(CLIENT_VERSION_HEADER, options.clientVersion);
111
+ if (options.methodMapHash) {
112
+ headers.set(METHOD_MAP_HASH_HEADER, options.methodMapHash);
113
+ }
114
+ const response = await baseFetch(input, { ...init, headers });
115
+ assertServerMinClient(
116
+ response.headers.get(SERVER_MIN_CLIENT_HEADER),
117
+ options
118
+ );
119
+ return response;
120
+ }) as typeof fetch;
121
+ }