coakka-v2-connector-node 1.3.1

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 (57) hide show
  1. package/CONSUMING.md +48 -0
  2. package/LICENSE.md +115 -0
  3. package/README.md +147 -0
  4. package/dist/index.d.ts +6 -0
  5. package/dist/index.js +5 -0
  6. package/dist/models.d.ts +119 -0
  7. package/dist/models.js +150 -0
  8. package/dist/orchestrator.d.ts +80 -0
  9. package/dist/orchestrator.js +260 -0
  10. package/dist/packaging.d.ts +4 -0
  11. package/dist/packaging.js +4 -0
  12. package/dist/protobuf.d.ts +63 -0
  13. package/dist/protobuf.js +323 -0
  14. package/dist/runtime-client.d.ts +124 -0
  15. package/dist/runtime-client.js +922 -0
  16. package/dist/runtime-ffi.d.ts +105 -0
  17. package/dist/runtime-ffi.js +282 -0
  18. package/native/.gitkeep +1 -0
  19. package/native/linux-aarch64/libcoakka_runtime_v2-1.3.1+bda2ef5.so +0 -0
  20. package/native/linux-aarch64/libcoakka_runtime_v2.so +0 -0
  21. package/native/linux-x86_64/libcoakka_runtime_v2-1.3.1+bda2ef5.so +0 -0
  22. package/native/linux-x86_64/libcoakka_runtime_v2.so +0 -0
  23. package/native/macos-aarch64/libcoakka_runtime_v2-1.3.1+bda2ef5.dylib +0 -0
  24. package/native/macos-aarch64/libcoakka_runtime_v2.dylib +0 -0
  25. package/native/windows-aarch64/libcoakka_runtime_v2-1.3.1+bda2ef5.dll +0 -0
  26. package/native/windows-aarch64/libcoakka_runtime_v2.dll +0 -0
  27. package/native/windows-x86_64/libcoakka_runtime_v2-1.3.1+bda2ef5.dll +0 -0
  28. package/native/windows-x86_64/libcoakka_runtime_v2.dll +0 -0
  29. package/package.json +35 -0
  30. package/vendor/koffi/LICENSE.txt +22 -0
  31. package/vendor/koffi/build/koffi/darwin_arm64/koffi.node +0 -0
  32. package/vendor/koffi/build/koffi/darwin_x64/koffi.node +0 -0
  33. package/vendor/koffi/build/koffi/freebsd_arm64/koffi.node +0 -0
  34. package/vendor/koffi/build/koffi/freebsd_ia32/koffi.node +0 -0
  35. package/vendor/koffi/build/koffi/freebsd_x64/koffi.node +0 -0
  36. package/vendor/koffi/build/koffi/linux_arm64/koffi.node +0 -0
  37. package/vendor/koffi/build/koffi/linux_armhf/koffi.node +0 -0
  38. package/vendor/koffi/build/koffi/linux_ia32/koffi.node +0 -0
  39. package/vendor/koffi/build/koffi/linux_loong64/koffi.node +0 -0
  40. package/vendor/koffi/build/koffi/linux_riscv64d/koffi.node +0 -0
  41. package/vendor/koffi/build/koffi/linux_x64/koffi.node +0 -0
  42. package/vendor/koffi/build/koffi/musl_arm64/koffi.node +0 -0
  43. package/vendor/koffi/build/koffi/musl_x64/koffi.node +0 -0
  44. package/vendor/koffi/build/koffi/openbsd_ia32/koffi.node +0 -0
  45. package/vendor/koffi/build/koffi/openbsd_x64/koffi.node +0 -0
  46. package/vendor/koffi/build/koffi/win32_arm64/koffi.exp +0 -0
  47. package/vendor/koffi/build/koffi/win32_arm64/koffi.lib +0 -0
  48. package/vendor/koffi/build/koffi/win32_arm64/koffi.node +0 -0
  49. package/vendor/koffi/build/koffi/win32_ia32/koffi.exp +0 -0
  50. package/vendor/koffi/build/koffi/win32_ia32/koffi.lib +0 -0
  51. package/vendor/koffi/build/koffi/win32_ia32/koffi.node +0 -0
  52. package/vendor/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  53. package/vendor/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  54. package/vendor/koffi/build/koffi/win32_x64/koffi.node +0 -0
  55. package/vendor/koffi/index.d.ts +288 -0
  56. package/vendor/koffi/index.js +634 -0
  57. package/vendor/koffi/package.json +38 -0
@@ -0,0 +1,922 @@
1
+ import { closeSync, readSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { BusinessStatus, DeliveryHint, MessageKind, PayloadFormat, PayloadIdentity, RequestTerminalEventKind, RouteStrategy, requireValidConnectorConfig, requireValidRuntimeOverloadPolicy, } from "./models.js";
4
+ import { decodeDeadletter, decodeEnvelope, encodeControlEnvelope, encodeEnvelope, encodeRouteSnapshotPayload, } from "./protobuf.js";
5
+ import { COAKKA_ABI_VERSION, HOST_HANDLES_ENABLE_MONITOR, HOST_HANDLES_SEPARATE_DELIVERED_REQUEST_LANE, NativeRuntimeBindings, } from "./runtime-ffi.js";
6
+ export class DeadletterError extends Error {
7
+ deadletter;
8
+ constructor(deadletter) {
9
+ super(`deadletter reason=${deadletter.reason} detail=${deadletter.detail}`);
10
+ this.deadletter = deadletter;
11
+ }
12
+ }
13
+ class BufferedAsyncStream {
14
+ bufferCapacity;
15
+ onClose;
16
+ queue = [];
17
+ waiters = [];
18
+ closed = false;
19
+ constructor(bufferCapacity, onClose) {
20
+ this.bufferCapacity = bufferCapacity;
21
+ this.onClose = onClose;
22
+ }
23
+ [Symbol.asyncIterator]() {
24
+ return this;
25
+ }
26
+ async next() {
27
+ if (this.queue.length > 0) {
28
+ return { value: this.queue.shift(), done: false };
29
+ }
30
+ if (this.closed) {
31
+ return { value: undefined, done: true };
32
+ }
33
+ return await new Promise((resolve) => {
34
+ this.waiters.push(resolve);
35
+ });
36
+ }
37
+ async return() {
38
+ this.close();
39
+ return { value: undefined, done: true };
40
+ }
41
+ push(event) {
42
+ if (this.closed) {
43
+ return false;
44
+ }
45
+ const waiter = this.waiters.shift();
46
+ if (waiter != null) {
47
+ waiter({ value: event, done: false });
48
+ return true;
49
+ }
50
+ if (this.queue.length >= this.bufferCapacity) {
51
+ return false;
52
+ }
53
+ this.queue.push(event);
54
+ return true;
55
+ }
56
+ close() {
57
+ if (this.closed) {
58
+ return;
59
+ }
60
+ this.closed = true;
61
+ this.onClose();
62
+ while (this.waiters.length > 0) {
63
+ const waiter = this.waiters.shift();
64
+ waiter?.({ value: undefined, done: true });
65
+ }
66
+ this.queue.length = 0;
67
+ }
68
+ }
69
+ class FrameReader {
70
+ fd;
71
+ onFrame;
72
+ onError;
73
+ buffer = Buffer.alloc(0);
74
+ closed = false;
75
+ timer = null;
76
+ constructor(fd, onFrame, onError) {
77
+ this.fd = fd;
78
+ this.onFrame = onFrame;
79
+ this.onError = onError;
80
+ this.schedulePoll();
81
+ }
82
+ close() {
83
+ this.closed = true;
84
+ if (this.timer != null) {
85
+ clearTimeout(this.timer);
86
+ this.timer = null;
87
+ }
88
+ }
89
+ handleChunk(chunk) {
90
+ this.buffer = this.buffer.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.buffer, chunk]);
91
+ while (this.buffer.length >= 8) {
92
+ const payloadLength = Number(this.buffer.readBigUInt64LE(0));
93
+ if (!Number.isSafeInteger(payloadLength)) {
94
+ this.onError(new Error(`frame length too large=${payloadLength}`));
95
+ return;
96
+ }
97
+ if (this.buffer.length < 8 + payloadLength) {
98
+ return;
99
+ }
100
+ const frame = Buffer.from(this.buffer.subarray(8, 8 + payloadLength));
101
+ this.buffer = this.buffer.subarray(8 + payloadLength);
102
+ this.onFrame(frame);
103
+ }
104
+ }
105
+ schedulePoll() {
106
+ this.timer = setTimeout(() => this.poll(), 5);
107
+ }
108
+ poll() {
109
+ if (this.closed) {
110
+ return;
111
+ }
112
+ try {
113
+ while (!this.closed) {
114
+ const chunk = Buffer.alloc(64 * 1024);
115
+ const bytesRead = readSync(this.fd, chunk, 0, chunk.length, null);
116
+ if (bytesRead === 0) {
117
+ break;
118
+ }
119
+ this.handleChunk(chunk.subarray(0, bytesRead));
120
+ if (bytesRead < chunk.length) {
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ catch (error) {
126
+ if (isRetryableReadError(error)) {
127
+ // ignore EAGAIN/EWOULDBLOCK on nonblocking runtime pipes
128
+ }
129
+ else if (!this.closed) {
130
+ this.onError(asError(error));
131
+ }
132
+ }
133
+ finally {
134
+ if (!this.closed) {
135
+ this.schedulePoll();
136
+ }
137
+ }
138
+ }
139
+ }
140
+ class SignalReader {
141
+ fd;
142
+ onError;
143
+ buffer = Buffer.alloc(0);
144
+ queue = [];
145
+ waiters = new Set();
146
+ closed = false;
147
+ timer = null;
148
+ constructor(fd, onError) {
149
+ this.fd = fd;
150
+ this.onError = onError;
151
+ this.schedulePoll();
152
+ }
153
+ async next(timeoutMs) {
154
+ if (this.queue.length > 0) {
155
+ return this.queue.shift() ?? null;
156
+ }
157
+ return await new Promise((resolve) => {
158
+ const waiter = (value) => {
159
+ clearTimeout(timer);
160
+ this.waiters.delete(waiter);
161
+ resolve(value);
162
+ };
163
+ const timer = setTimeout(() => {
164
+ this.waiters.delete(waiter);
165
+ resolve(null);
166
+ }, timeoutMs);
167
+ this.waiters.add(waiter);
168
+ });
169
+ }
170
+ close() {
171
+ this.closed = true;
172
+ if (this.timer != null) {
173
+ clearTimeout(this.timer);
174
+ this.timer = null;
175
+ }
176
+ for (const waiter of this.waiters) {
177
+ waiter(null);
178
+ }
179
+ this.waiters.clear();
180
+ }
181
+ handleChunk(chunk) {
182
+ this.buffer = this.buffer.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.buffer, chunk]);
183
+ while (this.buffer.length >= 8) {
184
+ const signalCount = Number(this.buffer.readBigUInt64LE(0));
185
+ this.buffer = this.buffer.subarray(8);
186
+ const waiter = this.waiters.values().next().value;
187
+ if (waiter != null) {
188
+ waiter(signalCount);
189
+ }
190
+ else {
191
+ this.queue.push(signalCount);
192
+ }
193
+ }
194
+ }
195
+ schedulePoll() {
196
+ this.timer = setTimeout(() => this.poll(), 5);
197
+ }
198
+ poll() {
199
+ if (this.closed) {
200
+ return;
201
+ }
202
+ try {
203
+ while (!this.closed) {
204
+ const chunk = Buffer.alloc(8);
205
+ const bytesRead = readSync(this.fd, chunk, 0, chunk.length, null);
206
+ if (bytesRead === 0) {
207
+ break;
208
+ }
209
+ this.handleChunk(chunk.subarray(0, bytesRead));
210
+ if (bytesRead < chunk.length) {
211
+ break;
212
+ }
213
+ }
214
+ }
215
+ catch (error) {
216
+ if (isRetryableReadError(error)) {
217
+ // ignore EAGAIN/EWOULDBLOCK on nonblocking monitor pipes
218
+ }
219
+ else if (!this.closed) {
220
+ this.onError(asError(error));
221
+ }
222
+ }
223
+ finally {
224
+ if (!this.closed) {
225
+ this.schedulePoll();
226
+ }
227
+ }
228
+ }
229
+ }
230
+ export class NodeRuntimeClient {
231
+ config;
232
+ bindings;
233
+ runtime;
234
+ hostHandles;
235
+ pending = new Map();
236
+ trackedRequestsByMessageId = new Map();
237
+ trackedRequestMessageIdsByCorrelationId = new Map();
238
+ requestHandlers = new Map();
239
+ terminalEventSubscribers = new Map();
240
+ deadletterSubscribers = new Map();
241
+ requestReader;
242
+ responseReader;
243
+ deadletterReader;
244
+ monitorReader;
245
+ separateDeliveredRequestLane;
246
+ deliveredRequests = 0;
247
+ matchedResponses = 0;
248
+ matchedDeadletters = 0;
249
+ lateResponses = 0;
250
+ unhandledDeadletters = 0;
251
+ nextTerminalSubscriberId = 1;
252
+ nextDeadletterSubscriberId = 1;
253
+ terminalEventDropCount = 0;
254
+ deadletterObservationDropCount = 0;
255
+ closed = false;
256
+ constructor(runtimeLibPath, config) {
257
+ this.config = requireValidConnectorConfig(config);
258
+ this.bindings = new NativeRuntimeBindings(runtimeLibPath);
259
+ if (this.bindings.getAbiVersion() !== COAKKA_ABI_VERSION) {
260
+ throw new Error(`unexpected ABI version: ${this.bindings.getAbiVersion()}`);
261
+ }
262
+ this.runtime = this.bindings.createRuntime({
263
+ systemName: this.config.systemName,
264
+ nodeId: this.config.nodeId,
265
+ strictNoDrop: this.config.strictNoDrop ?? true,
266
+ queueCapacity: this.config.queueCapacity ?? 128,
267
+ });
268
+ this.hostHandles = this.bindings.getHostHandles(this.runtime, this.hostHandleFlags());
269
+ this.bindings.applyControlEnvelope(this.runtime, this.buildControlEnvelopeBytes());
270
+ this.bindings.startRuntime(this.runtime);
271
+ const deliveredRequestFd = this.hostHandles.deliveredRequestReadFd;
272
+ const responseFd = this.hostHandles.responseReadFd;
273
+ this.separateDeliveredRequestLane =
274
+ deliveredRequestFd >= 0 && deliveredRequestFd !== responseFd;
275
+ const requestReadFd = deliveredRequestFd >= 0 ? deliveredRequestFd : responseFd;
276
+ this.requestReader = new FrameReader(requestReadFd, (frame) => this.onRequestFrame(frame), (error) => this.onReaderError("request", error));
277
+ this.responseReader = this.separateDeliveredRequestLane
278
+ ? new FrameReader(responseFd, (frame) => this.onResponseFrame(frame), (error) => this.onReaderError("response", error))
279
+ : null;
280
+ this.deadletterReader = new FrameReader(this.hostHandles.deadletterReadFd, (frame) => this.onDeadletterFrame(frame), (error) => this.onReaderError("deadletter", error));
281
+ this.monitorReader = this.hostHandles.monitorReadFd >= 0
282
+ ? new SignalReader(this.hostHandles.monitorReadFd, (error) => this.onReaderError("monitor", error))
283
+ : null;
284
+ }
285
+ static nextMessageId(source) {
286
+ return `${source}-${randomUUID()}`;
287
+ }
288
+ static makeTypedRequest(source, target, payload, payloadIdentity, timeoutMs, operation, deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}, messageId = NodeRuntimeClient.nextMessageId(source), oneWay = false) {
289
+ const identity = payloadIdentity.requireTyped("typed request");
290
+ return {
291
+ message_id: messageId,
292
+ correlation_id: oneWay ? "" : messageId,
293
+ source,
294
+ target,
295
+ reply_to: oneWay ? "" : `${source}/replies`,
296
+ kind: MessageKind.REQUEST,
297
+ one_way: oneWay,
298
+ timeout_ms: timeoutMs,
299
+ payload,
300
+ headers: {
301
+ method: "POST",
302
+ operation,
303
+ ...headers,
304
+ },
305
+ status: BusinessStatus.OK,
306
+ error_code: "",
307
+ error_message: "",
308
+ delivery_hint: deliveryHint,
309
+ message_type: identity.messageType,
310
+ payload_schema_version: identity.payloadSchemaVersion,
311
+ payload_format: identity.payloadFormat,
312
+ };
313
+ }
314
+ static makeJsonRequest(source, target, payload, payloadIdentity, timeoutMs, operation, deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}, messageId = NodeRuntimeClient.nextMessageId(source), oneWay = false) {
315
+ return NodeRuntimeClient.makeTypedRequest(source, target, Buffer.from(JSON.stringify(payload)), payloadIdentity, timeoutMs, operation, deliveryHint, headers, messageId, oneWay);
316
+ }
317
+ static makeTypedReply(request, source, payload, payloadIdentity) {
318
+ const identity = payloadIdentity.requireTyped("typed reply");
319
+ const correlationId = request.correlation_id !== "" ? request.correlation_id : request.message_id;
320
+ return {
321
+ message_id: `${request.message_id}.reply.${source}`,
322
+ correlation_id: correlationId,
323
+ source,
324
+ target: request.source,
325
+ reply_to: "",
326
+ kind: MessageKind.RESPONSE,
327
+ one_way: false,
328
+ timeout_ms: request.timeout_ms,
329
+ payload,
330
+ headers: {},
331
+ status: BusinessStatus.OK,
332
+ error_code: "",
333
+ error_message: "",
334
+ delivery_hint: DeliveryHint.ROUTER_DEFAULT,
335
+ message_type: identity.messageType,
336
+ payload_schema_version: identity.payloadSchemaVersion,
337
+ payload_format: identity.payloadFormat,
338
+ };
339
+ }
340
+ static makeJsonReply(request, source, payload, payloadIdentity) {
341
+ return NodeRuntimeClient.makeTypedReply(request, source, Buffer.from(JSON.stringify(payload)), payloadIdentity);
342
+ }
343
+ static makeReplyFromRequestIdentity(request, source, payload) {
344
+ NodeRuntimeClient.requireTypedPayloadIdentity(request, "request");
345
+ return NodeRuntimeClient.makeTypedReply(request, source, payload, new PayloadIdentity(request.message_type, request.payload_schema_version, request.payload_format));
346
+ }
347
+ static makeJsonReplyFromRequestIdentity(request, source, payload) {
348
+ return NodeRuntimeClient.makeReplyFromRequestIdentity(request, source, Buffer.from(JSON.stringify(payload)));
349
+ }
350
+ static makeRawReply(request, source, payload) {
351
+ const correlationId = request.correlation_id !== "" ? request.correlation_id : request.message_id;
352
+ return {
353
+ message_id: `${request.message_id}.reply.${source}`,
354
+ correlation_id: correlationId,
355
+ source,
356
+ target: request.source,
357
+ reply_to: "",
358
+ kind: MessageKind.RESPONSE,
359
+ one_way: false,
360
+ timeout_ms: request.timeout_ms,
361
+ payload,
362
+ headers: {},
363
+ status: BusinessStatus.OK,
364
+ error_code: "",
365
+ error_message: "",
366
+ delivery_hint: DeliveryHint.ROUTER_DEFAULT,
367
+ message_type: "",
368
+ payload_schema_version: 0,
369
+ payload_format: PayloadFormat.UNSPECIFIED,
370
+ };
371
+ }
372
+ static makeRawRequest(messageId, source, target, replyTo, payload, timeoutMs = 1_000, deliveryHint = DeliveryHint.ROUTER_DEFAULT) {
373
+ return {
374
+ message_id: messageId,
375
+ correlation_id: messageId,
376
+ source,
377
+ target,
378
+ reply_to: replyTo,
379
+ kind: MessageKind.REQUEST,
380
+ one_way: false,
381
+ timeout_ms: timeoutMs,
382
+ payload,
383
+ headers: {},
384
+ status: BusinessStatus.OK,
385
+ error_code: "",
386
+ error_message: "",
387
+ delivery_hint: deliveryHint,
388
+ message_type: "",
389
+ payload_schema_version: 0,
390
+ payload_format: PayloadFormat.UNSPECIFIED,
391
+ };
392
+ }
393
+ static makeRawOneWay(messageId, source, target, payload, timeoutMs = 1_000, deliveryHint = DeliveryHint.ROUTER_DEFAULT) {
394
+ return {
395
+ ...NodeRuntimeClient.makeRawRequest(messageId, source, target, "", payload, timeoutMs, deliveryHint),
396
+ correlation_id: "",
397
+ reply_to: "",
398
+ one_way: true,
399
+ };
400
+ }
401
+ static requireTypedPayloadIdentity(envelope, what) {
402
+ new PayloadIdentity(envelope.message_type, envelope.payload_schema_version, envelope.payload_format).requireTyped(what);
403
+ }
404
+ async askTyped(source, target, payload, payloadIdentity, timeoutMs = 5_000, operation = "ask", deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}) {
405
+ const envelope = NodeRuntimeClient.makeTypedRequest(source, target, payload, payloadIdentity, timeoutMs, operation, deliveryHint, headers, NodeRuntimeClient.nextMessageId(source), false);
406
+ return await this.submitPendingRequest(envelope, timeoutMs, (normalized) => this.submitTypedEnvelope(normalized));
407
+ }
408
+ async askJson(source, target, payload, payloadIdentity, timeoutMs = 5_000, operation = "ask", deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}) {
409
+ const response = await this.askTyped(source, target, Buffer.from(JSON.stringify(payload)), payloadIdentity, timeoutMs, operation, deliveryHint, headers);
410
+ return JSON.parse(Buffer.from(response.payload).toString("utf8"));
411
+ }
412
+ async askRaw(request, timeoutMs) {
413
+ if (request.kind !== MessageKind.REQUEST) {
414
+ throw new Error("askRaw requires MESSAGE_KIND_REQUEST");
415
+ }
416
+ if (request.one_way) {
417
+ throw new Error("askRaw requires oneWay=false");
418
+ }
419
+ if (request.message_id === "") {
420
+ throw new Error("askRaw requires messageId");
421
+ }
422
+ const normalized = {
423
+ ...request,
424
+ correlation_id: request.correlation_id !== "" ? request.correlation_id : request.message_id,
425
+ };
426
+ const effectiveTimeoutMs = timeoutMs ?? (normalized.timeout_ms > 0 ? normalized.timeout_ms : 1_000);
427
+ return await this.submitPendingRequest(normalized, effectiveTimeoutMs, (tracked) => this.submitRawEnvelope(tracked));
428
+ }
429
+ submitRequestTyped(source, target, payload, payloadIdentity, timeoutMs = 5_000, operation = "ask", deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}) {
430
+ const envelope = NodeRuntimeClient.makeTypedRequest(source, target, payload, payloadIdentity, timeoutMs, operation, deliveryHint, headers, NodeRuntimeClient.nextMessageId(source), false);
431
+ return this.submitTrackedRequest(envelope, (normalized) => this.submitTypedEnvelope(normalized));
432
+ }
433
+ submitRequestJson(source, target, payload, payloadIdentity, timeoutMs = 5_000, operation = "ask", deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}) {
434
+ return this.submitRequestTyped(source, target, Buffer.from(JSON.stringify(payload)), payloadIdentity, timeoutMs, operation, deliveryHint, headers);
435
+ }
436
+ submitRequestRaw(request) {
437
+ return this.submitTrackedRequest(request, (normalized) => this.submitRawEnvelope(normalized));
438
+ }
439
+ sendOneWayTyped(source, target, payload, payloadIdentity, deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}) {
440
+ const envelope = NodeRuntimeClient.makeTypedRequest(source, target, payload, payloadIdentity, 0, "one_way", deliveryHint, headers, NodeRuntimeClient.nextMessageId(source), true);
441
+ this.submitTypedEnvelope(envelope);
442
+ }
443
+ sendOneWayJson(source, target, payload, payloadIdentity, deliveryHint = DeliveryHint.ROUTER_DEFAULT, headers = {}) {
444
+ this.sendOneWayTyped(source, target, Buffer.from(JSON.stringify(payload)), payloadIdentity, deliveryHint, headers);
445
+ }
446
+ submitRawEnvelope(envelope) {
447
+ this.bindings.submitEnvelope(this.runtime, encodeEnvelope(envelope));
448
+ }
449
+ submitTypedEnvelope(envelope) {
450
+ NodeRuntimeClient.requireTypedPayloadIdentity(envelope, "submitTypedEnvelope");
451
+ this.submitRawEnvelope(envelope);
452
+ }
453
+ submitEnvelope(envelope) {
454
+ this.submitRawEnvelope(envelope);
455
+ }
456
+ applyControlEnvelopeBytes(bytes) {
457
+ this.bindings.applyControlEnvelope(this.runtime, bytes);
458
+ }
459
+ applySnapshot(generation, routes, sourceConnector = this.config.systemName, seq = 1, overloadPolicy = this.config.overloadPolicy) {
460
+ const bytes = this.buildControlEnvelopeBytes(generation, routes, sourceConnector, seq, overloadPolicy);
461
+ this.applyControlEnvelopeBytes(bytes);
462
+ }
463
+ registerHandler(target, handler, typedReplies = true) {
464
+ if (this.requestHandlers.has(target)) {
465
+ throw new Error(`handler already registered for target=${target}`);
466
+ }
467
+ this.requestHandlers.set(target, { handler, typedReplies });
468
+ }
469
+ registerRawHandler(target, handler) {
470
+ this.registerHandler(target, handler, false);
471
+ }
472
+ terminalEvents(options = {}) {
473
+ const bufferCapacity = options.bufferCapacity ?? 16;
474
+ if (bufferCapacity < 1) {
475
+ throw new Error("terminalEvents requires bufferCapacity >= 1");
476
+ }
477
+ const subscriberId = this.nextTerminalSubscriberId++;
478
+ const stream = new BufferedAsyncStream(bufferCapacity, () => {
479
+ const active = this.terminalEventSubscribers.get(subscriberId);
480
+ if (active === stream) {
481
+ this.terminalEventSubscribers.delete(subscriberId);
482
+ }
483
+ if (options.signal != null) {
484
+ options.signal.removeEventListener("abort", closeStream);
485
+ }
486
+ });
487
+ const closeStream = () => {
488
+ stream.close();
489
+ };
490
+ this.terminalEventSubscribers.set(subscriberId, stream);
491
+ if (options.signal?.aborted) {
492
+ stream.close();
493
+ return stream;
494
+ }
495
+ options.signal?.addEventListener("abort", closeStream, { once: true });
496
+ return stream;
497
+ }
498
+ deadletters(options = {}) {
499
+ const bufferCapacity = options.bufferCapacity ?? 128;
500
+ if (bufferCapacity < 1) {
501
+ throw new Error("deadletters requires bufferCapacity >= 1");
502
+ }
503
+ const subscriberId = this.nextDeadletterSubscriberId++;
504
+ const stream = new BufferedAsyncStream(bufferCapacity, () => {
505
+ const active = this.deadletterSubscribers.get(subscriberId);
506
+ if (active === stream) {
507
+ this.deadletterSubscribers.delete(subscriberId);
508
+ }
509
+ if (options.signal != null) {
510
+ options.signal.removeEventListener("abort", closeStream);
511
+ }
512
+ });
513
+ const closeStream = () => {
514
+ stream.close();
515
+ };
516
+ this.deadletterSubscribers.set(subscriberId, stream);
517
+ if (options.signal?.aborted) {
518
+ stream.close();
519
+ return stream;
520
+ }
521
+ options.signal?.addEventListener("abort", closeStream, { once: true });
522
+ return stream;
523
+ }
524
+ snapshotStats() {
525
+ return {
526
+ pendingRequests: this.pending.size,
527
+ deliveredRequests: this.deliveredRequests,
528
+ matchedResponses: this.matchedResponses,
529
+ matchedDeadletters: this.matchedDeadletters,
530
+ lateResponses: this.lateResponses,
531
+ unhandledDeadletters: this.unhandledDeadletters,
532
+ terminalEventDropCount: this.terminalEventDropCount,
533
+ deadletterObservationDropCount: this.deadletterObservationDropCount,
534
+ };
535
+ }
536
+ healthSnapshot() {
537
+ return this.bindings.getHealth(this.runtime);
538
+ }
539
+ statsSnapshot() {
540
+ return this.bindings.getStats(this.runtime);
541
+ }
542
+ runtimeInfoSnapshot() {
543
+ return this.bindings.readRuntimeInfo();
544
+ }
545
+ runtimeConfigSnapshot() {
546
+ return this.bindings.getConfig(this.runtime);
547
+ }
548
+ monitorIsEnabled() {
549
+ return this.monitorReader != null;
550
+ }
551
+ monitorSnapshot(signalCount = 0) {
552
+ return {
553
+ signalCount,
554
+ health: this.healthSnapshot(),
555
+ stats: this.statsSnapshot(),
556
+ };
557
+ }
558
+ async awaitNextMonitor(timeoutMs = 1_000) {
559
+ if (this.monitorReader == null) {
560
+ throw new Error("runtime monitor is not enabled");
561
+ }
562
+ const signalCount = await this.monitorReader.next(timeoutMs);
563
+ return signalCount == null ? null : this.monitorSnapshot(signalCount);
564
+ }
565
+ async awaitAppliedGenerationAtLeast(generation, timeoutMs = 1_000) {
566
+ const deadline = Date.now() + timeoutMs;
567
+ const current = this.monitorSnapshot();
568
+ if (current.health.appliedGeneration >= generation) {
569
+ return current;
570
+ }
571
+ while (Date.now() < deadline) {
572
+ const remainingMs = Math.max(1, deadline - Date.now());
573
+ const snapshot = await this.awaitNextMonitor(remainingMs);
574
+ if (snapshot == null) {
575
+ return null;
576
+ }
577
+ if (snapshot.health.appliedGeneration >= generation) {
578
+ return snapshot;
579
+ }
580
+ }
581
+ return null;
582
+ }
583
+ runtimeSnapshot() {
584
+ return {
585
+ runtimeInfo: this.runtimeInfoSnapshot(),
586
+ runtimeConfig: this.runtimeConfigSnapshot(),
587
+ connector: {
588
+ ...this.snapshotStats(),
589
+ separateDeliveredRequestLane: this.separateDeliveredRequestLane,
590
+ systemName: this.config.systemName,
591
+ nodeId: this.config.nodeId,
592
+ },
593
+ health: this.healthSnapshot(),
594
+ stats: this.statsSnapshot(),
595
+ };
596
+ }
597
+ close() {
598
+ if (this.closed) {
599
+ return;
600
+ }
601
+ this.closed = true;
602
+ for (const pending of this.pending.values()) {
603
+ clearTimeout(pending.timeout);
604
+ pending.reject(new Error("runtime client closed"));
605
+ }
606
+ this.pending.clear();
607
+ this.trackedRequestsByMessageId.clear();
608
+ this.trackedRequestMessageIdsByCorrelationId.clear();
609
+ for (const stream of this.terminalEventSubscribers.values()) {
610
+ stream.close();
611
+ }
612
+ this.terminalEventSubscribers.clear();
613
+ for (const stream of this.deadletterSubscribers.values()) {
614
+ stream.close();
615
+ }
616
+ this.deadletterSubscribers.clear();
617
+ this.requestReader.close();
618
+ this.responseReader?.close();
619
+ this.deadletterReader.close();
620
+ this.monitorReader?.close();
621
+ try {
622
+ this.bindings.stopRuntime(this.runtime);
623
+ }
624
+ finally {
625
+ this.bindings.destroyRuntime(this.runtime);
626
+ for (const fd of [
627
+ this.hostHandles.responseReadFd,
628
+ this.hostHandles.deadletterReadFd,
629
+ this.hostHandles.requestWriteFd,
630
+ this.hostHandles.controlWriteFd,
631
+ this.hostHandles.monitorReadFd,
632
+ this.hostHandles.deliveredRequestReadFd,
633
+ ]) {
634
+ safeCloseFd(fd);
635
+ }
636
+ }
637
+ }
638
+ hostHandleFlags() {
639
+ let flags = 0;
640
+ if (this.config.enableMonitor ?? true) {
641
+ flags |= HOST_HANDLES_ENABLE_MONITOR;
642
+ }
643
+ if (this.config.separateDeliveredRequestLane ?? true) {
644
+ flags |= HOST_HANDLES_SEPARATE_DELIVERED_REQUEST_LANE;
645
+ }
646
+ return flags;
647
+ }
648
+ buildControlEnvelopeBytes(generation = this.config.generation ?? 1, routes = this.config.routes, sourceConnector = this.config.systemName, seq = 1, overloadPolicy = this.config.overloadPolicy) {
649
+ if (overloadPolicy != null) {
650
+ requireValidRuntimeOverloadPolicy(overloadPolicy);
651
+ }
652
+ const snapshotPayload = encodeRouteSnapshotPayload({
653
+ generation,
654
+ overload_policy: overloadPolicy == null ? undefined : {
655
+ ingress_mode: overloadPolicy.ingressMode ?? 0,
656
+ local_delivery_mode: overloadPolicy.localDeliveryMode ?? 0,
657
+ remote_outbound_mode: overloadPolicy.remoteOutboundMode ?? 0,
658
+ remote_outbound_reply_reserve_slots: overloadPolicy.remoteOutboundReplyReserveSlots ?? 0,
659
+ },
660
+ routes: routes.map((route) => ({
661
+ target: route.target,
662
+ strategy: route.strategy ?? RouteStrategy.SINGLE_OWNER,
663
+ route_key_hint: route.routeKeyHint ?? "",
664
+ flags: route.flags ?? 0,
665
+ endpoints: route.endpoints.map((endpoint) => ({
666
+ host: endpoint.host,
667
+ port: endpoint.port,
668
+ weight: endpoint.weight ?? 1,
669
+ flags: endpoint.flags ?? 0,
670
+ })),
671
+ })),
672
+ });
673
+ const envelope = {
674
+ seq,
675
+ generation,
676
+ kind: 1,
677
+ payload_format: 2,
678
+ payload_type: 1,
679
+ schema_version: 1,
680
+ payload: snapshotPayload,
681
+ metadata: {
682
+ source_connector: sourceConnector,
683
+ },
684
+ };
685
+ return encodeControlEnvelope(envelope);
686
+ }
687
+ async submitPendingRequest(request, timeoutMs, submit) {
688
+ const trackedRequest = this.trackRequest(request, "request");
689
+ return await new Promise((resolve, reject) => {
690
+ if (this.pending.has(trackedRequest.messageId)) {
691
+ this.forgetTrackedRequest(trackedRequest.messageId);
692
+ reject(new Error(`duplicate pending messageId=${trackedRequest.messageId}`));
693
+ return;
694
+ }
695
+ const timeout = setTimeout(() => {
696
+ this.pending.delete(trackedRequest.messageId);
697
+ this.forgetTrackedRequest(trackedRequest.messageId);
698
+ reject(new Error(`timed out waiting for ${trackedRequest.messageId}`));
699
+ }, timeoutMs);
700
+ this.pending.set(trackedRequest.messageId, { resolve, reject, timeout });
701
+ try {
702
+ submit(trackedRequest.request);
703
+ }
704
+ catch (error) {
705
+ clearTimeout(timeout);
706
+ this.pending.delete(trackedRequest.messageId);
707
+ this.forgetTrackedRequest(trackedRequest.messageId);
708
+ reject(error);
709
+ }
710
+ });
711
+ }
712
+ submitTrackedRequest(request, submit) {
713
+ const trackedRequest = this.trackRequest(request, "submitRequest");
714
+ try {
715
+ submit(trackedRequest.request);
716
+ return {
717
+ messageId: trackedRequest.messageId,
718
+ correlationId: trackedRequest.correlationId,
719
+ };
720
+ }
721
+ catch (error) {
722
+ this.forgetTrackedRequest(trackedRequest.messageId);
723
+ throw error;
724
+ }
725
+ }
726
+ onRequestFrame(frame) {
727
+ const envelope = decodeEnvelope(frame);
728
+ if (this.separateDeliveredRequestLane) {
729
+ if (envelope.kind !== MessageKind.REQUEST) {
730
+ throw new Error(`unexpected envelope kind=${envelope.kind} on delivered-request lane`);
731
+ }
732
+ this.dispatchRequest(envelope);
733
+ return;
734
+ }
735
+ if (envelope.kind === MessageKind.REQUEST) {
736
+ this.dispatchRequest(envelope);
737
+ }
738
+ else if (envelope.kind === MessageKind.RESPONSE) {
739
+ this.dispatchResponse(envelope);
740
+ }
741
+ else {
742
+ throw new Error(`unexpected envelope kind=${envelope.kind}`);
743
+ }
744
+ }
745
+ onResponseFrame(frame) {
746
+ const envelope = decodeEnvelope(frame);
747
+ if (envelope.kind === MessageKind.RESPONSE) {
748
+ this.dispatchResponse(envelope);
749
+ }
750
+ else if (envelope.kind === MessageKind.REQUEST) {
751
+ throw new Error("unexpected request on response-only lane");
752
+ }
753
+ else {
754
+ throw new Error(`unexpected envelope kind=${envelope.kind}`);
755
+ }
756
+ }
757
+ onDeadletterFrame(frame) {
758
+ this.dispatchDeadletter(decodeDeadletter(frame));
759
+ }
760
+ dispatchRequest(request) {
761
+ this.deliveredRequests += 1;
762
+ const registered = this.requestHandlers.get(request.target);
763
+ if (registered == null) {
764
+ return;
765
+ }
766
+ void Promise.resolve(registered.handler(request))
767
+ .then((reply) => {
768
+ if (!request.one_way && reply != null) {
769
+ if (registered.typedReplies) {
770
+ this.submitTypedEnvelope(reply);
771
+ }
772
+ else {
773
+ this.submitRawEnvelope(reply);
774
+ }
775
+ }
776
+ })
777
+ .catch((error) => {
778
+ console.error(`[coakka-node] request handler failed target=${request.target}`, error);
779
+ });
780
+ }
781
+ dispatchResponse(response) {
782
+ const trackedRequest = this.forgetTrackedRequestByCorrelationId(response.correlation_id);
783
+ if (trackedRequest == null) {
784
+ this.lateResponses += 1;
785
+ return;
786
+ }
787
+ this.publishTerminalEvent({
788
+ kind: RequestTerminalEventKind.RESPONSE,
789
+ requestMessageId: trackedRequest.messageId,
790
+ correlationId: trackedRequest.correlationId,
791
+ envelope: response,
792
+ });
793
+ const pending = this.pending.get(trackedRequest.messageId);
794
+ if (pending != null) {
795
+ clearTimeout(pending.timeout);
796
+ this.pending.delete(trackedRequest.messageId);
797
+ pending.resolve(response);
798
+ }
799
+ this.matchedResponses += 1;
800
+ }
801
+ dispatchDeadletter(deadletter) {
802
+ const trackedRequest = this.forgetTrackedRequest(deadletter.original_envelope.message_id);
803
+ this.publishDeadletterObservation(deadletter, trackedRequest);
804
+ if (trackedRequest == null) {
805
+ this.unhandledDeadletters += 1;
806
+ return;
807
+ }
808
+ this.publishTerminalEvent({
809
+ kind: RequestTerminalEventKind.DEADLETTER,
810
+ requestMessageId: trackedRequest.messageId,
811
+ correlationId: trackedRequest.correlationId,
812
+ deadletter,
813
+ });
814
+ const pending = this.pending.get(trackedRequest.messageId);
815
+ if (pending != null) {
816
+ clearTimeout(pending.timeout);
817
+ this.pending.delete(trackedRequest.messageId);
818
+ pending.reject(new DeadletterError(deadletter));
819
+ }
820
+ this.matchedDeadletters += 1;
821
+ }
822
+ publishDeadletterObservation(deadletter, trackedRequest) {
823
+ const observed = {
824
+ deadletter,
825
+ requestMessageId: trackedRequest?.messageId ?? (deadletter.original_envelope.message_id || null),
826
+ correlationId: trackedRequest?.correlationId ?? (deadletter.original_envelope.correlation_id || null),
827
+ matchedPendingRequest: trackedRequest != null,
828
+ };
829
+ for (const [subscriberId, stream] of this.deadletterSubscribers) {
830
+ if (stream.push(observed)) {
831
+ continue;
832
+ }
833
+ this.deadletterObservationDropCount += 1;
834
+ if (this.closed) {
835
+ this.deadletterSubscribers.delete(subscriberId);
836
+ }
837
+ }
838
+ }
839
+ onReaderError(lane, error) {
840
+ if (!this.closed) {
841
+ console.error(`[coakka-node] ${lane} reader failed`, error);
842
+ }
843
+ }
844
+ trackRequest(request, caller) {
845
+ if (request.kind !== MessageKind.REQUEST) {
846
+ throw new Error(`${caller} requires MESSAGE_KIND_REQUEST`);
847
+ }
848
+ if (request.one_way) {
849
+ throw new Error(`${caller} requires oneWay=false`);
850
+ }
851
+ if (request.message_id === "") {
852
+ throw new Error(`${caller} requires messageId`);
853
+ }
854
+ const normalized = {
855
+ ...request,
856
+ correlation_id: request.correlation_id !== "" ? request.correlation_id : request.message_id,
857
+ };
858
+ if (this.trackedRequestsByMessageId.has(normalized.message_id)) {
859
+ throw new Error(`duplicate tracked messageId=${normalized.message_id}`);
860
+ }
861
+ if (this.trackedRequestMessageIdsByCorrelationId.has(normalized.correlation_id)) {
862
+ throw new Error(`duplicate tracked correlationId=${normalized.correlation_id}`);
863
+ }
864
+ this.trackedRequestsByMessageId.set(normalized.message_id, {
865
+ messageId: normalized.message_id,
866
+ correlationId: normalized.correlation_id,
867
+ });
868
+ this.trackedRequestMessageIdsByCorrelationId.set(normalized.correlation_id, normalized.message_id);
869
+ return {
870
+ request: normalized,
871
+ messageId: normalized.message_id,
872
+ correlationId: normalized.correlation_id,
873
+ };
874
+ }
875
+ forgetTrackedRequest(messageId) {
876
+ const trackedRequest = this.trackedRequestsByMessageId.get(messageId) ?? null;
877
+ if (trackedRequest == null) {
878
+ return null;
879
+ }
880
+ this.trackedRequestsByMessageId.delete(messageId);
881
+ this.trackedRequestMessageIdsByCorrelationId.delete(trackedRequest.correlationId);
882
+ return trackedRequest;
883
+ }
884
+ forgetTrackedRequestByCorrelationId(correlationId) {
885
+ const messageId = this.trackedRequestMessageIdsByCorrelationId.get(correlationId);
886
+ if (messageId == null) {
887
+ return null;
888
+ }
889
+ this.trackedRequestMessageIdsByCorrelationId.delete(correlationId);
890
+ const trackedRequest = this.trackedRequestsByMessageId.get(messageId) ?? null;
891
+ if (trackedRequest != null) {
892
+ this.trackedRequestsByMessageId.delete(messageId);
893
+ }
894
+ return trackedRequest;
895
+ }
896
+ publishTerminalEvent(event) {
897
+ for (const stream of this.terminalEventSubscribers.values()) {
898
+ if (!stream.push(event)) {
899
+ this.terminalEventDropCount += 1;
900
+ }
901
+ }
902
+ }
903
+ }
904
+ function safeCloseFd(fd) {
905
+ if (fd >= 0) {
906
+ try {
907
+ closeSync(fd);
908
+ }
909
+ catch {
910
+ // ignore close races during shutdown
911
+ }
912
+ }
913
+ }
914
+ function isRetryableReadError(error) {
915
+ return (typeof error === "object" &&
916
+ error != null &&
917
+ "code" in error &&
918
+ (error.code === "EAGAIN" || error.code === "EWOULDBLOCK" || error.code === "EINTR"));
919
+ }
920
+ function asError(error) {
921
+ return error instanceof Error ? error : new Error(String(error));
922
+ }