service-bridge 1.6.0-dev.32 → 1.7.0-dev.34
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 +54 -31
- package/dist/express.js +2 -2
- package/dist/fastify.js +2 -2
- package/dist/index.js +186 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Node.js SDK for [ServiceBridge](https://servicebridge.dev) — production-ready
|
|
|
73
73
|
|
|
74
74
|
**Saga / distributed transactions** — DAG workflows with typed steps (`rpc`, `event`, `event_wait`, `sleep`, child workflow). Compensations and rollbacks via workflow step dependencies.
|
|
75
75
|
|
|
76
|
-
**AI agent orchestration** — Stream LLM tokens via realtime
|
|
76
|
+
**AI agent orchestration** — Stream LLM tokens via realtime trace streams with replay. Orchestrate multi-step AI pipelines as workflows.
|
|
77
77
|
|
|
78
78
|
**Full-stack observability** — Every RPC call, event delivery, workflow step, and HTTP request traced automatically. One timeline, one dashboard. Prometheus metrics and Loki-compatible log API included.
|
|
79
79
|
|
|
@@ -236,7 +236,7 @@ Every step above — RPC, event publish, event delivery, workflow execution —
|
|
|
236
236
|
- **Metrics** — Prometheus-compatible `/metrics` endpoint (30+ metric families)
|
|
237
237
|
- **Logs** — structured log ingest with Loki-compatible query API
|
|
238
238
|
- **Alerts** — runtime alerts for delivery failures, errors, and service health
|
|
239
|
-
- **Dashboard** — realtime web UI for
|
|
239
|
+
- **Dashboard** — realtime web UI for traces, events, workflows, jobs, DLQ, service map, and service keys
|
|
240
240
|
|
|
241
241
|
---
|
|
242
242
|
|
|
@@ -262,7 +262,7 @@ Every step above — RPC, event publish, event delivery, workflow execution —
|
|
|
262
262
|
### Cross-SDK parity notes
|
|
263
263
|
|
|
264
264
|
ServiceBridge keeps the core API shape consistent across Node.js, Go, and Python:
|
|
265
|
-
constructor, RPC, events, jobs, workflows, `
|
|
265
|
+
constructor, RPC, events, jobs, workflows, `executeWorkflow`, streams, serve/stop, and `ServiceBridgeError`.
|
|
266
266
|
|
|
267
267
|
Constructor-level defaults for `timeout`, `retries`, and `retryDelay` are available
|
|
268
268
|
across all three SDKs. Parity differences are naming-only (language idioms):
|
|
@@ -326,6 +326,15 @@ rpc<T = unknown>(fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
|
|
|
326
326
|
|
|
327
327
|
Calls a registered RPC handler on another service. Direct gRPC path, no proxy.
|
|
328
328
|
|
|
329
|
+
**Function name formats** — `fn` accepts two formats:
|
|
330
|
+
|
|
331
|
+
| Format | Example | When to use |
|
|
332
|
+
|---|---|---|
|
|
333
|
+
| Plain name | `"charge"` | Function name is globally unique across all services. Resolved automatically via service discovery. |
|
|
334
|
+
| Canonical name | `"payments/charge"` | Multiple services expose a function with the same plain name, or you want to be explicit about the target service. |
|
|
335
|
+
|
|
336
|
+
Both formats are interchangeable when the name is unique globally. Canonical format is recommended in production for clarity and to avoid ambiguity as your service count grows.
|
|
337
|
+
|
|
329
338
|
`RpcOpts`:
|
|
330
339
|
|
|
331
340
|
| Option | Type | Description |
|
|
@@ -338,6 +347,10 @@ Calls a registered RPC handler on another service. Direct gRPC path, no proxy.
|
|
|
338
347
|
| `mode` | `"direct" \| "proxy"` | Transport mode. `"direct"` (default) connects directly to the worker. `"proxy"` routes through the control plane when direct connection is unavailable. |
|
|
339
348
|
|
|
340
349
|
```ts
|
|
350
|
+
// plain name — works when "get" is unique across services
|
|
351
|
+
const user = await sb.rpc<{ id: string; name: string }>("get", { id: "u_1" });
|
|
352
|
+
|
|
353
|
+
// canonical name — explicit service target, always unambiguous
|
|
341
354
|
const user = await sb.rpc<{ id: string; name: string }>("users/get", { id: "u_1" }, {
|
|
342
355
|
timeout: 5000,
|
|
343
356
|
retries: 2,
|
|
@@ -378,6 +391,16 @@ await sb.event("orders.created", { orderId: "ord_42" }, {
|
|
|
378
391
|
|
|
379
392
|
---
|
|
380
393
|
|
|
394
|
+
### `publishEvent(topic, payload?, opts?)`
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
publishEvent(topic: string, payload?: unknown, opts?: PublishEventOpts): Promise<string>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Publishes an event via the established worker session stream. Requires an active worker session — call after `serve()`. Resolves with `messageId` once the server confirms with `publish_ack`. Times out after 30 s if no ack. Use `event()` when not serving (e.g. caller-only services); use `publishEvent()` from within a worker for lower-latency publishing over the existing session.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
381
404
|
### `job(target, opts)`
|
|
382
405
|
|
|
383
406
|
```ts
|
|
@@ -458,44 +481,44 @@ await sb.workflow("checkout.flow", steps, { stepTimeoutMs: 60_000 });
|
|
|
458
481
|
|
|
459
482
|
---
|
|
460
483
|
|
|
461
|
-
### `
|
|
484
|
+
### `executeWorkflow(name, input?, opts?)`
|
|
462
485
|
|
|
463
486
|
```ts
|
|
464
|
-
|
|
487
|
+
executeWorkflow(name: string, input?: unknown, opts?: ExecuteWorkflowOpts): Promise<{ traceId: string; groupTraceId: string }>
|
|
465
488
|
```
|
|
466
489
|
|
|
467
|
-
Starts a workflow
|
|
468
|
-
An alternative to scheduling via `job(target, { via: "workflow" })` — triggers the
|
|
490
|
+
Starts a workflow execution on demand. The workflow must be registered first via `workflow()`.
|
|
491
|
+
An alternative to scheduling via `job(target, { via: "workflow" })` — triggers the execution immediately.
|
|
469
492
|
|
|
470
493
|
| Parameter | Type | Default | Description |
|
|
471
494
|
|---|---|---|---|
|
|
472
495
|
| `name` | `string` | required | Name of a previously registered workflow. |
|
|
473
496
|
| `input` | `unknown` | `undefined` | Optional JSON-serializable input payload. |
|
|
474
497
|
|
|
475
|
-
Returns `{
|
|
498
|
+
Returns `{ traceId, groupTraceId }`. Use `traceId` with `watchTrace()` to observe execution in real time.
|
|
476
499
|
|
|
477
|
-
`
|
|
500
|
+
`ExecuteWorkflowOpts`:
|
|
478
501
|
|
|
479
502
|
| Option | Type | Description |
|
|
480
503
|
|---|---|---|
|
|
481
|
-
| `traceId` | `string` | Override trace ID for this workflow
|
|
504
|
+
| `traceId` | `string` | Override trace ID for this workflow execution. |
|
|
482
505
|
|
|
483
506
|
```ts
|
|
484
|
-
const {
|
|
507
|
+
const { traceId, groupTraceId } = await sb.executeWorkflow("user.onboarding", { userId: "u_123" });
|
|
485
508
|
```
|
|
486
509
|
|
|
487
510
|
---
|
|
488
511
|
|
|
489
|
-
### `
|
|
512
|
+
### `cancelWorkflow(traceId)`
|
|
490
513
|
|
|
491
514
|
```ts
|
|
492
|
-
|
|
515
|
+
cancelWorkflow(traceId: string): Promise<void>
|
|
493
516
|
```
|
|
494
517
|
|
|
495
518
|
Cancels a running workflow instance.
|
|
496
519
|
|
|
497
520
|
```ts
|
|
498
|
-
await sb.
|
|
521
|
+
await sb.cancelWorkflow("trace_01HQ...XYZ");
|
|
499
522
|
```
|
|
500
523
|
|
|
501
524
|
---
|
|
@@ -542,7 +565,7 @@ sb.handleRpc("ai/generate", async (payload: { prompt: string }, ctx) => {
|
|
|
542
565
|
|
|
543
566
|
| Method | Signature | Description |
|
|
544
567
|
|---|---|---|
|
|
545
|
-
| `write` | `write(data: unknown, key?: string): Promise<void>` | Append a real-time chunk to the
|
|
568
|
+
| `write` | `write(data: unknown, key?: string): Promise<void>` | Append a real-time chunk to the trace stream. |
|
|
546
569
|
| `end` | `end(key?: string): Promise<void>` | No-op placeholder for API symmetry (lifecycle managed by runtime). |
|
|
547
570
|
|
|
548
571
|
---
|
|
@@ -584,7 +607,7 @@ a consumer, the delivery moves to DLQ with reason `delivery_ttl_exceeded`.
|
|
|
584
607
|
- `ctx.retry(delayMs?)` — ask for redelivery with optional delay
|
|
585
608
|
- `ctx.reject(reason)` — move to DLQ immediately, bypassing remaining retries
|
|
586
609
|
- `ctx.refs` — metadata (`topic`, `groupName`, `messageId`, `attempt`, `headers`)
|
|
587
|
-
- `ctx.stream.write(...)` — append real-time chunks to
|
|
610
|
+
- `ctx.stream.write(...)` — append real-time chunks to trace stream
|
|
588
611
|
|
|
589
612
|
```ts
|
|
590
613
|
sb.handleEvent("orders.*", async (payload, ctx) => {
|
|
@@ -702,32 +725,32 @@ await sb.registerHttpEndpoint({
|
|
|
702
725
|
|
|
703
726
|
---
|
|
704
727
|
|
|
705
|
-
### `
|
|
728
|
+
### `watchTrace(traceId, opts?)`
|
|
706
729
|
|
|
707
730
|
```ts
|
|
708
|
-
|
|
731
|
+
watchTrace(traceId: string, opts?: WatchTraceOpts): AsyncIterable<TraceStreamEvent>
|
|
709
732
|
```
|
|
710
733
|
|
|
711
|
-
Subscribes to a
|
|
712
|
-
identifier used by `ctx.stream.write(...)
|
|
734
|
+
Subscribes to a trace stream with replay and live updates. `traceId` is the stream
|
|
735
|
+
identifier used by `ctx.stream.write(...)`.
|
|
713
736
|
|
|
714
|
-
`
|
|
737
|
+
`WatchTraceOpts`:
|
|
715
738
|
|
|
716
739
|
| Option | Type | Default | Description |
|
|
717
740
|
|---|---|---|---|
|
|
718
741
|
| `key` | `string` | `""` | Stream key filter (`""` = all keys). |
|
|
719
742
|
| `fromSequence` | `number` | `0` | Replay from sequence cursor. |
|
|
720
743
|
|
|
721
|
-
`
|
|
744
|
+
`TraceStreamEvent`:
|
|
722
745
|
|
|
723
746
|
| Field | Type | Description |
|
|
724
747
|
|---|---|---|
|
|
725
|
-
| `type` | `"chunk" \| "
|
|
726
|
-
| `
|
|
748
|
+
| `type` | `"chunk" \| "trace_complete"` | Event kind. |
|
|
749
|
+
| `traceId` | `string` | Trace identifier being watched. |
|
|
727
750
|
| `key` | `string` | Stream lane key. |
|
|
728
751
|
| `sequence` | `number` | Monotonic sequence number. |
|
|
729
752
|
| `data` | `unknown` | JSON-decoded chunk payload. |
|
|
730
|
-
| `
|
|
753
|
+
| `traceStatus` | `string \| undefined` | Final status on `trace_complete`. |
|
|
731
754
|
|
|
732
755
|
Behavior:
|
|
733
756
|
|
|
@@ -737,11 +760,11 @@ Behavior:
|
|
|
737
760
|
- Enforces internal queue limit `256`; overflow is fatal (consumer must drain promptly).
|
|
738
761
|
|
|
739
762
|
```ts
|
|
740
|
-
for await (const evt of sb.
|
|
763
|
+
for await (const evt of sb.watchTrace(traceId, { key: "output", fromSequence: 0 })) {
|
|
741
764
|
if (evt.type === "chunk") {
|
|
742
765
|
process.stdout.write(String((evt.data as { token?: string }).token ?? ""));
|
|
743
766
|
}
|
|
744
|
-
if (evt.type === "
|
|
767
|
+
if (evt.type === "trace_complete") break;
|
|
745
768
|
}
|
|
746
769
|
```
|
|
747
770
|
|
|
@@ -766,18 +789,18 @@ if (tc) {
|
|
|
766
789
|
}
|
|
767
790
|
```
|
|
768
791
|
|
|
769
|
-
#### `
|
|
792
|
+
#### `withTraceContext(ctx, fn)`
|
|
770
793
|
|
|
771
794
|
```ts
|
|
772
|
-
|
|
795
|
+
withTraceContext<T>(ctx: { traceId: string; spanId: string }, fn: () => T): T
|
|
773
796
|
```
|
|
774
797
|
|
|
775
798
|
Runs a function inside an explicit trace context.
|
|
776
799
|
|
|
777
800
|
```ts
|
|
778
|
-
import {
|
|
801
|
+
import { withTraceContext } from "service-bridge";
|
|
779
802
|
|
|
780
|
-
|
|
803
|
+
withTraceContext({ traceId: "trace-1", spanId: "span-1" }, async () => {
|
|
781
804
|
await sb.event("audit.log", { action: "user.login" });
|
|
782
805
|
});
|
|
783
806
|
```
|
package/dist/express.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// http/src/express.ts
|
|
2
|
-
import {
|
|
2
|
+
import { withTraceContext } from "service-bridge";
|
|
3
3
|
|
|
4
4
|
// http/src/trace.ts
|
|
5
5
|
function parseTraceparent(traceparent) {
|
|
@@ -86,7 +86,7 @@ function servicebridgeMiddleware(options) {
|
|
|
86
86
|
};
|
|
87
87
|
res.on("finish", onFinish);
|
|
88
88
|
res.on("close", onFinish);
|
|
89
|
-
|
|
89
|
+
withTraceContext({ traceId: span.traceId, spanId: span.spanId }, () => {
|
|
90
90
|
next();
|
|
91
91
|
});
|
|
92
92
|
};
|
package/dist/fastify.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// http/src/fastify.ts
|
|
2
|
-
import {
|
|
2
|
+
import { withTraceContext } from "service-bridge";
|
|
3
3
|
|
|
4
4
|
// http/src/trace.ts
|
|
5
5
|
function parseTraceparent(traceparent) {
|
|
@@ -111,7 +111,7 @@ function wrapHandler(handler) {
|
|
|
111
111
|
const traceId = request.traceId;
|
|
112
112
|
const spanId = request.spanId;
|
|
113
113
|
if (traceId && spanId) {
|
|
114
|
-
return
|
|
114
|
+
return withTraceContext({ traceId, spanId }, () => handler(request, reply));
|
|
115
115
|
}
|
|
116
116
|
return handler(request, reply);
|
|
117
117
|
};
|
package/dist/index.js
CHANGED
|
@@ -336,7 +336,7 @@ var traceStorage = new AsyncLocalStorage;
|
|
|
336
336
|
function getTraceContext() {
|
|
337
337
|
return traceStorage.getStore();
|
|
338
338
|
}
|
|
339
|
-
function
|
|
339
|
+
function withTraceContext(ctx, fn) {
|
|
340
340
|
return traceStorage.run(ctx, fn);
|
|
341
341
|
}
|
|
342
342
|
var servicebridgePackageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(Buffer.from(DESCRIPTOR_SET_BASE64, "base64"), {
|
|
@@ -948,6 +948,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
948
948
|
let workerSessionReconnectTimer = null;
|
|
949
949
|
let workerSessionPingTimer = null;
|
|
950
950
|
let workerSessionPositionTimer = null;
|
|
951
|
+
const pendingPublishAcks = new Map;
|
|
951
952
|
let v2Session = null;
|
|
952
953
|
let serveState = null;
|
|
953
954
|
let heartbeatTimer = null;
|
|
@@ -1012,6 +1013,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1012
1013
|
parent_span_id: op.parentSpanId,
|
|
1013
1014
|
fn: op.fn,
|
|
1014
1015
|
span_type: op.spanType ?? "",
|
|
1016
|
+
target_service: op.targetService ?? "",
|
|
1015
1017
|
started_at: op.startedAt,
|
|
1016
1018
|
input: op.inputBuf,
|
|
1017
1019
|
attempt: op.attempt
|
|
@@ -1033,6 +1035,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1033
1035
|
parent_span_id: op.parentSpanId ?? "",
|
|
1034
1036
|
fn: op.fn,
|
|
1035
1037
|
span_type: op.spanType ?? "",
|
|
1038
|
+
target_service: op.targetService ?? "",
|
|
1036
1039
|
started_at: op.startedAt,
|
|
1037
1040
|
duration_ms: op.durationMs,
|
|
1038
1041
|
success: op.success,
|
|
@@ -1272,7 +1275,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1272
1275
|
function normalizeUnknownErrorMessage(error) {
|
|
1273
1276
|
return error instanceof Error ? error.message : String(error);
|
|
1274
1277
|
}
|
|
1275
|
-
function reportCallStartAsync(traceId, spanId, parentSpanId, fn, startedAt, inputBuf, attempt, spanType) {
|
|
1278
|
+
function reportCallStartAsync(traceId, spanId, parentSpanId, fn, startedAt, inputBuf, attempt, spanType, targetService) {
|
|
1276
1279
|
if (!isOnline) {
|
|
1277
1280
|
enqueueOffline({
|
|
1278
1281
|
type: "reportCallStart",
|
|
@@ -1281,6 +1284,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1281
1284
|
parentSpanId,
|
|
1282
1285
|
fn,
|
|
1283
1286
|
spanType,
|
|
1287
|
+
targetService,
|
|
1284
1288
|
startedAt,
|
|
1285
1289
|
inputBuf,
|
|
1286
1290
|
attempt
|
|
@@ -1293,6 +1297,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1293
1297
|
parentSpanId,
|
|
1294
1298
|
fn,
|
|
1295
1299
|
spanType,
|
|
1300
|
+
targetService,
|
|
1296
1301
|
startedAt,
|
|
1297
1302
|
inputBuf,
|
|
1298
1303
|
attempt
|
|
@@ -1310,6 +1315,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1310
1315
|
parentSpanId,
|
|
1311
1316
|
fn,
|
|
1312
1317
|
spanType,
|
|
1318
|
+
targetService,
|
|
1313
1319
|
startedAt,
|
|
1314
1320
|
inputBuf,
|
|
1315
1321
|
attempt
|
|
@@ -1319,7 +1325,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1319
1325
|
reportSDKError("report-call-start", err);
|
|
1320
1326
|
});
|
|
1321
1327
|
}
|
|
1322
|
-
function reportCallAsync(traceId, spanId, fn, startedAt, inputBuf, success, attempt, outputBuf, error = "", spanType, parentSpanId = "") {
|
|
1328
|
+
function reportCallAsync(traceId, spanId, fn, startedAt, inputBuf, success, attempt, outputBuf, error = "", spanType, parentSpanId = "", targetService) {
|
|
1323
1329
|
const durationMs = computeDurationMs(startedAt);
|
|
1324
1330
|
if (!isOnline) {
|
|
1325
1331
|
enqueueOffline({
|
|
@@ -1328,6 +1334,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1328
1334
|
spanId,
|
|
1329
1335
|
fn,
|
|
1330
1336
|
spanType,
|
|
1337
|
+
targetService,
|
|
1331
1338
|
parentSpanId,
|
|
1332
1339
|
startedAt,
|
|
1333
1340
|
durationMs,
|
|
@@ -1344,6 +1351,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1344
1351
|
spanId,
|
|
1345
1352
|
fn,
|
|
1346
1353
|
spanType,
|
|
1354
|
+
targetService,
|
|
1347
1355
|
parentSpanId,
|
|
1348
1356
|
startedAt,
|
|
1349
1357
|
durationMs,
|
|
@@ -1365,6 +1373,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1365
1373
|
spanId,
|
|
1366
1374
|
fn,
|
|
1367
1375
|
spanType,
|
|
1376
|
+
targetService,
|
|
1368
1377
|
parentSpanId,
|
|
1369
1378
|
startedAt,
|
|
1370
1379
|
durationMs,
|
|
@@ -1482,7 +1491,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1482
1491
|
registrationFollowUpPromise = registrationSyncPromise.catch(() => {}).then(() => {
|
|
1483
1492
|
registrationFollowUpPromise = null;
|
|
1484
1493
|
if (serveState)
|
|
1485
|
-
return reconcileRegistrations(reason
|
|
1494
|
+
return reconcileRegistrations(`${reason}-followup`);
|
|
1486
1495
|
});
|
|
1487
1496
|
}
|
|
1488
1497
|
return registrationFollowUpPromise;
|
|
@@ -1605,12 +1614,14 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1605
1614
|
});
|
|
1606
1615
|
}, delayMs);
|
|
1607
1616
|
}
|
|
1608
|
-
function makeStreamWriter(
|
|
1617
|
+
function makeStreamWriter(traceId) {
|
|
1609
1618
|
const append = (data, key = "default") => {
|
|
1610
|
-
if (!isOnline || !
|
|
1619
|
+
if (!isOnline || !traceId)
|
|
1611
1620
|
return Promise.resolve();
|
|
1612
1621
|
const dataBuf = Buffer.from(JSON.stringify(data));
|
|
1613
|
-
if (sessionSend({
|
|
1622
|
+
if (sessionSend({
|
|
1623
|
+
stream_append: { trace_id: traceId, key, data: dataBuf }
|
|
1624
|
+
})) {
|
|
1614
1625
|
return Promise.resolve();
|
|
1615
1626
|
}
|
|
1616
1627
|
return Promise.resolve();
|
|
@@ -1628,7 +1639,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1628
1639
|
return fnHandlers.get(name.slice(slash + 1));
|
|
1629
1640
|
return;
|
|
1630
1641
|
}
|
|
1631
|
-
async function invokeRpcHandler(req,
|
|
1642
|
+
async function invokeRpcHandler(req, traceId) {
|
|
1632
1643
|
const entry = lookupFnHandler(req.function_name || "");
|
|
1633
1644
|
if (!entry) {
|
|
1634
1645
|
return {
|
|
@@ -1660,7 +1671,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1660
1671
|
const rpcCtx = {
|
|
1661
1672
|
traceId: traceCtx.traceId,
|
|
1662
1673
|
spanId: traceCtx.spanId,
|
|
1663
|
-
stream: makeStreamWriter(
|
|
1674
|
+
stream: makeStreamWriter(traceId)
|
|
1664
1675
|
};
|
|
1665
1676
|
try {
|
|
1666
1677
|
const result = await entry.handler(parsed, rpcCtx);
|
|
@@ -1675,7 +1686,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1675
1686
|
}
|
|
1676
1687
|
});
|
|
1677
1688
|
}
|
|
1678
|
-
async function invokeEventHandler(req,
|
|
1689
|
+
async function invokeEventHandler(req, traceId) {
|
|
1679
1690
|
const groupName = typeof req.group_name === "string" ? req.group_name : undefined;
|
|
1680
1691
|
const entry = groupName ? eventHandlers.get(groupName) : undefined;
|
|
1681
1692
|
if (!entry) {
|
|
@@ -1714,7 +1725,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1714
1725
|
reject(reason) {
|
|
1715
1726
|
rejectReason = reason || "rejected_by_consumer";
|
|
1716
1727
|
},
|
|
1717
|
-
stream: makeStreamWriter(
|
|
1728
|
+
stream: makeStreamWriter(traceId)
|
|
1718
1729
|
};
|
|
1719
1730
|
try {
|
|
1720
1731
|
await entry.handler(parsed, ctx);
|
|
@@ -1749,8 +1760,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1749
1760
|
});
|
|
1750
1761
|
return;
|
|
1751
1762
|
}
|
|
1752
|
-
const
|
|
1753
|
-
const
|
|
1763
|
+
const traceIdMeta = call.metadata.get("servicebridge-trace-id");
|
|
1764
|
+
const traceId = typeof traceIdMeta[0] === "string" ? traceIdMeta[0] : "";
|
|
1754
1765
|
const workerTraceId = call.request.trace_id || crypto.randomUUID();
|
|
1755
1766
|
const workerSpanId = call.request.span_id || crypto.randomUUID();
|
|
1756
1767
|
const workerParentSpanId = call.request.parent_span_id || "";
|
|
@@ -1759,14 +1770,14 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1759
1770
|
const rpcStartedAt = Date.now();
|
|
1760
1771
|
const rpcInputBuf = call.request.payload ?? Buffer.alloc(0);
|
|
1761
1772
|
reportCallStartAsync(workerTraceId, workerSpanId, workerParentSpanId, fnName, rpcStartedAt, rpcInputBuf, 1, workerSpanType);
|
|
1762
|
-
const response = await invokeRpcHandler(call.request,
|
|
1773
|
+
const response = await invokeRpcHandler(call.request, traceId);
|
|
1763
1774
|
reportCallAsync(workerTraceId, workerSpanId, fnName, rpcStartedAt, rpcInputBuf, response.success ?? true, 1, response.output ?? Buffer.alloc(0), response.error ?? "", workerSpanType, workerParentSpanId);
|
|
1764
1775
|
cb(null, response);
|
|
1765
1776
|
}
|
|
1766
1777
|
async function handleDeliver(call, cb) {
|
|
1767
1778
|
const req = call.request;
|
|
1768
|
-
const
|
|
1769
|
-
const
|
|
1779
|
+
const traceIdMeta = call.metadata.get("servicebridge-trace-id");
|
|
1780
|
+
const traceId = typeof traceIdMeta[0] === "string" ? traceIdMeta[0] : "";
|
|
1770
1781
|
const result = await invokeEventHandler({
|
|
1771
1782
|
message_id: String(req.message_id ?? ""),
|
|
1772
1783
|
group_name: String(req.group_name ?? ""),
|
|
@@ -1776,7 +1787,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1776
1787
|
trace_id: String(req.trace_id ?? ""),
|
|
1777
1788
|
parent_span_id: String(req.parent_span_id ?? ""),
|
|
1778
1789
|
attempt: Number(req.attempt ?? 0)
|
|
1779
|
-
},
|
|
1790
|
+
}, traceId);
|
|
1780
1791
|
cb(null, {
|
|
1781
1792
|
ack: result.ack === true,
|
|
1782
1793
|
error: result.error ?? "",
|
|
@@ -1826,6 +1837,71 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1826
1837
|
try {
|
|
1827
1838
|
if (deadlineUnixMs > 0 && Date.now() > deadlineUnixMs) {
|
|
1828
1839
|
result.error = "command deadline exceeded";
|
|
1840
|
+
} else if (command.task_command) {
|
|
1841
|
+
const tc = command.task_command;
|
|
1842
|
+
if (tc.type === 1) {
|
|
1843
|
+
const fn = tc.meta?.fn ?? "";
|
|
1844
|
+
const tcParentSpanId = String(tc.parent_span_id ?? "");
|
|
1845
|
+
const tcReq = {
|
|
1846
|
+
function_name: fn,
|
|
1847
|
+
payload: tc.payload ? Buffer.from(tc.payload) : Buffer.alloc(0),
|
|
1848
|
+
trace_id: String(tc.trace_id ?? ""),
|
|
1849
|
+
span_id: String(tc.span_id ?? ""),
|
|
1850
|
+
parent_span_id: tcParentSpanId
|
|
1851
|
+
};
|
|
1852
|
+
const tcTraceId = tcReq.trace_id || crypto.randomUUID();
|
|
1853
|
+
const tcSpanId = tcReq.span_id || crypto.randomUUID();
|
|
1854
|
+
const tcStartedAt = Date.now();
|
|
1855
|
+
const tcStreamTraceId = tc.meta?.trace_id ?? "";
|
|
1856
|
+
reportCallStartAsync(tcTraceId, tcSpanId, tcParentSpanId, fn, tcStartedAt, tcReq.payload, tc.attempt ?? 1, "rpc");
|
|
1857
|
+
const tcResult = await invokeRpcHandler(tcReq, tcStreamTraceId);
|
|
1858
|
+
reportCallAsync(tcTraceId, tcSpanId, fn, tcStartedAt, tcReq.payload, tcResult.success, tc.attempt ?? 1, tcResult.output ?? Buffer.alloc(0), tcResult.error ?? "", "rpc", tcParentSpanId);
|
|
1859
|
+
result.success = tcResult.success;
|
|
1860
|
+
result.output = tcResult.output ?? Buffer.alloc(0);
|
|
1861
|
+
result.error = tcResult.error ?? "";
|
|
1862
|
+
} else if (tc.type === 2) {
|
|
1863
|
+
const evTraceId = String(tc.trace_id ?? "");
|
|
1864
|
+
const evParentSpanId = String(tc.parent_span_id ?? "");
|
|
1865
|
+
const evSpanId = String(tc.span_id ?? "") || crypto.randomUUID();
|
|
1866
|
+
const evTopic = tc.meta?.topic ?? "";
|
|
1867
|
+
const evGroupName = tc.meta?.group_name ?? "";
|
|
1868
|
+
const evMessageId = tc.meta?.message_id ?? "";
|
|
1869
|
+
const evStreamTraceId = tc.meta?.trace_id ?? "";
|
|
1870
|
+
const evAttempt = tc.attempt ?? 0;
|
|
1871
|
+
const evPayload = tc.payload ? Buffer.from(tc.payload) : Buffer.alloc(0);
|
|
1872
|
+
const reservedMetaKeys = new Set([
|
|
1873
|
+
"fn",
|
|
1874
|
+
"topic",
|
|
1875
|
+
"group_name",
|
|
1876
|
+
"message_id",
|
|
1877
|
+
"trace_id"
|
|
1878
|
+
]);
|
|
1879
|
+
const evHeaders = {};
|
|
1880
|
+
for (const [k, v] of Object.entries(tc.meta ?? {})) {
|
|
1881
|
+
if (!reservedMetaKeys.has(k))
|
|
1882
|
+
evHeaders[k] = v;
|
|
1883
|
+
}
|
|
1884
|
+
const evStartedAt = Date.now();
|
|
1885
|
+
reportCallStartAsync(evTraceId, evSpanId, evParentSpanId, evTopic, evStartedAt, evPayload, Number(evAttempt), "event");
|
|
1886
|
+
const evResult = await invokeEventHandler({
|
|
1887
|
+
message_id: evMessageId,
|
|
1888
|
+
group_name: evGroupName,
|
|
1889
|
+
topic: evTopic,
|
|
1890
|
+
payload: evPayload,
|
|
1891
|
+
headers: evHeaders,
|
|
1892
|
+
trace_id: evTraceId,
|
|
1893
|
+
parent_span_id: evParentSpanId,
|
|
1894
|
+
attempt: Number(evAttempt)
|
|
1895
|
+
}, evStreamTraceId);
|
|
1896
|
+
reportCallAsync(evTraceId, evSpanId, evTopic, evStartedAt, evPayload, evResult.ack === true, Number(evAttempt), Buffer.alloc(0), evResult.error ?? "", "event", evParentSpanId);
|
|
1897
|
+
result.success = evResult.ack === true;
|
|
1898
|
+
result.ack = evResult.ack === true;
|
|
1899
|
+
result.retry_after_ms = evResult.retry_after_ms ?? 0;
|
|
1900
|
+
result.reject_reason = evResult.reject_reason ?? "";
|
|
1901
|
+
result.error = evResult.error ?? "";
|
|
1902
|
+
} else {
|
|
1903
|
+
result.error = `unsupported task_command type: ${tc.type}`;
|
|
1904
|
+
}
|
|
1829
1905
|
} else if (command.rpc) {
|
|
1830
1906
|
const rpc = command.rpc;
|
|
1831
1907
|
const rpcParentSpanId = String(rpc.parent_span_id ?? "");
|
|
@@ -1842,7 +1918,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1842
1918
|
const fnName = rpcReq.function_name;
|
|
1843
1919
|
const rpcStartedAt = Date.now();
|
|
1844
1920
|
reportCallStartAsync(workerTraceId, workerSpanId, rpcParentSpanId, fnName, rpcStartedAt, rpcReq.payload, 1, rpcSpanType);
|
|
1845
|
-
const rpcResult = await invokeRpcHandler(rpcReq, String(rpc.
|
|
1921
|
+
const rpcResult = await invokeRpcHandler(rpcReq, String(rpc.trace_id ?? ""));
|
|
1846
1922
|
reportCallAsync(workerTraceId, workerSpanId, fnName, rpcStartedAt, rpcReq.payload, rpcResult.success, 1, rpcResult.output ?? Buffer.alloc(0), rpcResult.error ?? "", rpcSpanType, rpcParentSpanId);
|
|
1847
1923
|
result.success = rpcResult.success;
|
|
1848
1924
|
result.output = rpcResult.output ?? Buffer.alloc(0);
|
|
@@ -1858,7 +1934,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1858
1934
|
trace_id: String(event.trace_id ?? ""),
|
|
1859
1935
|
parent_span_id: String(event.parent_span_id ?? ""),
|
|
1860
1936
|
attempt: Number(event.attempt ?? 0)
|
|
1861
|
-
}, String(event.
|
|
1937
|
+
}, String(event.trace_id ?? ""));
|
|
1862
1938
|
result.success = eventResult.ack === true;
|
|
1863
1939
|
result.ack = eventResult.ack === true;
|
|
1864
1940
|
result.retry_after_ms = eventResult.retry_after_ms ?? 0;
|
|
@@ -1989,6 +2065,20 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1989
2065
|
if (msg.flow_update && v2Session) {
|
|
1990
2066
|
v2Session.onFlowControlUpdate(msg.flow_update.new_window_size ?? 0, msg.flow_update.reason ?? "");
|
|
1991
2067
|
}
|
|
2068
|
+
if (msg.publish_ack) {
|
|
2069
|
+
const ack = msg.publish_ack;
|
|
2070
|
+
const reqId = ack.request_id ?? "";
|
|
2071
|
+
const pending = pendingPublishAcks.get(reqId);
|
|
2072
|
+
if (pending) {
|
|
2073
|
+
pendingPublishAcks.delete(reqId);
|
|
2074
|
+
clearTimeout(pending.timer);
|
|
2075
|
+
if (ack.error) {
|
|
2076
|
+
pending.reject(new Error(ack.error));
|
|
2077
|
+
} else {
|
|
2078
|
+
pending.resolve(ack.message_id ?? "");
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
1992
2082
|
if (msg.dispatch) {
|
|
1993
2083
|
processWorkerSessionCommand(msg.dispatch).catch((err) => {
|
|
1994
2084
|
reportSDKError("worker-session-command", err);
|
|
@@ -2048,11 +2138,14 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2048
2138
|
const outputSchema = fmeta?.outputSchema;
|
|
2049
2139
|
const inputBuf = inputSchema ? encodeWithSchema(inputSchema, payload) : toJsonBuffer(payload);
|
|
2050
2140
|
const telemetryInputBuf = toJsonBuffer(payload);
|
|
2141
|
+
const _parsedCanonical = parseCanonicalFunctionName(canonical);
|
|
2142
|
+
const rpcFnName = _parsedCanonical?.fnName ?? canonical;
|
|
2143
|
+
const rpcServiceName = _parsedCanonical?.serviceName ?? "";
|
|
2051
2144
|
const rootSpanId = crypto.randomUUID();
|
|
2052
2145
|
const parentSpanId = opts?.parentSpanId ?? tc?.spanId ?? "";
|
|
2053
2146
|
const rootStartedAt = Date.now();
|
|
2054
2147
|
const hasRetries = maxRetries > 0;
|
|
2055
|
-
reportCallStartAsync(traceId, rootSpanId, parentSpanId,
|
|
2148
|
+
reportCallStartAsync(traceId, rootSpanId, parentSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, 1, "rpc", rpcServiceName);
|
|
2056
2149
|
return traceStorage.run({ traceId, spanId: rootSpanId }, async () => {
|
|
2057
2150
|
if (opts?.mode === "proxy") {
|
|
2058
2151
|
return await new Promise((resolve, reject) => {
|
|
@@ -2064,15 +2157,15 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2064
2157
|
timeout_ms: timeout
|
|
2065
2158
|
}, meta, { deadline: new Date(Date.now() + timeout) }, (err, res) => {
|
|
2066
2159
|
if (err) {
|
|
2067
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2160
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, false, 1, undefined, String(err), "rpc", parentSpanId, rpcServiceName);
|
|
2068
2161
|
return reject(normalizeServiceError(err, fn, "control-plane"));
|
|
2069
2162
|
}
|
|
2070
2163
|
if (!res?.success) {
|
|
2071
2164
|
const errMsg = res?.error ?? "proxy call failed";
|
|
2072
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2165
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, false, 1, undefined, errMsg, "rpc", parentSpanId, rpcServiceName);
|
|
2073
2166
|
return reject(normalizeServiceError(new Error(errMsg), fn, "worker"));
|
|
2074
2167
|
}
|
|
2075
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2168
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, true, 1, res.output, "", "rpc", parentSpanId, rpcServiceName);
|
|
2076
2169
|
try {
|
|
2077
2170
|
const out = res.output?.length ? outputSchema ? decodeWithSchema(outputSchema, res.output) : JSON.parse(res.output.toString()) : undefined;
|
|
2078
2171
|
resolve(out);
|
|
@@ -2089,10 +2182,9 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2089
2182
|
for (let attempt = 1;attempt <= maxRetries + 1; attempt++) {
|
|
2090
2183
|
const attemptSpanId = hasRetries ? crypto.randomUUID() : rootSpanId;
|
|
2091
2184
|
const attemptParentId = hasRetries ? rootSpanId : parentSpanId;
|
|
2092
|
-
const attemptFn = canonical;
|
|
2093
2185
|
const attemptStartedAt = hasRetries ? Date.now() : rootStartedAt;
|
|
2094
2186
|
if (hasRetries) {
|
|
2095
|
-
reportCallStartAsync(traceId, attemptSpanId, attemptParentId,
|
|
2187
|
+
reportCallStartAsync(traceId, attemptSpanId, attemptParentId, rpcFnName, attemptStartedAt, telemetryInputBuf, attempt, "rpc", rpcServiceName);
|
|
2096
2188
|
}
|
|
2097
2189
|
try {
|
|
2098
2190
|
const attemptTimeoutMs = timeout;
|
|
@@ -2141,22 +2233,22 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2141
2233
|
const result = outputSchema ? decodeWithSchema(outputSchema, outputBuf) : outputBuf?.length ? JSON.parse(outputBuf.toString()) : {};
|
|
2142
2234
|
const encodedOutput = outputBuf?.length ? toJsonBuffer(result) : Buffer.alloc(0);
|
|
2143
2235
|
if (hasRetries) {
|
|
2144
|
-
reportCallAsync(traceId, attemptSpanId,
|
|
2145
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2236
|
+
reportCallAsync(traceId, attemptSpanId, rpcFnName, attemptStartedAt, telemetryInputBuf, true, attempt, encodedOutput, "", "rpc", attemptParentId, rpcServiceName);
|
|
2237
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, true, attempt, encodedOutput, "", "rpc", parentSpanId, rpcServiceName);
|
|
2146
2238
|
} else {
|
|
2147
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2239
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, true, attempt, encodedOutput, "", "rpc", parentSpanId, rpcServiceName);
|
|
2148
2240
|
}
|
|
2149
2241
|
return result;
|
|
2150
2242
|
} catch (e) {
|
|
2151
2243
|
lastError = e;
|
|
2152
2244
|
const errMsg = normalizeUnknownErrorMessage(e);
|
|
2153
2245
|
if (hasRetries) {
|
|
2154
|
-
reportCallAsync(traceId, attemptSpanId,
|
|
2246
|
+
reportCallAsync(traceId, attemptSpanId, rpcFnName, attemptStartedAt, telemetryInputBuf, false, attempt, undefined, errMsg, "rpc", attemptParentId, rpcServiceName);
|
|
2155
2247
|
if (attempt > maxRetries) {
|
|
2156
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2248
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, false, attempt, undefined, errMsg, "rpc", parentSpanId, rpcServiceName);
|
|
2157
2249
|
}
|
|
2158
2250
|
} else {
|
|
2159
|
-
reportCallAsync(traceId, rootSpanId,
|
|
2251
|
+
reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, false, attempt, undefined, errMsg, "rpc", parentSpanId, rpcServiceName);
|
|
2160
2252
|
}
|
|
2161
2253
|
if (attempt <= maxRetries) {
|
|
2162
2254
|
await new Promise((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));
|
|
@@ -2184,6 +2276,36 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2184
2276
|
}, meta, unaryDeadlineOptions(), (err, res) => err ? reject(normalizeServiceError(err, `publish:${topic}`)) : resolve(res?.message_id ?? ""));
|
|
2185
2277
|
});
|
|
2186
2278
|
},
|
|
2279
|
+
publishEvent(topic, payload, opts) {
|
|
2280
|
+
const stream = workerSessionStream;
|
|
2281
|
+
if (!stream) {
|
|
2282
|
+
return Promise.reject(new Error("publishEvent requires an active worker session (call serve() first)"));
|
|
2283
|
+
}
|
|
2284
|
+
const tc = traceStorage.getStore();
|
|
2285
|
+
const requestId = crypto.randomUUID();
|
|
2286
|
+
const msg = {
|
|
2287
|
+
request_id: requestId,
|
|
2288
|
+
topic,
|
|
2289
|
+
payload: toJsonBuffer(payload),
|
|
2290
|
+
headers: toWireStringMap(opts?.headers),
|
|
2291
|
+
trace_id: opts?.traceId ?? tc?.traceId ?? "",
|
|
2292
|
+
parent_span_id: opts?.parentSpanId ?? tc?.spanId ?? ""
|
|
2293
|
+
};
|
|
2294
|
+
return new Promise((resolve, reject) => {
|
|
2295
|
+
const timer = setTimeout(() => {
|
|
2296
|
+
pendingPublishAcks.delete(requestId);
|
|
2297
|
+
reject(new Error(`publishEvent timed out after 30 s (topic="${topic}", request_id="${requestId}")`));
|
|
2298
|
+
}, 30000);
|
|
2299
|
+
pendingPublishAcks.set(requestId, { resolve, reject, timer });
|
|
2300
|
+
try {
|
|
2301
|
+
stream.write({ publish_message: msg });
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
pendingPublishAcks.delete(requestId);
|
|
2304
|
+
clearTimeout(timer);
|
|
2305
|
+
reject(normalizeServiceError(err, `publish-event:${topic}`));
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
},
|
|
2187
2309
|
handleRpc(fn, handler, opts) {
|
|
2188
2310
|
fnHandlers.set(fn, { handler, opts: opts ?? {} });
|
|
2189
2311
|
return svc;
|
|
@@ -2343,36 +2465,35 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2343
2465
|
}
|
|
2344
2466
|
return name;
|
|
2345
2467
|
},
|
|
2346
|
-
async
|
|
2468
|
+
async executeWorkflow(name, input, opts) {
|
|
2347
2469
|
await _controlReady;
|
|
2348
2470
|
const payload = toJsonBuffer(input ?? {});
|
|
2349
2471
|
return new Promise((resolve, reject) => {
|
|
2350
|
-
stub.
|
|
2472
|
+
stub.ExecuteWorkflow({
|
|
2351
2473
|
name,
|
|
2352
2474
|
input: payload,
|
|
2353
2475
|
service_name: service
|
|
2354
2476
|
}, meta, unaryDeadlineOptions(), (err, res) => {
|
|
2355
2477
|
if (err) {
|
|
2356
|
-
reject(normalizeServiceError(err, "
|
|
2478
|
+
reject(normalizeServiceError(err, "execute-workflow"));
|
|
2357
2479
|
return;
|
|
2358
2480
|
}
|
|
2359
2481
|
resolve({
|
|
2360
|
-
runId: res?.run_id ?? "",
|
|
2361
2482
|
traceId: res?.trace_id ?? ""
|
|
2362
2483
|
});
|
|
2363
2484
|
});
|
|
2364
2485
|
});
|
|
2365
2486
|
},
|
|
2366
|
-
async
|
|
2487
|
+
async cancelWorkflow(traceId) {
|
|
2367
2488
|
await _controlReady;
|
|
2368
2489
|
await new Promise((resolve, reject) => {
|
|
2369
|
-
stub.CancelWorkflow({
|
|
2490
|
+
stub.CancelWorkflow({ trace_id: traceId }, meta, unaryDeadlineOptions(), (err, res) => {
|
|
2370
2491
|
if (err) {
|
|
2371
|
-
reject(normalizeServiceError(err, "cancel-workflow
|
|
2492
|
+
reject(normalizeServiceError(err, "cancel-workflow"));
|
|
2372
2493
|
return;
|
|
2373
2494
|
}
|
|
2374
2495
|
if (!res?.cancelled) {
|
|
2375
|
-
reject(new Error(`workflow
|
|
2496
|
+
reject(new Error(`workflow trace ${traceId} was not cancelled`));
|
|
2376
2497
|
return;
|
|
2377
2498
|
}
|
|
2378
2499
|
resolve();
|
|
@@ -2419,12 +2540,12 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2419
2540
|
}
|
|
2420
2541
|
}
|
|
2421
2542
|
},
|
|
2422
|
-
|
|
2543
|
+
watchTrace(traceId, opts) {
|
|
2423
2544
|
const key = opts?.key ?? "";
|
|
2424
2545
|
const fromSeq = opts?.fromSequence ?? 0;
|
|
2425
|
-
const
|
|
2426
|
-
const
|
|
2427
|
-
const
|
|
2546
|
+
const WATCH_TRACE_QUEUE_LIMIT = 256;
|
|
2547
|
+
const WATCH_TRACE_RETRY_MIN_MS = 500;
|
|
2548
|
+
const WATCH_TRACE_RETRY_MAX_MS = 5000;
|
|
2428
2549
|
return {
|
|
2429
2550
|
[Symbol.asyncIterator]() {
|
|
2430
2551
|
let stream = null;
|
|
@@ -2432,7 +2553,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2432
2553
|
let cancelled = false;
|
|
2433
2554
|
let done = false;
|
|
2434
2555
|
let fatalError = null;
|
|
2435
|
-
let reconnectDelay =
|
|
2556
|
+
let reconnectDelay = WATCH_TRACE_RETRY_MIN_MS;
|
|
2436
2557
|
let lastSequence = fromSeq;
|
|
2437
2558
|
const queue = [];
|
|
2438
2559
|
const waiters = [];
|
|
@@ -2458,21 +2579,21 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2458
2579
|
data = JSON.parse(rawData.toString());
|
|
2459
2580
|
} catch (cause) {
|
|
2460
2581
|
throw new ServiceBridgeError({
|
|
2461
|
-
message: `
|
|
2582
|
+
message: `watchTrace received non-JSON chunk for ${traceId}`,
|
|
2462
2583
|
component: "sdk",
|
|
2463
|
-
operation: `watch-
|
|
2584
|
+
operation: `watch-trace:${traceId}`,
|
|
2464
2585
|
severity: "fatal",
|
|
2465
2586
|
cause
|
|
2466
2587
|
});
|
|
2467
2588
|
}
|
|
2468
2589
|
}
|
|
2469
2590
|
return {
|
|
2470
|
-
type: chunk.type === "
|
|
2471
|
-
|
|
2591
|
+
type: chunk.type === "trace_complete" ? "trace_complete" : "chunk",
|
|
2592
|
+
traceId,
|
|
2472
2593
|
key: chunk.key || key,
|
|
2473
2594
|
sequence: Number(chunk.sequence ?? 0),
|
|
2474
2595
|
data,
|
|
2475
|
-
|
|
2596
|
+
traceStatus: chunk.trace_status || undefined
|
|
2476
2597
|
};
|
|
2477
2598
|
}
|
|
2478
2599
|
function clearReconnectTimer() {
|
|
@@ -2514,11 +2635,11 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2514
2635
|
}
|
|
2515
2636
|
return;
|
|
2516
2637
|
}
|
|
2517
|
-
if (queue.length >=
|
|
2638
|
+
if (queue.length >= WATCH_TRACE_QUEUE_LIMIT) {
|
|
2518
2639
|
fail(new ServiceBridgeError({
|
|
2519
|
-
message: `
|
|
2640
|
+
message: `watchTrace consumer is not draining fast enough for ${traceId}`,
|
|
2520
2641
|
component: "sdk",
|
|
2521
|
-
operation: `watch-
|
|
2642
|
+
operation: `watch-trace:${traceId}`,
|
|
2522
2643
|
severity: "fatal"
|
|
2523
2644
|
}));
|
|
2524
2645
|
return;
|
|
@@ -2537,14 +2658,14 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2537
2658
|
waiters.length = 0;
|
|
2538
2659
|
}
|
|
2539
2660
|
function shouldRetry(err) {
|
|
2540
|
-
if (metadataValue(err, "servicebridge-
|
|
2661
|
+
if (metadataValue(err, "servicebridge-trace-stream-retryable") === "true") {
|
|
2541
2662
|
return true;
|
|
2542
2663
|
}
|
|
2543
2664
|
const code = err?.code;
|
|
2544
2665
|
return code === grpc.status.UNAVAILABLE || code === grpc.status.UNKNOWN || code === grpc.status.DEADLINE_EXCEEDED || code === grpc.status.RESOURCE_EXHAUSTED;
|
|
2545
2666
|
}
|
|
2546
2667
|
function maybeUpdateResumeSequence(err) {
|
|
2547
|
-
const parsed = Number(metadataValue(err, "servicebridge-
|
|
2668
|
+
const parsed = Number(metadataValue(err, "servicebridge-trace-stream-resume-from") ?? 0);
|
|
2548
2669
|
if (Number.isFinite(parsed) && parsed > lastSequence) {
|
|
2549
2670
|
lastSequence = parsed;
|
|
2550
2671
|
}
|
|
@@ -2557,7 +2678,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2557
2678
|
reconnectTimer = null;
|
|
2558
2679
|
start();
|
|
2559
2680
|
}, delay);
|
|
2560
|
-
reconnectDelay = Math.min(reconnectDelay * 2,
|
|
2681
|
+
reconnectDelay = Math.min(reconnectDelay * 2, WATCH_TRACE_RETRY_MAX_MS);
|
|
2561
2682
|
}
|
|
2562
2683
|
function start() {
|
|
2563
2684
|
if (cancelled || done || fatalError || stream)
|
|
@@ -2565,25 +2686,25 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2565
2686
|
if (!dataPlaneStub) {
|
|
2566
2687
|
fail(new ServiceBridgeError({
|
|
2567
2688
|
message: "DataPlane service is not available in this runtime version",
|
|
2568
|
-
component: "watch-
|
|
2569
|
-
operation: "watch-
|
|
2689
|
+
component: "watch-trace",
|
|
2690
|
+
operation: "watch-trace-unavailable",
|
|
2570
2691
|
severity: "fatal"
|
|
2571
2692
|
}));
|
|
2572
2693
|
return;
|
|
2573
2694
|
}
|
|
2574
2695
|
try {
|
|
2575
|
-
const current = dataPlaneStub.
|
|
2696
|
+
const current = dataPlaneStub.WatchTrace({ trace_id: traceId, key, from_seq: lastSequence }, meta);
|
|
2576
2697
|
let endedWithOK = false;
|
|
2577
2698
|
stream = current;
|
|
2578
2699
|
current.on("data", (chunk) => {
|
|
2579
2700
|
if (stream !== current || cancelled || done || fatalError)
|
|
2580
2701
|
return;
|
|
2581
|
-
reconnectDelay =
|
|
2702
|
+
reconnectDelay = WATCH_TRACE_RETRY_MIN_MS;
|
|
2582
2703
|
let event;
|
|
2583
2704
|
try {
|
|
2584
2705
|
event = parseChunk(chunk);
|
|
2585
2706
|
} catch (err) {
|
|
2586
|
-
fail(err instanceof ServiceBridgeError ? err : normalizeServiceError(err, `watch-
|
|
2707
|
+
fail(err instanceof ServiceBridgeError ? err : normalizeServiceError(err, `watch-trace:${traceId}`));
|
|
2587
2708
|
return;
|
|
2588
2709
|
}
|
|
2589
2710
|
if (event.sequence > 0) {
|
|
@@ -2592,7 +2713,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2592
2713
|
lastSequence = event.sequence;
|
|
2593
2714
|
}
|
|
2594
2715
|
enqueue(event);
|
|
2595
|
-
if (event.type === "
|
|
2716
|
+
if (event.type === "trace_complete") {
|
|
2596
2717
|
finish();
|
|
2597
2718
|
}
|
|
2598
2719
|
});
|
|
@@ -2602,10 +2723,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2602
2723
|
clearStream(false);
|
|
2603
2724
|
maybeUpdateResumeSequence(err);
|
|
2604
2725
|
if (!shouldRetry(err)) {
|
|
2605
|
-
fail(normalizeServiceError(err, `watch-
|
|
2726
|
+
fail(normalizeServiceError(err, `watch-trace:${traceId}`));
|
|
2606
2727
|
return;
|
|
2607
2728
|
}
|
|
2608
|
-
reconnectDelay = metadataValue(err, "servicebridge-
|
|
2729
|
+
reconnectDelay = metadataValue(err, "servicebridge-trace-stream-disconnect-reason") ? WATCH_TRACE_RETRY_MIN_MS : reconnectDelay;
|
|
2609
2730
|
scheduleReconnect();
|
|
2610
2731
|
});
|
|
2611
2732
|
current.on("status", (statusInfo) => {
|
|
@@ -2633,7 +2754,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
2633
2754
|
} catch (err) {
|
|
2634
2755
|
maybeUpdateResumeSequence(err);
|
|
2635
2756
|
if (!shouldRetry(err)) {
|
|
2636
|
-
fail(normalizeServiceError(err, `watch-
|
|
2757
|
+
fail(normalizeServiceError(err, `watch-trace:${traceId}`));
|
|
2637
2758
|
return;
|
|
2638
2759
|
}
|
|
2639
2760
|
scheduleReconnect();
|
|
@@ -2728,9 +2849,9 @@ function captureConsole(svc) {
|
|
|
2728
2849
|
}
|
|
2729
2850
|
}
|
|
2730
2851
|
export {
|
|
2852
|
+
withTraceContext,
|
|
2731
2853
|
validateV2Config,
|
|
2732
2854
|
servicebridge,
|
|
2733
|
-
runWithTraceContext,
|
|
2734
2855
|
getTraceContext,
|
|
2735
2856
|
V2SessionClient,
|
|
2736
2857
|
ServiceBridgeError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "service-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-dev.34",
|
|
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": [
|