@cortexkit/subc-client 0.3.3 → 0.4.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.
@@ -0,0 +1,60 @@
1
+ const connectionToken = new WeakMap<RouteHandle, object>();
2
+
3
+ /**
4
+ * Immutable identity for one route binding on one live socket. Only channel and
5
+ * epoch cross the wire; the connection token remains private to this SDK.
6
+ */
7
+ export class RouteHandle {
8
+ readonly channel: number;
9
+ readonly epoch: number;
10
+
11
+ private constructor(channel: number, epoch: number, token: object) {
12
+ if (!Number.isInteger(channel) || channel <= 0 || channel > 0xffff) {
13
+ throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
14
+ }
15
+ if (!Number.isInteger(epoch) || epoch <= 0 || epoch > 0xffff_ffff) {
16
+ throw new RangeError(`route epoch must be an integer in 1..4294967295, got ${epoch}`);
17
+ }
18
+ this.channel = channel;
19
+ this.epoch = epoch;
20
+ connectionToken.set(this, token);
21
+ Object.freeze(this);
22
+ }
23
+
24
+ private static create(channel: number, epoch: number, token: object): RouteHandle {
25
+ return new RouteHandle(channel, epoch, token);
26
+ }
27
+ }
28
+
29
+ /** @internal SDK factory; not re-exported from the package surface. */
30
+ export function createRouteHandle(channel: number, epoch: number, token: object): RouteHandle {
31
+ const factory = RouteHandle as unknown as {
32
+ create(channel: number, epoch: number, token: object): RouteHandle;
33
+ };
34
+ return factory.create(channel, epoch, token);
35
+ }
36
+
37
+ /** A route handle belongs to another connection or is no longer installed. */
38
+ export class StaleRouteHandleError extends Error {
39
+ readonly code = "stale_route_handle";
40
+
41
+ constructor(readonly handle: RouteHandle) {
42
+ super(`route handle (${handle.channel}, ${handle.epoch}) is not live on the current connection`);
43
+ this.name = "StaleRouteHandleError";
44
+ }
45
+ }
46
+
47
+ /** @internal */
48
+ export function newConnectionToken(): object {
49
+ return Object.freeze({});
50
+ }
51
+
52
+ /** @internal */
53
+ export function belongsToConnection(handle: RouteHandle, token: object): boolean {
54
+ return connectionToken.get(handle) === token;
55
+ }
56
+
57
+ /** @internal */
58
+ export function sameRouteHandle(left: RouteHandle, right: RouteHandle): boolean {
59
+ return left === right;
60
+ }
package/src/socket.ts CHANGED
@@ -6,6 +6,16 @@
6
6
 
7
7
  import net from "node:net";
8
8
 
9
+ import {
10
+ decodeHeader,
11
+ DecodeError,
12
+ FROZEN_PREFIX_LEN,
13
+ HEADER_LEN,
14
+ MAX_FRAME_BODY_LEN,
15
+ PROTOCOL_VERSION,
16
+ type Frame,
17
+ } from "./envelope.js";
18
+
9
19
  export class SocketClosedError extends Error {}
10
20
  export class SocketTimeoutError extends Error {}
11
21
 
@@ -99,6 +109,33 @@ export class SubcSocket {
99
109
  });
100
110
  }
101
111
 
112
+ /**
113
+ * Read one envelope frame. The frozen five-byte prefix is validated before
114
+ * waiting for the rest of the header, so a stale 17-byte v1 sender fails
115
+ * promptly instead of leaving this reader blocked for four bytes.
116
+ */
117
+ async readFrame(
118
+ headerDeadlineMs: number,
119
+ bodyDeadlineMs: number,
120
+ onHeader?: () => void,
121
+ ): Promise<Frame> {
122
+ const prefix = await this.readExact(FROZEN_PREFIX_LEN, headerDeadlineMs);
123
+ const version = prefix[4]!;
124
+ if (version !== PROTOCOL_VERSION) throw new DecodeError(`unsupported envelope version ${version}`, "unsupported_version");
125
+
126
+ const remainder = await this.readExact(HEADER_LEN - FROZEN_PREFIX_LEN, headerDeadlineMs);
127
+ const headerBytes = new Uint8Array(HEADER_LEN);
128
+ headerBytes.set(prefix);
129
+ headerBytes.set(remainder, FROZEN_PREFIX_LEN);
130
+ const header = decodeHeader(headerBytes);
131
+ if (header.len > MAX_FRAME_BODY_LEN) {
132
+ throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
133
+ }
134
+ onHeader?.();
135
+ const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineMs);
136
+ return { header, body };
137
+ }
138
+
102
139
  /** Read exactly `n` bytes, rejecting if `deadlineMs` (epoch ms) passes first. */
103
140
  readExact(n: number, deadlineMs: number): Promise<Uint8Array> {
104
141
  if (this.waiter) {