service-bridge 1.8.1-dev.36 → 1.8.2-dev.38

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 +17 -29
  2. package/dist/index.js +23 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -99,7 +99,7 @@ const sb = servicebridge(
99
99
  process.env.SERVICEBRIDGE_SERVICE_KEY!,
100
100
  );
101
101
 
102
- sb.handleRpc("charge", async (payload: { orderId: string; amount: number }) => {
102
+ sb.handleRpc("payment.charge", async (payload: { orderId: string; amount: number }) => {
103
103
  return { ok: true, txId: `tx_${Date.now()}`, orderId: payload.orderId };
104
104
  });
105
105
 
@@ -116,7 +116,7 @@ const sb = servicebridge(
116
116
  process.env.SERVICEBRIDGE_SERVICE_KEY!,
117
117
  );
118
118
 
119
- const result = await sb.rpc<{ ok: boolean; txId: string }>("payments/charge", {
119
+ const result = await sb.rpc<{ ok: boolean; txId: string }>("payments", "payment.charge", {
120
120
  orderId: "ord_42",
121
121
  amount: 4990,
122
122
  });
@@ -153,7 +153,7 @@ import { servicebridge } from "service-bridge";
153
153
 
154
154
  const payments = servicebridge("localhost:14445", process.env.SERVICEBRIDGE_SERVICE_KEY!);
155
155
 
156
- payments.handleRpc("charge", async (payload: { orderId: string; amount: number }, ctx) => {
156
+ payments.handleRpc("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 ...
@@ -171,7 +171,7 @@ await payments.serve({ host: "localhost" });
171
171
  const orders = 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 }>("payments/charge", {
174
+ const charge = await orders.rpc<{ ok: boolean; txId: string }>("payments", "payment.charge", {
175
175
  orderId: "ord_42",
176
176
  amount: 4990,
177
177
  });
@@ -203,8 +203,8 @@ await notifications.serve({ host: "localhost" });
203
203
  // --- Orchestrate as a workflow ---
204
204
 
205
205
  await orders.workflow("order.fulfillment", [
206
- { id: "reserve", type: "rpc", ref: "inventory/reserve" },
207
- { id: "charge", type: "rpc", ref: "payments/charge", deps: ["reserve"] },
206
+ { id: "reserve", type: "rpc", ref: "inventory/inventory.reserve" },
207
+ { id: "charge", type: "rpc", ref: "payments/payment.charge", deps: ["reserve"] },
208
208
  { id: "wait_dlv", type: "event_wait", ref: "shipping.delivered", deps: ["charge"] },
209
209
  { id: "notify", type: "event", ref: "orders.fulfilled", deps: ["wait_dlv"] },
210
210
  ]);
@@ -318,22 +318,15 @@ type WorkerTLSOpts = {
318
318
 
319
319
  ---
320
320
 
321
- ### `rpc(fn, payload?, opts?)`
321
+ ### `rpc(service, fn, payload?, opts?)`
322
322
 
323
323
  ```ts
324
- rpc<T = unknown>(fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
324
+ rpc<T = unknown>(service: string, fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
325
325
  ```
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.
329
+ **Arguments** — `service` is the callee’s logical name; `fn` is the name used in `handleRpc` (e.g. `payment.charge`). Use **dot notation** in `fn` to group methods. Do not put `/` in `fn`.
337
330
 
338
331
  `RpcOpts`:
339
332
 
@@ -347,11 +340,7 @@ Both formats are interchangeable when the name is unique globally. Canonical for
347
340
  | `mode` | `"direct" \| "proxy"` | Transport mode. `"direct"` (default) connects directly to the worker. `"proxy"` routes through the control plane when direct connection is unavailable. |
348
341
 
349
342
  ```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
354
- const user = await sb.rpc<{ id: string; name: string }>("users/get", { id: "u_1" }, {
343
+ const user = await sb.rpc<{ id: string; name: string }>("users", "user.get", { id: "u_1" }, {
355
344
  timeout: 5000,
356
345
  retries: 2,
357
346
  });
@@ -421,7 +410,7 @@ Registers a scheduled or delayed job.
421
410
  | `retryPolicyJson` | `string` | Retry policy JSON string. |
422
411
 
423
412
  ```ts
424
- await sb.job("billing/collect", {
413
+ await sb.job("billing/billing.collect", {
425
414
  cron: "0 * * * *",
426
415
  timezone: "UTC",
427
416
  via: "rpc",
@@ -466,8 +455,8 @@ interface WorkflowOpts {
466
455
 
467
456
  ```ts
468
457
  await sb.workflow("order.fulfillment", [
469
- { id: "reserve", type: "rpc", ref: "inventory/reserve" },
470
- { id: "charge", type: "rpc", ref: "payments/charge", deps: ["reserve"] },
458
+ { id: "reserve", type: "rpc", ref: "inventory/inventory.reserve" },
459
+ { id: "charge", type: "rpc", ref: "payments/payment.charge", deps: ["reserve"] },
471
460
  { id: "wait_5m", type: "sleep", durationMs: 300_000, deps: ["charge"] },
472
461
  { id: "notify", type: "event", ref: "orders.fulfilled", deps: ["wait_5m"] },
473
462
  ]);
@@ -554,7 +543,7 @@ Registers an RPC handler. Chainable.
554
543
  | `allowedCallers` | `string[]` | Allow-list of caller service names. |
555
544
 
556
545
  ```ts
557
- sb.handleRpc("ai/generate", async (payload: { prompt: string }, ctx) => {
546
+ sb.handleRpc("ai.generate", async (payload: { prompt: string }, ctx) => {
558
547
  await ctx?.stream.write({ token: "Hello" }, "output");
559
548
  await ctx?.stream.write({ token: " world" }, "output");
560
549
  return { text: "Hello world" };
@@ -830,7 +819,7 @@ app.use(servicebridgeMiddleware({
830
819
  }));
831
820
 
832
821
  app.get("/users/:id", async (req, res) => {
833
- const user = await req.servicebridge.rpc("users/get", { id: req.params.id });
822
+ const user = await req.servicebridge.rpc("users", "user.get", { id: req.params.id });
834
823
  res.json(user);
835
824
  });
836
825
  ```
@@ -886,7 +875,7 @@ await app.register(servicebridgePlugin, {
886
875
  });
887
876
 
888
877
  app.get("/users/:id", wrapHandler(async (request, reply) => {
889
- const user = await request.servicebridge.rpc("users/get", {
878
+ const user = await request.servicebridge.rpc("users", "user.get", {
890
879
  id: (request.params as any).id,
891
880
  });
892
881
  return reply.send(user);
@@ -983,7 +972,7 @@ const sb = servicebridge(
983
972
  import { servicebridge, ServiceBridgeError } from "service-bridge";
984
973
 
985
974
  try {
986
- await sb.rpc("payments/charge", { orderId: "ord_1" });
975
+ await sb.rpc("payments", "payment.charge", { orderId: "ord_1" });
987
976
  } catch (e) {
988
977
  if (e instanceof ServiceBridgeError) {
989
978
  console.error(e.component, e.operation, e.severity, e.retryable, e.code);
@@ -1055,7 +1044,6 @@ import { V2SessionClient, validateV2Config } from 'service-bridge';
1055
1044
 
1056
1045
  const cfg = {
1057
1046
  serverAddress: 'localhost:9090',
1058
- serviceName: 'my-worker',
1059
1047
  instanceId: 'worker-1',
1060
1048
  zone: 'us-east-1a',
1061
1049
  transportMode: 'direct' as const,
package/dist/index.js CHANGED
@@ -177,7 +177,6 @@ class V2SessionClient {
177
177
  constructor(config) {
178
178
  this.config = {
179
179
  serverAddress: config.serverAddress,
180
- serviceName: config.serviceName,
181
180
  instanceId: config.instanceId,
182
181
  zone: config.zone ?? "",
183
182
  transportMode: config.transportMode ?? "direct",
@@ -209,7 +208,6 @@ class V2SessionClient {
209
208
  const rs = this.position.getResumeState(this.resumeToken, this.epoch);
210
209
  return {
211
210
  identity: {
212
- serviceName: this.config.serviceName,
213
211
  instanceId: this.config.instanceId,
214
212
  transport: this.config.transportMode
215
213
  },
@@ -309,8 +307,6 @@ class V2SessionClient {
309
307
  function validateV2Config(cfg) {
310
308
  if (!cfg.serverAddress)
311
309
  throw new Error("servicebridge: serverAddress is required");
312
- if (!cfg.serviceName)
313
- throw new Error("servicebridge: serviceName is required");
314
310
  if (!cfg.instanceId)
315
311
  throw new Error("servicebridge: instanceId is required");
316
312
  if (cfg.transportMode && cfg.transportMode !== "direct" && cfg.transportMode !== "proxy") {
@@ -632,7 +628,7 @@ function reportSDKError(operation, err, component = "sdk") {
632
628
  }
633
629
  return normalized;
634
630
  }
635
- function parseCanonicalFunctionName(target) {
631
+ function parseRegistryCanonicalName(target) {
636
632
  const canonicalName = target.trim();
637
633
  if (!canonicalName)
638
634
  return null;
@@ -1065,7 +1061,6 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1065
1061
  function pushLog(level, msg, attrs) {
1066
1062
  const tc = traceStorage.getStore();
1067
1063
  const entry = {
1068
- service_name: service,
1069
1064
  level,
1070
1065
  message: msg,
1071
1066
  timestamp_ns: String(Date.now() * 1e6),
@@ -1147,7 +1142,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1147
1142
  const endpoints = parseEndpointsFromWire(res.endpoints);
1148
1143
  const isNewFunction = !functionMeta.has(canonicalName);
1149
1144
  if (isNewFunction) {
1150
- const parsed = parseCanonicalFunctionName(canonicalName);
1145
+ const parsed = parseRegistryCanonicalName(canonicalName);
1151
1146
  if (parsed) {
1152
1147
  functionMeta.set(canonicalName, {
1153
1148
  canonicalName,
@@ -1533,7 +1528,6 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1533
1528
  await new Promise((resolve, reject) => {
1534
1529
  stub.Reconcile({
1535
1530
  identity: {
1536
- service_name: service,
1537
1531
  instance_id: state.instanceId,
1538
1532
  endpoint: state.endpoint,
1539
1533
  transport: state.transport
@@ -1991,7 +1985,6 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1991
1985
  if (!v2Session) {
1992
1986
  v2Session = new V2SessionClient({
1993
1987
  serverAddress: target,
1994
- serviceName: service,
1995
1988
  instanceId: serveState.instanceId,
1996
1989
  transportMode: "direct",
1997
1990
  maxInflight: serveState.maxInFlight,
@@ -2004,7 +1997,6 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2004
1997
  stream.write({
2005
1998
  hello: {
2006
1999
  identity: {
2007
- service_name: hf.identity.serviceName,
2008
2000
  instance_id: hf.identity.instanceId,
2009
2001
  endpoint: serveState.endpoint,
2010
2002
  transport: serveState.transport
@@ -2120,35 +2112,40 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2120
2112
  }
2121
2113
  }
2122
2114
  const svc = {
2123
- async rpc(fn, payload, opts) {
2115
+ async rpc(targetService, fn, payload, opts) {
2124
2116
  try {
2125
2117
  await _controlReady;
2126
2118
  } catch (err) {
2127
2119
  throw normalizeServiceError(err, "control-plane");
2128
2120
  }
2121
+ const svcName = targetService.trim();
2122
+ const fname = fn.trim();
2123
+ if (!svcName || !fname) {
2124
+ throw new Error("rpc: service and fn are required");
2125
+ }
2126
+ const lookupKey = `${svcName}/${fname}`;
2129
2127
  const tc = traceStorage.getStore();
2130
2128
  const traceId = opts?.traceId ?? tc?.traceId ?? crypto.randomUUID();
2131
2129
  const maxRetries = normalizeNonNegativeInt(opts?.retries ?? globalOpts.retries ?? 3, 3);
2132
2130
  const baseDelay = normalizePositiveInt(opts?.retryDelay ?? globalOpts.retryDelay ?? 300, 300);
2133
2131
  const timeout = normalizePositiveInt(opts?.timeout ?? globalOpts.timeout ?? 30000, 30000);
2134
- let canonical = resolveCanonical(fn);
2132
+ let canonical = resolveCanonical(lookupKey);
2135
2133
  if (!canonical) {
2136
- await doLookupFunction(fn);
2137
- canonical = resolveCanonical(fn);
2134
+ await doLookupFunction(lookupKey);
2135
+ canonical = resolveCanonical(lookupKey);
2138
2136
  }
2139
2137
  if (!canonical)
2140
- throw normalizeServiceError(new Error(`No endpoints available for RPC: ${fn}`), fn, "worker");
2138
+ throw normalizeServiceError(new Error(`No endpoints available for RPC: ${lookupKey}`), lookupKey, "worker");
2141
2139
  const fmeta = functionMeta.get(canonical);
2142
2140
  if (fmeta && !containsOrAll(fmeta.allowedCallers, service)) {
2143
- throw new Error(`Service "${service}" is not allowed to call "${fn}". ` + `Permitted callers: ${fmeta.allowedCallers.join(", ")}`);
2141
+ throw new Error(`Service "${service}" is not allowed to call "${lookupKey}". ` + `Permitted callers: ${fmeta.allowedCallers.join(", ")}`);
2144
2142
  }
2145
2143
  const inputSchema = fmeta?.inputSchema;
2146
2144
  const outputSchema = fmeta?.outputSchema;
2147
2145
  const inputBuf = inputSchema ? encodeWithSchema(inputSchema, payload) : toJsonBuffer(payload);
2148
2146
  const telemetryInputBuf = toJsonBuffer(payload);
2149
- const _parsedCanonical = parseCanonicalFunctionName(canonical);
2150
- const rpcFnName = _parsedCanonical?.fnName ?? canonical;
2151
- const rpcServiceName = _parsedCanonical?.serviceName ?? "";
2147
+ const rpcFnName = fname;
2148
+ const rpcServiceName = svcName;
2152
2149
  const rootSpanId = crypto.randomUUID();
2153
2150
  const parentSpanId = opts?.parentSpanId ?? tc?.spanId ?? "";
2154
2151
  const rootStartedAt = Date.now();
@@ -2166,12 +2163,12 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2166
2163
  }, meta, { deadline: new Date(Date.now() + timeout) }, (err, res) => {
2167
2164
  if (err) {
2168
2165
  reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, false, 1, undefined, String(err), "rpc", parentSpanId, rpcServiceName);
2169
- return reject(normalizeServiceError(err, fn, "control-plane"));
2166
+ return reject(normalizeServiceError(err, lookupKey, "control-plane"));
2170
2167
  }
2171
2168
  if (!res?.success) {
2172
2169
  const errMsg = res?.error ?? "proxy call failed";
2173
2170
  reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, false, 1, undefined, errMsg, "rpc", parentSpanId, rpcServiceName);
2174
- return reject(normalizeServiceError(new Error(errMsg), fn, "worker"));
2171
+ return reject(normalizeServiceError(new Error(errMsg), lookupKey, "worker"));
2175
2172
  }
2176
2173
  reportCallAsync(traceId, rootSpanId, rpcFnName, rootStartedAt, telemetryInputBuf, true, 1, res.output, "", "rpc", parentSpanId, rpcServiceName);
2177
2174
  try {
@@ -2197,8 +2194,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2197
2194
  try {
2198
2195
  const attemptTimeoutMs = timeout;
2199
2196
  const deadline = new Date(Date.now() + attemptTimeoutMs);
2200
- const fn2 = fmeta?.fnName ?? canonical;
2201
- if (!fn2)
2197
+ const workerFnName = fmeta?.fnName ?? fname;
2198
+ if (!workerFnName)
2202
2199
  throw new Error("unreachable");
2203
2200
  const res = await new Promise((resolve, reject) => {
2204
2201
  let settled = false;
@@ -2225,7 +2222,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2225
2222
  };
2226
2223
  try {
2227
2224
  unaryCall = workerClient.Handle({
2228
- function_name: fn2,
2225
+ function_name: workerFnName,
2229
2226
  payload: inputBuf,
2230
2227
  trace_id: traceId,
2231
2228
  span_id: attemptSpanId,
@@ -2263,7 +2260,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2263
2260
  }
2264
2261
  }
2265
2262
  }
2266
- throw normalizeServiceError(lastError, fn, "worker");
2263
+ throw normalizeServiceError(lastError, lookupKey, "worker");
2267
2264
  });
2268
2265
  },
2269
2266
  event(topic, payload, opts) {
@@ -2474,8 +2471,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2474
2471
  return new Promise((resolve, reject) => {
2475
2472
  stub.ExecuteWorkflow({
2476
2473
  workflow_name: name,
2477
- input: payload,
2478
- service_name: service
2474
+ input: payload
2479
2475
  }, meta, unaryDeadlineOptions(), (err, res) => {
2480
2476
  if (err) {
2481
2477
  reject(normalizeServiceError(err, "execute-workflow"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "service-bridge",
3
- "version": "1.8.1-dev.36",
3
+ "version": "1.8.2-dev.38",
4
4
  "type": "module",
5
5
  "description": "ServiceBridge SDK for Node.js — one self-hosted runtime for RPC, events, workflows, and jobs without a service mesh or sidecars. Direct gRPC between workers; durable events, jobs, tracing, auto mTLS. One Go runtime + PostgreSQL.",
6
6
  "keywords": [