@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.
- package/README.md +27 -13
- package/dist/client.d.ts +49 -47
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +316 -247
- package/dist/client.js.map +1 -1
- package/dist/envelope.d.ts +24 -17
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +91 -72
- package/dist/envelope.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +35 -8
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +221 -71
- package/dist/provider.js.map +1 -1
- package/dist/route-handle.d.ts +25 -0
- package/dist/route-handle.d.ts.map +1 -0
- package/dist/route-handle.js +52 -0
- package/dist/route-handle.js.map +1 -0
- package/dist/socket.d.ts +7 -0
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +23 -0
- package/dist/socket.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +388 -262
- package/src/envelope.ts +136 -74
- package/src/index.ts +7 -0
- package/src/provider.ts +361 -94
- package/src/route-handle.ts +60 -0
- package/src/socket.ts +37 -0
package/src/envelope.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// Byte-for-byte port of subc-protocol's
|
|
2
|
-
// Source of truth: crates/subc-protocol/src/lib.rs. Keep field offsets,
|
|
3
|
-
//
|
|
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 =
|
|
7
|
-
export const HEADER_LEN =
|
|
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
|
-
|
|
47
|
-
|
|
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;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
76
|
-
export function encodeHeader(
|
|
77
|
-
const
|
|
78
|
-
const view = new DataView(
|
|
79
|
-
view.setUint32(0,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
view.setUint16(7,
|
|
84
|
-
view.
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
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
|
|
110
|
-
if (
|
|
111
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
126
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
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:
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
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,
|