@goplus123/core-api 1.0.6 → 1.0.7

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,11 +260,19 @@ const sdk = initSDK({
260
260
 
261
261
  对应实现:[`initSDK`](./src/index.ts#L372-L578)
262
262
 
263
- ### React Native:注入 gRPC invoker(解决 RN 真机解析 gRPC-Web 响应问题)
263
+ ### React Native:配置 gRPC invoker(Invoke)(解决 RN 真机 gRPC-Web 兼容问题)
264
264
 
265
265
  当后端仅提供 gRPC-Web,而 React Native 环境下 `@connectrpc/connect-web` 的 `fetch`/二进制响应解析存在兼容性问题时,可以通过 `grpc.invoker` 注入一层“可替换的底层调用器”,在不改动上层调用方式(`sdk.xxx` / `requestApi` / spec)前提下,切换为 RN 可用的 gRPC-Web 实现。
266
266
 
267
267
  这个 `invoker` 只是一个“可插拔的 unary 调用实现”,不限定必须使用某个第三方库。
268
+ 内置的 `builtin` invoker 使用的是 gRPC-Web(二进制)协议:`application/grpc-web+proto`。
269
+
270
+ `grpc.invoker` 支持两种形态(优先级从高到低):
271
+
272
+ - `invoker: GrpcInvoker`:使用外部传入的自定义 invoker
273
+ - `invoker: 'builtin'`:使用 SDK 内置的 gRPC-Web unary invoker(基于 `fetch` + `toBinary/fromBinary`)
274
+
275
+ 注意:使用 `invoker: 'builtin'` 时,请确保 `grpc.protocol` 为 `'grpc-web'`。
268
276
 
269
277
  `invoker.unary()` 的入参约定:
270
278
 
@@ -275,7 +283,37 @@ const sdk = initSDK({
275
283
  - `headers`:最终要发出去的 headers(已合并 auth/getHeaders/静态 headers 与 callOptions.headers)
276
284
  - `callOptions`:透传的调用选项(例如 timeout),按你的底层实现自行使用
277
285
 
278
- 示例 A(推荐):在 RN 工程内用 `fetch` 走 gRPC-Web(二进制),复用本 SDK 的 bufbuild 生成物(不需要重新编译 proto)。这个方案不需要引入 `@improbable-eng/grpc-web-react-native-transport`:
286
+ 示例 0(推荐):直接启用内置 invoker(不需要在 RN 工程维护一套 invoke 代码):
287
+
288
+ ```ts
289
+ import { initSDK, fetchGuid } from '@goplus123/core-api'
290
+
291
+ const sdk = initSDK({
292
+ ws: {
293
+ url: 'wss://example.com/websocket',
294
+ protocols: fetchGuid(),
295
+ },
296
+ grpc: {
297
+ baseUrl: 'https://example.com/cms',
298
+ protocol: 'grpc-web',
299
+ invoker: 'builtin',
300
+ },
301
+ http: {
302
+ baseUrl: 'https://example.com/api',
303
+ },
304
+ headerConfig: {
305
+ version: '1.0.0',
306
+ platformId: '50',
307
+ isReload: false,
308
+ deviceType: 1,
309
+ deviceId: '',
310
+ childPlatformId: '50',
311
+ },
312
+ defaultTransport: 'auto',
313
+ })
314
+ ```
315
+
316
+ 示例 A(自定义):在 RN 工程内用 `fetch` 走 gRPC-Web(二进制),复用本 SDK 的 bufbuild 生成物(不需要重新编译 proto)。这个方案不需要引入 `@improbable-eng/grpc-web-react-native-transport`:
279
317
 
280
318
  ```ts
281
319
  import { initSDK, fetchGuid } from '@goplus123/core-api'
@@ -352,6 +390,49 @@ function capitalizeRpcName(methodName: string): string {
352
390
  return methodName[0]!.toUpperCase() + methodName.slice(1)
353
391
  }
354
392
 
393
+ function resolveServiceMethod(service: any, methodName: string, method?: any): any | undefined {
394
+ if (method) return method
395
+
396
+ const expectedLocalName = methodName
397
+ const expectedRpcName = capitalizeRpcName(methodName)
398
+
399
+ const direct = service?.method
400
+ if (Array.isArray(direct)) {
401
+ return direct.find((m: any) => {
402
+ const localName = String(m?.localName ?? '')
403
+ const rpcName = String(m?.name ?? '')
404
+ return (
405
+ localName === expectedLocalName ||
406
+ localName === expectedRpcName ||
407
+ rpcName === expectedLocalName ||
408
+ rpcName === expectedRpcName
409
+ )
410
+ })
411
+ }
412
+ if (direct && typeof direct === 'object') {
413
+ return (direct as any)[expectedLocalName] ?? (direct as any)[expectedRpcName]
414
+ }
415
+
416
+ const methods = service?.methods
417
+ if (Array.isArray(methods)) {
418
+ return methods.find((m: any) => {
419
+ const localName = String(m?.localName ?? '')
420
+ const rpcName = String(m?.name ?? '')
421
+ return (
422
+ localName === expectedLocalName ||
423
+ localName === expectedRpcName ||
424
+ rpcName === expectedLocalName ||
425
+ rpcName === expectedRpcName
426
+ )
427
+ })
428
+ }
429
+ if (methods && typeof methods === 'object') {
430
+ return (methods as any)[expectedLocalName] ?? (methods as any)[expectedRpcName]
431
+ }
432
+
433
+ return undefined
434
+ }
435
+
355
436
  function getGrpcWebPath(service: any, methodName: string, method: any): string {
356
437
  const typeName = String(service?.typeName ?? '')
357
438
  const rpcName = String(method?.name ?? method?.rpc?.name ?? '') || capitalizeRpcName(methodName)
@@ -367,14 +448,7 @@ async function rnGrpcWebUnary(args: {
367
448
  headers: Record<string, string>
368
449
  callOptions?: { timeoutMs?: number }
369
450
  }): 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
- })()
451
+ const method = resolveServiceMethod(args.service, args.methodName, args.method)
378
452
  if (!method) {
379
453
  throw new Error(`gRPC method not found: ${String(args.service?.typeName ?? '')}.${args.methodName}`)
380
454
  }
@@ -447,7 +521,10 @@ const sdk = initSDK({
447
521
  })
448
522
  ```
449
523
 
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 生成物)而对不上类型与序列化方式。
524
+ 补充说明:
525
+
526
+ - 自定义 invoker 时,建议优先使用 `args.method`(SDK 解析好的方法描述),避免在 RN 侧重复解析 `service.method/service.methods` 的结构差异。
527
+ - 如果你已经在 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
528
 
452
529
  ### 2) 直接调用(推荐)
453
530
 
package/dist/index.cjs CHANGED
@@ -39,7 +39,174 @@ module.exports = __toCommonJS(index_exports);
39
39
  // src/client/grpcClient.ts
40
40
  var import_connect = require("@connectrpc/connect");
41
41
  var import_connect_web = require("@connectrpc/connect-web");
42
+ var import_protobuf2 = require("@bufbuild/protobuf");
43
+
44
+ // src/client/invokers/builtinGrpcWebInvoker.ts
42
45
  var import_protobuf = require("@bufbuild/protobuf");
46
+ function createBuiltinGrpcWebInvoker(options) {
47
+ const fetchImpl = options.fetch ?? globalThis.fetch;
48
+ const logger = options.logger;
49
+ if (!fetchImpl) {
50
+ return {
51
+ unary: async () => {
52
+ throw new Error("fetch is not available in current environment");
53
+ }
54
+ };
55
+ }
56
+ const grpcWebEncodeUnary = (messageBytes) => {
57
+ const out = new Uint8Array(5 + messageBytes.byteLength);
58
+ out[0] = 0;
59
+ const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
60
+ view.setUint32(1, messageBytes.byteLength, false);
61
+ out.set(messageBytes, 5);
62
+ return out;
63
+ };
64
+ const bytesToAscii = (bytes) => {
65
+ let s = "";
66
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
67
+ return s;
68
+ };
69
+ const parseGrpcWebTrailers = (bytes) => {
70
+ const text = bytesToAscii(bytes);
71
+ const out = {};
72
+ for (const line of text.split("\r\n")) {
73
+ if (!line) continue;
74
+ const idx = line.indexOf(":");
75
+ if (idx < 0) continue;
76
+ const k = line.slice(0, idx).trim().toLowerCase();
77
+ const v = line.slice(idx + 1).trim();
78
+ out[k] = v;
79
+ }
80
+ return out;
81
+ };
82
+ const grpcWebDecodeUnary = (body) => {
83
+ let offset = 0;
84
+ let message;
85
+ let trailers = {};
86
+ while (offset + 5 <= body.byteLength) {
87
+ const flags = body[offset];
88
+ const length = new DataView(body.buffer, body.byteOffset + offset + 1, 4).getUint32(0, false);
89
+ offset += 5;
90
+ if (offset + length > body.byteLength) break;
91
+ const frame = body.subarray(offset, offset + length);
92
+ offset += length;
93
+ const isTrailer = (flags & 128) === 128;
94
+ if (isTrailer) {
95
+ trailers = { ...trailers, ...parseGrpcWebTrailers(frame) };
96
+ } else if (message == null) {
97
+ message = frame;
98
+ }
99
+ }
100
+ return { message, trailers };
101
+ };
102
+ const joinUrl = (baseUrl, path) => {
103
+ return baseUrl.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
104
+ };
105
+ const capitalizeRpcName = (methodName) => {
106
+ if (!methodName) return methodName;
107
+ return methodName[0].toUpperCase() + methodName.slice(1);
108
+ };
109
+ const getGrpcWebPath = (service, methodName, method) => {
110
+ const typeName = String(service?.typeName ?? "");
111
+ const rpcName = String(method?.name ?? "") || capitalizeRpcName(methodName);
112
+ return `${typeName}/${rpcName}`;
113
+ };
114
+ const safeDecodeGrpcMessage = (value) => {
115
+ if (!value) return value;
116
+ try {
117
+ return decodeURIComponent(value);
118
+ } catch {
119
+ return value;
120
+ }
121
+ };
122
+ return {
123
+ unary: async (args) => {
124
+ const service = String(args.service?.typeName ?? "");
125
+ const startedAt = Date.now();
126
+ let url;
127
+ const contentType = "application/grpc-web+proto";
128
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
129
+ const timeoutMs = args.callOptions?.timeoutMs;
130
+ const timeoutId = controller && typeof timeoutMs === "number" ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
131
+ try {
132
+ const method = args.method;
133
+ if (!method) {
134
+ throw new Error(`gRPC method not found: ${service}.${args.methodName}`);
135
+ }
136
+ url = joinUrl(args.baseUrl, getGrpcWebPath(args.service, args.methodName, method));
137
+ const requestBytes = (0, import_protobuf.toBinary)(method.input, args.request);
138
+ const body = grpcWebEncodeUnary(requestBytes);
139
+ logger?.debug?.("[Grpc:Invoker:Builtin:Start]", {
140
+ service,
141
+ methodName: args.methodName,
142
+ baseUrl: args.baseUrl,
143
+ url,
144
+ timeoutMs,
145
+ requestBytes: requestBytes.byteLength
146
+ });
147
+ const res = await fetchImpl(url, {
148
+ method: "POST",
149
+ headers: {
150
+ ...args.headers,
151
+ "content-type": contentType,
152
+ accept: contentType,
153
+ "x-grpc-web": "1"
154
+ },
155
+ body: body.buffer,
156
+ signal: controller?.signal
157
+ });
158
+ logger?.debug?.("[Grpc:Invoker:Builtin:Http]", {
159
+ service,
160
+ methodName: args.methodName,
161
+ url,
162
+ ok: res.ok,
163
+ status: res.status,
164
+ statusText: res.statusText,
165
+ durationMs: Date.now() - startedAt
166
+ });
167
+ if (!res.ok) {
168
+ throw new Error(`gRPC fetch failed: ${res.status} ${res.statusText}`);
169
+ }
170
+ const raw = new Uint8Array(await res.arrayBuffer());
171
+ const decoded = grpcWebDecodeUnary(raw);
172
+ const statusFromHeaders = typeof res.headers?.get === "function" ? res.headers.get("grpc-status") : void 0;
173
+ const msgFromHeaders = typeof res.headers?.get === "function" ? res.headers.get("grpc-message") : void 0;
174
+ const grpcStatus = decoded.trailers["grpc-status"] ?? statusFromHeaders ?? "0";
175
+ const grpcMessage = safeDecodeGrpcMessage(
176
+ decoded.trailers["grpc-message"] ?? msgFromHeaders
177
+ );
178
+ logger?.debug?.("[Grpc:Invoker:Builtin:End]", {
179
+ service,
180
+ methodName: args.methodName,
181
+ url,
182
+ grpcStatus,
183
+ grpcMessage,
184
+ durationMs: Date.now() - startedAt
185
+ });
186
+ if (grpcStatus !== "0") {
187
+ const msg = grpcMessage ?? "gRPC request failed";
188
+ throw new Error(`gRPC status=${grpcStatus} message=${msg}`);
189
+ }
190
+ if (!decoded.message) throw new Error("gRPC response message missing");
191
+ return (0, import_protobuf.fromBinary)(method.output, decoded.message);
192
+ } catch (error) {
193
+ logger?.error?.("[Grpc:Invoker:Builtin:Error]", {
194
+ service,
195
+ methodName: args.methodName,
196
+ baseUrl: args.baseUrl,
197
+ url,
198
+ durationMs: Date.now() - startedAt,
199
+ error
200
+ });
201
+ throw error;
202
+ } finally {
203
+ if (timeoutId) clearTimeout(timeoutId);
204
+ }
205
+ }
206
+ };
207
+ }
208
+
209
+ // src/client/grpcClient.ts
43
210
  var GrpcApiError = class extends Error {
44
211
  name = "GrpcApiError";
45
212
  service;
@@ -70,6 +237,7 @@ var GrpcClient = class {
70
237
  getHeaders;
71
238
  logger;
72
239
  invoker;
240
+ invokerMode;
73
241
  invokerClientByKey;
74
242
  constructor(config) {
75
243
  this.baseUrl = config.baseUrl;
@@ -80,7 +248,6 @@ var GrpcClient = class {
80
248
  this.getToken = config.getToken;
81
249
  this.headers = config.headers;
82
250
  this.getHeaders = config.getHeaders;
83
- this.invoker = config.invoker;
84
251
  this.logger = config.logger ?? (config.debug ? { debug: console.log, info: console.log, warn: console.warn, error: console.error } : void 0);
85
252
  this.interceptors = [
86
253
  this.createRpcFromInterceptor(),
@@ -91,6 +258,8 @@ var GrpcClient = class {
91
258
  this.transportByBaseUrl = /* @__PURE__ */ new Map();
92
259
  this.transport = this.createTransport(this.baseUrl);
93
260
  this.invokerClientByKey = /* @__PURE__ */ new Map();
261
+ this.invokerMode = config.invoker === "builtin" ? "builtin" : config.invoker ? "custom" : "none";
262
+ this.invoker = config.invoker === "builtin" ? createBuiltinGrpcWebInvoker({ fetch: this.fetchImpl, logger: this.logger }) : config.invoker;
94
263
  }
95
264
  createTransport(baseUrl) {
96
265
  const cached = this.transportByBaseUrl.get(baseUrl);
@@ -258,6 +427,12 @@ var GrpcClient = class {
258
427
  const baseUrl = this.pickBaseUrl(typeName);
259
428
  try {
260
429
  if (this.invoker) {
430
+ this.logger?.debug?.("[Grpc:Invoker]", {
431
+ mode: this.invokerMode,
432
+ service: typeName,
433
+ methodName,
434
+ baseUrl
435
+ });
261
436
  const method2 = this.getServiceMethodDesc(service, methodName);
262
437
  const headers = await this.attachInvokerHeaders(
263
438
  this.normalizeHeaders(options?.headers),
@@ -319,7 +494,7 @@ var GrpcClient = class {
319
494
  return (0, import_connect.createClient)(service, transport);
320
495
  }
321
496
  unary(schema, payload, call, options) {
322
- const req = (0, import_protobuf.create)(schema, payload);
497
+ const req = (0, import_protobuf2.create)(schema, payload);
323
498
  return call(req, options);
324
499
  }
325
500
  /**
@@ -1513,7 +1688,7 @@ var WsRpcClient = class {
1513
1688
  };
1514
1689
 
1515
1690
  // src/client/unifiedApiClient.ts
1516
- var import_protobuf2 = require("@bufbuild/protobuf");
1691
+ var import_protobuf3 = require("@bufbuild/protobuf");
1517
1692
 
1518
1693
  // src/client/interceptors.ts
1519
1694
  function isBizError(err) {
@@ -1750,7 +1925,7 @@ var UnifiedApiClient = class {
1750
1925
  throw new Error(`gRPC method not found: ${service.typeName}.${methodName}`);
1751
1926
  }
1752
1927
  const callOptions = ctx.meta?.callOptions;
1753
- const req = endpoint.grpc.requestSchema ? (0, import_protobuf2.create)(endpoint.grpc.requestSchema, ctx.request ?? {}) : ctx.request;
1928
+ const req = endpoint.grpc.requestSchema ? (0, import_protobuf3.create)(endpoint.grpc.requestSchema, ctx.request ?? {}) : ctx.request;
1754
1929
  const headers = { ...callOptions?.headers ?? {}, ...ctx.headers ?? {} };
1755
1930
  return await method(req, { ...callOptions ?? {}, headers });
1756
1931
  }
@@ -1975,7 +2150,7 @@ var file_auth_MayaService = /* @__PURE__ */ (0, import_codegenv241.fileDesc)("Ch
1975
2150
  var MayaService = /* @__PURE__ */ (0, import_codegenv241.serviceDesc)(file_auth_MayaService, 0);
1976
2151
 
1977
2152
  // src/modules/auth.ts
1978
- var import_protobuf3 = require("@bufbuild/protobuf");
2153
+ var import_protobuf4 = require("@bufbuild/protobuf");
1979
2154
  var authService = (grpc) => {
1980
2155
  const client = grpc.getService(FrontendAuthService);
1981
2156
  const faceId = grpc.getService(FrontendFaceIdService);