@service-bridge/node 0.1.3

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 (51) hide show
  1. package/README.md +854 -0
  2. package/biome.json +28 -0
  3. package/bun.lock +249 -0
  4. package/dist/express.d.ts +51 -0
  5. package/dist/express.js +129 -0
  6. package/dist/fastify.d.ts +43 -0
  7. package/dist/fastify.js +122 -0
  8. package/dist/index.js +34410 -0
  9. package/dist/trace.d.ts +19 -0
  10. package/http/dist/express.d.ts +51 -0
  11. package/http/dist/express.d.ts.map +1 -0
  12. package/http/dist/express.test.d.ts +2 -0
  13. package/http/dist/express.test.d.ts.map +1 -0
  14. package/http/dist/fastify.d.ts +43 -0
  15. package/http/dist/fastify.d.ts.map +1 -0
  16. package/http/dist/fastify.test.d.ts +2 -0
  17. package/http/dist/fastify.test.d.ts.map +1 -0
  18. package/http/dist/index.d.ts +7 -0
  19. package/http/dist/index.d.ts.map +1 -0
  20. package/http/dist/trace.d.ts +19 -0
  21. package/http/dist/trace.d.ts.map +1 -0
  22. package/http/dist/trace.test.d.ts +2 -0
  23. package/http/dist/trace.test.d.ts.map +1 -0
  24. package/http/package.json +48 -0
  25. package/http/src/express.test.ts +125 -0
  26. package/http/src/express.ts +209 -0
  27. package/http/src/fastify.test.ts +142 -0
  28. package/http/src/fastify.ts +159 -0
  29. package/http/src/index.ts +10 -0
  30. package/http/src/sdk-augment.d.ts +11 -0
  31. package/http/src/servicebridge.d.ts +23 -0
  32. package/http/src/trace.test.ts +97 -0
  33. package/http/src/trace.ts +56 -0
  34. package/http/tsconfig.json +17 -0
  35. package/http/tsconfig.test.json +6 -0
  36. package/package.json +65 -0
  37. package/sdk/dist/generated/servicebridge-package-definition.d.ts +4709 -0
  38. package/sdk/dist/grpc-client.d.ts +304 -0
  39. package/sdk/dist/grpc-client.test.d.ts +1 -0
  40. package/sdk/dist/index.d.ts +2 -0
  41. package/sdk/package.json +30 -0
  42. package/sdk/scripts/generate-proto.ts +65 -0
  43. package/sdk/src/generated/servicebridge-package-definition.ts +5198 -0
  44. package/sdk/src/grpc-client.d.ts +305 -0
  45. package/sdk/src/grpc-client.d.ts.map +1 -0
  46. package/sdk/src/grpc-client.test.ts +422 -0
  47. package/sdk/src/grpc-client.ts +2924 -0
  48. package/sdk/src/index.d.ts +3 -0
  49. package/sdk/src/index.d.ts.map +1 -0
  50. package/sdk/src/index.ts +29 -0
  51. package/sdk/tsconfig.json +13 -0
@@ -0,0 +1,422 @@
1
+ // @ts-nocheck
2
+ import { describe, expect, it, mock } from "bun:test";
3
+ import { EventEmitter } from "node:events";
4
+
5
+ type UnaryCallback = (err: unknown, res?: unknown) => void;
6
+ type ReportCallback = (err: unknown) => void;
7
+ type WorkerCallback = (err: unknown, res?: unknown) => void;
8
+ type LookupCallback = (
9
+ err: unknown,
10
+ res?: {
11
+ found?: boolean;
12
+ canonical_name?: string;
13
+ endpoints?: {
14
+ endpoints?: Array<{
15
+ endpoint: string;
16
+ transport?: string;
17
+ instance_id?: string;
18
+ weight?: number;
19
+ }>;
20
+ };
21
+ },
22
+ ) => void;
23
+
24
+ const grpcState = {
25
+ publishCalls: [] as unknown[],
26
+ workerCalls: [] as unknown[],
27
+ reportCallStartCalls: [] as unknown[],
28
+ reportCallCalls: [] as unknown[],
29
+ lookupCalls: [] as unknown[],
30
+ publishImpl: (_req: unknown, cb: UnaryCallback) =>
31
+ cb(null, { message_id: "msg-1" }),
32
+ registerJobImpl: (_req: unknown, cb: UnaryCallback) =>
33
+ cb(null, { id: "job-1" }),
34
+ registerWorkflowImpl: (_req: unknown, cb: UnaryCallback) =>
35
+ cb(null, { id: "wf-1" }),
36
+ reportCallStartImpl: (_req: unknown, cb: ReportCallback) => cb(null),
37
+ reportCallImpl: (_req: unknown, cb: ReportCallback) => cb(null),
38
+ lookupImpl: (_req: { fn_name: string }, cb: LookupCallback) =>
39
+ cb(null, { found: false }),
40
+ workerHandleImpl: (
41
+ _req: unknown,
42
+ _meta: unknown,
43
+ _opts: unknown,
44
+ cb: WorkerCallback,
45
+ ) =>
46
+ cb(null, {
47
+ success: true,
48
+ output: Buffer.from(JSON.stringify({ ok: true })),
49
+ }),
50
+ reset() {
51
+ this.publishCalls = [];
52
+ this.workerCalls = [];
53
+ this.reportCallStartCalls = [];
54
+ this.reportCallCalls = [];
55
+ this.lookupCalls = [];
56
+ this.publishImpl = (_req: unknown, cb: UnaryCallback) =>
57
+ cb(null, { message_id: "msg-1" });
58
+ this.registerJobImpl = (_req: unknown, cb: UnaryCallback) =>
59
+ cb(null, { id: "job-1" });
60
+ this.registerWorkflowImpl = (_req: unknown, cb: UnaryCallback) =>
61
+ cb(null, { id: "wf-1" });
62
+ this.reportCallStartImpl = (_req: unknown, cb: ReportCallback) => cb(null);
63
+ this.reportCallImpl = (_req: unknown, cb: ReportCallback) => cb(null);
64
+ this.lookupImpl = (_req: { fn_name: string }, cb: LookupCallback) =>
65
+ cb(null, { found: false });
66
+ this.workerHandleImpl = (
67
+ _req: unknown,
68
+ _meta: unknown,
69
+ _opts: unknown,
70
+ cb: WorkerCallback,
71
+ ) =>
72
+ cb(null, {
73
+ success: true,
74
+ output: Buffer.from(JSON.stringify({ ok: true })),
75
+ });
76
+ },
77
+ };
78
+
79
+ class FakeMetadata {
80
+ store = new Map<string, unknown[]>();
81
+
82
+ add(key: string, value: unknown) {
83
+ const normalized = key.toLowerCase();
84
+ const list = this.store.get(normalized) ?? [];
85
+ list.push(value);
86
+ this.store.set(normalized, list);
87
+ }
88
+
89
+ get(key: string) {
90
+ return this.store.get(key.toLowerCase()) ?? [];
91
+ }
92
+ }
93
+
94
+ class FakeControlPlaneClient {
95
+ close() {}
96
+
97
+ LookupFunction(
98
+ req: { fn_name: string },
99
+ _meta: unknown,
100
+ _opts: unknown,
101
+ cb: LookupCallback,
102
+ ) {
103
+ grpcState.lookupCalls.push(req);
104
+ grpcState.lookupImpl(req, cb);
105
+ }
106
+
107
+ Publish(req: unknown, _meta: unknown, _opts: unknown, cb: UnaryCallback) {
108
+ grpcState.publishCalls.push(req);
109
+ grpcState.publishImpl(req, cb);
110
+ }
111
+
112
+ RegisterJob(req: unknown, _meta: unknown, _opts: unknown, cb: UnaryCallback) {
113
+ grpcState.registerJobImpl(req, cb);
114
+ }
115
+
116
+ RegisterWorkflow(
117
+ req: unknown,
118
+ _meta: unknown,
119
+ _opts: unknown,
120
+ cb: UnaryCallback,
121
+ ) {
122
+ grpcState.registerWorkflowImpl(req, cb);
123
+ }
124
+
125
+ ReportCallStart(
126
+ req: unknown,
127
+ _meta: unknown,
128
+ _opts: unknown,
129
+ cb: ReportCallback,
130
+ ) {
131
+ grpcState.reportCallStartCalls.push(req);
132
+ grpcState.reportCallStartImpl(req, cb);
133
+ }
134
+
135
+ ReportCall(req: unknown, _meta: unknown, _opts: unknown, cb: ReportCallback) {
136
+ grpcState.reportCallCalls.push(req);
137
+ grpcState.reportCallImpl(req, cb);
138
+ }
139
+
140
+ ReportLog(_req: unknown, _meta: unknown, _opts: unknown, cb: ReportCallback) {
141
+ cb(null);
142
+ }
143
+
144
+ Heartbeat(_req: unknown, _meta: unknown, _opts: unknown, cb: ReportCallback) {
145
+ cb(null);
146
+ }
147
+
148
+ RegisterFunction(
149
+ _req: unknown,
150
+ _meta: unknown,
151
+ _opts: unknown,
152
+ cb: ReportCallback,
153
+ ) {
154
+ cb(null);
155
+ }
156
+
157
+ RegisterConsumerGroup(
158
+ _req: unknown,
159
+ _meta: unknown,
160
+ _opts: unknown,
161
+ cb: ReportCallback,
162
+ ) {
163
+ cb(null);
164
+ }
165
+
166
+ RegisterGroupMember(
167
+ _req: unknown,
168
+ _meta: unknown,
169
+ _opts: unknown,
170
+ cb: ReportCallback,
171
+ ) {
172
+ cb(null);
173
+ }
174
+
175
+ AppendStream(
176
+ _req: unknown,
177
+ _meta: unknown,
178
+ _opts: unknown,
179
+ cb: ReportCallback,
180
+ ) {
181
+ cb(null);
182
+ }
183
+
184
+ WatchRun(_req: unknown, _meta: unknown) {
185
+ return new EventEmitter();
186
+ }
187
+ }
188
+
189
+ class FakeWorkerClient {
190
+ endpoint: string;
191
+
192
+ constructor(endpoint: string) {
193
+ this.endpoint = endpoint;
194
+ }
195
+
196
+ close() {}
197
+
198
+ Handle(req: unknown, meta: unknown, opts: unknown, cb: WorkerCallback) {
199
+ grpcState.workerCalls.push({ endpoint: this.endpoint, req, meta, opts });
200
+ grpcState.workerHandleImpl(req, meta, opts, cb);
201
+ }
202
+ }
203
+
204
+ /** A no-op Channel stub — FakeWorkerClient ignores channelOverride. */
205
+ class FakeChannel {
206
+ close() {}
207
+ getConnectivityState() {
208
+ return 0;
209
+ }
210
+ }
211
+
212
+ mock.module("@grpc/grpc-js", () => ({
213
+ status: {
214
+ CANCELLED: 1,
215
+ UNKNOWN: 2,
216
+ DEADLINE_EXCEEDED: 4,
217
+ NOT_FOUND: 5,
218
+ RESOURCE_EXHAUSTED: 8,
219
+ FAILED_PRECONDITION: 9,
220
+ UNAVAILABLE: 14,
221
+ },
222
+ Metadata: FakeMetadata,
223
+ Channel: FakeChannel,
224
+ loadPackageDefinition() {
225
+ return {
226
+ servicebridge: {
227
+ ServiceBridge: FakeControlPlaneClient,
228
+ ServiceBridgeWorker: FakeWorkerClient,
229
+ },
230
+ };
231
+ },
232
+ credentials: {
233
+ createInsecure() {
234
+ return {};
235
+ },
236
+ createSsl() {
237
+ return {};
238
+ },
239
+ },
240
+ Server: class {},
241
+ ServerCredentials: {
242
+ createSsl() {
243
+ return {};
244
+ },
245
+ createInsecure() {
246
+ return {};
247
+ },
248
+ },
249
+ }));
250
+
251
+ const sdk = await import("./grpc-client");
252
+
253
+ async function flushMicrotasks() {
254
+ await new Promise((resolve) => setTimeout(resolve, 0));
255
+ }
256
+
257
+ const TEST_WORKER_TLS = {
258
+ caCert: "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----",
259
+ cert: "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----",
260
+ key: "-----BEGIN EC PRIVATE KEY-----\nMIIB\n-----END EC PRIVATE KEY-----",
261
+ };
262
+
263
+ const sb = (opts?: Record<string, unknown>) =>
264
+ sdk.servicebridge("127.0.0.1:14445", "service-key", "caller", {
265
+ workerTLS: TEST_WORKER_TLS,
266
+ ...opts,
267
+ });
268
+
269
+ /** Configure a canonical function for lookup. */
270
+ function configureLookup(
271
+ canonicalName: string,
272
+ endpoints: Array<{ endpoint: string }>,
273
+ ) {
274
+ const prev = grpcState.lookupImpl;
275
+ grpcState.lookupImpl = (req, cb) => {
276
+ if (req.fn_name === canonicalName) {
277
+ cb(null, {
278
+ found: true,
279
+ canonical_name: canonicalName,
280
+ endpoints: {
281
+ endpoints: endpoints.map((e) => ({
282
+ ...e,
283
+ transport: "tls",
284
+ weight: 1,
285
+ })),
286
+ },
287
+ });
288
+ } else {
289
+ prev(req, cb);
290
+ }
291
+ };
292
+ }
293
+
294
+ describe("servicebridge sdk", () => {
295
+ it("throws when function has no endpoints (LookupFunction returns not found)", async () => {
296
+ grpcState.reset();
297
+ const client = sb();
298
+ await flushMicrotasks();
299
+
300
+ // lookupImpl returns { found: false } by default
301
+ await expect(client.rpc("svc/echo", { value: 1 })).rejects.toThrow(
302
+ "No endpoints available for RPC: svc/echo",
303
+ );
304
+ client.stop();
305
+ });
306
+
307
+ it("rpc after stop() throws no endpoints error", async () => {
308
+ grpcState.reset();
309
+ const client = sb();
310
+ await flushMicrotasks();
311
+ client.stop();
312
+
313
+ await expect(client.rpc("svc/echo", { value: 1 })).rejects.toThrow(
314
+ "No endpoints available for RPC: svc/echo",
315
+ );
316
+ });
317
+
318
+ it("publishes events with trace context from AsyncLocalStorage", async () => {
319
+ grpcState.reset();
320
+ const client = sb();
321
+ // Wait for _controlReady.then(() => { isOnline = true }) to fire.
322
+ await flushMicrotasks();
323
+
324
+ const messageId = await sdk.runWithTraceContext(
325
+ { traceId: "trace-1", spanId: "span-1" },
326
+ () =>
327
+ client.event(
328
+ "order.created",
329
+ { value: 1 },
330
+ {
331
+ headers: { source: "test" },
332
+ idempotencyKey: "idem-1",
333
+ },
334
+ ),
335
+ );
336
+
337
+ expect(messageId).toBe("msg-1");
338
+ expect(grpcState.publishCalls).toHaveLength(1);
339
+ expect(grpcState.publishCalls[0]).toMatchObject({
340
+ topic: "order.created",
341
+ trace_id: "trace-1",
342
+ parent_span_id: "span-1",
343
+ producer_service: "caller",
344
+ idempotency_key: "idem-1",
345
+ headers: { source: "test" },
346
+ });
347
+ client.stop();
348
+ });
349
+
350
+ it("queues offline events and flushes them once online", async () => {
351
+ grpcState.reset();
352
+ const client = sb();
353
+
354
+ // Call event() synchronously — isOnline is still false (microtask from
355
+ // _controlReady.then() has not run yet). Do NOT await so no microtasks fire.
356
+ const offlineResultPromise = client.event("offline.topic", { value: 1 });
357
+ expect(grpcState.publishCalls).toHaveLength(0);
358
+
359
+ // Now await the promise. This yields, lets _controlReady.then() run first
360
+ // (M1 before M2 in the microtask queue), which sets isOnline=true and
361
+ // flushes the offline queue before the test continuation resumes.
362
+ const offlineResult = await offlineResultPromise;
363
+ expect(offlineResult).toBe("");
364
+ expect(grpcState.publishCalls).toHaveLength(1);
365
+ expect(grpcState.publishCalls[0]).toMatchObject({ topic: "offline.topic" });
366
+ client.stop();
367
+ });
368
+
369
+ it("rejects ambiguous rpc aliases", async () => {
370
+ grpcState.reset();
371
+ const client = sb();
372
+ await flushMicrotasks();
373
+
374
+ // Seed both canonical names into the local registry via warm-up rpc calls.
375
+ configureLookup("svc-a/echo", [{ endpoint: "127.0.0.1:5001" }]);
376
+ configureLookup("svc-b/echo", [{ endpoint: "127.0.0.1:5002" }]);
377
+
378
+ await client.rpc("svc-a/echo", { value: 1 });
379
+ await client.rpc("svc-b/echo", { value: 1 });
380
+
381
+ // Both canonical names share short name "echo" → alias map marks it ambiguous.
382
+ await expect(client.rpc("echo", { value: 1 })).rejects.toThrow(
383
+ 'RPC target "echo" is ambiguous; use canonical service/fn',
384
+ );
385
+ client.stop();
386
+ });
387
+
388
+ it("retries rpc on connection error and succeeds on the next attempt", async () => {
389
+ grpcState.reset();
390
+
391
+ let handleCallCount = 0;
392
+ grpcState.workerHandleImpl = (
393
+ _req: unknown,
394
+ _meta: unknown,
395
+ _opts: unknown,
396
+ cb: WorkerCallback,
397
+ ) => {
398
+ handleCallCount++;
399
+ if (handleCallCount === 1) {
400
+ cb({ code: 14, message: "unavailable" });
401
+ return;
402
+ }
403
+ cb(null, {
404
+ success: true,
405
+ output: Buffer.from(JSON.stringify({ ok: true })),
406
+ });
407
+ };
408
+
409
+ configureLookup("svc/echo", [
410
+ { endpoint: "127.0.0.1:5001" },
411
+ { endpoint: "127.0.0.1:5002" },
412
+ ]);
413
+
414
+ const client = sb({ retries: 1, retryDelay: 1 });
415
+ await flushMicrotasks();
416
+
417
+ const result = await client.rpc("svc/echo", { value: 1 });
418
+ expect(result).toEqual({ ok: true });
419
+ expect(grpcState.workerCalls).toHaveLength(2);
420
+ client.stop();
421
+ });
422
+ });