@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 +88 -11
- package/dist/index.cjs +180 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +176 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
示例
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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);
|