@rstmdb/client 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/index.d.cts +888 -0
- package/dist/index.d.ts +888 -0
- package/dist/index.js +1332 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1289 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1289 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import * as net from 'net';
|
|
3
|
+
import * as tls from 'tls';
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
|
|
7
|
+
// src/protocol/crc32c.ts
|
|
8
|
+
var CRC32C_TABLE = new Uint32Array(256);
|
|
9
|
+
(function initTable() {
|
|
10
|
+
const POLYNOMIAL = 2197175160;
|
|
11
|
+
for (let i = 0; i < 256; i++) {
|
|
12
|
+
let crc = i;
|
|
13
|
+
for (let j = 0; j < 8; j++) {
|
|
14
|
+
if (crc & 1) {
|
|
15
|
+
crc = crc >>> 1 ^ POLYNOMIAL;
|
|
16
|
+
} else {
|
|
17
|
+
crc = crc >>> 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
CRC32C_TABLE[i] = crc >>> 0;
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
function crc32c(data) {
|
|
24
|
+
let crc = 4294967295;
|
|
25
|
+
for (let i = 0; i < data.length; i++) {
|
|
26
|
+
const byte = data[i];
|
|
27
|
+
const tableIndex = (crc ^ byte) & 255;
|
|
28
|
+
crc = crc >>> 8 ^ CRC32C_TABLE[tableIndex];
|
|
29
|
+
}
|
|
30
|
+
return (crc ^ 4294967295) >>> 0;
|
|
31
|
+
}
|
|
32
|
+
function verifyCrc32c(data, expected) {
|
|
33
|
+
return crc32c(data) === expected;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/errors/codes.ts
|
|
37
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
38
|
+
ErrorCode2["UNSUPPORTED_PROTOCOL"] = "UNSUPPORTED_PROTOCOL";
|
|
39
|
+
ErrorCode2["BAD_REQUEST"] = "BAD_REQUEST";
|
|
40
|
+
ErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
|
|
41
|
+
ErrorCode2["AUTH_FAILED"] = "AUTH_FAILED";
|
|
42
|
+
ErrorCode2["NOT_FOUND"] = "NOT_FOUND";
|
|
43
|
+
ErrorCode2["MACHINE_NOT_FOUND"] = "MACHINE_NOT_FOUND";
|
|
44
|
+
ErrorCode2["MACHINE_VERSION_EXISTS"] = "MACHINE_VERSION_EXISTS";
|
|
45
|
+
ErrorCode2["INSTANCE_NOT_FOUND"] = "INSTANCE_NOT_FOUND";
|
|
46
|
+
ErrorCode2["INSTANCE_EXISTS"] = "INSTANCE_EXISTS";
|
|
47
|
+
ErrorCode2["INVALID_TRANSITION"] = "INVALID_TRANSITION";
|
|
48
|
+
ErrorCode2["GUARD_FAILED"] = "GUARD_FAILED";
|
|
49
|
+
ErrorCode2["CONFLICT"] = "CONFLICT";
|
|
50
|
+
ErrorCode2["WAL_IO_ERROR"] = "WAL_IO_ERROR";
|
|
51
|
+
ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
52
|
+
ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
|
|
53
|
+
ErrorCode2["CONNECTION_FAILED"] = "CONNECTION_FAILED";
|
|
54
|
+
ErrorCode2["CONNECTION_CLOSED"] = "CONNECTION_CLOSED";
|
|
55
|
+
ErrorCode2["TIMEOUT"] = "TIMEOUT";
|
|
56
|
+
return ErrorCode2;
|
|
57
|
+
})(ErrorCode || {});
|
|
58
|
+
function isRetryableCode(code) {
|
|
59
|
+
switch (code) {
|
|
60
|
+
case "CONNECTION_FAILED" /* CONNECTION_FAILED */:
|
|
61
|
+
case "CONNECTION_CLOSED" /* CONNECTION_CLOSED */:
|
|
62
|
+
case "TIMEOUT" /* TIMEOUT */:
|
|
63
|
+
case "RATE_LIMITED" /* RATE_LIMITED */:
|
|
64
|
+
case "WAL_IO_ERROR" /* WAL_IO_ERROR */:
|
|
65
|
+
case "INTERNAL_ERROR" /* INTERNAL_ERROR */:
|
|
66
|
+
return true;
|
|
67
|
+
default:
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/errors/base.ts
|
|
73
|
+
var RstmdbError = class _RstmdbError extends Error {
|
|
74
|
+
/** Error code */
|
|
75
|
+
code;
|
|
76
|
+
/** Whether the operation can be retried */
|
|
77
|
+
retryable;
|
|
78
|
+
/** Additional error details */
|
|
79
|
+
details;
|
|
80
|
+
constructor(message, code, options) {
|
|
81
|
+
super(message, { cause: options?.cause });
|
|
82
|
+
this.name = "RstmdbError";
|
|
83
|
+
this.code = code;
|
|
84
|
+
this.retryable = options?.retryable ?? isRetryableCode(code);
|
|
85
|
+
this.details = options?.details;
|
|
86
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create an error from a server response.
|
|
90
|
+
*/
|
|
91
|
+
static fromResponse(response) {
|
|
92
|
+
const code = response.code || "INTERNAL_ERROR" /* INTERNAL_ERROR */;
|
|
93
|
+
return new _RstmdbError(response.message, code, {
|
|
94
|
+
details: response.details
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/errors/classes.ts
|
|
100
|
+
var ConnectionError = class extends RstmdbError {
|
|
101
|
+
constructor(message, options) {
|
|
102
|
+
super(message, "CONNECTION_FAILED" /* CONNECTION_FAILED */, {
|
|
103
|
+
retryable: true,
|
|
104
|
+
...options
|
|
105
|
+
});
|
|
106
|
+
this.name = "ConnectionError";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var TimeoutError = class extends RstmdbError {
|
|
110
|
+
constructor(message, options) {
|
|
111
|
+
super(message, "TIMEOUT" /* TIMEOUT */, {
|
|
112
|
+
retryable: true,
|
|
113
|
+
...options
|
|
114
|
+
});
|
|
115
|
+
this.name = "TimeoutError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var ProtocolError = class extends RstmdbError {
|
|
119
|
+
constructor(message, code = "BAD_REQUEST" /* BAD_REQUEST */, options) {
|
|
120
|
+
super(message, code, {
|
|
121
|
+
retryable: false,
|
|
122
|
+
...options
|
|
123
|
+
});
|
|
124
|
+
this.name = "ProtocolError";
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var ServerError = class _ServerError extends RstmdbError {
|
|
128
|
+
constructor(message, code, options) {
|
|
129
|
+
super(message, code, options);
|
|
130
|
+
this.name = "ServerError";
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create a ServerError from a server response.
|
|
134
|
+
*/
|
|
135
|
+
static fromResponse(response) {
|
|
136
|
+
const code = response.code || "INTERNAL_ERROR" /* INTERNAL_ERROR */;
|
|
137
|
+
const message = response.message || "Unknown server error";
|
|
138
|
+
switch (code) {
|
|
139
|
+
case "NOT_FOUND" /* NOT_FOUND */:
|
|
140
|
+
case "MACHINE_NOT_FOUND" /* MACHINE_NOT_FOUND */:
|
|
141
|
+
case "INSTANCE_NOT_FOUND" /* INSTANCE_NOT_FOUND */:
|
|
142
|
+
return new NotFoundError(message, code, { details: response.details });
|
|
143
|
+
case "CONFLICT" /* CONFLICT */:
|
|
144
|
+
case "INSTANCE_EXISTS" /* INSTANCE_EXISTS */:
|
|
145
|
+
case "MACHINE_VERSION_EXISTS" /* MACHINE_VERSION_EXISTS */:
|
|
146
|
+
return new ConflictError(message, code, { details: response.details });
|
|
147
|
+
case "UNAUTHORIZED" /* UNAUTHORIZED */:
|
|
148
|
+
case "AUTH_FAILED" /* AUTH_FAILED */:
|
|
149
|
+
return new AuthenticationError(message, code, { details: response.details });
|
|
150
|
+
case "INVALID_TRANSITION" /* INVALID_TRANSITION */:
|
|
151
|
+
return new InvalidTransitionError(message, { details: response.details });
|
|
152
|
+
case "GUARD_FAILED" /* GUARD_FAILED */:
|
|
153
|
+
return new GuardFailedError(message, { details: response.details });
|
|
154
|
+
default:
|
|
155
|
+
return new _ServerError(message, code, { details: response.details });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var NotFoundError = class extends ServerError {
|
|
160
|
+
constructor(message, code = "NOT_FOUND" /* NOT_FOUND */, options) {
|
|
161
|
+
super(message, code, { retryable: false, ...options });
|
|
162
|
+
this.name = "NotFoundError";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var ConflictError = class extends ServerError {
|
|
166
|
+
constructor(message, code = "CONFLICT" /* CONFLICT */, options) {
|
|
167
|
+
super(message, code, { retryable: false, ...options });
|
|
168
|
+
this.name = "ConflictError";
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var AuthenticationError = class extends ServerError {
|
|
172
|
+
constructor(message, code = "UNAUTHORIZED" /* UNAUTHORIZED */, options) {
|
|
173
|
+
super(message, code, { retryable: false, ...options });
|
|
174
|
+
this.name = "AuthenticationError";
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var InvalidTransitionError = class extends ServerError {
|
|
178
|
+
constructor(message, options) {
|
|
179
|
+
super(message, "INVALID_TRANSITION" /* INVALID_TRANSITION */, { retryable: false, ...options });
|
|
180
|
+
this.name = "InvalidTransitionError";
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var GuardFailedError = class extends ServerError {
|
|
184
|
+
constructor(message, options) {
|
|
185
|
+
super(message, "GUARD_FAILED" /* GUARD_FAILED */, { retryable: false, ...options });
|
|
186
|
+
this.name = "GuardFailedError";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/protocol/frame.ts
|
|
191
|
+
var FRAME_MAGIC = 1380143192;
|
|
192
|
+
var PROTOCOL_VERSION = 1;
|
|
193
|
+
var HEADER_SIZE = 18;
|
|
194
|
+
var FrameFlags = /* @__PURE__ */ ((FrameFlags2) => {
|
|
195
|
+
FrameFlags2[FrameFlags2["NONE"] = 0] = "NONE";
|
|
196
|
+
FrameFlags2[FrameFlags2["CRC_PRESENT"] = 1] = "CRC_PRESENT";
|
|
197
|
+
FrameFlags2[FrameFlags2["COMPRESSED"] = 2] = "COMPRESSED";
|
|
198
|
+
FrameFlags2[FrameFlags2["STREAM"] = 4] = "STREAM";
|
|
199
|
+
FrameFlags2[FrameFlags2["END_STREAM"] = 8] = "END_STREAM";
|
|
200
|
+
return FrameFlags2;
|
|
201
|
+
})(FrameFlags || {});
|
|
202
|
+
var FrameEncoder = class {
|
|
203
|
+
useCrc;
|
|
204
|
+
constructor(options) {
|
|
205
|
+
this.useCrc = options?.useCrc ?? true;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Encode an object into an RCP frame.
|
|
209
|
+
*/
|
|
210
|
+
encode(payload) {
|
|
211
|
+
const payloadJson = JSON.stringify(payload);
|
|
212
|
+
const payloadBuffer = Buffer.from(payloadJson, "utf8");
|
|
213
|
+
const flags = this.useCrc ? 1 /* CRC_PRESENT */ : 0 /* NONE */;
|
|
214
|
+
const crcValue = this.useCrc ? crc32c(payloadBuffer) : 0;
|
|
215
|
+
return this.encodeRaw(payloadBuffer, flags, crcValue);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Encode a stream frame.
|
|
219
|
+
*/
|
|
220
|
+
encodeStream(payload, endStream = false) {
|
|
221
|
+
const payloadJson = JSON.stringify(payload);
|
|
222
|
+
const payloadBuffer = Buffer.from(payloadJson, "utf8");
|
|
223
|
+
let flags = 4 /* STREAM */;
|
|
224
|
+
if (this.useCrc) flags |= 1 /* CRC_PRESENT */;
|
|
225
|
+
if (endStream) flags |= 8 /* END_STREAM */;
|
|
226
|
+
const crcValue = this.useCrc ? crc32c(payloadBuffer) : 0;
|
|
227
|
+
return this.encodeRaw(payloadBuffer, flags, crcValue);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Encode raw payload with specified flags.
|
|
231
|
+
*/
|
|
232
|
+
encodeRaw(payload, flags, crcValue) {
|
|
233
|
+
const headerExtLen = 0;
|
|
234
|
+
const payloadLen = payload.length;
|
|
235
|
+
const frame = Buffer.allocUnsafe(HEADER_SIZE + headerExtLen + payloadLen);
|
|
236
|
+
frame.writeUInt32BE(FRAME_MAGIC, 0);
|
|
237
|
+
frame.writeUInt16BE(PROTOCOL_VERSION, 4);
|
|
238
|
+
frame.writeUInt16BE(flags, 6);
|
|
239
|
+
frame.writeUInt16BE(headerExtLen, 8);
|
|
240
|
+
frame.writeUInt32BE(payloadLen, 10);
|
|
241
|
+
frame.writeUInt32BE(crcValue, 14);
|
|
242
|
+
payload.copy(frame, HEADER_SIZE);
|
|
243
|
+
return frame;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
var FrameDecoder = class {
|
|
247
|
+
buffer = Buffer.alloc(0);
|
|
248
|
+
verifyCrc;
|
|
249
|
+
constructor(options) {
|
|
250
|
+
this.verifyCrc = options?.verifyCrc ?? true;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Append data to the internal buffer.
|
|
254
|
+
*/
|
|
255
|
+
append(data) {
|
|
256
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Try to decode a frame from the buffer.
|
|
260
|
+
* Returns null if not enough data is available.
|
|
261
|
+
*/
|
|
262
|
+
decode() {
|
|
263
|
+
if (this.buffer.length < HEADER_SIZE) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const magic = this.buffer.readUInt32BE(0);
|
|
267
|
+
if (magic !== FRAME_MAGIC) {
|
|
268
|
+
throw new ProtocolError(
|
|
269
|
+
`Invalid frame magic: expected 0x${FRAME_MAGIC.toString(16)} (RCPX), got 0x${magic.toString(16)}`,
|
|
270
|
+
"BAD_REQUEST" /* BAD_REQUEST */
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const version = this.buffer.readUInt16BE(4);
|
|
274
|
+
if (version !== PROTOCOL_VERSION) {
|
|
275
|
+
throw new ProtocolError(
|
|
276
|
+
`Unsupported protocol version: ${version}`,
|
|
277
|
+
"UNSUPPORTED_PROTOCOL" /* UNSUPPORTED_PROTOCOL */
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const flags = this.buffer.readUInt16BE(6);
|
|
281
|
+
const headerExtLen = this.buffer.readUInt16BE(8);
|
|
282
|
+
const payloadLen = this.buffer.readUInt32BE(10);
|
|
283
|
+
const crcValue = this.buffer.readUInt32BE(14);
|
|
284
|
+
const totalLen = HEADER_SIZE + headerExtLen + payloadLen;
|
|
285
|
+
if (this.buffer.length < totalLen) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const payloadStart = HEADER_SIZE + headerExtLen;
|
|
289
|
+
const payload = this.buffer.subarray(payloadStart, payloadStart + payloadLen);
|
|
290
|
+
if (this.verifyCrc && flags & 1 /* CRC_PRESENT */) {
|
|
291
|
+
if (!verifyCrc32c(payload, crcValue)) {
|
|
292
|
+
throw new ProtocolError("CRC32C checksum mismatch", "BAD_REQUEST" /* BAD_REQUEST */);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
this.buffer = this.buffer.subarray(totalLen);
|
|
296
|
+
return { flags, payload: Buffer.from(payload) };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Reset the decoder state.
|
|
300
|
+
*/
|
|
301
|
+
reset() {
|
|
302
|
+
this.buffer = Buffer.alloc(0);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get the current buffer length.
|
|
306
|
+
*/
|
|
307
|
+
get bufferedLength() {
|
|
308
|
+
return this.buffer.length;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/protocol/operations.ts
|
|
313
|
+
var Operation = /* @__PURE__ */ ((Operation2) => {
|
|
314
|
+
Operation2["HELLO"] = "HELLO";
|
|
315
|
+
Operation2["AUTH"] = "AUTH";
|
|
316
|
+
Operation2["PING"] = "PING";
|
|
317
|
+
Operation2["INFO"] = "INFO";
|
|
318
|
+
Operation2["PUT_MACHINE"] = "PUT_MACHINE";
|
|
319
|
+
Operation2["GET_MACHINE"] = "GET_MACHINE";
|
|
320
|
+
Operation2["LIST_MACHINES"] = "LIST_MACHINES";
|
|
321
|
+
Operation2["CREATE_INSTANCE"] = "CREATE_INSTANCE";
|
|
322
|
+
Operation2["GET_INSTANCE"] = "GET_INSTANCE";
|
|
323
|
+
Operation2["DELETE_INSTANCE"] = "DELETE_INSTANCE";
|
|
324
|
+
Operation2["APPLY_EVENT"] = "APPLY_EVENT";
|
|
325
|
+
Operation2["BATCH"] = "BATCH";
|
|
326
|
+
Operation2["WAL_READ"] = "WAL_READ";
|
|
327
|
+
Operation2["SNAPSHOT_INSTANCE"] = "SNAPSHOT_INSTANCE";
|
|
328
|
+
Operation2["COMPACT"] = "COMPACT";
|
|
329
|
+
Operation2["WATCH_INSTANCE"] = "WATCH_INSTANCE";
|
|
330
|
+
Operation2["WATCH_ALL"] = "WATCH_ALL";
|
|
331
|
+
Operation2["UNWATCH"] = "UNWATCH";
|
|
332
|
+
return Operation2;
|
|
333
|
+
})(Operation || {});
|
|
334
|
+
Object.values(Operation);
|
|
335
|
+
|
|
336
|
+
// src/protocol/messages.ts
|
|
337
|
+
function isResponseMessage(msg) {
|
|
338
|
+
return msg.type === "response";
|
|
339
|
+
}
|
|
340
|
+
function isStreamEventMessage(msg) {
|
|
341
|
+
return msg.type === "event";
|
|
342
|
+
}
|
|
343
|
+
function isStreamEndMessage(msg) {
|
|
344
|
+
return msg.type === "end";
|
|
345
|
+
}
|
|
346
|
+
function parseBigIntFields(obj, fields) {
|
|
347
|
+
const result = { ...obj };
|
|
348
|
+
for (const field of fields) {
|
|
349
|
+
const value = result[field];
|
|
350
|
+
if (typeof value === "string") {
|
|
351
|
+
result[field] = BigInt(value);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/connection.ts
|
|
358
|
+
var Connection = class extends EventEmitter {
|
|
359
|
+
config;
|
|
360
|
+
socket = null;
|
|
361
|
+
state = "DISCONNECTED" /* DISCONNECTED */;
|
|
362
|
+
encoder;
|
|
363
|
+
decoder;
|
|
364
|
+
pending = /* @__PURE__ */ new Map();
|
|
365
|
+
nextId = 1;
|
|
366
|
+
subscriptions;
|
|
367
|
+
reconnectAttempt = 0;
|
|
368
|
+
reconnectTimer = null;
|
|
369
|
+
closing = false;
|
|
370
|
+
constructor(config, subscriptions) {
|
|
371
|
+
super();
|
|
372
|
+
this.config = config;
|
|
373
|
+
this.subscriptions = subscriptions;
|
|
374
|
+
this.encoder = new FrameEncoder({ useCrc: true });
|
|
375
|
+
this.decoder = new FrameDecoder({ verifyCrc: true });
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Get the current connection state.
|
|
379
|
+
*/
|
|
380
|
+
getState() {
|
|
381
|
+
return this.state;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if connected.
|
|
385
|
+
*/
|
|
386
|
+
isConnected() {
|
|
387
|
+
return this.state === "CONNECTED" /* CONNECTED */;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Connect to the server.
|
|
391
|
+
*/
|
|
392
|
+
async connect() {
|
|
393
|
+
if (this.state !== "DISCONNECTED" /* DISCONNECTED */) {
|
|
394
|
+
throw new ConnectionError("Already connected or connecting");
|
|
395
|
+
}
|
|
396
|
+
this.closing = false;
|
|
397
|
+
this.state = "CONNECTING" /* CONNECTING */;
|
|
398
|
+
try {
|
|
399
|
+
await this.createSocket();
|
|
400
|
+
this.state = "HANDSHAKING" /* HANDSHAKING */;
|
|
401
|
+
await this.handshake();
|
|
402
|
+
this.state = "CONNECTED" /* CONNECTED */;
|
|
403
|
+
this.reconnectAttempt = 0;
|
|
404
|
+
this.emit("connect");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
407
|
+
this.cleanup();
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Close the connection.
|
|
413
|
+
*/
|
|
414
|
+
async close() {
|
|
415
|
+
this.closing = true;
|
|
416
|
+
if (this.reconnectTimer) {
|
|
417
|
+
clearTimeout(this.reconnectTimer);
|
|
418
|
+
this.reconnectTimer = null;
|
|
419
|
+
}
|
|
420
|
+
if (this.socket) {
|
|
421
|
+
this.state = "CLOSING" /* CLOSING */;
|
|
422
|
+
const error = new ConnectionError("Connection closed");
|
|
423
|
+
for (const pending of this.pending.values()) {
|
|
424
|
+
clearTimeout(pending.timeout);
|
|
425
|
+
pending.reject(error);
|
|
426
|
+
}
|
|
427
|
+
this.pending.clear();
|
|
428
|
+
this.subscriptions.closeAll(error);
|
|
429
|
+
await new Promise((resolve) => {
|
|
430
|
+
if (this.socket) {
|
|
431
|
+
this.socket.once("close", () => resolve());
|
|
432
|
+
this.socket.end();
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
if (this.socket) {
|
|
435
|
+
this.socket.destroy();
|
|
436
|
+
}
|
|
437
|
+
resolve();
|
|
438
|
+
}, 1e3);
|
|
439
|
+
} else {
|
|
440
|
+
resolve();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
this.cleanup();
|
|
444
|
+
}
|
|
445
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Send a request and wait for response.
|
|
449
|
+
*/
|
|
450
|
+
async request(op, params) {
|
|
451
|
+
if (this.state !== "CONNECTED" /* CONNECTED */ && this.state !== "HANDSHAKING" /* HANDSHAKING */) {
|
|
452
|
+
throw new ConnectionError("Not connected");
|
|
453
|
+
}
|
|
454
|
+
const socket = this.socket;
|
|
455
|
+
if (!socket) {
|
|
456
|
+
throw new ConnectionError("Socket not available");
|
|
457
|
+
}
|
|
458
|
+
const id = String(this.nextId++);
|
|
459
|
+
const message = { type: "request", id, op, params };
|
|
460
|
+
const frame = this.encoder.encode(message);
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
const timeout = setTimeout(() => {
|
|
463
|
+
this.pending.delete(id);
|
|
464
|
+
reject(new TimeoutError(`Request ${op} timed out after ${this.config.requestTimeout}ms`));
|
|
465
|
+
}, this.config.requestTimeout);
|
|
466
|
+
this.pending.set(id, {
|
|
467
|
+
id,
|
|
468
|
+
resolve: (response) => {
|
|
469
|
+
clearTimeout(timeout);
|
|
470
|
+
this.pending.delete(id);
|
|
471
|
+
if (response.status !== "ok") {
|
|
472
|
+
reject(
|
|
473
|
+
ServerError.fromResponse(
|
|
474
|
+
response.error || { code: "INTERNAL_ERROR" /* INTERNAL_ERROR */, message: "Unknown error" }
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
} else {
|
|
478
|
+
resolve(response.result);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
reject: (error) => {
|
|
482
|
+
clearTimeout(timeout);
|
|
483
|
+
this.pending.delete(id);
|
|
484
|
+
reject(error);
|
|
485
|
+
},
|
|
486
|
+
timeout
|
|
487
|
+
});
|
|
488
|
+
socket.write(frame, (err) => {
|
|
489
|
+
if (err) {
|
|
490
|
+
const pending = this.pending.get(id);
|
|
491
|
+
if (pending) {
|
|
492
|
+
clearTimeout(pending.timeout);
|
|
493
|
+
this.pending.delete(id);
|
|
494
|
+
reject(new ConnectionError(`Failed to send request: ${err.message}`, { cause: err }));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Create the TCP/TLS socket.
|
|
502
|
+
*/
|
|
503
|
+
async createSocket() {
|
|
504
|
+
return new Promise((resolve, reject) => {
|
|
505
|
+
const connectTimeout = setTimeout(() => {
|
|
506
|
+
if (this.socket) {
|
|
507
|
+
this.socket.destroy();
|
|
508
|
+
this.socket = null;
|
|
509
|
+
}
|
|
510
|
+
reject(new TimeoutError(`Connection timeout after ${this.config.connectTimeout}ms`));
|
|
511
|
+
}, this.config.connectTimeout);
|
|
512
|
+
const options = {
|
|
513
|
+
host: this.config.host,
|
|
514
|
+
port: this.config.port
|
|
515
|
+
};
|
|
516
|
+
const onConnect = () => {
|
|
517
|
+
clearTimeout(connectTimeout);
|
|
518
|
+
this.setupSocket();
|
|
519
|
+
resolve();
|
|
520
|
+
};
|
|
521
|
+
const onError = (err) => {
|
|
522
|
+
clearTimeout(connectTimeout);
|
|
523
|
+
reject(new ConnectionError(`Failed to connect: ${err.message}`, { cause: err }));
|
|
524
|
+
};
|
|
525
|
+
if (this.config.tls) {
|
|
526
|
+
const tlsOptions = {
|
|
527
|
+
...options,
|
|
528
|
+
...this.config.tls
|
|
529
|
+
};
|
|
530
|
+
this.socket = tls.connect(tlsOptions);
|
|
531
|
+
this.socket.once("secureConnect", onConnect);
|
|
532
|
+
} else {
|
|
533
|
+
this.socket = net.connect(options);
|
|
534
|
+
this.socket.once("connect", onConnect);
|
|
535
|
+
}
|
|
536
|
+
this.socket.once("error", onError);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Set up socket event handlers.
|
|
541
|
+
*/
|
|
542
|
+
setupSocket() {
|
|
543
|
+
if (!this.socket) return;
|
|
544
|
+
this.socket.on("data", (data) => this.handleData(data));
|
|
545
|
+
this.socket.on("close", () => {
|
|
546
|
+
const wasConnected = this.state === "CONNECTED" /* CONNECTED */;
|
|
547
|
+
this.cleanup();
|
|
548
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
549
|
+
if (!this.closing && wasConnected) {
|
|
550
|
+
this.emit("disconnect");
|
|
551
|
+
this.scheduleReconnect();
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
this.socket.on("error", (err) => {
|
|
555
|
+
this.emit("error", new ConnectionError(err.message, { cause: err }));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Handle incoming data.
|
|
560
|
+
*/
|
|
561
|
+
handleData(data) {
|
|
562
|
+
this.decoder.append(data);
|
|
563
|
+
let frame;
|
|
564
|
+
try {
|
|
565
|
+
while ((frame = this.decoder.decode()) !== null) {
|
|
566
|
+
this.handleFrame(frame);
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
this.emit("error", error);
|
|
570
|
+
this.socket?.destroy();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Handle a decoded frame.
|
|
575
|
+
*/
|
|
576
|
+
handleFrame(frame) {
|
|
577
|
+
let message;
|
|
578
|
+
try {
|
|
579
|
+
message = JSON.parse(frame.payload.toString("utf8"));
|
|
580
|
+
} catch {
|
|
581
|
+
throw new ProtocolError("Invalid JSON payload");
|
|
582
|
+
}
|
|
583
|
+
if (isResponseMessage(message)) {
|
|
584
|
+
const pending = this.pending.get(message.id);
|
|
585
|
+
if (pending) {
|
|
586
|
+
pending.resolve(message);
|
|
587
|
+
}
|
|
588
|
+
} else if (isStreamEventMessage(message)) {
|
|
589
|
+
this.subscriptions.dispatch(message);
|
|
590
|
+
} else if (isStreamEndMessage(message)) {
|
|
591
|
+
this.subscriptions.end(message.subscription_id);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Perform the handshake (HELLO + AUTH).
|
|
596
|
+
*/
|
|
597
|
+
async handshake() {
|
|
598
|
+
const helloParams = {
|
|
599
|
+
protocol_version: 1,
|
|
600
|
+
wire_modes: ["binary_json"],
|
|
601
|
+
features: ["idempotency", "batch"]
|
|
602
|
+
};
|
|
603
|
+
if (this.config.clientName) {
|
|
604
|
+
helloParams["client_name"] = this.config.clientName;
|
|
605
|
+
}
|
|
606
|
+
const helloResult = await this.request(
|
|
607
|
+
"HELLO" /* HELLO */,
|
|
608
|
+
helloParams
|
|
609
|
+
);
|
|
610
|
+
if (helloResult.protocol_version !== 1) {
|
|
611
|
+
throw new ProtocolError(
|
|
612
|
+
`Unsupported protocol version: ${helloResult.protocol_version}`,
|
|
613
|
+
"UNSUPPORTED_PROTOCOL" /* UNSUPPORTED_PROTOCOL */
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (this.config.authToken) {
|
|
617
|
+
await this.request("AUTH" /* AUTH */, {
|
|
618
|
+
method: "bearer",
|
|
619
|
+
token: this.config.authToken
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Schedule a reconnection attempt.
|
|
625
|
+
*/
|
|
626
|
+
scheduleReconnect() {
|
|
627
|
+
if (this.closing || !this.config.reconnect) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (this.reconnectAttempt >= this.config.reconnectMaxAttempts) {
|
|
631
|
+
this.emit(
|
|
632
|
+
"error",
|
|
633
|
+
new ConnectionError(
|
|
634
|
+
`Max reconnection attempts (${this.config.reconnectMaxAttempts}) reached`
|
|
635
|
+
)
|
|
636
|
+
);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
this.reconnectAttempt++;
|
|
640
|
+
const delay = this.config.reconnectInterval * Math.min(this.reconnectAttempt, 5);
|
|
641
|
+
this.reconnectTimer = setTimeout(() => {
|
|
642
|
+
this.reconnectTimer = null;
|
|
643
|
+
this.emit("reconnect", this.reconnectAttempt);
|
|
644
|
+
this.connect().catch(() => {
|
|
645
|
+
});
|
|
646
|
+
}, delay);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Clean up resources.
|
|
650
|
+
*/
|
|
651
|
+
cleanup() {
|
|
652
|
+
this.decoder.reset();
|
|
653
|
+
if (this.socket) {
|
|
654
|
+
this.socket.removeAllListeners();
|
|
655
|
+
this.socket = null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
var SubscriptionHandler = class extends EventEmitter {
|
|
660
|
+
subscriptionId;
|
|
661
|
+
events = [];
|
|
662
|
+
waiters = [];
|
|
663
|
+
closed = false;
|
|
664
|
+
closeError;
|
|
665
|
+
unsubscribeFn;
|
|
666
|
+
constructor(subscriptionId, unsubscribeFn) {
|
|
667
|
+
super();
|
|
668
|
+
this.subscriptionId = subscriptionId;
|
|
669
|
+
this.unsubscribeFn = unsubscribeFn;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Push an event to the subscription.
|
|
673
|
+
*/
|
|
674
|
+
push(event) {
|
|
675
|
+
if (this.closed) return;
|
|
676
|
+
const waiter = this.waiters.shift();
|
|
677
|
+
if (waiter) {
|
|
678
|
+
waiter.resolve({ value: event, done: false });
|
|
679
|
+
} else {
|
|
680
|
+
this.events.push(event);
|
|
681
|
+
}
|
|
682
|
+
this.emit("event", event);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Close the subscription with an optional error.
|
|
686
|
+
*/
|
|
687
|
+
close(error) {
|
|
688
|
+
if (this.closed) return;
|
|
689
|
+
this.closed = true;
|
|
690
|
+
this.closeError = error;
|
|
691
|
+
for (const waiter of this.waiters) {
|
|
692
|
+
if (error) {
|
|
693
|
+
waiter.reject(error);
|
|
694
|
+
} else {
|
|
695
|
+
waiter.resolve({ value: void 0, done: true });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
this.waiters = [];
|
|
699
|
+
if (error) {
|
|
700
|
+
this.emit("error", error);
|
|
701
|
+
}
|
|
702
|
+
this.emit("end");
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* AsyncIterator implementation.
|
|
706
|
+
*/
|
|
707
|
+
async *[Symbol.asyncIterator]() {
|
|
708
|
+
while (true) {
|
|
709
|
+
if (this.closed) {
|
|
710
|
+
if (this.closeError) {
|
|
711
|
+
throw this.closeError;
|
|
712
|
+
}
|
|
713
|
+
while (this.events.length > 0) {
|
|
714
|
+
yield this.events.shift();
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (this.events.length > 0) {
|
|
719
|
+
yield this.events.shift();
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const result = await new Promise((resolve, reject) => {
|
|
723
|
+
this.waiters.push({ resolve, reject });
|
|
724
|
+
});
|
|
725
|
+
if (result.done) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
yield result.value;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Cancel the subscription.
|
|
733
|
+
*/
|
|
734
|
+
async unsubscribe() {
|
|
735
|
+
if (!this.closed) {
|
|
736
|
+
await this.unsubscribeFn();
|
|
737
|
+
this.close();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Check if the subscription is closed.
|
|
742
|
+
*/
|
|
743
|
+
get isClosed() {
|
|
744
|
+
return this.closed;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/streaming/manager.ts
|
|
749
|
+
var SubscriptionManager = class {
|
|
750
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
751
|
+
/**
|
|
752
|
+
* Register a new subscription.
|
|
753
|
+
*/
|
|
754
|
+
register(subscriptionId, handler) {
|
|
755
|
+
this.subscriptions.set(subscriptionId, handler);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Unregister a subscription.
|
|
759
|
+
*/
|
|
760
|
+
unregister(subscriptionId) {
|
|
761
|
+
const handler = this.subscriptions.get(subscriptionId);
|
|
762
|
+
if (handler) {
|
|
763
|
+
handler.close();
|
|
764
|
+
this.subscriptions.delete(subscriptionId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get a subscription handler.
|
|
769
|
+
*/
|
|
770
|
+
get(subscriptionId) {
|
|
771
|
+
return this.subscriptions.get(subscriptionId);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Check if a subscription exists.
|
|
775
|
+
*/
|
|
776
|
+
has(subscriptionId) {
|
|
777
|
+
return this.subscriptions.has(subscriptionId);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Dispatch an event to the appropriate subscription.
|
|
781
|
+
*/
|
|
782
|
+
dispatch(message) {
|
|
783
|
+
const handler = this.subscriptions.get(message.subscription_id);
|
|
784
|
+
if (!handler) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const event = {
|
|
788
|
+
subscriptionId: message.subscription_id,
|
|
789
|
+
instanceId: message.instance_id,
|
|
790
|
+
machine: message.machine,
|
|
791
|
+
version: message.version,
|
|
792
|
+
walOffset: BigInt(message.wal_offset),
|
|
793
|
+
fromState: message.from_state,
|
|
794
|
+
toState: message.to_state,
|
|
795
|
+
event: message.event,
|
|
796
|
+
payload: message.payload,
|
|
797
|
+
ctx: message.ctx
|
|
798
|
+
};
|
|
799
|
+
handler.push(event);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* End a subscription (e.g., when server sends end message).
|
|
803
|
+
*/
|
|
804
|
+
end(subscriptionId, error) {
|
|
805
|
+
const handler = this.subscriptions.get(subscriptionId);
|
|
806
|
+
if (handler) {
|
|
807
|
+
handler.close(error);
|
|
808
|
+
this.subscriptions.delete(subscriptionId);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Close all subscriptions (e.g., on disconnect).
|
|
813
|
+
*/
|
|
814
|
+
closeAll(error) {
|
|
815
|
+
for (const handler of this.subscriptions.values()) {
|
|
816
|
+
handler.close(error);
|
|
817
|
+
}
|
|
818
|
+
this.subscriptions.clear();
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Get the number of active subscriptions.
|
|
822
|
+
*/
|
|
823
|
+
get size() {
|
|
824
|
+
return this.subscriptions.size;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// src/types/config.ts
|
|
829
|
+
var DEFAULT_CONFIG = {
|
|
830
|
+
port: 7401,
|
|
831
|
+
connectTimeout: 1e4,
|
|
832
|
+
requestTimeout: 3e4,
|
|
833
|
+
reconnect: true,
|
|
834
|
+
reconnectInterval: 1e3,
|
|
835
|
+
reconnectMaxAttempts: 10
|
|
836
|
+
};
|
|
837
|
+
function resolveConfig(config) {
|
|
838
|
+
let tlsConfig;
|
|
839
|
+
if (config.tls === true) {
|
|
840
|
+
tlsConfig = { rejectUnauthorized: true };
|
|
841
|
+
} else if (config.tls && typeof config.tls === "object") {
|
|
842
|
+
tlsConfig = config.tls;
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
host: config.host,
|
|
846
|
+
port: config.port ?? DEFAULT_CONFIG.port,
|
|
847
|
+
connectTimeout: config.connectTimeout ?? DEFAULT_CONFIG.connectTimeout,
|
|
848
|
+
requestTimeout: config.requestTimeout ?? DEFAULT_CONFIG.requestTimeout,
|
|
849
|
+
authToken: config.authToken,
|
|
850
|
+
tls: tlsConfig,
|
|
851
|
+
reconnect: config.reconnect ?? DEFAULT_CONFIG.reconnect,
|
|
852
|
+
reconnectInterval: config.reconnectInterval ?? DEFAULT_CONFIG.reconnectInterval,
|
|
853
|
+
reconnectMaxAttempts: config.reconnectMaxAttempts ?? DEFAULT_CONFIG.reconnectMaxAttempts,
|
|
854
|
+
clientName: config.clientName
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
var ClientOptions = class _ClientOptions {
|
|
858
|
+
config;
|
|
859
|
+
constructor(host) {
|
|
860
|
+
this.config = { host };
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Create a new options builder with the given host.
|
|
864
|
+
*/
|
|
865
|
+
static create(host) {
|
|
866
|
+
return new _ClientOptions(host);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Set the server port.
|
|
870
|
+
* @default 7401
|
|
871
|
+
*/
|
|
872
|
+
port(port) {
|
|
873
|
+
this.config.port = port;
|
|
874
|
+
return this;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Set the authentication token.
|
|
878
|
+
*/
|
|
879
|
+
auth(token) {
|
|
880
|
+
this.config.authToken = token;
|
|
881
|
+
return this;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Configure TLS settings.
|
|
885
|
+
*
|
|
886
|
+
* @param config - TLS configuration, or `true` to enable with system CA
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* ```typescript
|
|
890
|
+
* // Enable TLS with system CA
|
|
891
|
+
* options.tls(true)
|
|
892
|
+
*
|
|
893
|
+
* // Custom CA certificate
|
|
894
|
+
* options.tls({ ca: fs.readFileSync('ca.pem') })
|
|
895
|
+
*
|
|
896
|
+
* // mTLS with client certificate
|
|
897
|
+
* options.tls({
|
|
898
|
+
* ca: fs.readFileSync('ca.pem'),
|
|
899
|
+
* cert: fs.readFileSync('client.pem'),
|
|
900
|
+
* key: fs.readFileSync('client-key.pem'),
|
|
901
|
+
* })
|
|
902
|
+
* ```
|
|
903
|
+
*/
|
|
904
|
+
tls(config) {
|
|
905
|
+
this.config.tls = config;
|
|
906
|
+
return this;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Set timeout values.
|
|
910
|
+
*/
|
|
911
|
+
timeout(options) {
|
|
912
|
+
if (options.connect !== void 0) {
|
|
913
|
+
this.config.connectTimeout = options.connect;
|
|
914
|
+
}
|
|
915
|
+
if (options.request !== void 0) {
|
|
916
|
+
this.config.requestTimeout = options.request;
|
|
917
|
+
}
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Configure reconnection behavior.
|
|
922
|
+
*/
|
|
923
|
+
reconnect(options) {
|
|
924
|
+
if (options.enabled !== void 0) {
|
|
925
|
+
this.config.reconnect = options.enabled;
|
|
926
|
+
}
|
|
927
|
+
if (options.interval !== void 0) {
|
|
928
|
+
this.config.reconnectInterval = options.interval;
|
|
929
|
+
}
|
|
930
|
+
if (options.maxAttempts !== void 0) {
|
|
931
|
+
this.config.reconnectMaxAttempts = options.maxAttempts;
|
|
932
|
+
}
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Set the client name (sent in HELLO handshake).
|
|
937
|
+
*/
|
|
938
|
+
clientName(name) {
|
|
939
|
+
this.config.clientName = name;
|
|
940
|
+
return this;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Build the configuration object.
|
|
944
|
+
*/
|
|
945
|
+
build() {
|
|
946
|
+
return { ...this.config };
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// src/client.ts
|
|
951
|
+
var Client = class _Client extends EventEmitter {
|
|
952
|
+
connection;
|
|
953
|
+
subscriptions;
|
|
954
|
+
config;
|
|
955
|
+
constructor(config) {
|
|
956
|
+
super();
|
|
957
|
+
this.config = config;
|
|
958
|
+
const resolvedConfig = resolveConfig(config);
|
|
959
|
+
this.subscriptions = new SubscriptionManager();
|
|
960
|
+
this.connection = new Connection(resolvedConfig, this.subscriptions);
|
|
961
|
+
this.connection.on("connect", () => this.emit("connect"));
|
|
962
|
+
this.connection.on("disconnect", (error) => this.emit("disconnect", error));
|
|
963
|
+
this.connection.on("error", (error) => this.emit("error", error));
|
|
964
|
+
this.connection.on("reconnect", (attempt) => this.emit("reconnect", attempt));
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Create a client and connect to the server.
|
|
968
|
+
*
|
|
969
|
+
* @param host - Server hostname
|
|
970
|
+
* @param port - Server port (default: 7401)
|
|
971
|
+
* @param options - Additional options
|
|
972
|
+
* @returns Connected client instance
|
|
973
|
+
*
|
|
974
|
+
* @example
|
|
975
|
+
* ```typescript
|
|
976
|
+
* const client = await Client.connect('localhost');
|
|
977
|
+
* const client = await Client.connect('localhost', 7401);
|
|
978
|
+
* const client = await Client.connect('localhost', 7401, { auth: 'token' });
|
|
979
|
+
* ```
|
|
980
|
+
*/
|
|
981
|
+
static async connect(host, port = 7401, options) {
|
|
982
|
+
const client = new _Client({
|
|
983
|
+
host,
|
|
984
|
+
port,
|
|
985
|
+
authToken: options?.auth,
|
|
986
|
+
tls: options?.tls,
|
|
987
|
+
clientName: options?.clientName
|
|
988
|
+
});
|
|
989
|
+
await client.connect();
|
|
990
|
+
return client;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Get the current configuration.
|
|
994
|
+
*/
|
|
995
|
+
getConfig() {
|
|
996
|
+
return this.config;
|
|
997
|
+
}
|
|
998
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
999
|
+
// Connection Lifecycle
|
|
1000
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1001
|
+
/**
|
|
1002
|
+
* Connect to the server.
|
|
1003
|
+
*/
|
|
1004
|
+
async connect() {
|
|
1005
|
+
await this.connection.connect();
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Close the connection.
|
|
1009
|
+
*/
|
|
1010
|
+
async close() {
|
|
1011
|
+
await this.connection.close();
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Check if connected to the server.
|
|
1015
|
+
*/
|
|
1016
|
+
isConnected() {
|
|
1017
|
+
return this.connection.getState() === "CONNECTED" /* CONNECTED */;
|
|
1018
|
+
}
|
|
1019
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1020
|
+
// System Operations
|
|
1021
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1022
|
+
/**
|
|
1023
|
+
* Ping the server.
|
|
1024
|
+
*/
|
|
1025
|
+
async ping() {
|
|
1026
|
+
await this.connection.request("PING" /* PING */);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Get server information.
|
|
1030
|
+
*/
|
|
1031
|
+
async info() {
|
|
1032
|
+
const result = await this.connection.request("INFO" /* INFO */);
|
|
1033
|
+
return {
|
|
1034
|
+
serverName: result.server_name,
|
|
1035
|
+
serverVersion: result.server_version,
|
|
1036
|
+
protocolVersion: result.protocol_version,
|
|
1037
|
+
maxPayloadBytes: result.max_payload_bytes,
|
|
1038
|
+
maxBatchOps: result.max_batch_ops,
|
|
1039
|
+
walSegmentSize: result.wal_segment_size,
|
|
1040
|
+
authRequired: result.auth_required,
|
|
1041
|
+
features: result.features
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1045
|
+
// Machine Operations
|
|
1046
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1047
|
+
/**
|
|
1048
|
+
* Create or update a state machine definition.
|
|
1049
|
+
*/
|
|
1050
|
+
async putMachine(machine, version, definition) {
|
|
1051
|
+
const result = await this.connection.request("PUT_MACHINE" /* PUT_MACHINE */, {
|
|
1052
|
+
machine,
|
|
1053
|
+
version,
|
|
1054
|
+
definition
|
|
1055
|
+
});
|
|
1056
|
+
return {
|
|
1057
|
+
machine: result.machine,
|
|
1058
|
+
version: result.version,
|
|
1059
|
+
storedChecksum: result.stored_checksum,
|
|
1060
|
+
created: result.created
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Get a state machine definition.
|
|
1065
|
+
*/
|
|
1066
|
+
async getMachine(machine, version) {
|
|
1067
|
+
const result = await this.connection.request("GET_MACHINE" /* GET_MACHINE */, {
|
|
1068
|
+
machine,
|
|
1069
|
+
version
|
|
1070
|
+
});
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* List all state machines.
|
|
1075
|
+
*/
|
|
1076
|
+
async listMachines(options) {
|
|
1077
|
+
const result = await this.connection.request(
|
|
1078
|
+
"LIST_MACHINES" /* LIST_MACHINES */,
|
|
1079
|
+
options
|
|
1080
|
+
);
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1084
|
+
// Instance Operations
|
|
1085
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1086
|
+
/**
|
|
1087
|
+
* Create a new state machine instance.
|
|
1088
|
+
*/
|
|
1089
|
+
async createInstance(machine, version, options) {
|
|
1090
|
+
const params = { machine, version };
|
|
1091
|
+
if (options?.instanceId) params["instance_id"] = options.instanceId;
|
|
1092
|
+
if (options?.initialCtx) params["initial_ctx"] = options.initialCtx;
|
|
1093
|
+
if (options?.idempotencyKey) params["idempotency_key"] = options.idempotencyKey;
|
|
1094
|
+
const result = await this.connection.request("CREATE_INSTANCE" /* CREATE_INSTANCE */, params);
|
|
1095
|
+
return {
|
|
1096
|
+
instanceId: result.instance_id,
|
|
1097
|
+
state: result.state,
|
|
1098
|
+
walOffset: BigInt(result.wal_offset)
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Get instance state and context.
|
|
1103
|
+
*/
|
|
1104
|
+
async getInstance(instanceId) {
|
|
1105
|
+
const result = await this.connection.request("GET_INSTANCE" /* GET_INSTANCE */, { instance_id: instanceId });
|
|
1106
|
+
return {
|
|
1107
|
+
machine: result.machine,
|
|
1108
|
+
version: result.version,
|
|
1109
|
+
state: result.state,
|
|
1110
|
+
ctx: result.ctx,
|
|
1111
|
+
lastEventId: result.last_event_id,
|
|
1112
|
+
lastWalOffset: BigInt(result.last_wal_offset)
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Delete an instance.
|
|
1117
|
+
*/
|
|
1118
|
+
async deleteInstance(instanceId, options) {
|
|
1119
|
+
const params = { instance_id: instanceId };
|
|
1120
|
+
if (options?.idempotencyKey) params["idempotency_key"] = options.idempotencyKey;
|
|
1121
|
+
await this.connection.request("DELETE_INSTANCE" /* DELETE_INSTANCE */, params);
|
|
1122
|
+
}
|
|
1123
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1124
|
+
// Event Operations
|
|
1125
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1126
|
+
/**
|
|
1127
|
+
* Apply an event to an instance.
|
|
1128
|
+
*/
|
|
1129
|
+
async applyEvent(instanceId, event, options) {
|
|
1130
|
+
const params = {
|
|
1131
|
+
instance_id: instanceId,
|
|
1132
|
+
event
|
|
1133
|
+
};
|
|
1134
|
+
if (options?.payload) params["payload"] = options.payload;
|
|
1135
|
+
if (options?.expectedState) params["expected_state"] = options.expectedState;
|
|
1136
|
+
if (options?.expectedWalOffset !== void 0) {
|
|
1137
|
+
params["expected_wal_offset"] = Number(options.expectedWalOffset);
|
|
1138
|
+
}
|
|
1139
|
+
if (options?.eventId) params["event_id"] = options.eventId;
|
|
1140
|
+
if (options?.idempotencyKey) params["idempotency_key"] = options.idempotencyKey;
|
|
1141
|
+
const result = await this.connection.request("APPLY_EVENT" /* APPLY_EVENT */, params);
|
|
1142
|
+
return {
|
|
1143
|
+
fromState: result.from_state,
|
|
1144
|
+
toState: result.to_state,
|
|
1145
|
+
ctx: result.ctx,
|
|
1146
|
+
walOffset: BigInt(result.wal_offset),
|
|
1147
|
+
applied: result.applied,
|
|
1148
|
+
eventId: result.event_id
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Execute multiple operations in a batch.
|
|
1153
|
+
*/
|
|
1154
|
+
async batch(operations, options) {
|
|
1155
|
+
const serializedOps = operations.map((op) => {
|
|
1156
|
+
const params = {};
|
|
1157
|
+
for (const [key, value] of Object.entries(op.params)) {
|
|
1158
|
+
const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
1159
|
+
params[snakeKey] = value;
|
|
1160
|
+
}
|
|
1161
|
+
return { op: op.op, params };
|
|
1162
|
+
});
|
|
1163
|
+
const result = await this.connection.request("BATCH" /* BATCH */, {
|
|
1164
|
+
ops: serializedOps,
|
|
1165
|
+
...options
|
|
1166
|
+
});
|
|
1167
|
+
return {
|
|
1168
|
+
results: result.results.map((r) => ({
|
|
1169
|
+
status: r.status,
|
|
1170
|
+
result: r.result ? parseBigIntFields(r.result, ["wal_offset"]) : void 0,
|
|
1171
|
+
error: r.error ? ServerError.fromResponse(r.error) : void 0
|
|
1172
|
+
})),
|
|
1173
|
+
walOffset: result.wal_offset ? BigInt(result.wal_offset) : void 0
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1177
|
+
// WAL Operations
|
|
1178
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1179
|
+
/**
|
|
1180
|
+
* Read entries from the write-ahead log.
|
|
1181
|
+
*/
|
|
1182
|
+
async walRead(fromOffset, options) {
|
|
1183
|
+
const result = await this.connection.request("WAL_READ" /* WAL_READ */, {
|
|
1184
|
+
fromOffset: fromOffset.toString(),
|
|
1185
|
+
...options
|
|
1186
|
+
});
|
|
1187
|
+
return {
|
|
1188
|
+
entries: result.entries.map((e) => ({
|
|
1189
|
+
...e,
|
|
1190
|
+
offset: BigInt(e.offset)
|
|
1191
|
+
})),
|
|
1192
|
+
hasMore: result.hasMore,
|
|
1193
|
+
nextOffset: result.nextOffset ? BigInt(result.nextOffset) : void 0
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Create a snapshot of an instance.
|
|
1198
|
+
*/
|
|
1199
|
+
async snapshotInstance(instanceId) {
|
|
1200
|
+
const result = await this.connection.request("SNAPSHOT_INSTANCE" /* SNAPSHOT_INSTANCE */, { instance_id: instanceId });
|
|
1201
|
+
return {
|
|
1202
|
+
instanceId,
|
|
1203
|
+
walOffset: BigInt(result.wal_offset),
|
|
1204
|
+
sizeBytes: 0
|
|
1205
|
+
// Not provided by server
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Trigger WAL compaction.
|
|
1210
|
+
*/
|
|
1211
|
+
async compact(options) {
|
|
1212
|
+
const params = {};
|
|
1213
|
+
if (options?.force) params["force_snapshot"] = options.force;
|
|
1214
|
+
const result = await this.connection.request("COMPACT" /* COMPACT */, params);
|
|
1215
|
+
return {
|
|
1216
|
+
snapshotsCreated: result.snapshots_created,
|
|
1217
|
+
segmentsDeleted: result.segments_deleted,
|
|
1218
|
+
bytesReclaimed: BigInt(result.bytes_reclaimed)
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1222
|
+
// Watch/Streaming Operations
|
|
1223
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1224
|
+
/**
|
|
1225
|
+
* Watch a specific instance for state changes.
|
|
1226
|
+
*/
|
|
1227
|
+
async watchInstance(instanceId, options) {
|
|
1228
|
+
const params = {
|
|
1229
|
+
instance_id: instanceId
|
|
1230
|
+
};
|
|
1231
|
+
if (options?.includeCtx !== void 0) params["include_ctx"] = options.includeCtx;
|
|
1232
|
+
if (options?.fromOffset !== void 0) params["from_offset"] = Number(options.fromOffset);
|
|
1233
|
+
const result = await this.connection.request(
|
|
1234
|
+
"WATCH_INSTANCE" /* WATCH_INSTANCE */,
|
|
1235
|
+
params
|
|
1236
|
+
);
|
|
1237
|
+
const handler = new SubscriptionHandler(
|
|
1238
|
+
result.subscription_id,
|
|
1239
|
+
() => this.unwatch(result.subscription_id)
|
|
1240
|
+
);
|
|
1241
|
+
this.subscriptions.register(result.subscription_id, handler);
|
|
1242
|
+
return handler;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Watch all instances matching the filter.
|
|
1246
|
+
*/
|
|
1247
|
+
async watchAll(options) {
|
|
1248
|
+
const params = {};
|
|
1249
|
+
if (options?.includeCtx !== void 0) params["include_ctx"] = options.includeCtx;
|
|
1250
|
+
if (options?.fromOffset !== void 0) params["from_offset"] = Number(options.fromOffset);
|
|
1251
|
+
if (options?.machines) params["machines"] = options.machines;
|
|
1252
|
+
if (options?.fromStates) params["from_states"] = options.fromStates;
|
|
1253
|
+
if (options?.toStates) params["to_states"] = options.toStates;
|
|
1254
|
+
if (options?.events) params["events"] = options.events;
|
|
1255
|
+
const result = await this.connection.request(
|
|
1256
|
+
"WATCH_ALL" /* WATCH_ALL */,
|
|
1257
|
+
params
|
|
1258
|
+
);
|
|
1259
|
+
const handler = new SubscriptionHandler(
|
|
1260
|
+
result.subscription_id,
|
|
1261
|
+
() => this.unwatch(result.subscription_id)
|
|
1262
|
+
);
|
|
1263
|
+
this.subscriptions.register(result.subscription_id, handler);
|
|
1264
|
+
return handler;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Cancel a watch subscription.
|
|
1268
|
+
*/
|
|
1269
|
+
async unwatch(subscriptionId) {
|
|
1270
|
+
await this.connection.request("UNWATCH" /* UNWATCH */, { subscription_id: subscriptionId });
|
|
1271
|
+
this.subscriptions.unregister(subscriptionId);
|
|
1272
|
+
}
|
|
1273
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1274
|
+
on(event, listener) {
|
|
1275
|
+
return super.on(event, listener);
|
|
1276
|
+
}
|
|
1277
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1278
|
+
once(event, listener) {
|
|
1279
|
+
return super.once(event, listener);
|
|
1280
|
+
}
|
|
1281
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1282
|
+
off(event, listener) {
|
|
1283
|
+
return super.off(event, listener);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
export { AuthenticationError, Client, ClientOptions, ConflictError, ConnectionError, DEFAULT_CONFIG, ErrorCode, FRAME_MAGIC, FrameDecoder, FrameEncoder, FrameFlags, GuardFailedError, HEADER_SIZE, InvalidTransitionError, NotFoundError, Operation, PROTOCOL_VERSION, ProtocolError, RstmdbError, ServerError, TimeoutError };
|
|
1288
|
+
//# sourceMappingURL=index.mjs.map
|
|
1289
|
+
//# sourceMappingURL=index.mjs.map
|