@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,302 @@
1
+ import { ProtoClientError } from '../errors';
2
+ import type { ResolvedTransportConfig } from '../transport';
3
+ import type { CodecRegistry, StreamCallInput, UnaryCallInput } from '../types';
4
+
5
+ type NativeBridge = {
6
+ unary: (input: {
7
+ baseUrl: string;
8
+ rpcPath: string;
9
+ requestBytes: Uint8Array;
10
+ headers: Record<string, string>;
11
+ timeoutMs: number;
12
+ }) => Promise<Uint8Array>;
13
+ stream?: (input: {
14
+ baseUrl: string;
15
+ rpcPath: string;
16
+ requestBytes: Uint8Array;
17
+ headers: Record<string, string>;
18
+ timeoutMs: number;
19
+ }) => AsyncIterable<Uint8Array>;
20
+ };
21
+
22
+ type NativeGrpcModule = {
23
+ unary: (
24
+ baseUrl: string,
25
+ rpcPath: string,
26
+ requestBase64: string,
27
+ headersJson: string,
28
+ timeoutMs: number
29
+ ) => Promise<string>;
30
+ isAvailable?: () => Promise<boolean>;
31
+ };
32
+
33
+ declare global {
34
+ // Optional native module injected by the host app / TurboModule
35
+ // eslint-disable-next-line no-var
36
+ var __REACT_NATIVE_PROTO_NATIVE_GRPC__: NativeBridge | undefined;
37
+ }
38
+
39
+ type BufferLike = {
40
+ from(
41
+ data: string | Uint8Array,
42
+ encoding?: string
43
+ ): Uint8Array & { toString(encoding: string): string };
44
+ };
45
+
46
+ function getGlobalBuffer(): BufferLike | undefined {
47
+ const candidate = (globalThis as { Buffer?: BufferLike }).Buffer;
48
+ return typeof candidate?.from === 'function' ? candidate : undefined;
49
+ }
50
+
51
+ /** Encode bytes to base64 without requiring a Buffer polyfill on RN. */
52
+ export function bytesToBase64(bytes: Uint8Array): string {
53
+ const BufferCtor = getGlobalBuffer();
54
+ if (BufferCtor) {
55
+ return BufferCtor.from(bytes).toString('base64');
56
+ }
57
+ let binary = '';
58
+ for (let i = 0; i < bytes.length; i += 1) {
59
+ binary += String.fromCharCode(bytes[i]!);
60
+ }
61
+ if (typeof btoa !== 'function') {
62
+ throw new Error('base64 encode unavailable (no Buffer or btoa)');
63
+ }
64
+ return btoa(binary);
65
+ }
66
+
67
+ /** Decode base64 to bytes without requiring a Buffer polyfill on RN. */
68
+ export function base64ToBytes(base64: string): Uint8Array {
69
+ const BufferCtor = getGlobalBuffer();
70
+ if (BufferCtor) {
71
+ const buf = BufferCtor.from(base64, 'base64');
72
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
73
+ }
74
+ if (typeof atob !== 'function') {
75
+ throw new Error('base64 decode unavailable (no Buffer or atob)');
76
+ }
77
+ const binary = atob(base64);
78
+ const out = new Uint8Array(binary.length);
79
+ for (let i = 0; i < binary.length; i += 1) {
80
+ out[i] = binary.charCodeAt(i);
81
+ }
82
+ return out;
83
+ }
84
+
85
+ function getNativeModule(): NativeGrpcModule | undefined {
86
+ try {
87
+ // Optional peer — do not hard-depend at module load time.
88
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
89
+ const rn = require('react-native') as {
90
+ NativeModules?: { ReactNativeProtoNativeGrpc?: NativeGrpcModule };
91
+ };
92
+ const mod = rn.NativeModules?.ReactNativeProtoNativeGrpc;
93
+ if (mod && typeof mod.unary === 'function') {
94
+ return mod;
95
+ }
96
+ } catch {
97
+ // Not running in React Native, or NativeModules unavailable.
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ function createBridgeFromNativeModule(mod: NativeGrpcModule): NativeBridge {
103
+ return {
104
+ async unary({ baseUrl, rpcPath, requestBytes, headers, timeoutMs }) {
105
+ const responseBase64 = await mod.unary(
106
+ baseUrl,
107
+ rpcPath,
108
+ bytesToBase64(requestBytes),
109
+ JSON.stringify(headers ?? {}),
110
+ timeoutMs
111
+ );
112
+ if (typeof responseBase64 !== 'string') {
113
+ throw new Error('Native unary did not return a base64 string');
114
+ }
115
+ return base64ToBytes(responseBase64);
116
+ },
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Wire `NativeModules.ReactNativeProtoNativeGrpc` into
122
+ * `global.__REACT_NATIVE_PROTO_NATIVE_GRPC__` for the JS transport layer.
123
+ * Returns true when the bridge was installed.
124
+ */
125
+ export function installNativeGrpcBridgeFromNativeModule(): boolean {
126
+ const mod = getNativeModule();
127
+ if (!mod) {
128
+ return false;
129
+ }
130
+ globalThis.__REACT_NATIVE_PROTO_NATIVE_GRPC__ =
131
+ createBridgeFromNativeModule(mod);
132
+ return true;
133
+ }
134
+
135
+ function getBridge(): NativeBridge {
136
+ const existing = globalThis.__REACT_NATIVE_PROTO_NATIVE_GRPC__;
137
+ if (existing && typeof existing.unary === 'function') {
138
+ return existing;
139
+ }
140
+
141
+ if (installNativeGrpcBridgeFromNativeModule()) {
142
+ const installed = globalThis.__REACT_NATIVE_PROTO_NATIVE_GRPC__;
143
+ if (installed && typeof installed.unary === 'function') {
144
+ return installed;
145
+ }
146
+ }
147
+
148
+ throw new ProtoClientError({
149
+ code: 'UNSUPPORTED',
150
+ message:
151
+ 'Native gRPC bridge is not installed. Link the native module or use transport "http" / "connect".',
152
+ });
153
+ }
154
+
155
+ async function headersFromConfig(
156
+ config: ResolvedTransportConfig,
157
+ rpcPath: string
158
+ ): Promise<Record<string, string>> {
159
+ if (!config.getHeaders) {
160
+ return {};
161
+ }
162
+ try {
163
+ return await config.getHeaders();
164
+ } catch (cause) {
165
+ throw new ProtoClientError({
166
+ code: 'INVALID_ARGUMENT',
167
+ message: 'getHeaders() failed',
168
+ rpcPath,
169
+ cause,
170
+ });
171
+ }
172
+ }
173
+
174
+ export async function nativeGrpcUnary(
175
+ config: ResolvedTransportConfig,
176
+ codecs: CodecRegistry,
177
+ input: UnaryCallInput
178
+ ): Promise<Record<string, unknown>> {
179
+ const bridge = getBridge();
180
+ const { entry, request } = input;
181
+ const requestCodec = codecs.get(entry.requestType);
182
+ const responseCodec = codecs.get(entry.responseType);
183
+ if (!requestCodec || !responseCodec) {
184
+ throw new ProtoClientError({
185
+ code: 'INVALID_ARGUMENT',
186
+ message: `Missing codec for ${entry.requestType} or ${entry.responseType}`,
187
+ rpcPath: entry.rpcPath,
188
+ });
189
+ }
190
+
191
+ let requestBytes: Uint8Array;
192
+ try {
193
+ requestBytes = requestCodec.encode(request ?? {});
194
+ } catch (cause) {
195
+ throw new ProtoClientError({
196
+ code: 'INVALID_ARGUMENT',
197
+ message: `Failed to encode request for ${entry.rpcPath}`,
198
+ rpcPath: entry.rpcPath,
199
+ cause,
200
+ });
201
+ }
202
+
203
+ const headers = await headersFromConfig(config, entry.rpcPath);
204
+
205
+ let responseBytes: Uint8Array;
206
+ try {
207
+ responseBytes = await bridge.unary({
208
+ baseUrl: config.baseUrl,
209
+ rpcPath: entry.rpcPath,
210
+ requestBytes,
211
+ headers,
212
+ timeoutMs: config.timeoutMs,
213
+ });
214
+ } catch (cause) {
215
+ throw new ProtoClientError({
216
+ code: 'RPC',
217
+ message: `Native gRPC call failed for ${entry.rpcPath}`,
218
+ rpcPath: entry.rpcPath,
219
+ cause,
220
+ retryable: true,
221
+ });
222
+ }
223
+
224
+ if (responseBytes.byteLength > config.maxResponseBytes) {
225
+ throw new ProtoClientError({
226
+ code: 'PAYLOAD_TOO_LARGE',
227
+ message: `Native response exceeds maxResponseBytes ${config.maxResponseBytes}`,
228
+ rpcPath: entry.rpcPath,
229
+ });
230
+ }
231
+
232
+ try {
233
+ return responseCodec.decode(responseBytes);
234
+ } catch (cause) {
235
+ throw new ProtoClientError({
236
+ code: 'DECODE',
237
+ message: `Failed to decode native gRPC response for ${entry.rpcPath}`,
238
+ rpcPath: entry.rpcPath,
239
+ cause,
240
+ });
241
+ }
242
+ }
243
+
244
+ export async function* nativeGrpcStream(
245
+ config: ResolvedTransportConfig,
246
+ codecs: CodecRegistry,
247
+ input: StreamCallInput
248
+ ): AsyncGenerator<Record<string, unknown>, void, undefined> {
249
+ const bridge = getBridge();
250
+ if (typeof bridge.stream !== 'function') {
251
+ throw new ProtoClientError({
252
+ code: 'UNSUPPORTED',
253
+ message: 'Native gRPC bridge does not implement streaming',
254
+ rpcPath: input.entry.rpcPath,
255
+ });
256
+ }
257
+
258
+ const { entry, request } = input;
259
+ const requestCodec = codecs.get(entry.requestType);
260
+ const responseCodec = codecs.get(entry.responseType);
261
+ if (!requestCodec || !responseCodec) {
262
+ throw new ProtoClientError({
263
+ code: 'INVALID_ARGUMENT',
264
+ message: `Missing codec for ${entry.requestType} or ${entry.responseType}`,
265
+ rpcPath: entry.rpcPath,
266
+ });
267
+ }
268
+
269
+ const requestBytes = requestCodec.encode(request ?? {});
270
+ const headers = await headersFromConfig(config, entry.rpcPath);
271
+ const frames = bridge.stream({
272
+ baseUrl: config.baseUrl,
273
+ rpcPath: entry.rpcPath,
274
+ requestBytes,
275
+ headers,
276
+ timeoutMs: config.timeoutMs,
277
+ });
278
+
279
+ for await (const frame of frames) {
280
+ if (frame.byteLength > config.maxResponseBytes) {
281
+ throw new ProtoClientError({
282
+ code: 'PAYLOAD_TOO_LARGE',
283
+ message: 'Native stream frame exceeds maxResponseBytes',
284
+ rpcPath: entry.rpcPath,
285
+ });
286
+ }
287
+ const message = responseCodec.decode(frame);
288
+ if (input.onMessage) {
289
+ input.onMessage(message);
290
+ }
291
+ yield message;
292
+ }
293
+ }
294
+
295
+ export function isNativeGrpcAvailable(): boolean {
296
+ if (
297
+ typeof globalThis.__REACT_NATIVE_PROTO_NATIVE_GRPC__?.unary === 'function'
298
+ ) {
299
+ return true;
300
+ }
301
+ return getNativeModule() != null;
302
+ }
@@ -0,0 +1,160 @@
1
+ import { ProtoClientError } from './errors';
2
+
3
+ export type QueuedMutation = {
4
+ id: string;
5
+ label: string;
6
+ run: () => Promise<unknown>;
7
+ enqueuedAt: number;
8
+ };
9
+
10
+ export type OfflineQueueOptions = {
11
+ /** Max queued items (default 50) */
12
+ maxSize?: number;
13
+ /** Called whenever queue length changes */
14
+ onChange?: (size: number) => void;
15
+ /** Persist/rehydrate hooks for app storage */
16
+ storage?: {
17
+ load: () => Promise<QueuedMutation[]> | QueuedMutation[];
18
+ save: (items: QueuedMutation[]) => Promise<void> | void;
19
+ };
20
+ };
21
+
22
+ /**
23
+ * Queue failed (or deferred) mutations and flush when back online.
24
+ * Does not auto-detect network — call `setOnline(true/false)` or `flush()` yourself.
25
+ */
26
+ export class OfflineMutationQueue {
27
+ private items: QueuedMutation[] = [];
28
+ private readonly maxSize: number;
29
+ private readonly onChange?: (size: number) => void;
30
+ private readonly storage?: OfflineQueueOptions['storage'];
31
+ private online = true;
32
+ private flushing = false;
33
+
34
+ constructor(options: OfflineQueueOptions = {}) {
35
+ this.maxSize = options.maxSize ?? 50;
36
+ this.onChange = options.onChange;
37
+ this.storage = options.storage;
38
+ }
39
+
40
+ async hydrate(): Promise<void> {
41
+ if (!this.storage) return;
42
+ const loaded = await this.storage.load();
43
+ this.items = Array.isArray(loaded) ? loaded.slice(0, this.maxSize) : [];
44
+ this.emit();
45
+ }
46
+
47
+ setOnline(online: boolean): void {
48
+ this.online = online;
49
+ if (online) {
50
+ void this.flush();
51
+ }
52
+ }
53
+
54
+ isOnline(): boolean {
55
+ return this.online;
56
+ }
57
+
58
+ size(): number {
59
+ return this.items.length;
60
+ }
61
+
62
+ /**
63
+ * Run immediately when online; otherwise enqueue and resolve after flush.
64
+ */
65
+ async runOrEnqueue<T>(
66
+ label: string,
67
+ run: () => Promise<T>
68
+ ): Promise<T> {
69
+ if (this.online) {
70
+ return run();
71
+ }
72
+
73
+ return new Promise<T>((resolve, reject) => {
74
+ if (this.items.length >= this.maxSize) {
75
+ reject(
76
+ new ProtoClientError({
77
+ code: 'INVALID_ARGUMENT',
78
+ message: `Offline queue is full (max ${this.maxSize})`,
79
+ })
80
+ );
81
+ return;
82
+ }
83
+
84
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
85
+ this.items.push({
86
+ id,
87
+ label,
88
+ enqueuedAt: Date.now(),
89
+ run: async () => {
90
+ const result = await run();
91
+ resolve(result);
92
+ return result;
93
+ },
94
+ });
95
+ this.emit();
96
+ void this.persist();
97
+ });
98
+ }
99
+
100
+ enqueue(label: string, run: () => Promise<unknown>): string {
101
+ if (this.items.length >= this.maxSize) {
102
+ throw new ProtoClientError({
103
+ code: 'INVALID_ARGUMENT',
104
+ message: `Offline queue is full (max ${this.maxSize})`,
105
+ });
106
+ }
107
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
108
+ this.items.push({ id, label, run, enqueuedAt: Date.now() });
109
+ this.emit();
110
+ void this.persist();
111
+ return id;
112
+ }
113
+
114
+ async flush(): Promise<{ ok: number; failed: number }> {
115
+ if (this.flushing) {
116
+ return { ok: 0, failed: 0 };
117
+ }
118
+ this.flushing = true;
119
+ let ok = 0;
120
+ let failed = 0;
121
+
122
+ try {
123
+ while (this.items.length > 0 && this.online) {
124
+ const item = this.items[0];
125
+ if (!item) break;
126
+ try {
127
+ await item.run();
128
+ this.items.shift();
129
+ ok += 1;
130
+ this.emit();
131
+ await this.persist();
132
+ } catch {
133
+ failed += 1;
134
+ break;
135
+ }
136
+ }
137
+ } finally {
138
+ this.flushing = false;
139
+ }
140
+
141
+ return { ok, failed };
142
+ }
143
+
144
+ clear(): void {
145
+ this.items = [];
146
+ this.emit();
147
+ void this.persist();
148
+ }
149
+
150
+ private emit(): void {
151
+ if (this.onChange) {
152
+ this.onChange(this.items.length);
153
+ }
154
+ }
155
+
156
+ private async persist(): Promise<void> {
157
+ if (!this.storage) return;
158
+ await this.storage.save(this.items);
159
+ }
160
+ }
@@ -0,0 +1,65 @@
1
+ import type { MethodMapEntry } from './types';
2
+
3
+ export type PathTemplatePreset =
4
+ | 'connect'
5
+ | 'grpc-web'
6
+ | 'envoy'
7
+ | 'grpc-gateway'
8
+ | 'restish';
9
+
10
+ function packagePrefix(entry: MethodMapEntry): string {
11
+ return entry.packageName ? `${entry.packageName}.` : '';
12
+ }
13
+
14
+ function lowerFirst(value: string): string {
15
+ if (!value) return value;
16
+ return value.charAt(0).toLowerCase() + value.slice(1);
17
+ }
18
+
19
+ /**
20
+ * Built-in path builders for common gateways and proxies.
21
+ *
22
+ * - connect / grpc-web / envoy: `/{package}.{Service}/{Method}`
23
+ * - grpc-gateway: `/{package}.{Service}/{Method}` (HTTP body RPC style)
24
+ * - restish: `/v1/{service}/{method}` (lowercase camel segments)
25
+ */
26
+ export function createPathForMethod(
27
+ preset: PathTemplatePreset = 'connect'
28
+ ): (entry: MethodMapEntry) => string {
29
+ switch (preset) {
30
+ case 'restish':
31
+ return (entry) =>
32
+ `/v1/${lowerFirst(entry.serviceName)}/${lowerFirst(entry.methodName)}`;
33
+ case 'grpc-gateway':
34
+ case 'connect':
35
+ case 'grpc-web':
36
+ case 'envoy':
37
+ default:
38
+ return (entry) =>
39
+ `/${packagePrefix(entry)}${entry.serviceName}/${entry.methodName}`;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Custom template. Placeholders:
45
+ * `{package}` `{service}` `{method}` `{rpcPath}`
46
+ * `{serviceCamel}` `{methodCamel}`
47
+ */
48
+ export function pathFromTemplate(
49
+ template: string
50
+ ): (entry: MethodMapEntry) => string {
51
+ return (entry) =>
52
+ template
53
+ .split('{package}')
54
+ .join(entry.packageName)
55
+ .split('{service}')
56
+ .join(entry.serviceName)
57
+ .split('{method}')
58
+ .join(entry.methodName)
59
+ .split('{rpcPath}')
60
+ .join(entry.rpcPath)
61
+ .split('{serviceCamel}')
62
+ .join(lowerFirst(entry.serviceName))
63
+ .split('{methodCamel}')
64
+ .join(lowerFirst(entry.methodName));
65
+ }
@@ -0,0 +1,40 @@
1
+ import type {
2
+ QueryKey,
3
+ UseMutationOptions,
4
+ UseQueryOptions,
5
+ } from '@tanstack/react-query';
6
+ import { useMutation, useQuery } from '@tanstack/react-query';
7
+
8
+ type UnaryFn<TReq, TRes> = (request: TReq) => Promise<TRes>;
9
+
10
+ /**
11
+ * Thin React Query wrapper around a generated unary client method.
12
+ */
13
+ export function useProtoQuery<TReq extends object, TRes>(
14
+ method: UnaryFn<TReq, TRes>,
15
+ request: TReq,
16
+ options?: Omit<
17
+ UseQueryOptions<TRes, Error, TRes, QueryKey>,
18
+ 'queryKey' | 'queryFn'
19
+ > & { queryKey?: QueryKey }
20
+ ) {
21
+ const queryKey = options?.queryKey ?? ['proto', method.name, request];
22
+ return useQuery({
23
+ ...options,
24
+ queryKey,
25
+ queryFn: () => method(request),
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Thin React Query mutation wrapper around a generated unary client method.
31
+ */
32
+ export function useProtoMutation<TReq extends object, TRes>(
33
+ method: UnaryFn<TReq, TRes>,
34
+ options?: UseMutationOptions<TRes, Error, TReq>
35
+ ) {
36
+ return useMutation({
37
+ ...options,
38
+ mutationFn: (request: TReq) => method(request),
39
+ });
40
+ }