@goplus123/core-api 1.0.4 → 1.0.6

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
@@ -260,6 +260,195 @@ const sdk = initSDK({
260
260
 
261
261
  对应实现:[`initSDK`](./src/index.ts#L372-L578)
262
262
 
263
+ ### React Native:注入 gRPC invoker(解决 RN 真机解析 gRPC-Web 响应问题)
264
+
265
+ 当后端仅提供 gRPC-Web,而 React Native 环境下 `@connectrpc/connect-web` 的 `fetch`/二进制响应解析存在兼容性问题时,可以通过 `grpc.invoker` 注入一层“可替换的底层调用器”,在不改动上层调用方式(`sdk.xxx` / `requestApi` / spec)前提下,切换为 RN 可用的 gRPC-Web 实现。
266
+
267
+ 这个 `invoker` 只是一个“可插拔的 unary 调用实现”,不限定必须使用某个第三方库。
268
+
269
+ `invoker.unary()` 的入参约定:
270
+
271
+ - `baseUrl`:本次请求实际使用的 baseUrl(已根据 service 自动路由)
272
+ - `service` / `methodName`:本次调用的服务与方法名
273
+ - `method`:服务方法描述(如果可获取),用于取 input/output schema 与 rpcName
274
+ - `request`:已经构造好的 protobuf message(上层已用 schema create 过)
275
+ - `headers`:最终要发出去的 headers(已合并 auth/getHeaders/静态 headers 与 callOptions.headers)
276
+ - `callOptions`:透传的调用选项(例如 timeout),按你的底层实现自行使用
277
+
278
+ 示例 A(推荐):在 RN 工程内用 `fetch` 走 gRPC-Web(二进制),复用本 SDK 的 bufbuild 生成物(不需要重新编译 proto)。这个方案不需要引入 `@improbable-eng/grpc-web-react-native-transport`:
279
+
280
+ ```ts
281
+ import { initSDK, fetchGuid } from '@goplus123/core-api'
282
+ import { fromBinary, toBinary } from '@bufbuild/protobuf'
283
+
284
+ function concatBytes(chunks: Uint8Array[]): Uint8Array {
285
+ let len = 0
286
+ for (const c of chunks) len += c.byteLength
287
+ const out = new Uint8Array(len)
288
+ let offset = 0
289
+ for (const c of chunks) {
290
+ out.set(c, offset)
291
+ offset += c.byteLength
292
+ }
293
+ return out
294
+ }
295
+
296
+ function grpcWebEncodeUnary(messageBytes: Uint8Array): Uint8Array {
297
+ const out = new Uint8Array(5 + messageBytes.byteLength)
298
+ out[0] = 0
299
+ const view = new DataView(out.buffer, out.byteOffset, out.byteLength)
300
+ view.setUint32(1, messageBytes.byteLength, false)
301
+ out.set(messageBytes, 5)
302
+ return out
303
+ }
304
+
305
+ function bytesToAscii(bytes: Uint8Array): string {
306
+ let s = ''
307
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!)
308
+ return s
309
+ }
310
+
311
+ function parseGrpcWebTrailers(bytes: Uint8Array): Record<string, string> {
312
+ const text = bytesToAscii(bytes)
313
+ const out: Record<string, string> = {}
314
+ for (const line of text.split('\r\n')) {
315
+ if (!line) continue
316
+ const idx = line.indexOf(':')
317
+ if (idx < 0) continue
318
+ const k = line.slice(0, idx).trim().toLowerCase()
319
+ const v = line.slice(idx + 1).trim()
320
+ out[k] = v
321
+ }
322
+ return out
323
+ }
324
+
325
+ function grpcWebDecodeUnary(body: Uint8Array): { message?: Uint8Array; trailers: Record<string, string> } {
326
+ let offset = 0
327
+ let message: Uint8Array | undefined
328
+ let trailers: Record<string, string> = {}
329
+ while (offset + 5 <= body.byteLength) {
330
+ const flags = body[offset]!
331
+ const length = new DataView(body.buffer, body.byteOffset + offset + 1, 4).getUint32(0, false)
332
+ offset += 5
333
+ if (offset + length > body.byteLength) break
334
+ const frame = body.subarray(offset, offset + length)
335
+ offset += length
336
+ const isTrailer = (flags & 0x80) === 0x80
337
+ if (isTrailer) {
338
+ trailers = { ...trailers, ...parseGrpcWebTrailers(frame) }
339
+ } else if (message == null) {
340
+ message = frame
341
+ }
342
+ }
343
+ return { message, trailers }
344
+ }
345
+
346
+ function joinUrl(baseUrl: string, path: string): string {
347
+ return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '')
348
+ }
349
+
350
+ function capitalizeRpcName(methodName: string): string {
351
+ if (!methodName) return methodName
352
+ return methodName[0]!.toUpperCase() + methodName.slice(1)
353
+ }
354
+
355
+ function getGrpcWebPath(service: any, methodName: string, method: any): string {
356
+ const typeName = String(service?.typeName ?? '')
357
+ const rpcName = String(method?.name ?? method?.rpc?.name ?? '') || capitalizeRpcName(methodName)
358
+ return `${typeName}/${rpcName}`
359
+ }
360
+
361
+ async function rnGrpcWebUnary(args: {
362
+ baseUrl: string
363
+ service: any
364
+ methodName: string
365
+ method?: any
366
+ request: any
367
+ headers: Record<string, string>
368
+ callOptions?: { timeoutMs?: number }
369
+ }): Promise<any> {
370
+ const method =
371
+ args.method ??
372
+ (() => {
373
+ const ms = args.service?.methods
374
+ if (Array.isArray(ms)) return ms.find((m: any) => m?.localName === args.methodName || m?.name === args.methodName)
375
+ if (ms && typeof ms === 'object') return (ms as any)[args.methodName]
376
+ return undefined
377
+ })()
378
+ if (!method) {
379
+ throw new Error(`gRPC method not found: ${String(args.service?.typeName ?? '')}.${args.methodName}`)
380
+ }
381
+
382
+ const url = joinUrl(args.baseUrl, getGrpcWebPath(args.service, args.methodName, method))
383
+ const contentType = 'application/grpc-web+proto'
384
+
385
+ if (!method.input || !method.output) {
386
+ throw new Error(`gRPC method schema missing: ${String(args.service?.typeName ?? '')}.${args.methodName}`)
387
+ }
388
+ const requestBytes = toBinary(method.input, args.request)
389
+ const body = grpcWebEncodeUnary(requestBytes)
390
+
391
+ const controller = new AbortController()
392
+ const timeoutMs = args.callOptions?.timeoutMs
393
+ const timeoutId = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : undefined
394
+ const res = await fetch(url, {
395
+ method: 'POST',
396
+ headers: {
397
+ ...args.headers,
398
+ 'content-type': contentType,
399
+ accept: contentType,
400
+ 'x-grpc-web': '1',
401
+ },
402
+ body,
403
+ signal: controller.signal,
404
+ })
405
+ try {
406
+ if (!res.ok) {
407
+ throw new Error(`gRPC fetch failed: ${res.status} ${res.statusText}`)
408
+ }
409
+ const raw = new Uint8Array(await res.arrayBuffer())
410
+ const decoded = grpcWebDecodeUnary(raw)
411
+ const grpcStatus = decoded.trailers['grpc-status'] ?? '0'
412
+ if (grpcStatus !== '0') {
413
+ const msg = decoded.trailers['grpc-message'] ?? 'gRPC request failed'
414
+ throw new Error(`gRPC status=${grpcStatus} message=${msg}`)
415
+ }
416
+ if (!decoded.message) throw new Error('gRPC response message missing')
417
+ return fromBinary(method.output, decoded.message)
418
+ } finally {
419
+ if (timeoutId) clearTimeout(timeoutId)
420
+ }
421
+ }
422
+
423
+ const rnGrpcInvoker = { unary: rnGrpcWebUnary }
424
+
425
+ const sdk = initSDK({
426
+ ws: {
427
+ url: 'wss://example.com/websocket',
428
+ protocols: fetchGuid(),
429
+ },
430
+ grpc: {
431
+ baseUrl: 'https://example.com/cms',
432
+ protocol: 'grpc-web',
433
+ invoker: rnGrpcInvoker,
434
+ },
435
+ http: {
436
+ baseUrl: 'https://example.com/api',
437
+ },
438
+ headerConfig: {
439
+ version: '1.0.0',
440
+ platformId: '50',
441
+ isReload: false,
442
+ deviceType: 1,
443
+ deviceId: '',
444
+ childPlatformId: '50',
445
+ },
446
+ defaultTransport: 'auto',
447
+ })
448
+ ```
449
+
450
+ 补充说明:如果你已经在 RN 工程里有基于 `@improbable-eng/grpc-web` / `@improbable-eng/grpc-web-react-native-transport` 的 `rnGrpcWebUnary` 封装,也可以直接把它塞进 `invoker.unary`。但需要确保它能消费本 SDK 生成的 bufbuild message 与 schema(`toBinary/fromBinary`),否则通常会因为“message/descriptor 体系不一致”(improbable 常见搭配是另一套 proto 生成物)而对不上类型与序列化方式。
451
+
263
452
  ### 2) 直接调用(推荐)
264
453
 
265
454
  优先使用 service client(类型提示完整,第二参随 transport 自动变化):
package/dist/index.cjs CHANGED
@@ -186,13 +186,52 @@ var GrpcClient = class {
186
186
  getInvokerClientKey(service, baseUrl) {
187
187
  return `${String(service?.typeName ?? "")}@@${baseUrl}`;
188
188
  }
189
+ getServiceMethodMap(service) {
190
+ const out = {};
191
+ const addMethod = (m) => {
192
+ if (!m) return;
193
+ const localName = m.localName != null ? String(m.localName) : "";
194
+ const name = m.name != null ? String(m.name) : "";
195
+ if (localName) out[localName] = m;
196
+ if (name && !(name in out)) out[name] = m;
197
+ };
198
+ const direct = service?.method;
199
+ if (direct && typeof direct === "object") {
200
+ for (const [k, v] of Object.entries(direct)) {
201
+ if (v) out[k] = v;
202
+ addMethod(v);
203
+ }
204
+ }
205
+ const methods = service?.methods;
206
+ if (Array.isArray(methods)) {
207
+ for (const m of methods) addMethod(m);
208
+ } else if (methods && typeof methods === "object") {
209
+ for (const [k, v] of Object.entries(methods)) {
210
+ if (v) out[k] = v;
211
+ addMethod(v);
212
+ }
213
+ }
214
+ return Object.keys(out).length > 0 ? out : void 0;
215
+ }
216
+ getServiceMethodDesc(service, methodName) {
217
+ const methodMap = this.getServiceMethodMap(service);
218
+ if (!methodMap) return void 0;
219
+ const direct = methodMap[methodName];
220
+ if (direct) return direct;
221
+ for (const m of Object.values(methodMap)) {
222
+ if (!m) continue;
223
+ const localName = m.localName != null ? String(m.localName) : "";
224
+ const name = m.name != null ? String(m.name) : "";
225
+ if (localName === methodName || name === methodName) return m;
226
+ }
227
+ return void 0;
228
+ }
189
229
  getServiceViaInvoker(service) {
190
230
  const baseUrl = this.pickBaseUrl(service.typeName ?? "");
191
231
  const key = this.getInvokerClientKey(service, baseUrl);
192
232
  const cached = this.invokerClientByKey.get(key);
193
233
  if (cached) return cached;
194
- const methods = service?.methods;
195
- const hasMethodsMap = methods && typeof methods === "object";
234
+ const methodMap = this.getServiceMethodMap(service);
196
235
  const typeName = String(service?.typeName ?? "");
197
236
  const client = new Proxy(
198
237
  {},
@@ -200,7 +239,7 @@ var GrpcClient = class {
200
239
  get: (_target, prop) => {
201
240
  if (prop === "then") return void 0;
202
241
  if (typeof prop !== "string") return void 0;
203
- if (hasMethodsMap && !(prop in methods)) {
242
+ if (methodMap && !(prop in methodMap)) {
204
243
  return async () => {
205
244
  throw new Error(`gRPC method not found: ${typeName}.${prop}`);
206
245
  };
@@ -219,6 +258,7 @@ var GrpcClient = class {
219
258
  const baseUrl = this.pickBaseUrl(typeName);
220
259
  try {
221
260
  if (this.invoker) {
261
+ const method2 = this.getServiceMethodDesc(service, methodName);
222
262
  const headers = await this.attachInvokerHeaders(
223
263
  this.normalizeHeaders(options?.headers),
224
264
  methodName
@@ -233,6 +273,7 @@ var GrpcClient = class {
233
273
  baseUrl,
234
274
  service,
235
275
  methodName,
276
+ method: method2,
236
277
  request,
237
278
  headers,
238
279
  callOptions: options