@goplus123/core-api 1.0.4 → 1.0.5

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,46 @@ var GrpcClient = class {
186
186
  getInvokerClientKey(service, baseUrl) {
187
187
  return `${String(service?.typeName ?? "")}@@${baseUrl}`;
188
188
  }
189
+ getServiceMethodDesc(service, methodName) {
190
+ const methods = service?.methods;
191
+ if (!methods) return void 0;
192
+ if (Array.isArray(methods)) {
193
+ for (const m of methods) {
194
+ if (!m) continue;
195
+ const localName = String(m.localName ?? "");
196
+ const name = String(m.name ?? "");
197
+ if (localName === methodName || name === methodName) return m;
198
+ }
199
+ return void 0;
200
+ }
201
+ if (typeof methods === "object") {
202
+ return methods[methodName];
203
+ }
204
+ return void 0;
205
+ }
189
206
  getServiceViaInvoker(service) {
190
207
  const baseUrl = this.pickBaseUrl(service.typeName ?? "");
191
208
  const key = this.getInvokerClientKey(service, baseUrl);
192
209
  const cached = this.invokerClientByKey.get(key);
193
210
  if (cached) return cached;
194
211
  const methods = service?.methods;
195
- const hasMethodsMap = methods && typeof methods === "object";
212
+ const hasMethods = methods != null;
213
+ const methodMap = (() => {
214
+ if (!methods) return void 0;
215
+ if (Array.isArray(methods)) {
216
+ const out = {};
217
+ for (const m of methods) {
218
+ if (!m) continue;
219
+ const localName = String(m.localName ?? "");
220
+ const name = String(m.name ?? "");
221
+ if (localName) out[localName] = m;
222
+ if (name && !(name in out)) out[name] = m;
223
+ }
224
+ return out;
225
+ }
226
+ if (typeof methods === "object") return methods;
227
+ return void 0;
228
+ })();
196
229
  const typeName = String(service?.typeName ?? "");
197
230
  const client = new Proxy(
198
231
  {},
@@ -200,7 +233,7 @@ var GrpcClient = class {
200
233
  get: (_target, prop) => {
201
234
  if (prop === "then") return void 0;
202
235
  if (typeof prop !== "string") return void 0;
203
- if (hasMethodsMap && !(prop in methods)) {
236
+ if (hasMethods && methodMap && !(prop in methodMap)) {
204
237
  return async () => {
205
238
  throw new Error(`gRPC method not found: ${typeName}.${prop}`);
206
239
  };
@@ -219,6 +252,7 @@ var GrpcClient = class {
219
252
  const baseUrl = this.pickBaseUrl(typeName);
220
253
  try {
221
254
  if (this.invoker) {
255
+ const method2 = this.getServiceMethodDesc(service, methodName);
222
256
  const headers = await this.attachInvokerHeaders(
223
257
  this.normalizeHeaders(options?.headers),
224
258
  methodName
@@ -233,6 +267,7 @@ var GrpcClient = class {
233
267
  baseUrl,
234
268
  service,
235
269
  methodName,
270
+ method: method2,
236
271
  request,
237
272
  headers,
238
273
  callOptions: options