@cortexkit/subc-client 0.3.4 → 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.
package/src/envelope.ts CHANGED
@@ -1,10 +1,9 @@
1
- // Byte-for-byte port of subc-protocol's 17-byte envelope header.
2
- // Source of truth: crates/subc-protocol/src/lib.rs. Keep field offsets, the
3
- // little-endian encoding, and the frame-type/flag numbering in lock-step with
4
- // the Rust; a one-byte drift here desynchronizes every frame on the wire.
1
+ // Byte-for-byte port of subc-protocol's fixed envelope header.
2
+ // Source of truth: crates/subc-protocol/src/lib.rs. Keep field offsets, little-
3
+ // endian encoding, and frame/flag numbering in lock-step with Rust.
5
4
 
6
- export const PROTOCOL_VERSION = 1;
7
- export const HEADER_LEN = 17;
5
+ export const PROTOCOL_VERSION = 2;
6
+ export const HEADER_LEN = 21;
8
7
  export const FROZEN_PREFIX_LEN = 5;
9
8
  export const MAX_FRAME_BODY_LEN = 64 * 1024 * 1024;
10
9
 
@@ -28,12 +27,7 @@ const FRAME_TYPE_MAX = FrameType.Goodbye;
28
27
 
29
28
  /** Cancel/Ping/Pong/Goodbye carry only a header (`len` must be 0). */
30
29
  export function isPureHeader(ty: FrameType): boolean {
31
- return (
32
- ty === FrameType.Cancel ||
33
- ty === FrameType.Ping ||
34
- ty === FrameType.Pong ||
35
- ty === FrameType.Goodbye
36
- );
30
+ return ty === FrameType.Cancel || ty === FrameType.Ping || ty === FrameType.Pong || ty === FrameType.Goodbye;
37
31
  }
38
32
 
39
33
  /** Scheduling priority carried in flags bits 1-2. */
@@ -43,19 +37,38 @@ export enum Priority {
43
37
  Background = 2,
44
38
  }
45
39
 
46
- const FLAG_BINARY = 0b0000_0001; // bit 0
47
- const FLAG_PRIORITY_MASK = 0b0000_0110; // bits 1-2
40
+ /** Admission behavior carried in flags bits 4-5. */
41
+ export enum AdmissionClass {
42
+ Normal = 0,
43
+ Expedite = 1,
44
+ Sheddable = 2,
45
+ }
46
+
47
+ const FLAG_BINARY = 0b0000_0001;
48
+ const FLAG_PRIORITY_MASK = 0b0000_0110;
48
49
  const FLAG_PRIORITY_SHIFT = 1;
49
- const FLAG_LAST = 0b0000_1000; // bit 3
50
- const FLAG_RESERVED_MASK = 0b1111_0000; // bits 4-7 must be zero
51
-
52
- /** Build the flags byte from typed components (mirrors Flags::new). */
53
- export function buildFlags(binary: boolean, priority: Priority, last: boolean): number {
54
- let b = 0;
55
- if (binary) b |= FLAG_BINARY;
56
- b |= priority << FLAG_PRIORITY_SHIFT;
57
- if (last) b |= FLAG_LAST;
58
- return b;
50
+ const FLAG_LAST = 0b0000_1000;
51
+ const FLAG_ADMISSION_MASK = 0b0011_0000;
52
+ const FLAG_ADMISSION_SHIFT = 4;
53
+ const FLAG_RESERVED_MASK = 0b1100_0000;
54
+
55
+ /** Build flags from typed components. Admission defaults to NORMAL. */
56
+ export function buildFlags(
57
+ binary: boolean,
58
+ priority: Priority,
59
+ last: boolean,
60
+ admissionClass: AdmissionClass = AdmissionClass.Normal,
61
+ ): number {
62
+ let flags = 0;
63
+ if (binary) flags |= FLAG_BINARY;
64
+ flags |= priority << FLAG_PRIORITY_SHIFT;
65
+ if (last) flags |= FLAG_LAST;
66
+ flags |= admissionClass << FLAG_ADMISSION_SHIFT;
67
+ return flags;
68
+ }
69
+
70
+ export function admissionClass(flags: number): AdmissionClass {
71
+ return ((flags & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT) as AdmissionClass;
59
72
  }
60
73
 
61
74
  export interface EnvelopeHeader {
@@ -64,6 +77,7 @@ export interface EnvelopeHeader {
64
77
  ty: FrameType;
65
78
  flags: number;
66
79
  channel: number;
80
+ epoch: number;
67
81
  corr: bigint;
68
82
  }
69
83
 
@@ -72,97 +86,145 @@ export interface Frame {
72
86
  body: Uint8Array;
73
87
  }
74
88
 
75
- /** Serialize a header to its fixed 17-byte little-endian form. */
76
- export function encodeHeader(h: EnvelopeHeader): Uint8Array {
77
- const buf = new Uint8Array(HEADER_LEN);
78
- const view = new DataView(buf.buffer);
79
- view.setUint32(0, h.len, true);
80
- buf[4] = h.ver;
81
- buf[5] = h.ty;
82
- buf[6] = h.flags;
83
- view.setUint16(7, h.channel, true);
84
- view.setBigUint64(9, h.corr, true);
85
- return buf;
89
+ /** Serialize a header to its fixed 21-byte little-endian form. */
90
+ export function encodeHeader(header: EnvelopeHeader): Uint8Array {
91
+ const buffer = new Uint8Array(HEADER_LEN);
92
+ const view = new DataView(buffer.buffer);
93
+ view.setUint32(0, header.len, true);
94
+ buffer[4] = header.ver;
95
+ buffer[5] = header.ty;
96
+ buffer[6] = header.flags;
97
+ view.setUint16(7, header.channel, true);
98
+ view.setUint32(9, header.epoch, true);
99
+ view.setBigUint64(13, header.corr, true);
100
+ return buffer;
86
101
  }
87
102
 
88
- export class DecodeError extends Error {}
103
+ export type DecodeErrorCode =
104
+ | "too_short_for_prefix"
105
+ | "unsupported_version"
106
+ | "too_short_for_header"
107
+ | "unknown_frame_type"
108
+ | "reserved_flag_bits"
109
+ | "reserved_priority_bits"
110
+ | "reserved_admission_class"
111
+ | "sheddable_illegal_frame_type"
112
+ | "nonzero_epoch_on_control_channel"
113
+ | "pure_header_frame_with_body"
114
+ | "frame_body_too_large"
115
+ | "frame_length_mismatch";
89
116
 
90
- /**
91
- * Decode a header from the front of `bytes`, following the frozen-prefix
92
- * discipline: need 5 bytes for len+ver, dispatch full header length on ver,
93
- * then validate. Mirrors decode_header — never throws on a structurally short
94
- * buffer beyond the typed DecodeError.
95
- */
117
+ /** Typed envelope decode failure mirroring the Rust wire taxonomy. */
118
+ export class DecodeError extends Error {
119
+ constructor(message: string, readonly code: DecodeErrorCode) {
120
+ super(message);
121
+ this.name = "DecodeError";
122
+ }
123
+ }
124
+
125
+ /** Decode and validate a header from the front of `bytes`. */
96
126
  export function decodeHeader(bytes: Uint8Array): EnvelopeHeader {
97
127
  if (bytes.length < FROZEN_PREFIX_LEN) {
98
- throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`);
128
+ throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`, "too_short_for_prefix");
99
129
  }
100
130
  const ver = bytes[4]!;
101
- if (ver !== PROTOCOL_VERSION) {
102
- throw new DecodeError(`unsupported envelope version ${ver}`);
103
- }
131
+ if (ver !== PROTOCOL_VERSION) throw new DecodeError(`unsupported envelope version ${ver}`, "unsupported_version");
104
132
  if (bytes.length < HEADER_LEN) {
105
- throw new DecodeError(`header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`);
133
+ throw new DecodeError(
134
+ `header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`,
135
+ "too_short_for_header",
136
+ );
106
137
  }
138
+
107
139
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
108
140
  const len = view.getUint32(0, true);
109
- const tyByte = bytes[5]!;
110
- if (tyByte > FRAME_TYPE_MAX) {
111
- throw new DecodeError(`unknown frame type byte ${tyByte}`);
112
- }
113
- const ty = tyByte as FrameType;
141
+ const typeByte = bytes[5]!;
142
+ if (typeByte > FRAME_TYPE_MAX) throw new DecodeError(`unknown frame type byte ${typeByte}`, "unknown_frame_type");
143
+ const ty = typeByte as FrameType;
114
144
  const flags = bytes[6]!;
115
145
  if ((flags & FLAG_RESERVED_MASK) !== 0) {
116
- throw new DecodeError(`reserved flag bits set in flags 0b${flags.toString(2)}`);
146
+ throw new DecodeError(
147
+ `reserved flag bits set in flags 0b${flags.toString(2).padStart(8, "0")}`,
148
+ "reserved_flag_bits",
149
+ );
117
150
  }
118
151
  if (((flags & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT) === 0b11) {
119
- throw new DecodeError(`reserved priority bits set in flags 0b${flags.toString(2)}`);
152
+ throw new DecodeError(
153
+ `reserved priority bits set in flags 0b${flags.toString(2).padStart(8, "0")}`,
154
+ "reserved_priority_bits",
155
+ );
120
156
  }
121
- if (isPureHeader(ty) && len !== 0) {
122
- throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`);
157
+ const admission = (flags & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT;
158
+ if (admission === 0b11) {
159
+ throw new DecodeError(
160
+ `reserved admission class set in flags 0b${flags.toString(2).padStart(8, "0")}`,
161
+ "reserved_admission_class",
162
+ );
163
+ }
164
+ if (admission === AdmissionClass.Sheddable && ty !== FrameType.Push && ty !== FrameType.StreamData) {
165
+ throw new DecodeError(
166
+ `SHEDDABLE admission class is illegal on ${FrameType[ty]} in flags 0b${flags.toString(2).padStart(8, "0")}`,
167
+ "sheddable_illegal_frame_type",
168
+ );
123
169
  }
124
170
  const channel = view.getUint16(7, true);
125
- const corr = view.getBigUint64(9, true);
126
- return { len, ver, ty, flags, channel, corr };
171
+ const epoch = view.getUint32(9, true);
172
+ if (channel === 0 && epoch !== 0) {
173
+ throw new DecodeError(
174
+ `control channel carried nonzero epoch ${epoch}`,
175
+ "nonzero_epoch_on_control_channel",
176
+ );
177
+ }
178
+ if (isPureHeader(ty) && len !== 0) {
179
+ throw new DecodeError(
180
+ `pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`,
181
+ "pure_header_frame_with_body",
182
+ );
183
+ }
184
+ return { len, ver, ty, flags, channel, epoch, corr: view.getBigUint64(13, true) };
127
185
  }
128
186
 
129
- /** Build a full current-version frame, enforcing the body-length cap and the pure-header rule. */
187
+ /** Build a current-version frame and validate its complete header. */
130
188
  export function buildFrame(
131
189
  ty: FrameType,
132
190
  flags: number,
133
191
  channel: number,
192
+ epoch: number,
134
193
  corr: bigint,
135
194
  body: Uint8Array,
136
195
  ): Frame {
137
- return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, corr, body);
196
+ return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, epoch, corr, body);
138
197
  }
139
198
 
140
- /** Build a full frame while preserving a peer-negotiated envelope version. */
199
+ /** Build a frame while preserving the peer's exact supported version. */
141
200
  export function buildFrameWithVersion(
142
201
  ver: number,
143
202
  ty: FrameType,
144
203
  flags: number,
145
204
  channel: number,
205
+ epoch: number,
146
206
  corr: bigint,
147
207
  body: Uint8Array,
148
208
  ): Frame {
149
209
  if (body.length > MAX_FRAME_BODY_LEN) {
150
- throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`);
210
+ throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
151
211
  }
152
- if (isPureHeader(ty) && body.length !== 0) {
153
- throw new DecodeError(`pure-header frame ${FrameType[ty]} cannot carry a body`);
154
- }
155
- return {
156
- header: { len: body.length, ver, ty, flags, channel, corr },
157
- body,
158
- };
212
+ const header = { len: body.length, ver, ty, flags, channel, epoch, corr };
213
+ decodeHeader(encodeHeader(header));
214
+ return { header, body };
159
215
  }
160
216
 
161
- /** Encode a frame to wire bytes: 17-byte header followed by `len` body bytes. */
217
+ /** Encode a frame to wire bytes: header followed by exactly `len` body bytes. */
162
218
  export function encodeFrame(frame: Frame): Uint8Array {
219
+ if (frame.header.len !== frame.body.length) {
220
+ throw new DecodeError(
221
+ `frame header length ${frame.header.len} does not match body length ${frame.body.length}`,
222
+ "frame_length_mismatch",
223
+ );
224
+ }
163
225
  const header = encodeHeader(frame.header);
164
- const out = new Uint8Array(header.length + frame.body.length);
165
- out.set(header, 0);
166
- out.set(frame.body, header.length);
167
- return out;
226
+ const output = new Uint8Array(header.length + frame.body.length);
227
+ output.set(header, 0);
228
+ output.set(frame.body, header.length);
229
+ return output;
168
230
  }
package/src/index.ts CHANGED
@@ -21,7 +21,11 @@ export {
21
21
  type SubscribeOptions,
22
22
  type Subscription,
23
23
  type CloseRouteOptions,
24
+ type ManagedCloseRouteOptions,
25
+ type RoutePollKind,
26
+ type RoutePollResult,
24
27
  } from "./client.js";
28
+ export { RouteHandle, StaleRouteHandleError } from "./route-handle.js";
25
29
  export {
26
30
  readConnectionFile,
27
31
  ConnectionFileError,
@@ -31,6 +35,7 @@ export {
31
35
  export {
32
36
  FrameType,
33
37
  Priority,
38
+ AdmissionClass,
34
39
  PROTOCOL_VERSION,
35
40
  HEADER_LEN,
36
41
  buildFrame,
@@ -40,6 +45,7 @@ export {
40
45
  decodeHeader,
41
46
  encodeHeader,
42
47
  DecodeError,
48
+ type DecodeErrorCode,
43
49
  type Frame,
44
50
  type EnvelopeHeader,
45
51
  } from "./envelope.js";
@@ -60,6 +66,7 @@ export {
60
66
  managementSurfaceManifest,
61
67
  jsonProviderHandler,
62
68
  type ProviderRequestContext,
69
+ type ProviderEmitOptions,
63
70
  type BindDecision,
64
71
  type Principal,
65
72
  type BindingsInput,