@sealant/api-contracts-next 0.40.0-next.760 → 0.40.0-next.763
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.
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* refused upgrade, so v2 accepts the upgrade first and answers with one text frame: `ready`, after
|
|
5
5
|
* which binary frames carry payload both ways, or `refused`, after which the server closes with
|
|
6
6
|
* {@link FORWARD_REFUSED_CLOSE_CODE}. A request without an upgrade gets 426 and is never dialled.
|
|
7
|
+
*
|
|
8
|
+
* After `ready`, each direction is bounded by {@link FORWARD_V2_WINDOW}. A receiver sends
|
|
9
|
+
* `progress` with the cumulative bytes and frames it has consumed; a sender keeps what it has sent
|
|
10
|
+
* within the last `progress` plus the window, and sends nothing before the first `progress`
|
|
11
|
+
* (`{0,0}`), which opens the receiver's window. A receiver resets a sender that goes past it.
|
|
12
|
+
* `fin` ends one direction with the totals sent; `reset` ends both at once. Core sends a `progress`
|
|
13
|
+
* naming exactly what reached the workspace before its `fin`, so a client can tell whether
|
|
14
|
+
* everything it wrote arrived. {@link ForwardFlow} keeps this ledger for either side.
|
|
7
15
|
*/
|
|
8
16
|
import { Schema } from "effect";
|
|
9
17
|
export declare const FORWARD_V2_SUBPROTOCOL = "sealant.forward.v2";
|
|
@@ -35,16 +43,26 @@ export declare const FORWARD_REFUSAL_STATUS: {
|
|
|
35
43
|
readonly "binding-unverifiable": 409;
|
|
36
44
|
readonly "binding-unsupported": 409;
|
|
37
45
|
};
|
|
38
|
-
|
|
46
|
+
/**
|
|
47
|
+
* What a forward reaches: whatever holds the port (`port`), or only a process of the session that
|
|
48
|
+
* `session` names (`session`), as the workspace runtime verified when it connected.
|
|
49
|
+
*/
|
|
50
|
+
export declare const forwardBindingSchema: Schema.Union<readonly [Schema.Struct<{
|
|
39
51
|
readonly kind: Schema.Literal<"port">;
|
|
40
|
-
}
|
|
52
|
+
}>, Schema.Struct<{
|
|
53
|
+
readonly kind: Schema.Literal<"session">;
|
|
54
|
+
readonly session: Schema.String;
|
|
55
|
+
}>]>;
|
|
41
56
|
export type ForwardBinding = typeof forwardBindingSchema.Type;
|
|
42
57
|
export declare const forwardReadyFrameSchema: Schema.Struct<{
|
|
43
58
|
readonly t: Schema.Literal<"ready">;
|
|
44
59
|
readonly version: Schema.Literal<2>;
|
|
45
|
-
readonly binding: Schema.Struct<{
|
|
60
|
+
readonly binding: Schema.Union<readonly [Schema.Struct<{
|
|
46
61
|
readonly kind: Schema.Literal<"port">;
|
|
47
|
-
}
|
|
62
|
+
}>, Schema.Struct<{
|
|
63
|
+
readonly kind: Schema.Literal<"session">;
|
|
64
|
+
readonly session: Schema.String;
|
|
65
|
+
}>]>;
|
|
48
66
|
}>;
|
|
49
67
|
export type ForwardReadyFrame = typeof forwardReadyFrameSchema.Type;
|
|
50
68
|
/**
|
|
@@ -68,9 +86,12 @@ export declare const forwardRefusedFrame: (reason: "binding-unsupported" | "bind
|
|
|
68
86
|
export declare const forwardOpenFrameSchema: Schema.Union<readonly [Schema.Struct<{
|
|
69
87
|
readonly t: Schema.Literal<"ready">;
|
|
70
88
|
readonly version: Schema.Literal<2>;
|
|
71
|
-
readonly binding: Schema.Struct<{
|
|
89
|
+
readonly binding: Schema.Union<readonly [Schema.Struct<{
|
|
72
90
|
readonly kind: Schema.Literal<"port">;
|
|
73
|
-
}
|
|
91
|
+
}>, Schema.Struct<{
|
|
92
|
+
readonly kind: Schema.Literal<"session">;
|
|
93
|
+
readonly session: Schema.String;
|
|
94
|
+
}>]>;
|
|
74
95
|
}>, Schema.Struct<{
|
|
75
96
|
readonly t: Schema.Literal<"refused">;
|
|
76
97
|
readonly reason: Schema.String;
|
|
@@ -78,3 +99,87 @@ export declare const forwardOpenFrameSchema: Schema.Union<readonly [Schema.Struc
|
|
|
78
99
|
readonly status: Schema.Int;
|
|
79
100
|
}>]>;
|
|
80
101
|
export type ForwardOpenFrame = typeof forwardOpenFrameSchema.Type;
|
|
102
|
+
/** What each direction of a v2 forward may have in flight beyond what its receiver consumed. */
|
|
103
|
+
export declare const FORWARD_V2_WINDOW: {
|
|
104
|
+
readonly bytes: number;
|
|
105
|
+
readonly frames: 32;
|
|
106
|
+
};
|
|
107
|
+
export declare const forwardProgressFrameSchema: Schema.Struct<{
|
|
108
|
+
readonly t: Schema.Literal<"progress">;
|
|
109
|
+
readonly bytes: Schema.String;
|
|
110
|
+
readonly frames: Schema.String;
|
|
111
|
+
}>;
|
|
112
|
+
export type ForwardProgressFrame = typeof forwardProgressFrameSchema.Type;
|
|
113
|
+
export declare const forwardFinFrameSchema: Schema.Struct<{
|
|
114
|
+
readonly t: Schema.Literal<"fin">;
|
|
115
|
+
readonly bytes: Schema.String;
|
|
116
|
+
readonly frames: Schema.String;
|
|
117
|
+
}>;
|
|
118
|
+
export type ForwardFinFrame = typeof forwardFinFrameSchema.Type;
|
|
119
|
+
export declare const forwardResetReasonSchema: Schema.Literals<readonly ["window-exceeded", "protocol-violation", "aborted", "target-error", "runtime-lost", "owner-exited"]>;
|
|
120
|
+
export type ForwardResetReason = typeof forwardResetReasonSchema.Type;
|
|
121
|
+
/** Decoded with any `reason`, so a client keeps a reset from a Core newer than itself. */
|
|
122
|
+
export declare const forwardResetFrameSchema: Schema.Struct<{
|
|
123
|
+
readonly t: Schema.Literal<"reset">;
|
|
124
|
+
readonly reason: Schema.String;
|
|
125
|
+
}>;
|
|
126
|
+
export type ForwardResetFrame = typeof forwardResetFrameSchema.Type;
|
|
127
|
+
/** The text frames either side sends after `ready`. */
|
|
128
|
+
export declare const forwardFlowFrameSchema: Schema.Union<readonly [Schema.Struct<{
|
|
129
|
+
readonly t: Schema.Literal<"progress">;
|
|
130
|
+
readonly bytes: Schema.String;
|
|
131
|
+
readonly frames: Schema.String;
|
|
132
|
+
}>, Schema.Struct<{
|
|
133
|
+
readonly t: Schema.Literal<"fin">;
|
|
134
|
+
readonly bytes: Schema.String;
|
|
135
|
+
readonly frames: Schema.String;
|
|
136
|
+
}>, Schema.Struct<{
|
|
137
|
+
readonly t: Schema.Literal<"reset">;
|
|
138
|
+
readonly reason: Schema.String;
|
|
139
|
+
}>]>;
|
|
140
|
+
export type ForwardFlowFrame = typeof forwardFlowFrameSchema.Type;
|
|
141
|
+
/** What one frame from the peer meant, once the forward is open. */
|
|
142
|
+
export type ForwardInbound = {
|
|
143
|
+
readonly kind: "data";
|
|
144
|
+
readonly data: Uint8Array;
|
|
145
|
+
} | {
|
|
146
|
+
readonly kind: "progress";
|
|
147
|
+
} | {
|
|
148
|
+
readonly kind: "fin";
|
|
149
|
+
} | {
|
|
150
|
+
readonly kind: "reset";
|
|
151
|
+
readonly reason: string;
|
|
152
|
+
} | {
|
|
153
|
+
readonly kind: "violation";
|
|
154
|
+
readonly reason: "window-exceeded" | "protocol-violation";
|
|
155
|
+
};
|
|
156
|
+
/** A chunk as the binary frames it is sent in. */
|
|
157
|
+
export declare function forwardFrames(chunk: Uint8Array): Generator<Uint8Array>;
|
|
158
|
+
/**
|
|
159
|
+
* Both directions of an open v2 forward as one side sees them, with no I/O: what it sent against
|
|
160
|
+
* what its peer consumed, and what it received and consumed against what it granted. Core and the
|
|
161
|
+
* SDK each drive one from their socket.
|
|
162
|
+
*
|
|
163
|
+
* A grant is made when its `progress` is sent, so a side that holds a `progress` back while its
|
|
164
|
+
* socket is backed up also holds back its peer, and the next `progress` it sends names the latest
|
|
165
|
+
* count: acknowledgements never queue.
|
|
166
|
+
*/
|
|
167
|
+
export declare class ForwardFlow {
|
|
168
|
+
#private;
|
|
169
|
+
/** The first `progress`, which lets the peer start sending. */
|
|
170
|
+
open(): ForwardProgressFrame;
|
|
171
|
+
receive(frame: string | Uint8Array): ForwardInbound;
|
|
172
|
+
/** Whether one frame of `bytes` fits in what the peer has granted. */
|
|
173
|
+
canSend(bytes: number): boolean;
|
|
174
|
+
sent(bytes: number): void;
|
|
175
|
+
/** Whether the peer has said it consumed everything this side sent. */
|
|
176
|
+
get delivered(): boolean;
|
|
177
|
+
fin(): ForwardFinFrame;
|
|
178
|
+
consumed(bytes: number): void;
|
|
179
|
+
/** Whether what was consumed since the last `progress` reaches half the window. */
|
|
180
|
+
get progressDue(): boolean;
|
|
181
|
+
/** The `progress` to send now, when one is due. Taking it makes the grant. */
|
|
182
|
+
takeProgress(): ForwardProgressFrame | undefined;
|
|
183
|
+
/** A `progress` naming exactly what was consumed, unless the last one already did. */
|
|
184
|
+
takeFinalProgress(): ForwardProgressFrame | undefined;
|
|
185
|
+
}
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* refused upgrade, so v2 accepts the upgrade first and answers with one text frame: `ready`, after
|
|
5
5
|
* which binary frames carry payload both ways, or `refused`, after which the server closes with
|
|
6
6
|
* {@link FORWARD_REFUSED_CLOSE_CODE}. A request without an upgrade gets 426 and is never dialled.
|
|
7
|
+
*
|
|
8
|
+
* After `ready`, each direction is bounded by {@link FORWARD_V2_WINDOW}. A receiver sends
|
|
9
|
+
* `progress` with the cumulative bytes and frames it has consumed; a sender keeps what it has sent
|
|
10
|
+
* within the last `progress` plus the window, and sends nothing before the first `progress`
|
|
11
|
+
* (`{0,0}`), which opens the receiver's window. A receiver resets a sender that goes past it.
|
|
12
|
+
* `fin` ends one direction with the totals sent; `reset` ends both at once. Core sends a `progress`
|
|
13
|
+
* naming exactly what reached the workspace before its `fin`, so a client can tell whether
|
|
14
|
+
* everything it wrote arrived. {@link ForwardFlow} keeps this ledger for either side.
|
|
7
15
|
*/
|
|
8
|
-
import { Schema } from "effect";
|
|
16
|
+
import { Option, Schema } from "effect";
|
|
9
17
|
export const FORWARD_V2_SUBPROTOCOL = "sealant.forward.v2";
|
|
10
18
|
/** The largest binary frame either side may send. */
|
|
11
19
|
export const FORWARD_V2_MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -54,7 +62,14 @@ export const FORWARD_REFUSAL_STATUS = {
|
|
|
54
62
|
"binding-unverifiable": 409,
|
|
55
63
|
"binding-unsupported": 409,
|
|
56
64
|
};
|
|
57
|
-
|
|
65
|
+
/**
|
|
66
|
+
* What a forward reaches: whatever holds the port (`port`), or only a process of the session that
|
|
67
|
+
* `session` names (`session`), as the workspace runtime verified when it connected.
|
|
68
|
+
*/
|
|
69
|
+
export const forwardBindingSchema = Schema.Union([
|
|
70
|
+
Schema.Struct({ kind: Schema.Literal("port") }),
|
|
71
|
+
Schema.Struct({ kind: Schema.Literal("session"), session: Schema.String }),
|
|
72
|
+
]);
|
|
58
73
|
export const forwardReadyFrameSchema = Schema.Struct({
|
|
59
74
|
t: Schema.Literal("ready"),
|
|
60
75
|
version: Schema.Literal(2),
|
|
@@ -81,3 +96,160 @@ export const forwardOpenFrameSchema = Schema.Union([
|
|
|
81
96
|
forwardReadyFrameSchema,
|
|
82
97
|
forwardRefusedFrameSchema,
|
|
83
98
|
]);
|
|
99
|
+
/** What each direction of a v2 forward may have in flight beyond what its receiver consumed. */
|
|
100
|
+
export const FORWARD_V2_WINDOW = { bytes: 256 * 1024, frames: 32 };
|
|
101
|
+
const counterSchema = Schema.String.check(Schema.isPattern(/^(?:0|[1-9][0-9]{0,14})$/));
|
|
102
|
+
export const forwardProgressFrameSchema = Schema.Struct({
|
|
103
|
+
t: Schema.Literal("progress"),
|
|
104
|
+
bytes: counterSchema,
|
|
105
|
+
frames: counterSchema,
|
|
106
|
+
});
|
|
107
|
+
export const forwardFinFrameSchema = Schema.Struct({
|
|
108
|
+
t: Schema.Literal("fin"),
|
|
109
|
+
bytes: counterSchema,
|
|
110
|
+
frames: counterSchema,
|
|
111
|
+
});
|
|
112
|
+
export const forwardResetReasonSchema = Schema.Literals([
|
|
113
|
+
"window-exceeded",
|
|
114
|
+
"protocol-violation",
|
|
115
|
+
"aborted",
|
|
116
|
+
"target-error",
|
|
117
|
+
"runtime-lost",
|
|
118
|
+
"owner-exited",
|
|
119
|
+
]);
|
|
120
|
+
/** Decoded with any `reason`, so a client keeps a reset from a Core newer than itself. */
|
|
121
|
+
export const forwardResetFrameSchema = Schema.Struct({
|
|
122
|
+
t: Schema.Literal("reset"),
|
|
123
|
+
reason: Schema.String,
|
|
124
|
+
});
|
|
125
|
+
/** The text frames either side sends after `ready`. */
|
|
126
|
+
export const forwardFlowFrameSchema = Schema.Union([
|
|
127
|
+
forwardProgressFrameSchema,
|
|
128
|
+
forwardFinFrameSchema,
|
|
129
|
+
forwardResetFrameSchema,
|
|
130
|
+
]);
|
|
131
|
+
const NOTHING = { bytes: 0, frames: 0 };
|
|
132
|
+
const plusFrame = (count, bytes) => ({
|
|
133
|
+
bytes: count.bytes + bytes,
|
|
134
|
+
frames: count.frames + 1,
|
|
135
|
+
});
|
|
136
|
+
const countOf = (frame) => ({
|
|
137
|
+
bytes: Number(frame.bytes),
|
|
138
|
+
frames: Number(frame.frames),
|
|
139
|
+
});
|
|
140
|
+
const sameCount = (a, b) => a.bytes === b.bytes && a.frames === b.frames;
|
|
141
|
+
const withinWindow = (count, granted) => count.bytes <= granted.bytes + FORWARD_V2_WINDOW.bytes &&
|
|
142
|
+
count.frames <= granted.frames + FORWARD_V2_WINDOW.frames;
|
|
143
|
+
const counted = (t, count) => ({
|
|
144
|
+
t,
|
|
145
|
+
bytes: String(count.bytes),
|
|
146
|
+
frames: String(count.frames),
|
|
147
|
+
});
|
|
148
|
+
const decodeFlowFrame = Schema.decodeUnknownOption(Schema.fromJsonString(forwardFlowFrameSchema));
|
|
149
|
+
const violation = (reason) => ({
|
|
150
|
+
kind: "violation",
|
|
151
|
+
reason,
|
|
152
|
+
});
|
|
153
|
+
/** A chunk as the binary frames it is sent in. */
|
|
154
|
+
export function* forwardFrames(chunk) {
|
|
155
|
+
for (let offset = 0; offset < chunk.byteLength; offset += FORWARD_V2_MAX_FRAME_BYTES) {
|
|
156
|
+
yield chunk.subarray(offset, offset + FORWARD_V2_MAX_FRAME_BYTES);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Both directions of an open v2 forward as one side sees them, with no I/O: what it sent against
|
|
161
|
+
* what its peer consumed, and what it received and consumed against what it granted. Core and the
|
|
162
|
+
* SDK each drive one from their socket.
|
|
163
|
+
*
|
|
164
|
+
* A grant is made when its `progress` is sent, so a side that holds a `progress` back while its
|
|
165
|
+
* socket is backed up also holds back its peer, and the next `progress` it sends names the latest
|
|
166
|
+
* count: acknowledgements never queue.
|
|
167
|
+
*/
|
|
168
|
+
export class ForwardFlow {
|
|
169
|
+
#sent = NOTHING;
|
|
170
|
+
#peerConsumed;
|
|
171
|
+
#received = NOTHING;
|
|
172
|
+
#consumed = NOTHING;
|
|
173
|
+
#granted;
|
|
174
|
+
#finReceived = false;
|
|
175
|
+
/** The first `progress`, which lets the peer start sending. */
|
|
176
|
+
open() {
|
|
177
|
+
this.#granted = this.#consumed;
|
|
178
|
+
return counted("progress", this.#consumed);
|
|
179
|
+
}
|
|
180
|
+
receive(frame) {
|
|
181
|
+
if (typeof frame !== "string") {
|
|
182
|
+
if (this.#finReceived)
|
|
183
|
+
return violation("protocol-violation");
|
|
184
|
+
this.#received = plusFrame(this.#received, frame.byteLength);
|
|
185
|
+
const granted = this.#granted;
|
|
186
|
+
return granted !== undefined && withinWindow(this.#received, granted)
|
|
187
|
+
? { kind: "data", data: frame }
|
|
188
|
+
: violation("window-exceeded");
|
|
189
|
+
}
|
|
190
|
+
const decoded = decodeFlowFrame(frame);
|
|
191
|
+
if (Option.isNone(decoded))
|
|
192
|
+
return violation("protocol-violation");
|
|
193
|
+
switch (decoded.value.t) {
|
|
194
|
+
case "progress":
|
|
195
|
+
return this.#acceptPeerProgress(countOf(decoded.value))
|
|
196
|
+
? { kind: "progress" }
|
|
197
|
+
: violation("protocol-violation");
|
|
198
|
+
case "fin":
|
|
199
|
+
if (this.#finReceived || !sameCount(countOf(decoded.value), this.#received)) {
|
|
200
|
+
return violation("protocol-violation");
|
|
201
|
+
}
|
|
202
|
+
this.#finReceived = true;
|
|
203
|
+
return { kind: "fin" };
|
|
204
|
+
case "reset":
|
|
205
|
+
return { kind: "reset", reason: decoded.value.reason };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
#acceptPeerProgress(consumed) {
|
|
209
|
+
const previous = this.#peerConsumed ?? NOTHING;
|
|
210
|
+
if (consumed.bytes > this.#sent.bytes ||
|
|
211
|
+
consumed.frames > this.#sent.frames ||
|
|
212
|
+
consumed.bytes < previous.bytes ||
|
|
213
|
+
consumed.frames < previous.frames) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
this.#peerConsumed = consumed;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
/** Whether one frame of `bytes` fits in what the peer has granted. */
|
|
220
|
+
canSend(bytes) {
|
|
221
|
+
const consumed = this.#peerConsumed;
|
|
222
|
+
return consumed !== undefined && withinWindow(plusFrame(this.#sent, bytes), consumed);
|
|
223
|
+
}
|
|
224
|
+
sent(bytes) {
|
|
225
|
+
this.#sent = plusFrame(this.#sent, bytes);
|
|
226
|
+
}
|
|
227
|
+
/** Whether the peer has said it consumed everything this side sent. */
|
|
228
|
+
get delivered() {
|
|
229
|
+
return this.#peerConsumed !== undefined && sameCount(this.#peerConsumed, this.#sent);
|
|
230
|
+
}
|
|
231
|
+
fin() {
|
|
232
|
+
return counted("fin", this.#sent);
|
|
233
|
+
}
|
|
234
|
+
consumed(bytes) {
|
|
235
|
+
this.#consumed = plusFrame(this.#consumed, bytes);
|
|
236
|
+
}
|
|
237
|
+
/** Whether what was consumed since the last `progress` reaches half the window. */
|
|
238
|
+
get progressDue() {
|
|
239
|
+
const granted = this.#granted ?? NOTHING;
|
|
240
|
+
return (this.#consumed.bytes - granted.bytes >= FORWARD_V2_WINDOW.bytes / 2 ||
|
|
241
|
+
this.#consumed.frames - granted.frames >= FORWARD_V2_WINDOW.frames / 2);
|
|
242
|
+
}
|
|
243
|
+
/** The `progress` to send now, when one is due. Taking it makes the grant. */
|
|
244
|
+
takeProgress() {
|
|
245
|
+
if (!this.progressDue)
|
|
246
|
+
return undefined;
|
|
247
|
+
return this.open();
|
|
248
|
+
}
|
|
249
|
+
/** A `progress` naming exactly what was consumed, unless the last one already did. */
|
|
250
|
+
takeFinalProgress() {
|
|
251
|
+
if (this.#granted !== undefined && sameCount(this.#granted, this.#consumed))
|
|
252
|
+
return undefined;
|
|
253
|
+
return this.open();
|
|
254
|
+
}
|
|
255
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/api-contracts-next",
|
|
3
|
-
"version": "0.40.0-next.
|
|
3
|
+
"version": "0.40.0-next.763",
|
|
4
4
|
"description": "Contract-first HTTP API definitions for Sealant services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"effect": "^4.0.5"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "c741e10743b2c553b526f93565c256fe8fa4bd8b",
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
}
|