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