service-bridge 1.7.0-dev.50 → 1.7.0-dev.51

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.
Files changed (3) hide show
  1. package/README.md +175 -108
  2. package/dist/index.js +384 -361
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -92,31 +92,31 @@ bun add service-bridge
92
92
  ### 2. Create a worker (service that handles calls)
93
93
 
94
94
  ```ts
95
- import { servicebridge } from "service-bridge";
95
+ import { ServiceBridge } from "service-bridge";
96
96
 
97
- const sb = servicebridge(
97
+ const sb = new ServiceBridge(
98
98
  process.env.SERVICEBRIDGE_URL ?? "localhost:14445",
99
99
  process.env.SERVICEBRIDGE_SERVICE_KEY!,
100
100
  );
101
101
 
102
- sb.handleRpc("charge", async (payload: { orderId: string; amount: number }) => {
102
+ sb.rpc.handle("payment.charge", async (payload: { orderId: string; amount: number }) => {
103
103
  return { ok: true, txId: `tx_${Date.now()}`, orderId: payload.orderId };
104
104
  });
105
105
 
106
- await sb.serve({ host: "localhost" });
106
+ await sb.start({ host: "localhost" });
107
107
  ```
108
108
 
109
109
  ### 3. Call it from another service
110
110
 
111
111
  ```ts
112
- import { servicebridge } from "service-bridge";
112
+ import { ServiceBridge } from "service-bridge";
113
113
 
114
- const sb = servicebridge(
114
+ const sb = new ServiceBridge(
115
115
  process.env.SERVICEBRIDGE_URL ?? "localhost:14445",
116
116
  process.env.SERVICEBRIDGE_SERVICE_KEY!,
117
117
  );
118
118
 
119
- const result = await sb.rpc<{ ok: boolean; txId: string }>("payment.charge", {
119
+ const result = await sb.rpc.invoke<{ ok: boolean; txId: string }>("payment.charge", {
120
120
  orderId: "ord_42",
121
121
  amount: 4990,
122
122
  });
@@ -147,13 +147,13 @@ For manual Docker Compose setup, configuration reference, and all runtime enviro
147
147
  A complete order flow: HTTP request → RPC → Event → Event handler with streaming.
148
148
 
149
149
  ```ts
150
- import { servicebridge } from "service-bridge";
150
+ import { ServiceBridge } from "service-bridge";
151
151
 
152
152
  // --- Payments service (worker) ---
153
153
 
154
- const payments = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
154
+ const payments = new ServiceBridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
155
155
 
156
- payments.handleRpc("charge", async (payload: { orderId: string; amount: number }, ctx) => {
156
+ payments.rpc.handle("payment.charge", async (payload: { orderId: string; amount: number }, ctx) => {
157
157
  await ctx?.stream.write({ status: "charging", orderId: payload.orderId }, "progress");
158
158
 
159
159
  // ... charge logic ...
@@ -162,21 +162,21 @@ payments.handleRpc("charge", async (payload: { orderId: string; amount: number }
162
162
  return { ok: true, txId: `tx_${Date.now()}` };
163
163
  });
164
164
 
165
- await payments.serve({ host: "localhost" });
165
+ await payments.start({ host: "localhost" });
166
166
  ```
167
167
 
168
168
  ```ts
169
169
  // --- Orders service (caller + event publisher) ---
170
170
 
171
- const orders = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
171
+ const orders = new ServiceBridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
172
172
 
173
173
  // Call payments, then publish event
174
- const charge = await orders.rpc<{ ok: boolean; txId: string }>("payment.charge", {
174
+ const charge = await orders.rpc.invoke<{ ok: boolean; txId: string }>("payment.charge", {
175
175
  orderId: "ord_42",
176
176
  amount: 4990,
177
177
  });
178
178
 
179
- await orders.event("orders.completed", {
179
+ await orders.events.publish("orders.completed", {
180
180
  orderId: "ord_42",
181
181
  txId: charge.txId,
182
182
  }, {
@@ -188,25 +188,25 @@ await orders.event("orders.completed", {
188
188
  ```ts
189
189
  // --- Notifications service (event consumer) ---
190
190
 
191
- const notifications = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
191
+ const notifications = new ServiceBridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
192
192
 
193
- notifications.handleEvent("orders.*", async (payload, ctx) => {
193
+ notifications.events.handle("orders.*", async (payload, ctx) => {
194
194
  const body = payload as { orderId: string; txId: string };
195
195
  await ctx.stream.write({ status: "sending_email", orderId: body.orderId }, "progress");
196
196
  // ... send email ...
197
197
  });
198
198
 
199
- await notifications.serve({ host: "localhost" });
199
+ await notifications.start({ host: "localhost" });
200
200
  ```
201
201
 
202
202
  ```ts
203
203
  // --- Orchestrate as a workflow ---
204
204
 
205
- await orders.workflow("order.fulfillment", [
206
- { id: "reserve", type: "rpc", ref: "inventory.reserve" },
207
- { id: "charge", type: "rpc", ref: "payment.charge", deps: ["reserve"] },
208
- { id: "wait_dlv", type: "event_wait", ref: "shipping.delivered", deps: ["charge"] },
209
- { id: "notify", type: "event", ref: "orders.fulfilled", deps: ["wait_dlv"] },
205
+ await orders.workflows.run("order.fulfillment", [
206
+ { id: "reserve", type: "rpc", service: "inventory", ref: "inventory.reserve" },
207
+ { id: "charge", type: "rpc", service: "payment", ref: "payment.charge", deps: ["reserve"] },
208
+ { id: "wait_dlv", type: "event_wait", ref: "shipping.delivered", deps: ["charge"] },
209
+ { id: "notify", type: "event", ref: "orders.fulfilled", deps: ["wait_dlv"] },
210
210
  ]);
211
211
  ```
212
212
 
@@ -259,44 +259,51 @@ Every step above — RPC, event publish, event delivery, workflow execution —
259
259
 
260
260
  ## API Reference
261
261
 
262
+ ### `ServiceBridge` / `ServiceBridgeService` surface
263
+
264
+ Per-instance API for `new ServiceBridge(...)` (implements `ServiceBridgeService`):
265
+
266
+ - **Namespaces:** `rpc` (`handle`, `invoke`, `declare`), `events` (`handle`, `publish`, `publishWorker`, `declare`), `jobs` (`run`), `workflows` (`run`, `declare`).
267
+ - **Lifecycle:** `start(opts?)`, `stop()`.
268
+ - **Workflows:** `cancelWorkflow(traceId)`.
269
+ - **HTTP & traces:** `startHttpSpan(opts)`, `registerHttpEndpoint(opts)`, `watchTrace(traceId, opts?)`.
270
+ - **Module helpers (exported from `service-bridge`):** `getTraceContext`, `withTraceContext`, `ServiceBridgeError`, `mapGrpcStatus`, `SB`, `SB_MESSAGES`. (`captureConsole` exists internally for log capture but is not part of the public package exports.)
271
+
262
272
  ### Cross-SDK parity notes
263
273
 
264
274
  ServiceBridge keeps the core API shape consistent across Node.js, Go, and Python:
265
- constructor, RPC, events, jobs, workflows, `executeWorkflow`, streams, serve/stop, and `ServiceBridgeError`.
275
+ constructor, `rpc` / `events` / `jobs` / `workflows` namespaces, streams, `start`/`stop`, and `ServiceBridgeError`.
266
276
 
267
277
  Constructor-level defaults for `timeout`, `retries`, and `retryDelay` are available
268
278
  across all three SDKs. Parity differences are naming-only (language idioms):
269
279
 
270
280
  - Constructor TLS overrides: `workerTLS`/`caCert` (Node), `WorkerTLS`/`CACert` (Go), `worker_tls`/`ca_cert` (Python)
271
281
  - Handler hints: timeout/retryable/concurrency/prefetch are advisory in all SDKs
272
- - Shared `serve()` fields across SDKs: host, max in-flight, instance ID, weight, and per-serve TLS override
282
+ - Shared `start()` fields across SDKs: host, max in-flight, instance ID, weight, and per-start TLS override
273
283
 
274
- ### `servicebridge(url, serviceKey, opts?)`
284
+ ### `new ServiceBridge(url, serviceKey, opts?)`
275
285
 
276
286
  ```ts
277
- function servicebridge(
278
- url: string,
279
- serviceKey: string,
280
- serviceOrOpts?: string | ServiceBridgeOpts,
281
- maybeGlobalOpts?: ServiceBridgeOpts,
282
- ): ServiceBridgeService
287
+ class ServiceBridge {
288
+ constructor(url: string, serviceKey: string, opts?: ServiceBridgeOpts);
289
+ }
283
290
  ```
284
291
 
285
- Creates an SDK client instance.
286
- Service identity is resolved by the runtime from `serviceKey`; passing a third `service` argument is legacy-only.
292
+ Creates an SDK client instance. Service identity is resolved by the runtime from the sbv2 `serviceKey` (key id). Use `new ServiceBridge(...)` as the **public** entry point from the `service-bridge` package (the constructor delegates to the same internal client setup used by the SDK; a lower-level factory exists in source but is **not** exported from the published entry).
287
293
 
288
294
  `ServiceBridgeOpts`:
289
295
 
290
296
  | Option | Type | Default | Description |
291
297
  |---|---|---|---|
292
- | `timeout` | `number` | `30000` | Default hard timeout per RPC attempt (ms). |
293
- | `retries` | `number` | `3` | Default retry count for `rpc()`. |
294
- | `retryDelay` | `number` | `300` | Base backoff delay (ms) for `rpc()`. |
298
+ | `timeout` | `number` | `30000` | Default hard timeout per `rpc.invoke()` attempt (ms). |
299
+ | `retries` | `number` | `3` | Default retry count for `rpc.invoke()`. |
300
+ | `retryDelay` | `number` | `300` | Base backoff delay (ms) for `rpc.invoke()`. |
295
301
  | `discoveryRefreshMs` | `number` | `10000` | Discovery refresh period for endpoint updates. |
296
302
  | `queueMaxSize` | `number` | `1000` | Max offline queue size for control-plane writes. |
297
303
  | `queueOverflow` | `"drop-oldest" \| "drop-newest" \| "error"` | `"drop-oldest"` | Overflow strategy for offline queue. |
298
304
  | `heartbeatIntervalMs` | `number` | `10000` | Base heartbeat period for worker registrations. |
299
305
  | `captureLogs` | `boolean` | `true` | Forward `console.*` logs to ServiceBridge. |
306
+ | `strictOutboundDeclarations` | `boolean` | `false` | When `true`, every outbound `rpc.invoke()` must be preceded by `rpc.declare(fn)` for the resolved target. |
300
307
 
301
308
  ### Advanced TLS overrides
302
309
 
@@ -318,15 +325,15 @@ type WorkerTLSOpts = {
318
325
 
319
326
  ---
320
327
 
321
- ### `rpc(fn, payload?, opts?)`
328
+ ### `rpc.invoke(fn, payload?, opts?)`
322
329
 
323
330
  ```ts
324
- rpc<T = unknown>(fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
331
+ invoke<T = unknown>(fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
325
332
  ```
326
333
 
327
334
  Calls a registered RPC handler on another worker. Direct gRPC path, no proxy.
328
335
 
329
- **Function name** — `fn` is a single **global function name** (the same string passed to `handleRpc` on the callee), e.g. `payment.charge` or `user.get`. It must be unique in the catalog and **must not contain `/`**.
336
+ **Function name** — `fn` is a single **global function name** (the same string passed to `rpc.handle` on the callee), e.g. `payment.charge` or `user.get`. It must be unique in the catalog and **must not contain `/`**.
330
337
 
331
338
  `RpcOpts`:
332
339
 
@@ -340,15 +347,15 @@ Calls a registered RPC handler on another worker. Direct gRPC path, no proxy.
340
347
  | `mode` | `"direct" \| "proxy"` | Transport mode. `"direct"` (default) connects directly to the worker. `"proxy"` routes through the control plane when direct connection is unavailable. |
341
348
 
342
349
  ```ts
343
- const user = await sb.rpc<{ id: string; name: string }>("user.get", { id: "u_1" });
350
+ const user = await sb.rpc.invoke<{ id: string; name: string }>("user.get", { id: "u_1" });
344
351
 
345
- const user2 = await sb.rpc<{ id: string; name: string }>("user.get", { id: "u_1" }, {
352
+ const user2 = await sb.rpc.invoke<{ id: string; name: string }>("user.get", { id: "u_1" }, {
346
353
  timeout: 5000,
347
354
  retries: 2,
348
355
  });
349
356
  ```
350
357
 
351
- `rpc()` is bounded even when a downstream worker is silent:
358
+ `rpc.invoke()` is bounded even when a downstream worker is silent:
352
359
  each attempt has a hard local timeout, retries are finite (`retries + 1` total attempts),
353
360
  and after the final failed attempt the root RPC span is closed with `error`.
354
361
 
@@ -356,10 +363,20 @@ Retry delay uses exponential backoff: `retryDelay * 2^(attempt-1)`.
356
363
 
357
364
  ---
358
365
 
359
- ### `event(topic, payload?, opts?)`
366
+ ### `rpc.declare(fn)`
367
+
368
+ ```ts
369
+ declare(fn: string): void
370
+ ```
371
+
372
+ Declares an outbound RPC dependency for registration metadata. When `strictOutboundDeclarations` is `true`, you must call `rpc.declare(fn)` before `rpc.invoke(fn, ...)` for that function. Does not invoke the remote handler.
373
+
374
+ ---
375
+
376
+ ### `events.publish(topic, payload?, opts?)`
360
377
 
361
378
  ```ts
362
- event(topic: string, payload?: unknown, opts?: EventOpts): Promise<string>
379
+ publish(topic: string, payload?: unknown, opts?: EventOpts): Promise<string>
363
380
  ```
364
381
 
365
382
  Publishes a durable event. Returns `messageId` when online.
@@ -374,7 +391,7 @@ Publishes a durable event. Returns `messageId` when online.
374
391
  | `headers` | `Record<string, string>` | Custom metadata headers. |
375
392
 
376
393
  ```ts
377
- await sb.event("orders.created", { orderId: "ord_42" }, {
394
+ await sb.events.publish("orders.created", { orderId: "ord_42" }, {
378
395
  idempotencyKey: "order:ord_42",
379
396
  headers: { source: "checkout" },
380
397
  });
@@ -382,13 +399,38 @@ await sb.event("orders.created", { orderId: "ord_42" }, {
382
399
 
383
400
  ---
384
401
 
385
- ### `job(target, opts)`
402
+ ### `events.publishWorker(topic, payload?, opts?)`
403
+
404
+ ```ts
405
+ publishWorker(
406
+ topic: string,
407
+ payload?: unknown,
408
+ opts?: { traceId?: string; parentSpanId?: string; headers?: Record<string, string> },
409
+ ): Promise<string>
410
+ ```
411
+
412
+ Publishes over the worker session stream (after `start()`). If no worker session is active, the promise is rejected.
413
+
414
+ ---
415
+
416
+ ### `events.declare(topic)`
417
+
418
+ ```ts
419
+ declare(topic: string): void
420
+ ```
421
+
422
+ Declares an outbound event dependency for registration metadata (does not publish a message).
423
+
424
+ ---
425
+
426
+ ### `jobs.run(service, fn, opts)` / `jobs.run(target, opts)`
386
427
 
387
428
  ```ts
388
- job(target: string, opts: ScheduleOpts): Promise<string>
429
+ run(service: string, fn: string, opts: ScheduleOpts & { via: "rpc" }): Promise<string>
430
+ run(target: string, opts: ScheduleOpts & { via: "event" | "workflow" }): Promise<string>
389
431
  ```
390
432
 
391
- Registers a scheduled or delayed job.
433
+ Registers a scheduled or delayed job. Resolves to the registration key: `"${service}/${fn}"` for the RPC overload, or the `target` string for the `event` / `workflow` overload.
392
434
 
393
435
  `ScheduleOpts`:
394
436
 
@@ -402,7 +444,7 @@ Registers a scheduled or delayed job.
402
444
  | `retryPolicyJson` | `string` | Retry policy JSON string. |
403
445
 
404
446
  ```ts
405
- await sb.job("billing.collect", {
447
+ await sb.jobs.run("payments", "billing.collect", {
406
448
  cron: "0 * * * *",
407
449
  timezone: "UTC",
408
450
  via: "rpc",
@@ -411,10 +453,26 @@ await sb.job("billing.collect", {
411
453
 
412
454
  ---
413
455
 
414
- ### `workflow(name, steps, opts?)`
456
+ ### `workflows.run(name, steps, opts?)` — register DAG
457
+
458
+ TypeScript (single method; behavior depends on the second argument):
459
+
460
+ ```ts
461
+ run(
462
+ nameOrService: string,
463
+ stepsOrName: WorkflowStep[] | string,
464
+ inputOrOpts?: unknown,
465
+ opts?: ExecuteWorkflowOpts,
466
+ ): Promise<string | ExecuteWorkflowResult>
467
+ ```
468
+
469
+ - **Register:** when `stepsOrName` is `WorkflowStep[]`, `nameOrService` is the workflow name, `inputOrOpts` is optional `WorkflowOpts`, and the promise resolves to that name (`string`).
470
+ - **Execute:** when `stepsOrName` is a `string`, `nameOrService` is the target **service** name, `stepsOrName` is the workflow name, `inputOrOpts` is the optional execution input, and `opts` is optional `ExecuteWorkflowOpts` (see execute section below).
471
+
472
+ Overload as used when registering:
415
473
 
416
474
  ```ts
417
- workflow(name: string, steps: WorkflowStep[], opts?: WorkflowOpts): Promise<string>
475
+ run(name: string, steps: WorkflowStep[], opts?: WorkflowOpts): Promise<string>
418
476
  ```
419
477
 
420
478
  Registers (or updates) a workflow definition as a DAG of typed steps. Returns the workflow name.
@@ -425,13 +483,14 @@ Registers (or updates) a workflow definition as a DAG of typed steps. Returns th
425
483
  |---|---|---|
426
484
  | `id` | `string` | Unique step identifier in the DAG. |
427
485
  | `type` | `"rpc" \| "event" \| "event_wait" \| "sleep" \| "workflow"` | Step execution type. |
428
- | `ref` | `string` | Required for `rpc`, `event`, `event_wait`, `workflow`. |
486
+ | `service` | `string` | Required for `rpc` and `workflow`: target service that owns the function or child workflow. |
487
+ | `ref` | `string` | Target name: RPC function, event topic, waited topic, or child workflow name (per `type`). |
429
488
  | `deps` | `string[]` | Dependencies. Empty/omitted means root step. |
430
489
  | `if` | `string` | Optional filter expression (step is skipped if false). |
431
490
  | `timeoutMs` | `number` | Optional timeout for `rpc` and `event_wait` steps. |
432
491
  | `durationMs` | `number` | Required for `sleep` steps. |
433
492
 
434
- `WorkflowOpts`:
493
+ `WorkflowOpts` (third argument when registering a DAG — shape below; the interface is defined in the SDK but **not** re-exported from the main `service-bridge` package entry, so use an inline object in app code):
435
494
 
436
495
  ```ts
437
496
  interface WorkflowOpts {
@@ -446,9 +505,9 @@ interface WorkflowOpts {
446
505
  | `stepTimeoutMs` | `number` | `30000` (30 s) | Default per-step timeout in milliseconds. |
447
506
 
448
507
  ```ts
449
- await sb.workflow("order.fulfillment", [
450
- { id: "reserve", type: "rpc", ref: "inventory.reserve" },
451
- { id: "charge", type: "rpc", ref: "payment.charge", deps: ["reserve"] },
508
+ await sb.workflows.run("order.fulfillment", [
509
+ { id: "reserve", type: "rpc", service: "inventory", ref: "inventory.reserve" },
510
+ { id: "charge", type: "rpc", service: "payment", ref: "payment.charge", deps: ["reserve"] },
452
511
  { id: "wait_5m", type: "sleep", durationMs: 300_000, deps: ["charge"] },
453
512
  { id: "notify", type: "event", ref: "orders.fulfilled", deps: ["wait_5m"] },
454
513
  ]);
@@ -457,35 +516,47 @@ await sb.workflow("order.fulfillment", [
457
516
  With explicit limits:
458
517
 
459
518
  ```ts
460
- await sb.workflow("checkout.flow", steps, { stepTimeoutMs: 60_000 });
519
+ await sb.workflows.run("checkout.flow", steps, { stepTimeoutMs: 60_000 });
520
+ ```
521
+
522
+ ---
523
+
524
+ ### `workflows.declare(service, name)`
525
+
526
+ ```ts
527
+ declare(service: string, name: string): void
461
528
  ```
462
529
 
530
+ Declares an outbound workflow dependency for registration metadata (does not start an execution).
531
+
463
532
  ---
464
533
 
465
- ### `executeWorkflow(name, input?, opts?)`
534
+ ### `workflows.run(service, name, input?, opts?)` — execute
535
+
536
+ This is the same `run` method as above when the second argument is the workflow **name** (`string`), not a step array:
466
537
 
467
538
  ```ts
468
- executeWorkflow(name: string, input?: unknown, opts?: ExecuteWorkflowOpts): Promise<{ traceId: string; groupTraceId: string }>
539
+ run(service: string, name: string, input?: unknown, opts?: ExecuteWorkflowOpts): Promise<ExecuteWorkflowResult>
469
540
  ```
470
541
 
471
- Starts a workflow execution on demand. The workflow must be registered first via `workflow()`.
472
- An alternative to scheduling via `job(target, { via: "workflow" })` — triggers the execution immediately.
542
+ Starts a workflow execution on demand. The workflow must be registered first via `workflows.run(name, steps)`.
543
+ An alternative to scheduling via `jobs.run(target, { via: "workflow", ... })` — triggers the execution immediately.
473
544
 
474
545
  | Parameter | Type | Default | Description |
475
546
  |---|---|---|---|
476
- | `name` | `string` | required | Name of a previously registered workflow. |
477
- | `input` | `unknown` | `undefined` | Optional JSON-serializable input payload. |
547
+ | `service` / `name` | `string` | required | Target service and workflow name. |
548
+ | `input` | `unknown` | `undefined` | Optional JSON-serializable input payload (serialized as JSON for the runtime). |
478
549
 
479
- Returns `{ traceId, groupTraceId }`. Use `traceId` with `watchTrace()` to observe execution in real time.
550
+ Returns `{ traceId }`. Use `traceId` with `watchTrace()` to observe execution in real time.
480
551
 
481
- `ExecuteWorkflowOpts`:
552
+ `ExecuteWorkflowOpts` (optional fourth argument):
482
553
 
483
554
  | Option | Type | Description |
484
555
  |---|---|---|
485
- | `traceId` | `string` | Override trace ID for this workflow execution. |
556
+ | `traceId` | `string` | Declared on the exported type for API parity; the current Node implementation does **not** forward this field to the control plane (the gRPC request is built without it). Prefer relying on the returned `traceId`. |
486
557
 
487
558
  ```ts
488
- const { traceId, groupTraceId } = await sb.executeWorkflow("user.onboarding", { userId: "u_123" });
559
+ const { traceId } = await sb.workflows.run("users", "user.onboarding", { userId: "u_123" });
489
560
  ```
490
561
 
491
562
  ---
@@ -504,10 +575,10 @@ await sb.cancelWorkflow("trace_01HQ...XYZ");
504
575
 
505
576
  ---
506
577
 
507
- ### `handleRpc(fn, handler, opts?)`
578
+ ### `rpc.handle(fn, handler, opts?)`
508
579
 
509
580
  ```ts
510
- handleRpc(
581
+ handle(
511
582
  fn: string,
512
583
  handler: (payload: unknown, ctx?: RpcContext) => unknown | Promise<unknown>,
513
584
  opts?: HandleRpcOpts,
@@ -535,7 +606,7 @@ Registers an RPC handler. Chainable.
535
606
  | `allowedCallers` | `string[]` | Allow-list of caller service names. |
536
607
 
537
608
  ```ts
538
- sb.handleRpc("ai.generate", async (payload: { prompt: string }, ctx) => {
609
+ sb.rpc.handle("ai.generate", async (payload: { prompt: string }, ctx) => {
539
610
  await ctx?.stream.write({ token: "Hello" }, "output");
540
611
  await ctx?.stream.write({ token: " world" }, "output");
541
612
  return { text: "Hello world" };
@@ -551,10 +622,10 @@ sb.handleRpc("ai.generate", async (payload: { prompt: string }, ctx) => {
551
622
 
552
623
  ---
553
624
 
554
- ### `handleEvent(pattern, handler, opts?)`
625
+ ### `events.handle(pattern, handler, opts?)`
555
626
 
556
627
  ```ts
557
- handleEvent(
628
+ handle(
558
629
  pattern: string,
559
630
  handler: (payload: unknown, ctx: EventContext) => void | Promise<void>,
560
631
  opts?: HandleEventOpts,
@@ -567,13 +638,12 @@ Registers an event consumer handler. Chainable.
567
638
 
568
639
  | Option | Type | Description |
569
640
  |---|---|---|
570
- | `groupName` | `string` | Consumer group name. Default: `<service-key-id>.<pattern>`. |
571
641
  | `concurrency` | `number` | Advisory concurrency hint (currently not hard-enforced). |
572
642
  | `prefetch` | `number` | Advisory prefetch hint (currently not hard-enforced). |
573
643
  | `retryPolicyJson` | `string` | Retry policy JSON string. |
574
644
  | `filterExpr` | `string` | Server-side filter expression. |
575
645
 
576
- Duplicate `groupName` registration throws an error.
646
+ The consumer group name is fixed as `<service-key-id>.<pattern>` (derived from your sbv2 key and the pattern string). Registering a second handler for the same pattern throws a duplicate consumer-group error.
577
647
 
578
648
  **Delivery guarantee**: once a message is accepted by the runtime, delivery to each consumer group
579
649
  is guaranteed. If the consumer is offline, the message waits in the server-side queue and is
@@ -591,7 +661,7 @@ a consumer, the delivery moves to DLQ with reason `delivery_ttl_exceeded`.
591
661
  - `ctx.stream.write(...)` — append real-time chunks to trace stream
592
662
 
593
663
  ```ts
594
- sb.handleEvent("orders.*", async (payload, ctx) => {
664
+ sb.events.handle("orders.*", async (payload, ctx) => {
595
665
  const body = payload as { orderId?: string };
596
666
  if (!body.orderId) {
597
667
  ctx.reject("missing_order_id");
@@ -603,17 +673,17 @@ sb.handleEvent("orders.*", async (payload, ctx) => {
603
673
 
604
674
  ---
605
675
 
606
- ### `serve(opts?)`
676
+ ### `start(opts?)`
607
677
 
608
678
  ```ts
609
- serve(opts?: ServeOpts): Promise<void>
679
+ start(opts?: StartOpts): Promise<void>
610
680
  ```
611
681
 
612
682
  Starts the worker gRPC server and registers handlers with the control plane.
613
683
  The promise resolves once startup/registration is complete (it does not block
614
- the Node.js process). Throws immediately if no handlers are registered (neither `handleRpc()` nor `handleEvent()` have been called).
684
+ the Node.js process). Throws immediately if no handlers are registered (neither `rpc.handle()` nor `events.handle()` have been called).
615
685
 
616
- `ServeOpts`:
686
+ `StartOpts`:
617
687
 
618
688
  | Option | Type | Description |
619
689
  |---|---|---|
@@ -621,10 +691,10 @@ the Node.js process). Throws immediately if no handlers are registered (neither
621
691
  | `maxInFlight` | `number` | Max in-flight runtime-originated commands over `OpenWorkerSession`. Default: `128`. |
622
692
  | `instanceId` | `string` | Stable worker instance identifier. |
623
693
  | `weight` | `number` | Scheduling/discovery weight hint. |
624
- | `tls` | `WorkerTLSOpts` | Per-serve worker TLS override. |
694
+ | `tls` | `WorkerTLSOpts` | Per-start worker TLS override. |
625
695
 
626
696
  ```ts
627
- await sb.serve({
697
+ await sb.start({
628
698
  host: "localhost",
629
699
  instanceId: process.env.HOSTNAME,
630
700
  });
@@ -681,19 +751,18 @@ registerHttpEndpoint(opts: {
681
751
  }): Promise<void>
682
752
  ```
683
753
 
684
- Registers HTTP route metadata in the ServiceBridge service catalog.
685
- Also starts a periodic heartbeat to keep the HTTP endpoint alive in the registry.
754
+ Registers HTTP route metadata in the ServiceBridge service catalog (stored and sent on the next Reconcile). **Requires a completed worker `start()`**: until `start()` has finished successfully, the call resolves but does not record the route (HTTP middleware may invoke `registerHttpEndpoint` on first request; catalog entries appear only after `start()` has run).
686
755
 
687
756
  | Option | Type | Description |
688
757
  |---|---|---|
689
758
  | `method` | `string` | HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, etc. |
690
759
  | `route` | `string` | Route pattern with parameter placeholders, e.g. `"/users/:id"`. |
691
- | `instanceId` | `string` | Stable identifier for this process instance. |
692
- | `endpoint` | `string` | Reachable address, e.g. `"http://10.0.0.1:3000"`. |
760
+ | `instanceId` | `string` | Present on the public opts type; **not** applied by the current Node client when building `http_routes` for Reconcile (worker identity comes from `start()`). |
761
+ | `endpoint` | `string` | Same as above — use `start()` / deployment wiring for the reachable worker base URL. |
693
762
  | `allowedCallers` | `string[]` | Service names allowed to call (RBAC). |
694
763
  | `requestSchemaJson` | `string` | JSON schema for request validation metadata. |
695
764
  | `responseSchemaJson` | `string` | JSON schema for response validation metadata. |
696
- | `transport` | `string` | Transport label (e.g. `"http"`, `"https"`). |
765
+ | `transport` | `string` | Present on the public opts type; **not** sent per route in the current Node reconcile payload. |
697
766
 
698
767
  ```ts
699
768
  await sb.registerHttpEndpoint({
@@ -756,7 +825,7 @@ for await (const evt of sb.watchTrace(traceId, { key: "output", fromSequence: 0
756
825
  #### `getTraceContext()`
757
826
 
758
827
  ```ts
759
- getTraceContext(): { traceId: string; spanId: string } | undefined
828
+ getTraceContext(): TraceCtx | undefined
760
829
  ```
761
830
 
762
831
  Returns the current async-local trace context.
@@ -773,7 +842,7 @@ if (tc) {
773
842
  #### `withTraceContext(ctx, fn)`
774
843
 
775
844
  ```ts
776
- withTraceContext<T>(ctx: { traceId: string; spanId: string }, fn: () => T): T
845
+ withTraceContext<T>(ctx: TraceCtx, fn: () => T): T
777
846
  ```
778
847
 
779
848
  Runs a function inside an explicit trace context.
@@ -782,7 +851,7 @@ Runs a function inside an explicit trace context.
782
851
  import { withTraceContext } from "service-bridge";
783
852
 
784
853
  withTraceContext({ traceId: "trace-1", spanId: "span-1" }, async () => {
785
- await sb.event("audit.log", { action: "user.login" });
854
+ await sb.events.publish("audit.log", { action: "user.login" });
786
855
  });
787
856
  ```
788
857
 
@@ -798,10 +867,10 @@ npm install express
798
867
 
799
868
  ```ts
800
869
  import express from "express";
801
- import { servicebridge } from "service-bridge";
870
+ import { ServiceBridge } from "service-bridge";
802
871
  import { servicebridgeMiddleware, registerExpressRoutes } from "service-bridge/express";
803
872
 
804
- const sb = servicebridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY!);
873
+ const sb = new ServiceBridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY!);
805
874
  const app = express();
806
875
 
807
876
  app.use(servicebridgeMiddleware({
@@ -811,7 +880,7 @@ app.use(servicebridgeMiddleware({
811
880
  }));
812
881
 
813
882
  app.get("/users/:id", async (req, res) => {
814
- const user = await req.servicebridge.rpc("user.get", { id: req.params.id });
883
+ const user = await req.servicebridge.rpc.invoke("user.get", { id: req.params.id });
815
884
  res.json(user);
816
885
  });
817
886
  ```
@@ -854,10 +923,10 @@ npm install fastify
854
923
 
855
924
  ```ts
856
925
  import Fastify from "fastify";
857
- import { servicebridge } from "service-bridge";
926
+ import { ServiceBridge } from "service-bridge";
858
927
  import { servicebridgePlugin, wrapHandler } from "service-bridge/fastify";
859
928
 
860
- const sb = servicebridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY!);
929
+ const sb = new ServiceBridge(process.env.SERVICEBRIDGE_URL!, process.env.SERVICEBRIDGE_SERVICE_KEY!);
861
930
  const app = Fastify();
862
931
 
863
932
  await app.register(servicebridgePlugin, {
@@ -867,7 +936,7 @@ await app.register(servicebridgePlugin, {
867
936
  });
868
937
 
869
938
  app.get("/users/:id", wrapHandler(async (request, reply) => {
870
- const user = await request.servicebridge.rpc("user.get", {
939
+ const user = await request.servicebridge.rpc.invoke("user.get", {
871
940
  id: (request.params as any).id,
872
941
  });
873
942
  return reply.send(user);
@@ -926,11 +995,11 @@ Extracts trace context from HTTP headers. Supports W3C `traceparent`, `x-trace-i
926
995
  - Embedded/explicit CA PEM is validated with strict x509 parsing.
927
996
  - If `workerTLS` is not provided, SDK auto-provisions worker certs via gRPC `ProvisionWorkerCertificate`.
928
997
  - `workerTLS.cert` and `workerTLS.key` must be provided together.
929
- - `serve({ tls })` overrides global `workerTLS` for a specific worker instance.
998
+ - `start({ tls })` overrides global `workerTLS` for a specific worker instance.
930
999
 
931
1000
  ### Offline queue behavior
932
1001
 
933
- When the control plane is unavailable, SDK queues write operations (`event`, `job`, `workflow`, telemetry writes).
1002
+ When the control plane is unavailable, SDK queues write operations (`events.publish`, `jobs.run`, `workflows.run`, telemetry writes).
934
1003
 
935
1004
  - Queue size: `queueMaxSize` (default: 1000)
936
1005
  - Overflow policy: `queueOverflow` (default: `"drop-oldest"`)
@@ -940,7 +1009,7 @@ When the control plane is unavailable, SDK queues write operations (`event`, `jo
940
1009
 
941
1010
  ## Environment Variables
942
1011
 
943
- The SDK requires values you pass into `servicebridge(...)`. Common setup:
1012
+ The SDK requires values you pass into `new ServiceBridge(...)`. Common setup:
944
1013
 
945
1014
  | Variable | Required | Example | Description |
946
1015
  |---|---|---|---|
@@ -948,7 +1017,7 @@ The SDK requires values you pass into `servicebridge(...)`. Common setup:
948
1017
  | `SERVICEBRIDGE_SERVICE_KEY` | yes | `sbv2.<id>.<secret>.<ca>` | Service authentication key (sbv2 only) |
949
1018
 
950
1019
  ```ts
951
- const sb = servicebridge(
1020
+ const sb = new ServiceBridge(
952
1021
  process.env.SERVICEBRIDGE_URL ?? "localhost:14445",
953
1022
  process.env.SERVICEBRIDGE_SERVICE_KEY!,
954
1023
  );
@@ -961,13 +1030,13 @@ const sb = servicebridge(
961
1030
  `ServiceBridgeError` is exported for normalized SDK and runtime errors.
962
1031
 
963
1032
  ```ts
964
- import { servicebridge, ServiceBridgeError } from "service-bridge";
1033
+ import { ServiceBridge, ServiceBridgeError } from "service-bridge";
965
1034
 
966
1035
  try {
967
- await sb.rpc("payment.charge", { orderId: "ord_1" });
1036
+ await sb.rpc.invoke("payment.charge", { orderId: "ord_1" });
968
1037
  } catch (e) {
969
1038
  if (e instanceof ServiceBridgeError) {
970
- console.error(e.component, e.operation, e.severity, e.retryable, e.code);
1039
+ console.error(e.component, e.operation, e.severity, e.retryable, e.code, e.grpcStatus);
971
1040
  }
972
1041
  throw e;
973
1042
  }
@@ -978,8 +1047,9 @@ try {
978
1047
  | `component` | `string` | SDK subsystem (for example, `"rpc"` or `"event"`). |
979
1048
  | `operation` | `string` | Operation that failed. |
980
1049
  | `severity` | `"fatal" \| "retriable" \| "ignorable"` | Error classification. |
981
- | `retryable` | `boolean` | Whether retry is recommended. |
982
- | `code` | `number \| undefined` | gRPC status code (if available). |
1050
+ | `retryable` | `boolean` | Whether retry is recommended (`true` when `severity === "retriable"`). |
1051
+ | `code` | `ServiceBridgeErrorCode` | Stable SDK error id (`SB_*`). |
1052
+ | `grpcStatus` | `number \| undefined` | gRPC status code when the error came from gRPC. |
983
1053
  | `cause` | `unknown` | Original underlying error. |
984
1054
 
985
1055
  ---
@@ -1036,7 +1106,6 @@ import { V2SessionClient, validateV2Config } from 'service-bridge';
1036
1106
 
1037
1107
  const cfg = {
1038
1108
  serverAddress: 'localhost:9090',
1039
- serviceName: 'my-worker',
1040
1109
  instanceId: 'worker-1',
1041
1110
  zone: 'us-east-1a',
1042
1111
  transportMode: 'direct' as const,
@@ -1210,12 +1279,10 @@ const mode = session.resolveTransportMode('payment.charge'); // 'proxy'
1210
1279
  | `FunctionTransportOverride` | interface | Per-function transport override |
1211
1280
  | `ResumeState` | interface | Reconnect resume state |
1212
1281
 
1213
- Key types available for import:
1282
+ From the main entry `service-bridge`, types such as `ServiceBridgeOpts`, `RpcOpts`, `EventOpts`, `HandleRpcOpts`, `HandleEventOpts`, `ScheduleOpts`, `StartOpts`, `ExecuteWorkflowOpts`, and `ExecuteWorkflowResult` are available. The DAG shapes **`WorkflowStep` and `WorkflowOpts` are documented above but are not named exports** from that entry — use inline object literals (inference from `workflows.run(...)`) unless your toolchain exposes deep paths. Example:
1214
1283
 
1215
1284
  ```ts
1216
1285
  import type {
1217
- WorkflowStep,
1218
- WorkerTLSOpts,
1219
1286
  RpcContext,
1220
1287
  EventContext,
1221
1288
  StreamWriter,