service-bridge 1.0.17 → 1.1.0-dev.27
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 +10 -13
- package/dist/index.js +315 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,6 @@ import { servicebridge } from "service-bridge";
|
|
|
97
97
|
const sb = servicebridge(
|
|
98
98
|
process.env.SERVICEBRIDGE_URL ?? "localhost:14445",
|
|
99
99
|
process.env.SERVICEBRIDGE_SERVICE_KEY!,
|
|
100
|
-
"payments",
|
|
101
100
|
);
|
|
102
101
|
|
|
103
102
|
sb.handleRpc("charge", async (payload: { orderId: string; amount: number }) => {
|
|
@@ -115,7 +114,6 @@ import { servicebridge } from "service-bridge";
|
|
|
115
114
|
const sb = servicebridge(
|
|
116
115
|
process.env.SERVICEBRIDGE_URL ?? "localhost:14445",
|
|
117
116
|
process.env.SERVICEBRIDGE_SERVICE_KEY!,
|
|
118
|
-
"orders",
|
|
119
117
|
);
|
|
120
118
|
|
|
121
119
|
const result = await sb.rpc<{ ok: boolean; txId: string }>("payments/charge", {
|
|
@@ -153,7 +151,7 @@ import { servicebridge } from "service-bridge";
|
|
|
153
151
|
|
|
154
152
|
// --- Payments service (worker) ---
|
|
155
153
|
|
|
156
|
-
const payments = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY
|
|
154
|
+
const payments = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
|
|
157
155
|
|
|
158
156
|
payments.handleRpc("charge", async (payload: { orderId: string; amount: number }, ctx) => {
|
|
159
157
|
await ctx?.stream.write({ status: "charging", orderId: payload.orderId }, "progress");
|
|
@@ -170,7 +168,7 @@ await payments.serve({ host: "localhost" });
|
|
|
170
168
|
```ts
|
|
171
169
|
// --- Orders service (caller + event publisher) ---
|
|
172
170
|
|
|
173
|
-
const orders = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY
|
|
171
|
+
const orders = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
|
|
174
172
|
|
|
175
173
|
// Call payments, then publish event
|
|
176
174
|
const charge = await orders.rpc<{ ok: boolean; txId: string }>("payments/charge", {
|
|
@@ -190,7 +188,7 @@ await orders.event("orders.completed", {
|
|
|
190
188
|
```ts
|
|
191
189
|
// --- Notifications service (event consumer) ---
|
|
192
190
|
|
|
193
|
-
const notifications = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY
|
|
191
|
+
const notifications = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
|
|
194
192
|
|
|
195
193
|
notifications.handleEvent("orders.*", async (payload, ctx) => {
|
|
196
194
|
const body = payload as { orderId: string; txId: string };
|
|
@@ -273,18 +271,19 @@ across all three SDKs. Parity differences are naming-only (language idioms):
|
|
|
273
271
|
- Handler hints: timeout/retryable/concurrency/prefetch are advisory in all SDKs
|
|
274
272
|
- Shared `serve()` fields across SDKs: host, max in-flight, instance ID, weight, and per-serve TLS override
|
|
275
273
|
|
|
276
|
-
### `servicebridge(url, serviceKey,
|
|
274
|
+
### `servicebridge(url, serviceKey, opts?)`
|
|
277
275
|
|
|
278
276
|
```ts
|
|
279
277
|
function servicebridge(
|
|
280
278
|
url: string,
|
|
281
279
|
serviceKey: string,
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
serviceOrOpts?: string | ServiceBridgeOpts,
|
|
281
|
+
maybeGlobalOpts?: ServiceBridgeOpts,
|
|
284
282
|
): ServiceBridgeService
|
|
285
283
|
```
|
|
286
284
|
|
|
287
285
|
Creates an SDK client instance.
|
|
286
|
+
Service identity is resolved by the runtime from `serviceKey`; passing a third `service` argument is legacy-only.
|
|
288
287
|
|
|
289
288
|
`ServiceBridgeOpts`:
|
|
290
289
|
|
|
@@ -540,7 +539,7 @@ Registers an event consumer handler. Chainable.
|
|
|
540
539
|
|
|
541
540
|
| Option | Type | Description |
|
|
542
541
|
|---|---|---|
|
|
543
|
-
| `groupName` | `string` | Consumer group name. Default: `<service>.<pattern>`. |
|
|
542
|
+
| `groupName` | `string` | Consumer group name. Default: `<service-key-id>.<pattern>`. |
|
|
544
543
|
| `concurrency` | `number` | Advisory concurrency hint (currently not hard-enforced). |
|
|
545
544
|
| `prefetch` | `number` | Advisory prefetch hint (currently not hard-enforced). |
|
|
546
545
|
| `retryPolicyJson` | `string` | Retry policy JSON string. |
|
|
@@ -766,7 +765,7 @@ import express from "express";
|
|
|
766
765
|
import { servicebridge } from "service-bridge";
|
|
767
766
|
import { servicebridgeMiddleware, registerExpressRoutes } from "service-bridge/express";
|
|
768
767
|
|
|
769
|
-
const sb = servicebridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY
|
|
768
|
+
const sb = servicebridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY!);
|
|
770
769
|
const app = express();
|
|
771
770
|
|
|
772
771
|
app.use(servicebridgeMiddleware({
|
|
@@ -822,7 +821,7 @@ import Fastify from "fastify";
|
|
|
822
821
|
import { servicebridge } from "service-bridge";
|
|
823
822
|
import { servicebridgePlugin, wrapHandler } from "service-bridge/fastify";
|
|
824
823
|
|
|
825
|
-
const sb = servicebridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY
|
|
824
|
+
const sb = servicebridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY!);
|
|
826
825
|
const app = Fastify();
|
|
827
826
|
|
|
828
827
|
await app.register(servicebridgePlugin, {
|
|
@@ -895,13 +894,11 @@ The SDK requires values you pass into `servicebridge(...)`. Common setup:
|
|
|
895
894
|
|---|---|---|---|
|
|
896
895
|
| `SERVICEBRIDGE_URL` | yes | `localhost:14445` | gRPC control plane URL |
|
|
897
896
|
| `SERVICEBRIDGE_SERVICE_KEY` | yes | `sbv2.<id>.<secret>.<ca>` | Service authentication key (sbv2 only) |
|
|
898
|
-
| `SERVICEBRIDGE_SERVICE` | yes (worker mode) | `orders` | Service name in registry |
|
|
899
897
|
|
|
900
898
|
```ts
|
|
901
899
|
const sb = servicebridge(
|
|
902
900
|
process.env.SERVICEBRIDGE_URL ?? "localhost:14445",
|
|
903
901
|
process.env.SERVICEBRIDGE_SERVICE_KEY!,
|
|
904
|
-
process.env.SERVICEBRIDGE_SERVICE ?? "orders",
|
|
905
902
|
);
|
|
906
903
|
```
|
|
907
904
|
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,27 @@ class ServiceBridgeError extends Error {
|
|
|
90
90
|
this.operation = opts.operation;
|
|
91
91
|
this.severity = opts.severity;
|
|
92
92
|
this.retryable = opts.severity === "retriable";
|
|
93
|
-
this
|
|
93
|
+
Object.defineProperty(this, "cause", {
|
|
94
|
+
value: opts.cause,
|
|
95
|
+
writable: true,
|
|
96
|
+
enumerable: false,
|
|
97
|
+
configurable: true
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
toString() {
|
|
101
|
+
return `ServiceBridgeError [${this.severity}]: ${this.message} (${this.operation})`;
|
|
102
|
+
}
|
|
103
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
104
|
+
const lines = [
|
|
105
|
+
`ServiceBridgeError [${this.severity}]`,
|
|
106
|
+
` operation: ${this.operation}`,
|
|
107
|
+
` message: ${this.message}`
|
|
108
|
+
];
|
|
109
|
+
if (this.code !== undefined) {
|
|
110
|
+
lines.push(` code: ${this.code}`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join(`
|
|
113
|
+
`);
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
function containsValue(values, value) {
|
|
@@ -214,7 +234,14 @@ function normalizeServiceError(err, operation, component = "control-plane") {
|
|
|
214
234
|
}
|
|
215
235
|
const grpcErr = err;
|
|
216
236
|
const code = typeof grpcErr?.code === "number" ? grpcErr.code : undefined;
|
|
217
|
-
const
|
|
237
|
+
const details = typeof grpcErr?.details === "string" && grpcErr.details.trim() ? grpcErr.details.trim() : undefined;
|
|
238
|
+
const rawMsg = (() => {
|
|
239
|
+
const m = typeof grpcErr?.message === "string" && grpcErr.message ? grpcErr.message : err instanceof Error ? err.message : String(err);
|
|
240
|
+
return m.replace(/^\d+\s+[A-Z_]+:\s*/, "").trim() || m;
|
|
241
|
+
})();
|
|
242
|
+
const base = friendlyGrpcMessage(code, details ?? rawMsg);
|
|
243
|
+
const appendDetails = details && (code === grpc.status.INTERNAL || code === grpc.status.UNKNOWN || code === undefined);
|
|
244
|
+
const message = appendDetails ? `${base}: ${details}` : base;
|
|
218
245
|
return new ServiceBridgeError({
|
|
219
246
|
message,
|
|
220
247
|
code,
|
|
@@ -224,13 +251,74 @@ function normalizeServiceError(err, operation, component = "control-plane") {
|
|
|
224
251
|
cause: err
|
|
225
252
|
});
|
|
226
253
|
}
|
|
254
|
+
function friendlyGrpcMessage(code, rawMessage) {
|
|
255
|
+
if (code === undefined)
|
|
256
|
+
return rawMessage || "unknown error";
|
|
257
|
+
switch (code) {
|
|
258
|
+
case grpc.status.UNAVAILABLE:
|
|
259
|
+
return "control plane unavailable";
|
|
260
|
+
case grpc.status.DEADLINE_EXCEEDED:
|
|
261
|
+
return "request timed out";
|
|
262
|
+
case grpc.status.UNAUTHENTICATED:
|
|
263
|
+
return "authentication failed — check service key";
|
|
264
|
+
case grpc.status.PERMISSION_DENIED:
|
|
265
|
+
return "permission denied — check service key";
|
|
266
|
+
case grpc.status.RESOURCE_EXHAUSTED:
|
|
267
|
+
return "rate limit exceeded";
|
|
268
|
+
case grpc.status.NOT_FOUND:
|
|
269
|
+
return "resource not found";
|
|
270
|
+
case grpc.status.INTERNAL:
|
|
271
|
+
return "internal server error";
|
|
272
|
+
case grpc.status.FAILED_PRECONDITION:
|
|
273
|
+
return "precondition failed";
|
|
274
|
+
case grpc.status.CANCELLED:
|
|
275
|
+
return "request cancelled";
|
|
276
|
+
case grpc.status.UNIMPLEMENTED:
|
|
277
|
+
return "operation not supported by control plane";
|
|
278
|
+
case grpc.status.UNKNOWN:
|
|
279
|
+
return "unknown error from control plane";
|
|
280
|
+
default:
|
|
281
|
+
return rawMessage || "unknown error";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function friendlySDKMessage(operation, err) {
|
|
285
|
+
const base = err.message;
|
|
286
|
+
switch (operation) {
|
|
287
|
+
case "open-worker-session":
|
|
288
|
+
case "worker-session":
|
|
289
|
+
return err.severity === "retriable" ? `${base} — waiting for reconnect` : `worker session error: ${base}`;
|
|
290
|
+
case "worker-session-end":
|
|
291
|
+
return `worker session cleanup: ${base}`;
|
|
292
|
+
case "worker-session-command":
|
|
293
|
+
return `worker command error: ${base}`;
|
|
294
|
+
case "heartbeat":
|
|
295
|
+
return `heartbeat failed: ${base}`;
|
|
296
|
+
case "http-heartbeat":
|
|
297
|
+
return `HTTP heartbeat failed: ${base}`;
|
|
298
|
+
case "flush-offline-queue":
|
|
299
|
+
return `offline queue flush failed: ${base}`;
|
|
300
|
+
case "flush-on-ready":
|
|
301
|
+
case "flush-on-restore":
|
|
302
|
+
return `offline queue flush failed: ${base}`;
|
|
303
|
+
case "report-call-start":
|
|
304
|
+
case "report-call":
|
|
305
|
+
return `call report failed: ${base}`;
|
|
306
|
+
case "worker-force-shutdown":
|
|
307
|
+
case "worker-try-shutdown":
|
|
308
|
+
return `worker shutdown error: ${base}`;
|
|
309
|
+
case "close-control-plane-client":
|
|
310
|
+
return `control plane disconnect: ${base}`;
|
|
311
|
+
default:
|
|
312
|
+
return `${operation}: ${base}`;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
227
315
|
function reportSDKError(operation, err, component = "sdk") {
|
|
228
316
|
const normalized = normalizeServiceError(err, operation, component);
|
|
229
|
-
const
|
|
317
|
+
const friendly = friendlySDKMessage(operation, normalized);
|
|
230
318
|
if (normalized.severity === "fatal") {
|
|
231
|
-
console.error(
|
|
319
|
+
console.error(`[servicebridge] ${friendly}`);
|
|
232
320
|
} else {
|
|
233
|
-
console.warn(
|
|
321
|
+
console.warn(`[servicebridge] ${friendly}`);
|
|
234
322
|
}
|
|
235
323
|
return normalized;
|
|
236
324
|
}
|
|
@@ -260,6 +348,101 @@ function workerChannelOptions(tlsOpts) {
|
|
|
260
348
|
function workerClientCredentials(tlsOpts) {
|
|
261
349
|
return grpc.credentials.createSsl(toPemBuffer(tlsOpts?.caCert), toPemBuffer(tlsOpts?.key), toPemBuffer(tlsOpts?.cert));
|
|
262
350
|
}
|
|
351
|
+
var _wireStringMapMode = null;
|
|
352
|
+
function detectWireStringMapMode() {
|
|
353
|
+
const publishMethod = servicebridgeProto.ServiceBridge.service?.Publish;
|
|
354
|
+
const serialize = publishMethod?.requestSerialize;
|
|
355
|
+
if (typeof serialize !== "function") {
|
|
356
|
+
return "entries";
|
|
357
|
+
}
|
|
358
|
+
const baseReq = {
|
|
359
|
+
topic: "__probe__",
|
|
360
|
+
payload: Buffer.alloc(0),
|
|
361
|
+
trace_id: "",
|
|
362
|
+
parent_span_id: "",
|
|
363
|
+
producer_service: "",
|
|
364
|
+
idempotency_key: ""
|
|
365
|
+
};
|
|
366
|
+
try {
|
|
367
|
+
serialize({
|
|
368
|
+
...baseReq,
|
|
369
|
+
headers: { probe: "1" }
|
|
370
|
+
});
|
|
371
|
+
return "object";
|
|
372
|
+
} catch {
|
|
373
|
+
try {
|
|
374
|
+
serialize({
|
|
375
|
+
...baseReq,
|
|
376
|
+
headers: [{ key: "probe", value: "1" }]
|
|
377
|
+
});
|
|
378
|
+
return "entries";
|
|
379
|
+
} catch {
|
|
380
|
+
return "entries";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function wireStringMapMode() {
|
|
385
|
+
if (_wireStringMapMode === null) {
|
|
386
|
+
_wireStringMapMode = detectWireStringMapMode();
|
|
387
|
+
}
|
|
388
|
+
return _wireStringMapMode;
|
|
389
|
+
}
|
|
390
|
+
function normalizeStringMap(raw) {
|
|
391
|
+
const normalized = {};
|
|
392
|
+
if (!raw)
|
|
393
|
+
return normalized;
|
|
394
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
395
|
+
const k = key.trim();
|
|
396
|
+
if (!k)
|
|
397
|
+
continue;
|
|
398
|
+
if (typeof value === "string") {
|
|
399
|
+
normalized[k] = value;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (value == null) {
|
|
403
|
+
normalized[k] = "";
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
407
|
+
normalized[k] = String(value);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
normalized[k] = JSON.stringify(value);
|
|
412
|
+
} catch {
|
|
413
|
+
normalized[k] = String(value);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return normalized;
|
|
417
|
+
}
|
|
418
|
+
function toWireStringMap(raw) {
|
|
419
|
+
const normalized = normalizeStringMap(raw);
|
|
420
|
+
if (wireStringMapMode() === "entries") {
|
|
421
|
+
return Object.entries(normalized).map(([key, value]) => ({
|
|
422
|
+
key,
|
|
423
|
+
value
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
return normalized;
|
|
427
|
+
}
|
|
428
|
+
function fromWireStringMap(raw) {
|
|
429
|
+
if (Array.isArray(raw)) {
|
|
430
|
+
const out = {};
|
|
431
|
+
for (const entry of raw) {
|
|
432
|
+
if (typeof entry?.key !== "string")
|
|
433
|
+
continue;
|
|
434
|
+
const key = entry.key.trim();
|
|
435
|
+
if (!key)
|
|
436
|
+
continue;
|
|
437
|
+
out[key] = typeof entry.value === "string" ? entry.value : entry.value == null ? "" : String(entry.value);
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
if (raw && typeof raw === "object") {
|
|
442
|
+
return normalizeStringMap(raw);
|
|
443
|
+
}
|
|
444
|
+
return {};
|
|
445
|
+
}
|
|
263
446
|
async function provisionWorkerTLS(controlClient, md, serviceName, extraIps) {
|
|
264
447
|
const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
|
|
265
448
|
namedCurve: "P-256",
|
|
@@ -296,8 +479,16 @@ function ensureSbResolverRegistered() {
|
|
|
296
479
|
listener;
|
|
297
480
|
refreshTimer = null;
|
|
298
481
|
constructor(target, listener) {
|
|
299
|
-
|
|
300
|
-
|
|
482
|
+
const authority = (target.authority ?? "").trim();
|
|
483
|
+
const normalizedPath = (target.path ?? "").replace(/^\/+/, "");
|
|
484
|
+
if (authority && authority !== "localhost" && _sbContexts.has(authority)) {
|
|
485
|
+
this.clientId = authority;
|
|
486
|
+
this.canonicalName = normalizedPath;
|
|
487
|
+
} else {
|
|
488
|
+
const [clientIdFromPath = "", ...canonicalParts] = normalizedPath.split("/");
|
|
489
|
+
this.clientId = clientIdFromPath.trim();
|
|
490
|
+
this.canonicalName = canonicalParts.join("/").trim();
|
|
491
|
+
}
|
|
301
492
|
this.listener = listener;
|
|
302
493
|
this.init().catch(() => {});
|
|
303
494
|
}
|
|
@@ -309,7 +500,7 @@ function ensureSbResolverRegistered() {
|
|
|
309
500
|
error: {
|
|
310
501
|
code: 14,
|
|
311
502
|
details: `No SB context for clientId=${this.clientId}`,
|
|
312
|
-
metadata:
|
|
503
|
+
metadata: new grpc.Metadata
|
|
313
504
|
}
|
|
314
505
|
}, {}, null, "sb-resolver");
|
|
315
506
|
return;
|
|
@@ -336,7 +527,7 @@ function ensureSbResolverRegistered() {
|
|
|
336
527
|
error: {
|
|
337
528
|
code: 14,
|
|
338
529
|
details: `No live endpoints for ${this.canonicalName}`,
|
|
339
|
-
metadata:
|
|
530
|
+
metadata: new grpc.Metadata
|
|
340
531
|
}
|
|
341
532
|
}, {}, null, "sb-resolver");
|
|
342
533
|
return;
|
|
@@ -355,7 +546,11 @@ function ensureSbResolverRegistered() {
|
|
|
355
546
|
}
|
|
356
547
|
registerResolver("sb", SbResolver);
|
|
357
548
|
}
|
|
358
|
-
function servicebridge(url, serviceKey,
|
|
549
|
+
function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}) {
|
|
550
|
+
const service = typeof serviceOrOpts === "string" ? serviceOrOpts.trim() : "";
|
|
551
|
+
const globalOpts = typeof serviceOrOpts === "string" ? maybeGlobalOpts : serviceOrOpts ?? {};
|
|
552
|
+
const parsedServiceKey = parseServiceKeyV2(serviceKey);
|
|
553
|
+
const defaultConsumerPrefix = service || parsedServiceKey.keyId || "consumer";
|
|
359
554
|
const meta = new grpc.Metadata;
|
|
360
555
|
meta.add("x-service-key", serviceKey);
|
|
361
556
|
const rawUrl = url.trim();
|
|
@@ -444,7 +639,30 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
444
639
|
md.add("x-caller-service", service);
|
|
445
640
|
return md;
|
|
446
641
|
}
|
|
642
|
+
function sessionSend(msg) {
|
|
643
|
+
if (!workerSessionStream)
|
|
644
|
+
return false;
|
|
645
|
+
try {
|
|
646
|
+
workerSessionStream.write(msg);
|
|
647
|
+
return true;
|
|
648
|
+
} catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
447
652
|
async function sendReportCallStart(op) {
|
|
653
|
+
if (sessionSend({
|
|
654
|
+
telemetry_start: {
|
|
655
|
+
trace_id: op.traceId,
|
|
656
|
+
span_id: op.spanId,
|
|
657
|
+
parent_span_id: op.parentSpanId,
|
|
658
|
+
fn: op.fn,
|
|
659
|
+
started_at: String(op.startedAt),
|
|
660
|
+
input: op.inputBuf,
|
|
661
|
+
attempt: op.attempt
|
|
662
|
+
}
|
|
663
|
+
})) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
448
666
|
await new Promise((resolve, reject) => {
|
|
449
667
|
stub.ReportCallStart({
|
|
450
668
|
trace_id: op.traceId,
|
|
@@ -460,6 +678,22 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
460
678
|
});
|
|
461
679
|
}
|
|
462
680
|
async function sendReportCall(op) {
|
|
681
|
+
if (sessionSend({
|
|
682
|
+
telemetry_finish: {
|
|
683
|
+
trace_id: op.traceId,
|
|
684
|
+
span_id: op.spanId,
|
|
685
|
+
fn: op.fn,
|
|
686
|
+
started_at: String(op.startedAt),
|
|
687
|
+
duration_ms: String(op.durationMs),
|
|
688
|
+
success: op.success,
|
|
689
|
+
error: op.error ?? "",
|
|
690
|
+
input: op.inputBuf,
|
|
691
|
+
output: op.outputBuf ?? Buffer.alloc(0),
|
|
692
|
+
attempt: op.attempt
|
|
693
|
+
}
|
|
694
|
+
})) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
463
697
|
await new Promise((resolve, reject) => {
|
|
464
698
|
stub.ReportCall({
|
|
465
699
|
trace_id: op.traceId,
|
|
@@ -467,7 +701,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
467
701
|
fn: op.fn,
|
|
468
702
|
service_name: service,
|
|
469
703
|
started_at: String(op.startedAt),
|
|
470
|
-
duration_ms: String(
|
|
704
|
+
duration_ms: String(op.durationMs),
|
|
471
705
|
success: op.success,
|
|
472
706
|
error: op.error ?? "",
|
|
473
707
|
input: op.inputBuf,
|
|
@@ -482,12 +716,17 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
482
716
|
function flushLogs() {
|
|
483
717
|
if (logBatch.length === 0)
|
|
484
718
|
return;
|
|
485
|
-
const entries = logBatch
|
|
719
|
+
const entries = logBatch.map((entry) => ({
|
|
720
|
+
...entry,
|
|
721
|
+
attributes: toWireStringMap(entry.attributes)
|
|
722
|
+
}));
|
|
486
723
|
logBatch = [];
|
|
487
724
|
if (logFlushTimer !== null) {
|
|
488
725
|
clearTimeout(logFlushTimer);
|
|
489
726
|
logFlushTimer = null;
|
|
490
727
|
}
|
|
728
|
+
if (sessionSend({ telemetry_log: { entries } }))
|
|
729
|
+
return;
|
|
491
730
|
stub.ReportLog({ entries }, meta, unaryDeadlineOptions(), () => {});
|
|
492
731
|
}
|
|
493
732
|
function pushLog(level, msg, attrs) {
|
|
@@ -614,7 +853,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
614
853
|
throw new Error("worker TLS is not configured");
|
|
615
854
|
}
|
|
616
855
|
const creds = workerClientCredentials(tlsOpts);
|
|
617
|
-
ch = new grpc.Channel(`sb
|
|
856
|
+
ch = new grpc.Channel(`sb://localhost/${clientId}/${canonicalName}`, creds, {
|
|
618
857
|
...workerChannelOptions(tlsOpts ?? undefined),
|
|
619
858
|
"grpc.lb_policy_name": "round_robin"
|
|
620
859
|
});
|
|
@@ -676,18 +915,21 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
676
915
|
onlineRestoreTimer = null;
|
|
677
916
|
if (!stopped) {
|
|
678
917
|
isOnline = true;
|
|
918
|
+
const queueLen = offlineQueue.length;
|
|
919
|
+
console.info(`[servicebridge] reconnected to runtime${queueLen > 0 ? ` — flushing ${queueLen} queued operation(s)` : ""}`);
|
|
679
920
|
flushQueue().catch((err) => {
|
|
680
921
|
if (isConnectionError(err))
|
|
681
922
|
scheduleOnlineRestore();
|
|
682
923
|
else
|
|
683
924
|
reportSDKError("flush-on-restore", err);
|
|
684
925
|
});
|
|
926
|
+
scheduleNextHeartbeat(100);
|
|
685
927
|
}
|
|
686
928
|
}, 2000);
|
|
687
929
|
}
|
|
688
930
|
function isConnectionError(e) {
|
|
689
931
|
const code = e?.code;
|
|
690
|
-
return code === grpc.status.UNAVAILABLE || code === grpc.status.UNKNOWN;
|
|
932
|
+
return code === grpc.status.UNAVAILABLE || code === grpc.status.UNKNOWN || code === grpc.status.DEADLINE_EXCEEDED || code === grpc.status.RESOURCE_EXHAUSTED || code === grpc.status.INTERNAL;
|
|
691
933
|
}
|
|
692
934
|
function normalizeUnknownErrorMessage(error) {
|
|
693
935
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -716,6 +958,9 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
716
958
|
attempt
|
|
717
959
|
}).catch((err) => {
|
|
718
960
|
if (isConnectionError(err)) {
|
|
961
|
+
if (isOnline) {
|
|
962
|
+
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
963
|
+
}
|
|
719
964
|
isOnline = false;
|
|
720
965
|
scheduleOnlineRestore();
|
|
721
966
|
enqueueOffline({
|
|
@@ -734,6 +979,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
734
979
|
});
|
|
735
980
|
}
|
|
736
981
|
function reportCallAsync(traceId, spanId, fn, startedAt, inputBuf, success, attempt, outputBuf, error = "") {
|
|
982
|
+
const durationMs = computeDurationMs(startedAt);
|
|
737
983
|
if (!isOnline) {
|
|
738
984
|
enqueueOffline({
|
|
739
985
|
type: "reportCall",
|
|
@@ -741,6 +987,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
741
987
|
spanId,
|
|
742
988
|
fn,
|
|
743
989
|
startedAt,
|
|
990
|
+
durationMs,
|
|
744
991
|
inputBuf,
|
|
745
992
|
success,
|
|
746
993
|
attempt,
|
|
@@ -754,6 +1001,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
754
1001
|
spanId,
|
|
755
1002
|
fn,
|
|
756
1003
|
startedAt,
|
|
1004
|
+
durationMs,
|
|
757
1005
|
inputBuf,
|
|
758
1006
|
success,
|
|
759
1007
|
attempt,
|
|
@@ -761,6 +1009,9 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
761
1009
|
error
|
|
762
1010
|
}).catch((err) => {
|
|
763
1011
|
if (isConnectionError(err)) {
|
|
1012
|
+
if (isOnline) {
|
|
1013
|
+
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1014
|
+
}
|
|
764
1015
|
isOnline = false;
|
|
765
1016
|
scheduleOnlineRestore();
|
|
766
1017
|
enqueueOffline({
|
|
@@ -769,6 +1020,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
769
1020
|
spanId,
|
|
770
1021
|
fn,
|
|
771
1022
|
startedAt,
|
|
1023
|
+
durationMs,
|
|
772
1024
|
inputBuf,
|
|
773
1025
|
success,
|
|
774
1026
|
attempt,
|
|
@@ -781,6 +1033,10 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
781
1033
|
});
|
|
782
1034
|
}
|
|
783
1035
|
async function flushQueue() {
|
|
1036
|
+
if (offlineQueue.length === 0)
|
|
1037
|
+
return;
|
|
1038
|
+
const snapshot = offlineQueue.slice();
|
|
1039
|
+
let flushed = 0;
|
|
784
1040
|
while (offlineQueue.length > 0 && isOnline) {
|
|
785
1041
|
const op = offlineQueue[0];
|
|
786
1042
|
try {
|
|
@@ -789,7 +1045,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
789
1045
|
stub.Publish({
|
|
790
1046
|
topic: op.topic,
|
|
791
1047
|
payload: toJsonBuffer(op.payload),
|
|
792
|
-
headers: op.opts?.headers
|
|
1048
|
+
headers: toWireStringMap(op.opts?.headers),
|
|
793
1049
|
trace_id: op.opts?.traceId ?? "",
|
|
794
1050
|
parent_span_id: op.opts?.parentSpanId ?? "",
|
|
795
1051
|
producer_service: service,
|
|
@@ -824,8 +1080,12 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
824
1080
|
await sendReportCall(op);
|
|
825
1081
|
}
|
|
826
1082
|
offlineQueue.shift();
|
|
1083
|
+
flushed++;
|
|
827
1084
|
} catch (err) {
|
|
828
1085
|
if (isConnectionError(err)) {
|
|
1086
|
+
if (isOnline) {
|
|
1087
|
+
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1088
|
+
}
|
|
829
1089
|
isOnline = false;
|
|
830
1090
|
scheduleOnlineRestore();
|
|
831
1091
|
break;
|
|
@@ -834,6 +1094,18 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
834
1094
|
break;
|
|
835
1095
|
}
|
|
836
1096
|
}
|
|
1097
|
+
if (flushed > 0) {
|
|
1098
|
+
const remaining = offlineQueue.length;
|
|
1099
|
+
const sentOps = snapshot.slice(0, flushed);
|
|
1100
|
+
console.info(`[servicebridge] flushed ${flushed}/${snapshot.length} queued operation(s) to runtime` + ` (${formatQueueSummary(sentOps)})` + (remaining > 0 ? ` — ${remaining} still queued` : ""));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function formatQueueSummary(ops) {
|
|
1104
|
+
const counts = {};
|
|
1105
|
+
for (const op of ops) {
|
|
1106
|
+
counts[op.type] = (counts[op.type] ?? 0) + 1;
|
|
1107
|
+
}
|
|
1108
|
+
return Object.entries(counts).map(([type, n]) => `${n} ${type}`).join(", ");
|
|
837
1109
|
}
|
|
838
1110
|
function requirePeerCN(call, allowedCallers, isDeliver) {
|
|
839
1111
|
const peerCN = extractPeerCN(call);
|
|
@@ -858,6 +1130,17 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
858
1130
|
return;
|
|
859
1131
|
const state = serveState;
|
|
860
1132
|
const metrics = getProcessMetrics();
|
|
1133
|
+
const hbMsg = {
|
|
1134
|
+
endpoint: state.endpoint,
|
|
1135
|
+
group_names: [...registeredGroups],
|
|
1136
|
+
function_names: [...fnHandlers.keys()]
|
|
1137
|
+
};
|
|
1138
|
+
if (metrics.cpuPercent != null)
|
|
1139
|
+
hbMsg.cpu_percent = metrics.cpuPercent;
|
|
1140
|
+
if (metrics.ramMb != null)
|
|
1141
|
+
hbMsg.ram_mb = metrics.ramMb;
|
|
1142
|
+
if (sessionSend({ heartbeat: hbMsg }))
|
|
1143
|
+
return;
|
|
861
1144
|
const req = {
|
|
862
1145
|
service_name: service,
|
|
863
1146
|
instance_id: state.instanceId,
|
|
@@ -875,6 +1158,8 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
875
1158
|
});
|
|
876
1159
|
}
|
|
877
1160
|
async function sendHttpHeartbeat(instanceId) {
|
|
1161
|
+
if (sessionSend({ http_heartbeat: {} }))
|
|
1162
|
+
return;
|
|
878
1163
|
await new Promise((resolve, reject) => {
|
|
879
1164
|
stub.HeartbeatHttpEndpoint({ service_name: service, instance_id: instanceId }, meta, unaryDeadlineOptions(), (err, res) => {
|
|
880
1165
|
if (err) {
|
|
@@ -967,6 +1252,9 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
967
1252
|
}
|
|
968
1253
|
} catch (err) {
|
|
969
1254
|
if (isConnectionError(err)) {
|
|
1255
|
+
if (isOnline) {
|
|
1256
|
+
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1257
|
+
}
|
|
970
1258
|
isOnline = false;
|
|
971
1259
|
scheduleOnlineRestore();
|
|
972
1260
|
} else if (isRegistryResyncRequiredError(err)) {
|
|
@@ -996,12 +1284,12 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
996
1284
|
const append = (data, key = "default") => {
|
|
997
1285
|
if (!isOnline || !runId)
|
|
998
1286
|
return Promise.resolve();
|
|
1287
|
+
const dataBuf = Buffer.from(JSON.stringify(data));
|
|
1288
|
+
if (sessionSend({ append_stream: { run_id: runId, key, data: dataBuf } })) {
|
|
1289
|
+
return Promise.resolve();
|
|
1290
|
+
}
|
|
999
1291
|
return new Promise((resolve, reject) => {
|
|
1000
|
-
stub.AppendStream({
|
|
1001
|
-
run_id: runId,
|
|
1002
|
-
key,
|
|
1003
|
-
data: Buffer.from(JSON.stringify(data))
|
|
1004
|
-
}, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `append-stream:${runId}`)) : resolve());
|
|
1292
|
+
stub.AppendStream({ run_id: runId, key, data: dataBuf }, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `append-stream:${runId}`)) : resolve());
|
|
1005
1293
|
});
|
|
1006
1294
|
};
|
|
1007
1295
|
return {
|
|
@@ -1083,7 +1371,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
1083
1371
|
groupName: entry.groupName,
|
|
1084
1372
|
messageId: String(req.message_id ?? ""),
|
|
1085
1373
|
attempt: Number(req.attempt ?? 0),
|
|
1086
|
-
headers: req.headers
|
|
1374
|
+
headers: fromWireStringMap(req.headers)
|
|
1087
1375
|
},
|
|
1088
1376
|
retry(delayMs = 1000) {
|
|
1089
1377
|
shouldRetry = true;
|
|
@@ -1187,7 +1475,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
1187
1475
|
group_name: String(event.group_name ?? ""),
|
|
1188
1476
|
topic: String(event.topic ?? ""),
|
|
1189
1477
|
payload: event.payload ? Buffer.from(event.payload) : Buffer.alloc(0),
|
|
1190
|
-
headers: event.headers
|
|
1478
|
+
headers: fromWireStringMap(event.headers),
|
|
1191
1479
|
trace_id: String(event.trace_id ?? ""),
|
|
1192
1480
|
parent_span_id: String(event.parent_span_id ?? ""),
|
|
1193
1481
|
attempt: Number(event.attempt ?? 0)
|
|
@@ -1391,7 +1679,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
1391
1679
|
stub.Publish({
|
|
1392
1680
|
topic,
|
|
1393
1681
|
payload: toJsonBuffer(payload),
|
|
1394
|
-
headers: opts?.headers
|
|
1682
|
+
headers: toWireStringMap(opts?.headers),
|
|
1395
1683
|
trace_id: opts?.traceId ?? tc?.traceId ?? "",
|
|
1396
1684
|
parent_span_id: opts?.parentSpanId ?? tc?.spanId ?? "",
|
|
1397
1685
|
producer_service: service,
|
|
@@ -1405,7 +1693,7 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
1405
1693
|
},
|
|
1406
1694
|
handleEvent(pattern, handler, opts) {
|
|
1407
1695
|
const normalizedOpts = opts ?? {};
|
|
1408
|
-
const groupName = normalizedOpts.groupName || `${
|
|
1696
|
+
const groupName = normalizedOpts.groupName || `${defaultConsumerPrefix}.${pattern}`;
|
|
1409
1697
|
if (eventHandlers.has(groupName)) {
|
|
1410
1698
|
throw new Error(`Duplicate event consumer group "${groupName}". ` + "Use a distinct groupName for each handleEvent() registration.");
|
|
1411
1699
|
}
|
|
@@ -1423,9 +1711,6 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
1423
1711
|
if (fnHandlers.size === 0 && eventHandlers.size === 0) {
|
|
1424
1712
|
throw new Error("No handlers registered. Call handleRpc() or handleEvent() before serve().");
|
|
1425
1713
|
}
|
|
1426
|
-
if (!service.trim()) {
|
|
1427
|
-
throw new Error("serve() requires a non-empty service name");
|
|
1428
|
-
}
|
|
1429
1714
|
if (opts.maxInFlight != null && (!Number.isInteger(opts.maxInFlight) || opts.maxInFlight < 1)) {
|
|
1430
1715
|
throw new Error("serve() maxInFlight must be a positive integer");
|
|
1431
1716
|
}
|
|
@@ -1494,7 +1779,9 @@ function servicebridge(url, serviceKey, service = "", globalOpts = {}) {
|
|
|
1494
1779
|
closeWorkerSession();
|
|
1495
1780
|
serveState = null;
|
|
1496
1781
|
shutdownWorkerServerGracefully();
|
|
1497
|
-
|
|
1782
|
+
const fatal = normalizeServiceError(err, "serve");
|
|
1783
|
+
console.error(`[servicebridge] startup failed: ${fatal.message}`);
|
|
1784
|
+
throw fatal;
|
|
1498
1785
|
}
|
|
1499
1786
|
},
|
|
1500
1787
|
stop() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "service-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-dev.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ServiceBridge SDK for Node.js — production-ready RPC, durable events, workflows, jobs, and distributed tracing. One Go runtime + PostgreSQL replaces Istio, RabbitMQ, Temporal, and Jaeger.",
|
|
6
6
|
"keywords": [
|