@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 +189 -0
- package/dist/index.cjs +44 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +44 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 (
|
|
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
|