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

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 +20 -34
  2. package/dist/index.js +28 -32
  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
  ]);
@@ -277,13 +277,12 @@ across all three SDKs. Parity differences are naming-only (language idioms):
277
277
  function servicebridge(
278
278
  url: string,
279
279
  serviceKey: string,
280
- serviceOrOpts?: string | ServiceBridgeOpts,
281
- maybeGlobalOpts?: ServiceBridgeOpts,
280
+ opts?: ServiceBridgeOpts,
282
281
  ): ServiceBridgeService
283
282
  ```
284
283
 
285
284
  Creates an SDK client instance.
286
- Service identity is resolved by the runtime from `serviceKey`; passing a third `service` argument is legacy-only.
285
+ Service identity is resolved by the runtime from `serviceKey`.
287
286
 
288
287
  `ServiceBridgeOpts`:
289
288
 
@@ -318,22 +317,15 @@ type WorkerTLSOpts = {
318
317
 
319
318
  ---
320
319
 
321
- ### `rpc(fn, payload?, opts?)`
320
+ ### `rpc(service, fn, payload?, opts?)`
322
321
 
323
322
  ```ts
324
- rpc<T = unknown>(fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
323
+ rpc<T = unknown>(service: string, fn: string, payload?: unknown, opts?: RpcOpts): Promise<T>
325
324
  ```
326
325
 
327
326
  Calls a registered RPC handler on another service. Direct gRPC path, no proxy.
328
327
 
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.
328
+ **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
329
 
338
330
  `RpcOpts`:
339
331
 
@@ -347,11 +339,7 @@ Both formats are interchangeable when the name is unique globally. Canonical for
347
339
  | `mode` | `"direct" \| "proxy"` | Transport mode. `"direct"` (default) connects directly to the worker. `"proxy"` routes through the control plane when direct connection is unavailable. |
348
340
 
349
341
  ```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" }, {
342
+ const user = await sb.rpc<{ id: string; name: string }>("users", "user.get", { id: "u_1" }, {
355
343
  timeout: 5000,
356
344
  retries: 2,
357
345
  });
@@ -421,7 +409,7 @@ Registers a scheduled or delayed job.
421
409
  | `retryPolicyJson` | `string` | Retry policy JSON string. |
422
410
 
423
411
  ```ts
424
- await sb.job("billing/collect", {
412
+ await sb.job("billing/billing.collect", {
425
413
  cron: "0 * * * *",
426
414
  timezone: "UTC",
427
415
  via: "rpc",
@@ -466,8 +454,8 @@ interface WorkflowOpts {
466
454
 
467
455
  ```ts
468
456
  await sb.workflow("order.fulfillment", [
469
- { id: "reserve", type: "rpc", ref: "inventory/reserve" },
470
- { id: "charge", type: "rpc", ref: "payments/charge", deps: ["reserve"] },
457
+ { id: "reserve", type: "rpc", ref: "inventory/inventory.reserve" },
458
+ { id: "charge", type: "rpc", ref: "payments/payment.charge", deps: ["reserve"] },
471
459
  { id: "wait_5m", type: "sleep", durationMs: 300_000, deps: ["charge"] },
472
460
  { id: "notify", type: "event", ref: "orders.fulfilled", deps: ["wait_5m"] },
473
461
  ]);
@@ -554,7 +542,7 @@ Registers an RPC handler. Chainable.
554
542
  | `allowedCallers` | `string[]` | Allow-list of caller service names. |
555
543
 
556
544
  ```ts
557
- sb.handleRpc("ai/generate", async (payload: { prompt: string }, ctx) => {
545
+ sb.handleRpc("ai.generate", async (payload: { prompt: string }, ctx) => {
558
546
  await ctx?.stream.write({ token: "Hello" }, "output");
559
547
  await ctx?.stream.write({ token: " world" }, "output");
560
548
  return { text: "Hello world" };
@@ -586,13 +574,12 @@ Registers an event consumer handler. Chainable.
586
574
 
587
575
  | Option | Type | Description |
588
576
  |---|---|---|
589
- | `groupName` | `string` | Consumer group name. Default: `<service-key-id>.<pattern>`. |
590
577
  | `concurrency` | `number` | Advisory concurrency hint (currently not hard-enforced). |
591
578
  | `prefetch` | `number` | Advisory prefetch hint (currently not hard-enforced). |
592
579
  | `retryPolicyJson` | `string` | Retry policy JSON string. |
593
580
  | `filterExpr` | `string` | Server-side filter expression. |
594
581
 
595
- Duplicate `groupName` registration throws an error.
582
+ Duplicate pattern registration within the same service throws an error.
596
583
 
597
584
  **Delivery guarantee**: once a message is accepted by the runtime, delivery to each consumer group
598
585
  is guaranteed. If the consumer is offline, the message waits in the server-side queue and is
@@ -830,7 +817,7 @@ app.use(servicebridgeMiddleware({
830
817
  }));
831
818
 
832
819
  app.get("/users/:id", async (req, res) => {
833
- const user = await req.servicebridge.rpc("users/get", { id: req.params.id });
820
+ const user = await req.servicebridge.rpc("users", "user.get", { id: req.params.id });
834
821
  res.json(user);
835
822
  });
836
823
  ```
@@ -886,7 +873,7 @@ await app.register(servicebridgePlugin, {
886
873
  });
887
874
 
888
875
  app.get("/users/:id", wrapHandler(async (request, reply) => {
889
- const user = await request.servicebridge.rpc("users/get", {
876
+ const user = await request.servicebridge.rpc("users", "user.get", {
890
877
  id: (request.params as any).id,
891
878
  });
892
879
  return reply.send(user);
@@ -983,7 +970,7 @@ const sb = servicebridge(
983
970
  import { servicebridge, ServiceBridgeError } from "service-bridge";
984
971
 
985
972
  try {
986
- await sb.rpc("payments/charge", { orderId: "ord_1" });
973
+ await sb.rpc("payments", "payment.charge", { orderId: "ord_1" });
987
974
  } catch (e) {
988
975
  if (e instanceof ServiceBridgeError) {
989
976
  console.error(e.component, e.operation, e.severity, e.retryable, e.code);
@@ -1055,7 +1042,6 @@ import { V2SessionClient, validateV2Config } from 'service-bridge';
1055
1042
 
1056
1043
  const cfg = {
1057
1044
  serverAddress: 'localhost:9090',
1058
- serviceName: 'my-worker',
1059
1045
  instanceId: 'worker-1',
1060
1046
  zone: 'us-east-1a',
1061
1047
  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;
@@ -863,7 +859,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
863
859
  const service = typeof serviceOrOpts === "string" ? serviceOrOpts.trim() : "";
864
860
  const globalOpts = typeof serviceOrOpts === "string" ? maybeGlobalOpts : serviceOrOpts ?? {};
865
861
  const parsedServiceKey = parseServiceKeyV2(serviceKey);
866
- const defaultConsumerPrefix = service || parsedServiceKey.keyId || "consumer";
862
+ const defaultConsumerPrefix = parsedServiceKey.keyId || "consumer";
867
863
  const meta = new grpc.Metadata;
868
864
  meta.add("x-service-key", serviceKey);
869
865
  const rawUrl = url.trim();
@@ -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,
@@ -1401,7 +1396,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1401
1396
  headers: toWireStringMap(op.opts?.headers),
1402
1397
  trace_id: op.opts?.traceId ?? "",
1403
1398
  parent_span_id: op.opts?.parentSpanId ?? "",
1404
- producer_service: service,
1399
+ producer_service: defaultConsumerPrefix,
1405
1400
  idempotency_key: op.opts?.idempotencyKey ?? ""
1406
1401
  }, meta, unaryDeadlineOptions(), (err) => err ? rej(err) : res());
1407
1402
  });
@@ -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) {
@@ -2279,7 +2276,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2279
2276
  headers: toWireStringMap(opts?.headers),
2280
2277
  trace_id: opts?.traceId ?? tc?.traceId ?? "",
2281
2278
  parent_span_id: opts?.parentSpanId ?? tc?.spanId ?? "",
2282
- producer_service: service,
2279
+ producer_service: defaultConsumerPrefix,
2283
2280
  idempotency_key: opts?.idempotencyKey ?? ""
2284
2281
  }, meta, unaryDeadlineOptions(), (err, res) => err ? reject(normalizeServiceError(err, `publish:${topic}`)) : resolve(res?.message_id ?? ""));
2285
2282
  });
@@ -2320,9 +2317,9 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
2320
2317
  },
2321
2318
  handleEvent(pattern, handler, opts) {
2322
2319
  const normalizedOpts = opts ?? {};
2323
- const groupName = normalizedOpts.groupName || `${defaultConsumerPrefix}.${pattern}`;
2320
+ const groupName = `${defaultConsumerPrefix}.${pattern}`;
2324
2321
  if (eventHandlers.has(groupName)) {
2325
- throw new Error(`Duplicate event consumer group "${groupName}". ` + "Use a distinct groupName for each handleEvent() registration.");
2322
+ throw new Error(`Duplicate event consumer group "${groupName}". ` + "Use handleEvent() with a unique topic pattern per handler.");
2326
2323
  }
2327
2324
  eventHandlers.set(groupName, {
2328
2325
  groupName,
@@ -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.39",
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": [