@substrio/sdk 0.0.0-stage → 0.1.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.
@@ -0,0 +1,559 @@
1
+ // substrio_kv_tcp.mjs: SubstrioWire: Raw TCP Binary KV Client
2
+ // Zero-copy TCP transport for the Substrio KV protocol.
3
+ // Same API as KvWs but over length-prefixed TCP frames (no WebSocket overhead).
4
+ //
5
+ // Hot-path optimizations:
6
+ // - Pre-allocated 64KB send buffer with 4-byte header inline → single sock.write()
7
+ // - Buffer.write() directly into sendBuf → zero per-key/value allocations
8
+ // - Offset-based recv parsing → no copyWithin per frame
9
+ // - No intermediate arrays in put()/get()/del()
10
+ //
11
+ // Usage:
12
+ // const client = new KvTcp('127.0.0.1', 9001, 'default', masterKey, { maxInFlight: 16 });
13
+ // await client.ready;
14
+ // await client.register('bench');
15
+ // await client.put([['k1','v1'], ['k2','v2']]);
16
+ // const map = await client.get(['k1','k2']);
17
+ // client.close();
18
+
19
+ import net from 'net';
20
+
21
+ function normalizeMaxInFlight(value) {
22
+ const parsed = Number.parseInt(String(value ?? ''), 10);
23
+ if (Number.isFinite(parsed) && parsed > 0) {
24
+ return parsed;
25
+ }
26
+ return 16;
27
+ }
28
+
29
+ export class KvTcp {
30
+ static OP_PUT = 0x01;
31
+ static OP_GET = 0x02;
32
+ static OP_DEL = 0x03;
33
+ static OP_REG = 0x10;
34
+ static OP_ERR = 0xFF;
35
+ static OP_AUTH = 0xA0;
36
+ static OP_AUTH_OK = 0xA1;
37
+ static OP_AUTH_FAIL = 0xA2;
38
+
39
+ // Protocol limits: must match server's substrio_kv_tcp.h constants
40
+ static MAX_FRAME = 4 * 1024 * 1024; // 4MB max frame
41
+ static MAX_KEY = 65535; // 2-byte key length field (uint16)
42
+ static MAX_VALUE = 4 * 1024 * 1024; // 4MB max value (fits in frame)
43
+
44
+ constructor(host, port, tenant, key, options = {}) {
45
+ this._colId = 0;
46
+ this._queue = []; // pending responses for sent frames
47
+ this._queueHead = 0; // read cursor into _queue to avoid shift()
48
+ this._sendQueue = []; // pending frames waiting for an in-flight slot
49
+ this._recvBuf = null; // Accumulation buffer
50
+ this._recvLen = 0; // Total bytes in recvBuf
51
+ this._recvOff = 0; // Parse offset into recvBuf
52
+ this._inFlight = 0;
53
+ this._writeBlocked = false;
54
+ this._maxInFlight = normalizeMaxInFlight(options.maxInFlight);
55
+ this._closed = false;
56
+
57
+ // Reusable send buffers. Each in-flight write gets its own frame buffer
58
+ // so pipelined requests never alias the same underlying bytes.
59
+ this._sendBufBytes = 65540;
60
+ this._sendPool = [];
61
+
62
+ // Reusable result array for GET: avoids new Array() per call
63
+ this._getResult = [];
64
+
65
+ this._sock = net.connect(port, host);
66
+ this._sock.setNoDelay(true);
67
+
68
+ this._sock.on('data', (chunk) => this._onData(chunk));
69
+ this._sock.on('drain', () => {
70
+ this._writeBlocked = false;
71
+ this._flushSendQueue();
72
+ });
73
+ this._sock.on('error', (err) => this._failPending(err));
74
+ this._sock.on('close', () => {
75
+ this._closed = true;
76
+ this._failPending(new Error('KvTcp socket closed'));
77
+ });
78
+
79
+ // Auth on connect
80
+ this.ready = new Promise((resolve, reject) => {
81
+ this._sock.once('connect', () => {
82
+ const tenantBytes = Buffer.byteLength(tenant);
83
+ const keyBytes = Buffer.byteLength(key);
84
+ this._sendFrame(4 + 1 + 2 + tenantBytes + 2 + keyBytes, (buf) => {
85
+ let o = 4;
86
+ buf[o++] = 0xA0;
87
+ buf.writeUInt16LE(tenantBytes, o); o += 2;
88
+ buf.write(tenant, o, 'utf8'); o += tenantBytes;
89
+ buf.writeUInt16LE(keyBytes, o); o += 2;
90
+ buf.write(key, o, 'utf8'); o += keyBytes;
91
+ buf.writeUInt32LE(o - 4, 0);
92
+ return o;
93
+ }).then((resp) => {
94
+ if (resp[0] === 0xA1) resolve(this);
95
+ else reject(new Error('SubstrioWire auth failed'));
96
+ }, reject);
97
+ });
98
+ this._sock.once('error', reject);
99
+ });
100
+ }
101
+
102
+ get maxInFlight() {
103
+ return this._maxInFlight;
104
+ }
105
+
106
+ setMaxInFlight(value) {
107
+ this._maxInFlight = normalizeMaxInFlight(value);
108
+ while (this._sendPool.length > this._maxInFlight) {
109
+ this._sendPool.pop();
110
+ }
111
+ this._flushSendQueue();
112
+ return this._maxInFlight;
113
+ }
114
+
115
+ _onData(chunk) {
116
+ // Append chunk to recv buffer
117
+ if (this._recvLen === 0) {
118
+ // Fast path: no leftover, use chunk directly
119
+ this._recvBuf = chunk;
120
+ this._recvLen = chunk.length;
121
+ this._recvOff = 0;
122
+ } else {
123
+ // Compact if offset > half buffer to keep memory bounded
124
+ const avail = this._recvLen - this._recvOff;
125
+ if (this._recvOff > 0 && this._recvOff > (this._recvBuf.length >> 1)) {
126
+ if (avail > 0) this._recvBuf.copyWithin(0, this._recvOff, this._recvLen);
127
+ this._recvLen = avail;
128
+ this._recvOff = 0;
129
+ }
130
+ const needed = this._recvLen + chunk.length;
131
+ if (this._recvBuf.length < needed) {
132
+ const newBuf = Buffer.allocUnsafe(Math.max(needed * 2, 65536));
133
+ if (this._recvLen > this._recvOff) {
134
+ this._recvBuf.copy(newBuf, 0, this._recvOff, this._recvLen);
135
+ }
136
+ this._recvLen -= this._recvOff;
137
+ this._recvOff = 0;
138
+ this._recvBuf = newBuf;
139
+ }
140
+ chunk.copy(this._recvBuf, this._recvLen);
141
+ this._recvLen += chunk.length;
142
+ }
143
+
144
+ // Process complete frames using offset (no copyWithin per frame)
145
+ while (this._recvOff + 4 <= this._recvLen && this._queueHead < this._queue.length) {
146
+ const frameLen = this._recvBuf.readUInt32LE(this._recvOff);
147
+ const frameEnd = this._recvOff + 4 + frameLen;
148
+ if (frameEnd > this._recvLen) break;
149
+
150
+ // Return a subarray view (zero-copy for caller)
151
+ const payload = this._recvBuf.subarray(this._recvOff + 4, frameEnd);
152
+ this._recvOff = frameEnd;
153
+
154
+ const head = this._queue[this._queueHead++];
155
+ this._releasePendingBuffer(head);
156
+ head.resolve(payload);
157
+ if (this._inFlight > 0) {
158
+ this._inFlight--;
159
+ }
160
+ }
161
+
162
+ this._compactQueue();
163
+ this._flushSendQueue();
164
+
165
+ // If fully consumed, reset
166
+ if (this._recvOff === this._recvLen) {
167
+ this._recvLen = 0;
168
+ this._recvOff = 0;
169
+ }
170
+ }
171
+
172
+ _compactQueue() {
173
+ if (this._queueHead === 0) {
174
+ return;
175
+ }
176
+ if (this._queueHead === this._queue.length) {
177
+ this._queue.length = 0;
178
+ this._queueHead = 0;
179
+ return;
180
+ }
181
+ if (this._queueHead >= 64 && this._queueHead >= (this._queue.length >> 1)) {
182
+ this._queue = this._queue.slice(this._queueHead);
183
+ this._queueHead = 0;
184
+ }
185
+ }
186
+
187
+ _releasePendingBuffer(pending) {
188
+ if (!pending || !pending._sendBuf) {
189
+ return;
190
+ }
191
+ this._releaseSendBuffer(pending._sendBuf, pending._sendReusable === true);
192
+ pending._sendBuf = null;
193
+ pending._sendReusable = false;
194
+ }
195
+
196
+ _failPending(err) {
197
+ while (this._queueHead < this._queue.length) {
198
+ const pending = this._queue[this._queueHead++];
199
+ this._releasePendingBuffer(pending);
200
+ pending.reject(err);
201
+ }
202
+ this._compactQueue();
203
+ while (this._sendQueue.length > 0) {
204
+ this._sendQueue.shift().reject(err);
205
+ }
206
+ this._inFlight = 0;
207
+ this._writeBlocked = false;
208
+ }
209
+
210
+ _acquireSendBuffer(requiredBytes) {
211
+ const size = Math.max(requiredBytes, this._sendBufBytes);
212
+ if (size === this._sendBufBytes && this._sendPool.length > 0) {
213
+ return { buf: this._sendPool.pop(), reusable: true };
214
+ }
215
+ return { buf: Buffer.allocUnsafe(size), reusable: size === this._sendBufBytes };
216
+ }
217
+
218
+ _releaseSendBuffer(buf, reusable) {
219
+ if (!reusable || buf.length !== this._sendBufBytes) {
220
+ return;
221
+ }
222
+ if (this._sendPool.length < this._maxInFlight) {
223
+ this._sendPool.push(buf);
224
+ }
225
+ }
226
+
227
+ _flushSendQueue() {
228
+ if (!this._sock || this._closed || this._sock.destroyed) {
229
+ return;
230
+ }
231
+ while (!this._writeBlocked && this._inFlight < this._maxInFlight && this._sendQueue.length > 0) {
232
+ const pending = this._sendQueue.shift();
233
+ const { buf, reusable } = this._acquireSendBuffer(pending.requiredBytes);
234
+ let totalLen = 0;
235
+ try {
236
+ totalLen = pending.buildFrame(buf);
237
+ } catch (err) {
238
+ this._releaseSendBuffer(buf, reusable);
239
+ pending.reject(err);
240
+ continue;
241
+ }
242
+
243
+ this._queue.push(pending);
244
+ this._inFlight++;
245
+
246
+ pending._sendBuf = buf;
247
+ pending._sendReusable = reusable;
248
+
249
+ const wrote = this._sock.write(buf.subarray(0, totalLen));
250
+ if (!wrote) {
251
+ this._writeBlocked = true;
252
+ }
253
+ }
254
+ }
255
+
256
+ _sendFrame(requiredBytes, buildFrame) {
257
+ return new Promise((resolve, reject) => {
258
+ if (!this._sock || this._closed || this._sock.destroyed) {
259
+ reject(new Error('KvTcp socket is closed'));
260
+ return;
261
+ }
262
+ this._sendQueue.push({ resolve, reject, requiredBytes, buildFrame });
263
+ this._flushSendQueue();
264
+ });
265
+ }
266
+
267
+ /** Register a collection → get 1-byte handle. */
268
+ async register(name) {
269
+ const nameBytes = Buffer.byteLength(name);
270
+ const r = await this._sendFrame(4 + 1 + 1 + 2 + nameBytes, (buf) => {
271
+ let o = 4;
272
+ buf[o++] = 0x10;
273
+ buf[o++] = 0;
274
+ buf.writeUInt16LE(nameBytes, o); o += 2;
275
+ buf.write(name, o, 'utf8'); o += nameBytes;
276
+ buf.writeUInt32LE(o - 4, 0);
277
+ return o;
278
+ });
279
+ if (r[0] !== 0x10 || r[2] !== 1) throw new Error('KvTcp: register failed');
280
+ this._colId = r[1];
281
+ return this._colId;
282
+ }
283
+
284
+ /** PUT [[key,val],...] → number of successes. */
285
+ async put(pairs) {
286
+ let totalBytes = 8;
287
+ for (let i = 0; i < pairs.length; i++) {
288
+ const kb = Buffer.byteLength(pairs[i][0]);
289
+ const vb = Buffer.byteLength(pairs[i][1]);
290
+ if (kb > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${kb})`);
291
+ if (vb > KvTcp.MAX_VALUE) throw new Error(`KvTcp: value exceeds ${KvTcp.MAX_VALUE} bytes (got ${vb})`);
292
+ totalBytes += 6 + kb + vb;
293
+ }
294
+ if (totalBytes > KvTcp.MAX_FRAME) throw new Error(`KvTcp: frame exceeds ${KvTcp.MAX_FRAME} bytes (got ${totalBytes})`);
295
+
296
+ const r = await this._sendFrame(totalBytes, (buf) => {
297
+ let o = 4;
298
+ buf[o++] = 0x01;
299
+ buf[o++] = this._colId;
300
+ buf.writeUInt16LE(pairs.length, o); o += 2;
301
+
302
+ for (let i = 0; i < pairs.length; i++) {
303
+ const key = pairs[i][0];
304
+ const value = pairs[i][1];
305
+ const keyBytes = Buffer.byteLength(key);
306
+ const valueBytes = Buffer.byteLength(value);
307
+ buf.writeUInt16LE(keyBytes, o); o += 2;
308
+ buf.writeUInt32LE(valueBytes, o); o += 4;
309
+ buf.write(key, o, 'utf8'); o += keyBytes;
310
+ buf.write(value, o, 'utf8'); o += valueBytes;
311
+ }
312
+
313
+ buf.writeUInt32LE(o - 4, 0);
314
+ return o;
315
+ });
316
+ if (r[0] === 0xFF) throw new Error('KvTcp PUT error');
317
+ const cnt = r[1] | (r[2] << 8);
318
+ let ok = 0;
319
+ for (let i = 0; i < cnt; i++) if (r[3 + (i >> 3)] & (1 << (i & 7))) ok++;
320
+ return ok;
321
+ }
322
+
323
+ async put1(key, value) {
324
+ const keyBytes = Buffer.byteLength(key);
325
+ const valueBytes = Buffer.byteLength(value);
326
+ if (keyBytes > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${keyBytes})`);
327
+ if (valueBytes > KvTcp.MAX_VALUE) throw new Error(`KvTcp: value exceeds ${KvTcp.MAX_VALUE} bytes (got ${valueBytes})`);
328
+ const r = await this._sendFrame(4 + 1 + 1 + 2 + 2 + 4 + keyBytes + valueBytes, (buf) => {
329
+ let o = 4;
330
+ buf[o++] = 0x01;
331
+ buf[o++] = this._colId;
332
+ buf.writeUInt16LE(1, o); o += 2;
333
+ buf.writeUInt16LE(keyBytes, o); o += 2;
334
+ buf.writeUInt32LE(valueBytes, o); o += 4;
335
+ buf.write(key, o, 'utf8'); o += keyBytes;
336
+ buf.write(value, o, 'utf8'); o += valueBytes;
337
+ buf.writeUInt32LE(o - 4, 0);
338
+ return o;
339
+ });
340
+ if (r[0] === 0xFF) throw new Error('KvTcp PUT error');
341
+ return (r[3] & 1) === 1;
342
+ }
343
+
344
+ /** GET [key,...] → Reusable array of Buffer views (null for missing). True zero-copy.
345
+ * Returned Buffer views are valid until the next async I/O. Call .toString() if string needed. */
346
+ async get(keys) {
347
+ let totalBytes = 8;
348
+ for (let i = 0; i < keys.length; i++) {
349
+ const kb = Buffer.byteLength(keys[i]);
350
+ if (kb > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${kb})`);
351
+ totalBytes += 2 + kb;
352
+ }
353
+ if (totalBytes > KvTcp.MAX_FRAME) throw new Error(`KvTcp: frame exceeds ${KvTcp.MAX_FRAME} bytes (got ${totalBytes})`);
354
+
355
+ const r = await this._sendFrame(totalBytes, (buf) => {
356
+ let o = 4;
357
+ buf[o++] = 0x02;
358
+ buf[o++] = this._colId;
359
+ buf.writeUInt16LE(keys.length, o); o += 2;
360
+
361
+ for (let i = 0; i < keys.length; i++) {
362
+ const key = keys[i];
363
+ const keyBytes = Buffer.byteLength(key);
364
+ buf.writeUInt16LE(keyBytes, o); o += 2;
365
+ buf.write(key, o, 'utf8'); o += keyBytes;
366
+ }
367
+
368
+ buf.writeUInt32LE(o - 4, 0);
369
+ return o;
370
+ });
371
+ if (r[0] === 0xFF) throw new Error('KvTcp GET error');
372
+ const cnt = r[1] | (r[2] << 8);
373
+
374
+ // Reuse pre-allocated array: zero alloc
375
+ const result = this._getResult;
376
+ result.length = cnt;
377
+ let p = 3;
378
+ for (let i = 0; i < cnt && p < r.length; i++) {
379
+ if (r[p++] === 1 && p + 4 <= r.length) {
380
+ const vl = r.readUInt32LE(p); p += 4;
381
+ // Decode value: same work as Memcached client (latin1Slice = fastest V8 decode)
382
+ result[i] = r.latin1Slice(p, p + vl); p += vl;
383
+ } else {
384
+ result[i] = null;
385
+ }
386
+ }
387
+ return result;
388
+ }
389
+
390
+ async get1(key) {
391
+ const keyBytes = Buffer.byteLength(key);
392
+ if (keyBytes > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${keyBytes})`);
393
+ const r = await this._sendFrame(4 + 1 + 1 + 2 + 2 + keyBytes, (buf) => {
394
+ let o = 4;
395
+ buf[o++] = 0x02;
396
+ buf[o++] = this._colId;
397
+ buf.writeUInt16LE(1, o); o += 2;
398
+ buf.writeUInt16LE(keyBytes, o); o += 2;
399
+ buf.write(key, o, 'utf8'); o += keyBytes;
400
+ buf.writeUInt32LE(o - 4, 0);
401
+ return o;
402
+ });
403
+ if (r[0] === 0xFF) throw new Error('KvTcp GET error');
404
+ if (r.length < 4 || r[3] !== 1) {
405
+ return null;
406
+ }
407
+ const valueLen = r.readUInt32LE(4);
408
+ return r.latin1Slice(8, 8 + valueLen);
409
+ }
410
+
411
+ // ═══════════════════════════════════════════════════════════
412
+ // Ultra-lean single-key fast path: bypasses _sendFrame/_flushSendQueue.
413
+ // Writes directly to a pre-allocated buffer with zero intermediate
414
+ // queue operations. ~3x less JS overhead per roundtrip vs put1/get1.
415
+ // ═══════════════════════════════════════════════════════════
416
+
417
+ /** Fast single-key PUT: minimal JS overhead. */
418
+ putFast(key, value) {
419
+ const keyBytes = Buffer.byteLength(key);
420
+ const valueBytes = Buffer.byteLength(value);
421
+ if (keyBytes > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${keyBytes})`);
422
+ if (valueBytes > KvTcp.MAX_VALUE) throw new Error(`KvTcp: value exceeds ${KvTcp.MAX_VALUE} bytes (got ${valueBytes})`);
423
+ // Frame: [4B len][OP_PUT=0x01][colId][count=1 LE][2B klen][4B vlen][key][value]
424
+ const payloadLen = 10 + keyBytes + valueBytes;
425
+ const totalLen = 4 + payloadLen;
426
+
427
+ // Use exact-size buffer: avoids subarray() call
428
+ const buf = Buffer.allocUnsafe(totalLen);
429
+ buf.writeUInt32LE(payloadLen, 0);
430
+ buf[4] = 0x01;
431
+ buf[5] = this._colId;
432
+ buf[6] = 1; buf[7] = 0;
433
+ buf.writeUInt16LE(keyBytes, 8);
434
+ buf.writeUInt32LE(valueBytes, 10);
435
+ buf.write(key, 14, keyBytes, 'utf8');
436
+ buf.write(value, 14 + keyBytes, valueBytes, 'utf8');
437
+
438
+ return new Promise((resolve, reject) => {
439
+ // _sendBuf: null, buffer released to GC immediately after write.
440
+ // Node.js copies buffer contents to its internal write queue,
441
+ // so the original Buffer is safe to collect immediately.
442
+ this._queue.push({ resolve, reject, _sendBuf: null });
443
+ this._sock.write(buf);
444
+ });
445
+ }
446
+
447
+ /** Fast single-key GET: minimal JS overhead. */
448
+ getFast(key) {
449
+ const keyBytes = Buffer.byteLength(key);
450
+ if (keyBytes > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${keyBytes})`);
451
+ // Frame: [4B len][OP_GET=0x02][colId][count=1 LE][2B klen][key]
452
+ const payloadLen = 6 + keyBytes;
453
+ const totalLen = 4 + payloadLen;
454
+
455
+ const buf = Buffer.allocUnsafe(totalLen);
456
+ buf.writeUInt32LE(payloadLen, 0);
457
+ buf[4] = 0x02;
458
+ buf[5] = this._colId;
459
+ buf[6] = 1; buf[7] = 0;
460
+ buf.writeUInt16LE(keyBytes, 8);
461
+ buf.write(key, 10, keyBytes, 'utf8');
462
+
463
+ return new Promise((resolve, reject) => {
464
+ this._queue.push({ resolve, reject, _sendBuf: null });
465
+ this._sock.write(buf);
466
+ });
467
+ }
468
+
469
+ // ═══════════════════════════════════════════════════════════
470
+ // Pre-built frame API: ZERO per-call construction cost.
471
+ // Build the frame once, then fire repeatedly with sock.write().
472
+ // Ideal for hot-path benchmarks and production hot keys.
473
+ // ═══════════════════════════════════════════════════════════
474
+
475
+ /** Pre-build a PUT frame. Returns a Buffer ready for repeated sock.write().
476
+ * Usage:
477
+ * const frame = client.preparePut('hot:key', 'value');
478
+ * await client.sendPrepared(frame); // zero construction cost */
479
+ preparePut(key, value) {
480
+ const keyBytes = Buffer.byteLength(key);
481
+ const valueBytes = Buffer.byteLength(value);
482
+ if (keyBytes > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${keyBytes})`);
483
+ if (valueBytes > KvTcp.MAX_VALUE) throw new Error(`KvTcp: value exceeds ${KvTcp.MAX_VALUE} bytes (got ${valueBytes})`);
484
+ const payloadLen = 10 + keyBytes + valueBytes;
485
+ const buf = Buffer.allocUnsafe(4 + payloadLen);
486
+ buf.writeUInt32LE(payloadLen, 0);
487
+ buf[4] = 0x01;
488
+ buf[5] = this._colId;
489
+ buf[6] = 1; buf[7] = 0;
490
+ buf.writeUInt16LE(keyBytes, 8);
491
+ buf.writeUInt32LE(valueBytes, 10);
492
+ buf.write(key, 14, keyBytes, 'utf8');
493
+ buf.write(value, 14 + keyBytes, valueBytes, 'utf8');
494
+ return buf;
495
+ }
496
+
497
+ /** Pre-build a GET frame. */
498
+ prepareGet(key) {
499
+ const keyBytes = Buffer.byteLength(key);
500
+ if (keyBytes > KvTcp.MAX_KEY) throw new Error(`KvTcp: key exceeds ${KvTcp.MAX_KEY} bytes (got ${keyBytes})`);
501
+ const payloadLen = 6 + keyBytes;
502
+ const buf = Buffer.allocUnsafe(4 + payloadLen);
503
+ buf.writeUInt32LE(payloadLen, 0);
504
+ buf[4] = 0x02;
505
+ buf[5] = this._colId;
506
+ buf[6] = 1; buf[7] = 0;
507
+ buf.writeUInt16LE(keyBytes, 8);
508
+ buf.write(key, 10, keyBytes, 'utf8');
509
+ return buf;
510
+ }
511
+
512
+ /** Send a pre-built frame: absolute minimum JS overhead per call.
513
+ * Cost: 1 Promise + 1 queue push + 1 sock.write = ~250ns total. */
514
+ sendPrepared(frame) {
515
+ return new Promise((resolve, reject) => {
516
+ this._queue.push({ resolve, reject, _sendBuf: null });
517
+ this._sock.write(frame);
518
+ });
519
+ }
520
+
521
+ /** DEL [key,...] → number of deletions. */
522
+ async del(keys) {
523
+ let totalBytes = 8;
524
+ for (let i = 0; i < keys.length; i++) {
525
+ totalBytes += 2 + Buffer.byteLength(keys[i]);
526
+ }
527
+
528
+ const r = await this._sendFrame(totalBytes, (buf) => {
529
+ let o = 4;
530
+ buf[o++] = 0x03;
531
+ buf[o++] = this._colId;
532
+ buf.writeUInt16LE(keys.length, o); o += 2;
533
+
534
+ for (let i = 0; i < keys.length; i++) {
535
+ const key = keys[i];
536
+ const keyBytes = Buffer.byteLength(key);
537
+ buf.writeUInt16LE(keyBytes, o); o += 2;
538
+ buf.write(key, o, 'utf8'); o += keyBytes;
539
+ }
540
+
541
+ buf.writeUInt32LE(o - 4, 0);
542
+ return o;
543
+ });
544
+ if (r[0] === 0xFF) throw new Error('KvTcp DEL error');
545
+ const cnt = r[1] | (r[2] << 8);
546
+ let ok = 0;
547
+ for (let i = 0; i < cnt; i++) if (r[3 + (i >> 3)] & (1 << (i & 7))) ok++;
548
+ return ok;
549
+ }
550
+
551
+ close() {
552
+ if (this._sock) {
553
+ this._closed = true;
554
+ this._failPending(new Error('KvTcp socket closed'));
555
+ this._sock.destroy();
556
+ this._sock = null;
557
+ }
558
+ }
559
+ }
@@ -0,0 +1,28 @@
1
+ // Types of substrio_operation_client.mjs; the operation types are generated
2
+ // (substrio_operations.d.mts).
3
+ import type { SubstrioOperation, SubstrioOperations, SubstrioRoute, SubstrioMethod } from './substrio_operations.mjs';
4
+
5
+ /** What the operation calls go through: the SDK's Http. */
6
+ export interface SubstrioRequester {
7
+ req(method: SubstrioMethod, path: string, body?: unknown): Promise<unknown>;
8
+ }
9
+
10
+ export interface SubstrioRequest {
11
+ method: SubstrioMethod;
12
+ path: string;
13
+ body: unknown;
14
+ }
15
+
16
+ /** A catalog route as catalogRoute reads it: a SubstrioRoute with its OpenAPI parts. */
17
+ export interface SubstrioCatalogRoute extends SubstrioRoute {
18
+ readonly parameters: readonly { name: string; in: string; required?: boolean; description?: string; schema?: unknown }[];
19
+ readonly bodySchema: unknown;
20
+ }
21
+
22
+ export declare function routeParams(path: string): string[];
23
+ export declare function catalogRoute(path: string, method: string, op: object): SubstrioCatalogRoute;
24
+ export declare function pickRoute(operation: SubstrioOperation, input: unknown, method?: SubstrioMethod | null): SubstrioRoute;
25
+ export declare function buildRequest(route: SubstrioRoute, input: unknown, options?: { rawBody?: boolean }): SubstrioRequest;
26
+ export declare function callOperation(http: SubstrioRequester, operation: SubstrioOperation, input?: unknown, options?: { method?: SubstrioMethod }): Promise<unknown>;
27
+ export declare function bindOperations(http: SubstrioRequester): SubstrioOperations;
28
+ export declare function attachOperations(client: object, ops: SubstrioOperations): void;