@moqtap/codec 0.8.1 → 0.9.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/CHANGELOG.md +49 -0
- package/README.md +16 -4
- package/dist/chunk-BMJMY6FX.cjs +868 -0
- package/dist/chunk-MAXEHYVB.cjs +2362 -0
- package/dist/{chunk-3YAR3I3S.js → chunk-Q7NSUXXB.js} +3 -1
- package/dist/{chunk-VWXUT5R2.cjs → chunk-TVSFDQYE.cjs} +3 -1
- package/dist/chunk-WX5PU7HF.js +868 -0
- package/dist/chunk-YJGOD3Y4.js +2362 -0
- package/dist/{codec-BLYAy2OP.d.ts → codec--CVeqGQv.d.ts} +1 -1
- package/dist/{codec-DujHf55Z.d.ts → codec-BJ7BHuiZ.d.ts} +1 -1
- package/dist/{codec-D47r_-Rl.d.ts → codec-BKsVu3Fr.d.ts} +1 -1
- package/dist/{codec-BNS3gS77.d.ts → codec-BMwgZIgG.d.ts} +1 -1
- package/dist/{codec-CI0BjmHc.d.ts → codec-BNy5syCU.d.ts} +1 -1
- package/dist/{codec-v6Vloc7H.d.ts → codec-BQa6Qsav.d.ts} +1 -1
- package/dist/codec-Bym-K2q2.d.cts +49 -0
- package/dist/{codec-D2JbI2tp.d.ts → codec-C006F75E.d.ts} +1 -1
- package/dist/codec-C5evFHL-.d.ts +49 -0
- package/dist/{codec-DQWQSam3.d.cts → codec-C6Mx4bwz.d.cts} +1 -1
- package/dist/{codec-DL0njGkL.d.ts → codec-CA6nbbEk.d.ts} +1 -1
- package/dist/{codec-D89DWXbv.d.cts → codec-CSBFdPgP.d.cts} +1 -1
- package/dist/{codec-DvcJgYKq.d.cts → codec-CXCjya_G.d.cts} +1 -1
- package/dist/{codec-hvkJfCF5.d.cts → codec-C_xxsbKy.d.cts} +1 -1
- package/dist/{codec-BOyrk_C9.d.cts → codec-Cfo_z5ET.d.cts} +1 -1
- package/dist/{codec-xeuGZY77.d.cts → codec-CgZdadPt.d.cts} +2 -2
- package/dist/{codec-WwwUipaU.d.ts → codec-Cl73EfRW.d.ts} +1 -1
- package/dist/{codec-BOtv3R5j.d.cts → codec-CtvFK4aV.d.cts} +1 -1
- package/dist/{codec-DAePx2NL.d.ts → codec-CviAa4lp.d.ts} +2 -2
- package/dist/{codec-DJmV04i0.d.ts → codec-D4RvFQmu.d.ts} +1 -1
- package/dist/{codec-cou9XBMF.d.cts → codec-DAyiF5sq.d.cts} +1 -1
- package/dist/{codec-CyApDKwf.d.cts → codec-DJmqoIsp.d.cts} +1 -1
- package/dist/{codec-NMsTK0GX.d.cts → codec-DchsPfr3.d.cts} +1 -1
- package/dist/{codec-DplRgIKq.d.ts → codec-Dl_QIXMz.d.ts} +1 -1
- package/dist/{codec-N3g1lxQQ.d.cts → codec-DzLvG8Wb.d.cts} +1 -1
- package/dist/{codec-D95gT98n.d.cts → codec-DzkdW4kO.d.cts} +1 -1
- package/dist/{codec-CWj5iTxA.d.ts → codec-O-1Js7h4.d.ts} +1 -1
- package/dist/{codec-BqqoYdh_.d.cts → codec-vddqy0KY.d.cts} +1 -1
- package/dist/draft07-session.d.cts +3 -3
- package/dist/draft07-session.d.ts +3 -3
- package/dist/draft07.d.cts +3 -3
- package/dist/draft07.d.ts +3 -3
- package/dist/draft08-session.d.cts +3 -3
- package/dist/draft08-session.d.ts +3 -3
- package/dist/draft08.d.cts +4 -4
- package/dist/draft08.d.ts +4 -4
- package/dist/draft09-session.d.cts +3 -3
- package/dist/draft09-session.d.ts +3 -3
- package/dist/draft09.d.cts +4 -4
- package/dist/draft09.d.ts +4 -4
- package/dist/draft10-session.d.cts +3 -3
- package/dist/draft10-session.d.ts +3 -3
- package/dist/draft10.d.cts +4 -4
- package/dist/draft10.d.ts +4 -4
- package/dist/draft11-session.d.cts +3 -3
- package/dist/draft11-session.d.ts +3 -3
- package/dist/draft11.d.cts +4 -4
- package/dist/draft11.d.ts +4 -4
- package/dist/draft12-session.d.cts +3 -3
- package/dist/draft12-session.d.ts +3 -3
- package/dist/draft12.d.cts +4 -4
- package/dist/draft12.d.ts +4 -4
- package/dist/draft13-session.d.cts +3 -3
- package/dist/draft13-session.d.ts +3 -3
- package/dist/draft13.d.cts +4 -4
- package/dist/draft13.d.ts +4 -4
- package/dist/draft14-session.d.cts +3 -3
- package/dist/draft14-session.d.ts +3 -3
- package/dist/draft14.d.cts +4 -4
- package/dist/draft14.d.ts +4 -4
- package/dist/draft15-session.d.cts +3 -3
- package/dist/draft15-session.d.ts +3 -3
- package/dist/draft15.d.cts +4 -4
- package/dist/draft15.d.ts +4 -4
- package/dist/draft16-session.d.cts +3 -3
- package/dist/draft16-session.d.ts +3 -3
- package/dist/draft16.d.cts +4 -4
- package/dist/draft16.d.ts +4 -4
- package/dist/draft17-session.d.cts +3 -3
- package/dist/draft17-session.d.ts +3 -3
- package/dist/draft17.d.cts +3 -3
- package/dist/draft17.d.ts +3 -3
- package/dist/draft18-session.d.cts +3 -3
- package/dist/draft18-session.d.ts +3 -3
- package/dist/draft18.d.cts +3 -3
- package/dist/draft18.d.ts +3 -3
- package/dist/draft19-session.d.cts +3 -3
- package/dist/draft19-session.d.ts +3 -3
- package/dist/draft19.cjs +2 -2
- package/dist/draft19.d.cts +3 -3
- package/dist/draft19.d.ts +3 -3
- package/dist/draft19.js +1 -1
- package/dist/draft20-session.cjs +8 -0
- package/dist/draft20-session.d.cts +195 -0
- package/dist/draft20-session.d.ts +195 -0
- package/dist/draft20-session.js +8 -0
- package/dist/draft20.cjs +217 -0
- package/dist/draft20.d.cts +175 -0
- package/dist/draft20.d.ts +175 -0
- package/dist/draft20.js +217 -0
- package/dist/index.cjs +9 -3
- package/dist/index.d.cts +32 -17
- package/dist/index.d.ts +32 -17
- package/dist/index.js +8 -2
- package/dist/{session-types-KsqyVp6y.d.cts → session-types-B9idsih-.d.cts} +13 -1
- package/dist/{session-types-C0l_-Ups.d.ts → session-types-Dn7b3RZM.d.ts} +13 -1
- package/dist/session.cjs +5 -1
- package/dist/session.d.cts +5 -3
- package/dist/session.d.ts +5 -3
- package/dist/session.js +5 -1
- package/dist/types-CWCQwuvy.d.cts +359 -0
- package/dist/types-CWCQwuvy.d.ts +359 -0
- package/dist/{types-CdCSIfvI.d.cts → types-DG5_iKuv.d.cts} +1 -1
- package/dist/{types-CdCSIfvI.d.ts → types-DG5_iKuv.d.ts} +1 -1
- package/package.json +16 -3
- package/src/core/session-types.ts +7 -0
- package/src/core/types.ts +1 -0
- package/src/drafts/draft19/codec.ts +4 -2
- package/src/drafts/draft20/codec.ts +1737 -0
- package/src/drafts/draft20/data-streams.ts +1291 -0
- package/src/drafts/draft20/error-codes.ts +135 -0
- package/src/drafts/draft20/index.ts +165 -0
- package/src/drafts/draft20/messages.ts +68 -0
- package/src/drafts/draft20/rules.ts +107 -0
- package/src/drafts/draft20/session-fsm.ts +1063 -0
- package/src/drafts/draft20/session.ts +21 -0
- package/src/drafts/draft20/types.ts +455 -0
- package/src/index.ts +20 -2
- package/src/session.ts +4 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
// src/drafts/draft20/rules.ts
|
|
2
|
+
var CONTROL_MESSAGES = /* @__PURE__ */ new Set([
|
|
3
|
+
"setup",
|
|
4
|
+
"subscribe",
|
|
5
|
+
"subscribe_ok",
|
|
6
|
+
"request_update",
|
|
7
|
+
"publish_state_notify",
|
|
8
|
+
"publish",
|
|
9
|
+
"publish_done",
|
|
10
|
+
"publish_namespace",
|
|
11
|
+
"namespace",
|
|
12
|
+
"namespace_done",
|
|
13
|
+
"subscribe_namespace",
|
|
14
|
+
"subscribe_tracks",
|
|
15
|
+
"publish_skipped",
|
|
16
|
+
"fetch",
|
|
17
|
+
"fetch_ok",
|
|
18
|
+
"track_status",
|
|
19
|
+
"request_ok",
|
|
20
|
+
"request_error",
|
|
21
|
+
"goaway"
|
|
22
|
+
]);
|
|
23
|
+
var CLIENT_ONLY_MESSAGES = /* @__PURE__ */ new Set();
|
|
24
|
+
var SERVER_ONLY_MESSAGES = /* @__PURE__ */ new Set();
|
|
25
|
+
var BIDIRECTIONAL_MESSAGES = /* @__PURE__ */ new Set([
|
|
26
|
+
"setup",
|
|
27
|
+
"subscribe",
|
|
28
|
+
"subscribe_ok",
|
|
29
|
+
"request_update",
|
|
30
|
+
"publish_state_notify",
|
|
31
|
+
"publish",
|
|
32
|
+
"publish_done",
|
|
33
|
+
"publish_namespace",
|
|
34
|
+
"namespace",
|
|
35
|
+
"namespace_done",
|
|
36
|
+
"subscribe_namespace",
|
|
37
|
+
"subscribe_tracks",
|
|
38
|
+
"publish_skipped",
|
|
39
|
+
"fetch",
|
|
40
|
+
"fetch_ok",
|
|
41
|
+
"track_status",
|
|
42
|
+
"request_ok",
|
|
43
|
+
"request_error",
|
|
44
|
+
"goaway"
|
|
45
|
+
]);
|
|
46
|
+
var REQUEST_STREAM_OPENERS = /* @__PURE__ */ new Set([
|
|
47
|
+
"subscribe",
|
|
48
|
+
"publish",
|
|
49
|
+
"fetch",
|
|
50
|
+
"track_status",
|
|
51
|
+
"publish_namespace",
|
|
52
|
+
"subscribe_namespace",
|
|
53
|
+
"subscribe_tracks"
|
|
54
|
+
]);
|
|
55
|
+
function getLegalOutgoing(phase, _role) {
|
|
56
|
+
const legal = /* @__PURE__ */ new Set();
|
|
57
|
+
switch (phase) {
|
|
58
|
+
case "idle":
|
|
59
|
+
legal.add("setup");
|
|
60
|
+
break;
|
|
61
|
+
case "setup":
|
|
62
|
+
legal.add("setup");
|
|
63
|
+
break;
|
|
64
|
+
case "ready": {
|
|
65
|
+
for (const msg of BIDIRECTIONAL_MESSAGES) {
|
|
66
|
+
legal.add(msg);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "draining":
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return legal;
|
|
74
|
+
}
|
|
75
|
+
function getLegalIncoming(phase, role) {
|
|
76
|
+
const remoteRole = role === "client" ? "server" : "client";
|
|
77
|
+
return getLegalOutgoing(phase, remoteRole);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/drafts/draft20/session-fsm.ts
|
|
81
|
+
function violation(code, message, currentPhase, offendingMessage) {
|
|
82
|
+
return { code, message, currentPhase, offendingMessage };
|
|
83
|
+
}
|
|
84
|
+
function hasFillParameters(params) {
|
|
85
|
+
return params?.fill_parameters;
|
|
86
|
+
}
|
|
87
|
+
var Draft20SessionFSM = class {
|
|
88
|
+
_phase = "idle";
|
|
89
|
+
_role;
|
|
90
|
+
_setupDirection = null;
|
|
91
|
+
_subscriptions = /* @__PURE__ */ new Map();
|
|
92
|
+
_subscriptionMeta = /* @__PURE__ */ new Map();
|
|
93
|
+
_publishes = /* @__PURE__ */ new Map();
|
|
94
|
+
_fetches = /* @__PURE__ */ new Map();
|
|
95
|
+
_requests = /* @__PURE__ */ new Map();
|
|
96
|
+
_requestIds = /* @__PURE__ */ new Set();
|
|
97
|
+
_pendingSubscribes = [];
|
|
98
|
+
_pendingPublishes = [];
|
|
99
|
+
_pendingFetches = [];
|
|
100
|
+
/**
|
|
101
|
+
* The peer's MAX_REQUEST_UPDATES (Setup Option 0x08), learned from its SETUP.
|
|
102
|
+
* 0 means the peer does not limit REQUEST_UPDATE concurrency, and 0 is also
|
|
103
|
+
* the default when the option is absent.
|
|
104
|
+
*/
|
|
105
|
+
_peerMaxRequestUpdates = 0n;
|
|
106
|
+
/** The local endpoint's own advertised MAX_REQUEST_UPDATES. */
|
|
107
|
+
_localMaxRequestUpdates = 0n;
|
|
108
|
+
constructor(role) {
|
|
109
|
+
this._role = role;
|
|
110
|
+
}
|
|
111
|
+
get phase() {
|
|
112
|
+
return this._phase;
|
|
113
|
+
}
|
|
114
|
+
get role() {
|
|
115
|
+
return this._role;
|
|
116
|
+
}
|
|
117
|
+
get subscriptions() {
|
|
118
|
+
return this._subscriptions;
|
|
119
|
+
}
|
|
120
|
+
get announces() {
|
|
121
|
+
return /* @__PURE__ */ new Map();
|
|
122
|
+
}
|
|
123
|
+
get publishes() {
|
|
124
|
+
return this._publishes;
|
|
125
|
+
}
|
|
126
|
+
get fetches() {
|
|
127
|
+
return this._fetches;
|
|
128
|
+
}
|
|
129
|
+
get legalOutgoing() {
|
|
130
|
+
return getLegalOutgoing(this._phase, this._role);
|
|
131
|
+
}
|
|
132
|
+
get legalIncoming() {
|
|
133
|
+
return getLegalIncoming(this._phase, this._role);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Every fill fetch stream currently tracked, across all subscriptions, keyed
|
|
137
|
+
* by the Request ID that opened it.
|
|
138
|
+
*
|
|
139
|
+
* Fill fetch streams are unidirectional and carry no control messages, so
|
|
140
|
+
* nothing here is inferred from a message alone; the transport events that
|
|
141
|
+
* end one are reported with {@link closeFillFetchStream}.
|
|
142
|
+
*/
|
|
143
|
+
get fillFetchStreams() {
|
|
144
|
+
const all = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const meta of this._subscriptionMeta.values()) {
|
|
146
|
+
for (const [requestId, fill] of meta.fills) all.set(requestId, fill);
|
|
147
|
+
}
|
|
148
|
+
return all;
|
|
149
|
+
}
|
|
150
|
+
/** The fill fetch streams belonging to one subscription. */
|
|
151
|
+
fillFetchStreamsFor(subscribeId) {
|
|
152
|
+
return this._subscriptionMeta.get(subscribeId)?.fills ?? /* @__PURE__ */ new Map();
|
|
153
|
+
}
|
|
154
|
+
/** A subscription's current Forward State, or undefined if it is unknown here. */
|
|
155
|
+
forwardStateOf(subscribeId) {
|
|
156
|
+
return this._subscriptionMeta.get(subscribeId)?.forwardState;
|
|
157
|
+
}
|
|
158
|
+
/** What kind of request a Request ID names, or undefined if it names none. */
|
|
159
|
+
requestKindOf(requestId) {
|
|
160
|
+
return this._requests.get(requestId)?.kind;
|
|
161
|
+
}
|
|
162
|
+
checkDuplicateRequestId(requestId, msgType) {
|
|
163
|
+
if (this._requestIds.has(requestId)) {
|
|
164
|
+
return violation(
|
|
165
|
+
"DUPLICATE_REQUEST_ID",
|
|
166
|
+
`Request ID ${requestId} already in use`,
|
|
167
|
+
this._phase,
|
|
168
|
+
msgType
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
validateOutgoing(message) {
|
|
174
|
+
if (!this.legalOutgoing.has(message.type)) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
violation: violation(
|
|
178
|
+
this._phase === "idle" || this._phase === "setup" ? "MESSAGE_BEFORE_SETUP" : "UNEXPECTED_MESSAGE",
|
|
179
|
+
`Cannot send ${message.type} in phase ${this._phase}`,
|
|
180
|
+
this._phase,
|
|
181
|
+
message.type
|
|
182
|
+
)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { ok: true };
|
|
186
|
+
}
|
|
187
|
+
receive(message) {
|
|
188
|
+
return this.applyTransition(message, "inbound", void 0);
|
|
189
|
+
}
|
|
190
|
+
send(message) {
|
|
191
|
+
return this.applyTransition(message, "outbound", void 0);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Receive a message that arrived on a known request stream.
|
|
195
|
+
*
|
|
196
|
+
* Prefer this over {@link receive} for anything that carries no Request ID of
|
|
197
|
+
* its own — SUBSCRIBE_OK, REQUEST_OK, REQUEST_ERROR, PUBLISH_DONE, FETCH_OK
|
|
198
|
+
* and PUBLISH_STATE_NOTIFY. Without the stream, the request-kind and
|
|
199
|
+
* direction rules of Section 10.10 cannot be checked at all, and the
|
|
200
|
+
* response-matching below falls back to FIFO guessing.
|
|
201
|
+
*/
|
|
202
|
+
receiveOn(requestId, message) {
|
|
203
|
+
return this.applyTransition(message, "inbound", requestId);
|
|
204
|
+
}
|
|
205
|
+
/** Send a message on a known request stream. See {@link receiveOn}. */
|
|
206
|
+
sendOn(requestId, message) {
|
|
207
|
+
return this.applyTransition(message, "outbound", requestId);
|
|
208
|
+
}
|
|
209
|
+
applyTransition(message, direction, streamRequestId) {
|
|
210
|
+
const sideEffects = [];
|
|
211
|
+
switch (message.type) {
|
|
212
|
+
case "setup":
|
|
213
|
+
return this.handleSetup(message, direction);
|
|
214
|
+
case "goaway":
|
|
215
|
+
return this.handleGoAway(message, sideEffects);
|
|
216
|
+
case "subscribe":
|
|
217
|
+
return this.handleSubscribe(message, direction, sideEffects);
|
|
218
|
+
case "subscribe_ok":
|
|
219
|
+
return this.handleSubscribeOk(streamRequestId, sideEffects);
|
|
220
|
+
case "request_update":
|
|
221
|
+
return this.handleRequestUpdate(message, direction, streamRequestId, sideEffects);
|
|
222
|
+
case "publish_state_notify":
|
|
223
|
+
return this.handlePublishStateNotify(direction, streamRequestId, sideEffects);
|
|
224
|
+
case "publish":
|
|
225
|
+
return this.handlePublish(message, direction, sideEffects);
|
|
226
|
+
case "publish_done":
|
|
227
|
+
return this.handlePublishDone(streamRequestId, sideEffects);
|
|
228
|
+
case "fetch":
|
|
229
|
+
return this.handleFetch(message, direction, sideEffects);
|
|
230
|
+
case "fetch_ok":
|
|
231
|
+
return this.handleFetchOk(streamRequestId, sideEffects);
|
|
232
|
+
case "request_ok":
|
|
233
|
+
return this.handleRequestOk(streamRequestId, sideEffects);
|
|
234
|
+
case "request_error":
|
|
235
|
+
return this.handleRequestError(streamRequestId, sideEffects);
|
|
236
|
+
case "track_status":
|
|
237
|
+
return this.handleGenericRequest(message, "track_status", direction, sideEffects);
|
|
238
|
+
case "publish_namespace":
|
|
239
|
+
return this.handleGenericRequest(message, "publish_namespace", direction, sideEffects);
|
|
240
|
+
case "subscribe_namespace":
|
|
241
|
+
return this.handleGenericRequest(message, "subscribe_namespace", direction, sideEffects);
|
|
242
|
+
case "subscribe_tracks":
|
|
243
|
+
return this.handleGenericRequest(message, "subscribe_tracks", direction, sideEffects);
|
|
244
|
+
default:
|
|
245
|
+
return this.handleReadyPhaseMessage(message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
handleSetup(message, direction) {
|
|
249
|
+
const setup = message;
|
|
250
|
+
const advertised = setup.options.max_request_updates ?? 0n;
|
|
251
|
+
if (direction === "inbound") this._peerMaxRequestUpdates = advertised;
|
|
252
|
+
else this._localMaxRequestUpdates = advertised;
|
|
253
|
+
if (this._phase === "idle") {
|
|
254
|
+
this._setupDirection = direction;
|
|
255
|
+
this._phase = "setup";
|
|
256
|
+
return { ok: true, phase: this._phase, sideEffects: [] };
|
|
257
|
+
}
|
|
258
|
+
if (this._phase === "setup") {
|
|
259
|
+
if (direction === this._setupDirection) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
violation: violation(
|
|
263
|
+
"SETUP_VIOLATION",
|
|
264
|
+
`Second SETUP must be ${this._setupDirection === "inbound" ? "outbound" : "inbound"}, got ${direction}`,
|
|
265
|
+
this._phase,
|
|
266
|
+
"setup"
|
|
267
|
+
)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
this._phase = "ready";
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
phase: this._phase,
|
|
274
|
+
sideEffects: [{ type: "session-ready" }]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
violation: violation(
|
|
280
|
+
"SETUP_VIOLATION",
|
|
281
|
+
"SETUP not valid in current phase",
|
|
282
|
+
this._phase,
|
|
283
|
+
"setup"
|
|
284
|
+
)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
handleGoAway(message, sideEffects) {
|
|
288
|
+
if (this._phase !== "ready" && this._phase !== "draining") {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
violation: violation(
|
|
292
|
+
"UNEXPECTED_MESSAGE",
|
|
293
|
+
`GOAWAY not valid in phase ${this._phase}`,
|
|
294
|
+
this._phase,
|
|
295
|
+
"goaway"
|
|
296
|
+
)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
this._phase = "draining";
|
|
300
|
+
const goaway = message;
|
|
301
|
+
sideEffects.push({
|
|
302
|
+
type: "session-draining",
|
|
303
|
+
goAwayUri: goaway.new_session_uri
|
|
304
|
+
});
|
|
305
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
306
|
+
}
|
|
307
|
+
requireReady(msgType) {
|
|
308
|
+
if (this._phase !== "ready" && this._phase !== "draining") {
|
|
309
|
+
return violation(
|
|
310
|
+
this._phase === "idle" || this._phase === "setup" ? "MESSAGE_BEFORE_SETUP" : "UNEXPECTED_MESSAGE",
|
|
311
|
+
`${msgType} requires ready phase, current: ${this._phase}`,
|
|
312
|
+
this._phase,
|
|
313
|
+
msgType
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
handleSubscribe(message, direction, sideEffects) {
|
|
319
|
+
const err = this.requireReady(message.type);
|
|
320
|
+
if (err) return { ok: false, violation: err };
|
|
321
|
+
const sub = message;
|
|
322
|
+
const dupErr = this.checkDuplicateRequestId(sub.request_id, message.type);
|
|
323
|
+
if (dupErr) return { ok: false, violation: dupErr };
|
|
324
|
+
this._requestIds.add(sub.request_id);
|
|
325
|
+
const publisherSide = direction === "outbound" ? "remote" : "local";
|
|
326
|
+
this._subscriptions.set(sub.request_id, {
|
|
327
|
+
subscribeId: sub.request_id,
|
|
328
|
+
phase: "pending",
|
|
329
|
+
trackNamespace: sub.track_namespace,
|
|
330
|
+
trackName: sub.track_name
|
|
331
|
+
});
|
|
332
|
+
const forwardState = sub.parameters.forward === 0n ? 0 : 1;
|
|
333
|
+
this._subscriptionMeta.set(sub.request_id, {
|
|
334
|
+
publisherSide,
|
|
335
|
+
forwardState,
|
|
336
|
+
fills: /* @__PURE__ */ new Map()
|
|
337
|
+
});
|
|
338
|
+
this._requests.set(sub.request_id, {
|
|
339
|
+
kind: "subscription",
|
|
340
|
+
publisherSide,
|
|
341
|
+
outstandingUpdates: 0
|
|
342
|
+
});
|
|
343
|
+
this._pendingSubscribes.push(sub.request_id);
|
|
344
|
+
this.applyFillParameters(sub.request_id, sub.request_id, sub.parameters, sideEffects);
|
|
345
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Open a fill fetch stream if this message asked for one — draft-20 Sections
|
|
349
|
+
* 5.1.3 and 5.1.3.1, both new in draft-20.
|
|
350
|
+
*
|
|
351
|
+
* Three rules, all of which the draft states as bullets and all of which are
|
|
352
|
+
* easy to get wrong by analogy with draft-19's Joining FETCH:
|
|
353
|
+
*
|
|
354
|
+
* 1. Presence of FILL_PARAMETERS is the request. There is no separate
|
|
355
|
+
* message and no Fetch Type; an empty FILL_PARAMETERS still opens a
|
|
356
|
+
* stream, with every setting inherited from the subscription.
|
|
357
|
+
* 2. It only opens one "while Forward State is 1". FILL_PARAMETERS carried
|
|
358
|
+
* while Forward State is 0 opens nothing, and later transitioning to
|
|
359
|
+
* Forward State 1 without re-sending FILL_PARAMETERS opens nothing
|
|
360
|
+
* either — so this is checked at the moment the message is processed and
|
|
361
|
+
* never retried.
|
|
362
|
+
* 3. Streams accumulate. The new one is keyed by the opening message's
|
|
363
|
+
* Request ID and does not disturb any stream already open on the same
|
|
364
|
+
* subscription.
|
|
365
|
+
*
|
|
366
|
+
* The opened stream has no FETCH_OK: per DECISIONS.md D7 (SPEC-DELTA Section
|
|
367
|
+
* 11 Q9) there is no End Location and no End Of Track for a fill, and stream
|
|
368
|
+
* FIN is the only completion signal. Nothing here synthesizes one, and the
|
|
369
|
+
* stream is deliberately absent from {@link fetches}.
|
|
370
|
+
*/
|
|
371
|
+
applyFillParameters(subscribeId, openingRequestId, params, sideEffects) {
|
|
372
|
+
if (hasFillParameters(params) === void 0) return;
|
|
373
|
+
const meta = this._subscriptionMeta.get(subscribeId);
|
|
374
|
+
if (meta === void 0) return;
|
|
375
|
+
if (meta.forwardState !== 1) return;
|
|
376
|
+
meta.fills.set(openingRequestId, {
|
|
377
|
+
requestId: openingRequestId,
|
|
378
|
+
subscribeId,
|
|
379
|
+
phase: "open"
|
|
380
|
+
});
|
|
381
|
+
sideEffects.push({
|
|
382
|
+
type: "fill-fetch-stream-opened",
|
|
383
|
+
subscribeId,
|
|
384
|
+
requestId: openingRequestId
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Report the transport event that ended a fill fetch stream.
|
|
389
|
+
*
|
|
390
|
+
* A fill fetch stream carries no control message of its own, so its end is
|
|
391
|
+
* never inferable from the message sequence:
|
|
392
|
+
*
|
|
393
|
+
* - `'complete'` — the publisher FINed the stream. Section 5.1.3.1 makes
|
|
394
|
+
* that the ONLY completion signal; there is no FETCH_OK to read an End
|
|
395
|
+
* Location or an End Of Track flag from (DECISIONS.md D7 / Q9).
|
|
396
|
+
* - `'reset'` — the publisher reset it, which is how a fill failure is
|
|
397
|
+
* signalled, "because there is no REQUEST_ERROR associated with a fill
|
|
398
|
+
* fetch stream".
|
|
399
|
+
* - `'cancelled'` — the subscriber sent STOP_SENDING.
|
|
400
|
+
*
|
|
401
|
+
* Either way "Resetting or cancelling a fill fetch stream, by either
|
|
402
|
+
* endpoint, does not affect the subscription, which continues to deliver
|
|
403
|
+
* objects using subscribe subgroups and datagrams", so the subscription's
|
|
404
|
+
* own state is untouched here.
|
|
405
|
+
*/
|
|
406
|
+
closeFillFetchStream(requestId, phase) {
|
|
407
|
+
for (const meta of this._subscriptionMeta.values()) {
|
|
408
|
+
const fill = meta.fills.get(requestId);
|
|
409
|
+
if (fill === void 0) continue;
|
|
410
|
+
meta.fills.set(requestId, { ...fill, phase });
|
|
411
|
+
return [
|
|
412
|
+
{
|
|
413
|
+
type: "fill-fetch-stream-closed",
|
|
414
|
+
subscribeId: fill.subscribeId,
|
|
415
|
+
requestId,
|
|
416
|
+
reason: phase
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
}
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
handleSubscribeOk(streamRequestId, sideEffects) {
|
|
423
|
+
const err = this.requireReady("subscribe_ok");
|
|
424
|
+
if (err) return { ok: false, violation: err };
|
|
425
|
+
const requestId = streamRequestId ?? this._pendingSubscribes.shift();
|
|
426
|
+
if (requestId === void 0) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
violation: violation(
|
|
430
|
+
"UNEXPECTED_MESSAGE",
|
|
431
|
+
"SUBSCRIBE_OK with no pending subscribe",
|
|
432
|
+
this._phase,
|
|
433
|
+
"subscribe_ok"
|
|
434
|
+
)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (streamRequestId !== void 0) {
|
|
438
|
+
this.dropPending(this._pendingSubscribes, streamRequestId);
|
|
439
|
+
}
|
|
440
|
+
const existing = this._subscriptions.get(requestId);
|
|
441
|
+
if (existing && existing.phase === "pending") {
|
|
442
|
+
this._subscriptions.set(requestId, { ...existing, phase: "active" });
|
|
443
|
+
sideEffects.push({
|
|
444
|
+
type: "subscription-activated",
|
|
445
|
+
subscribeId: requestId
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
449
|
+
}
|
|
450
|
+
handleRequestUpdate(message, direction, streamRequestId, sideEffects) {
|
|
451
|
+
const err = this.requireReady(message.type);
|
|
452
|
+
if (err) return { ok: false, violation: err };
|
|
453
|
+
const update = message;
|
|
454
|
+
const dupErr = this.checkDuplicateRequestId(update.request_id, message.type);
|
|
455
|
+
if (dupErr) return { ok: false, violation: dupErr };
|
|
456
|
+
this._requestIds.add(update.request_id);
|
|
457
|
+
const targetId = streamRequestId;
|
|
458
|
+
const target = targetId !== void 0 ? this._requests.get(targetId) : void 0;
|
|
459
|
+
if (target !== void 0) {
|
|
460
|
+
const limit = direction === "inbound" ? this._localMaxRequestUpdates : this._peerMaxRequestUpdates;
|
|
461
|
+
if (limit > 0n && BigInt(target.outstandingUpdates) >= limit) {
|
|
462
|
+
return {
|
|
463
|
+
ok: false,
|
|
464
|
+
violation: violation(
|
|
465
|
+
"STATE_VIOLATION",
|
|
466
|
+
`REQUEST_UPDATE on request ${targetId} exceeds MAX_REQUEST_UPDATES (${limit})`,
|
|
467
|
+
this._phase,
|
|
468
|
+
"request_update"
|
|
469
|
+
)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
target.outstandingUpdates += 1;
|
|
473
|
+
}
|
|
474
|
+
if (targetId !== void 0 && target !== void 0) {
|
|
475
|
+
if (target.kind !== "subscription") {
|
|
476
|
+
if (hasFillParameters(update.parameters) !== void 0) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
violation: violation(
|
|
480
|
+
"STATE_VIOLATION",
|
|
481
|
+
`FILL_PARAMETERS may only appear in a REQUEST_UPDATE for a subscription, but request ${targetId} is a ${target.kind}`,
|
|
482
|
+
this._phase,
|
|
483
|
+
"request_update"
|
|
484
|
+
)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
const meta = this._subscriptionMeta.get(targetId);
|
|
489
|
+
if (meta !== void 0) {
|
|
490
|
+
if (update.parameters.forward !== void 0) {
|
|
491
|
+
meta.forwardState = update.parameters.forward === 0n ? 0 : 1;
|
|
492
|
+
}
|
|
493
|
+
this.applyFillParameters(targetId, update.request_id, update.parameters, sideEffects);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* PUBLISH_STATE_NOTIFY — draft-20 Section 10.10, new in draft-20.
|
|
501
|
+
*
|
|
502
|
+
* Three properties set it apart from every other control message, and all
|
|
503
|
+
* three are enforced here:
|
|
504
|
+
*
|
|
505
|
+
* - **Unilateral.** "The receiver does not respond with REQUEST_OK or
|
|
506
|
+
* REQUEST_ERROR", so nothing is queued as pending and nothing is
|
|
507
|
+
* dequeued. A REQUEST_OK arriving later belongs to some other request.
|
|
508
|
+
* - **Not rate-limited.** "The message is not subject to the
|
|
509
|
+
* MAX_REQUEST_UPDATES limit", so `outstandingUpdates` is deliberately not
|
|
510
|
+
* touched — that counter exists only for REQUEST_UPDATE.
|
|
511
|
+
* - **Publisher to subscriber, subscriptions only.** "An endpoint that
|
|
512
|
+
* receives a PUBLISH_STATE_NOTIFY for any other request type, or from the
|
|
513
|
+
* subscriber, MUST close the session with a PROTOCOL_VIOLATION."
|
|
514
|
+
*
|
|
515
|
+
* The last one needs the request stream, because the message has no Request
|
|
516
|
+
* ID field. Called through {@link receive}/{@link send} without one, the
|
|
517
|
+
* kind and direction rules cannot be evaluated and the message is allowed
|
|
518
|
+
* through unchecked rather than guessed at.
|
|
519
|
+
*/
|
|
520
|
+
handlePublishStateNotify(direction, streamRequestId, sideEffects) {
|
|
521
|
+
const err = this.requireReady("publish_state_notify");
|
|
522
|
+
if (err) return { ok: false, violation: err };
|
|
523
|
+
if (streamRequestId === void 0) {
|
|
524
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
525
|
+
}
|
|
526
|
+
const request = this._requests.get(streamRequestId);
|
|
527
|
+
if (request === void 0) {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
violation: violation(
|
|
531
|
+
"UNKNOWN_REQUEST_ID",
|
|
532
|
+
`PUBLISH_STATE_NOTIFY on unknown request ${streamRequestId}`,
|
|
533
|
+
this._phase,
|
|
534
|
+
"publish_state_notify"
|
|
535
|
+
)
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (request.kind !== "subscription") {
|
|
539
|
+
return {
|
|
540
|
+
ok: false,
|
|
541
|
+
violation: violation(
|
|
542
|
+
"STATE_VIOLATION",
|
|
543
|
+
`PUBLISH_STATE_NOTIFY applies only to subscriptions, but request ${streamRequestId} is a ${request.kind}`,
|
|
544
|
+
this._phase,
|
|
545
|
+
"publish_state_notify"
|
|
546
|
+
)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const fromPublisher = direction === "outbound" ? request.publisherSide === "local" : request.publisherSide === "remote";
|
|
550
|
+
if (!fromPublisher) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
violation: violation(
|
|
554
|
+
"ROLE_VIOLATION",
|
|
555
|
+
`PUBLISH_STATE_NOTIFY is sent only by the publisher; request ${streamRequestId} has the publisher on the other side`,
|
|
556
|
+
this._phase,
|
|
557
|
+
"publish_state_notify"
|
|
558
|
+
)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
sideEffects.push({ type: "subscription-state-notified", subscribeId: streamRequestId });
|
|
562
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
563
|
+
}
|
|
564
|
+
handlePublish(message, direction, sideEffects) {
|
|
565
|
+
const err = this.requireReady(message.type);
|
|
566
|
+
if (err) return { ok: false, violation: err };
|
|
567
|
+
const pub = message;
|
|
568
|
+
const dupErr = this.checkDuplicateRequestId(pub.request_id, message.type);
|
|
569
|
+
if (dupErr) return { ok: false, violation: dupErr };
|
|
570
|
+
this._requestIds.add(pub.request_id);
|
|
571
|
+
const publisherSide = direction === "outbound" ? "local" : "remote";
|
|
572
|
+
this._publishes.set(pub.request_id, {
|
|
573
|
+
requestId: pub.request_id,
|
|
574
|
+
phase: "pending"
|
|
575
|
+
});
|
|
576
|
+
this._subscriptions.set(pub.request_id, {
|
|
577
|
+
subscribeId: pub.request_id,
|
|
578
|
+
phase: "pending",
|
|
579
|
+
trackNamespace: pub.track_namespace,
|
|
580
|
+
trackName: pub.track_name
|
|
581
|
+
});
|
|
582
|
+
this._subscriptionMeta.set(pub.request_id, {
|
|
583
|
+
publisherSide,
|
|
584
|
+
forwardState: pub.parameters.forward === 0n ? 0 : 1,
|
|
585
|
+
fills: /* @__PURE__ */ new Map()
|
|
586
|
+
});
|
|
587
|
+
this._requests.set(pub.request_id, {
|
|
588
|
+
kind: "subscription",
|
|
589
|
+
publisherSide,
|
|
590
|
+
outstandingUpdates: 0
|
|
591
|
+
});
|
|
592
|
+
this._pendingPublishes.push(pub.request_id);
|
|
593
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
594
|
+
}
|
|
595
|
+
handlePublishDone(streamRequestId, sideEffects) {
|
|
596
|
+
const err = this.requireReady("publish_done");
|
|
597
|
+
if (err) return { ok: false, violation: err };
|
|
598
|
+
let requestId = streamRequestId;
|
|
599
|
+
if (requestId === void 0) {
|
|
600
|
+
for (const [reqId, pub2] of this._publishes) {
|
|
601
|
+
if (pub2.phase === "active") {
|
|
602
|
+
requestId = reqId;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (requestId === void 0) return { ok: true, phase: this._phase, sideEffects };
|
|
608
|
+
const pub = this._publishes.get(requestId);
|
|
609
|
+
if (pub !== void 0 && pub.phase !== "done") {
|
|
610
|
+
this._publishes.set(requestId, { ...pub, phase: "done" });
|
|
611
|
+
sideEffects.push({ type: "publish-ended", requestId, reason: "done" });
|
|
612
|
+
}
|
|
613
|
+
const sub = this._subscriptions.get(requestId);
|
|
614
|
+
if (sub !== void 0 && sub.phase !== "done") {
|
|
615
|
+
this._subscriptions.set(requestId, { ...sub, phase: "done" });
|
|
616
|
+
sideEffects.push({
|
|
617
|
+
type: "subscription-ended",
|
|
618
|
+
subscribeId: requestId,
|
|
619
|
+
reason: "publish_done"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const meta = this._subscriptionMeta.get(requestId);
|
|
623
|
+
if (meta !== void 0) {
|
|
624
|
+
for (const [fillId, fill] of meta.fills) {
|
|
625
|
+
if (fill.phase !== "open") continue;
|
|
626
|
+
meta.fills.set(fillId, { ...fill, phase: "reset" });
|
|
627
|
+
sideEffects.push({
|
|
628
|
+
type: "fill-fetch-stream-closed",
|
|
629
|
+
subscribeId: requestId,
|
|
630
|
+
requestId: fillId,
|
|
631
|
+
reason: "subscription-ended"
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
636
|
+
}
|
|
637
|
+
handleFetch(message, direction, sideEffects) {
|
|
638
|
+
const err = this.requireReady(message.type);
|
|
639
|
+
if (err) return { ok: false, violation: err };
|
|
640
|
+
const fetch = message;
|
|
641
|
+
const dupErr = this.checkDuplicateRequestId(fetch.request_id, message.type);
|
|
642
|
+
if (dupErr) return { ok: false, violation: dupErr };
|
|
643
|
+
this._requestIds.add(fetch.request_id);
|
|
644
|
+
this._fetches.set(fetch.request_id, {
|
|
645
|
+
requestId: fetch.request_id,
|
|
646
|
+
phase: "pending"
|
|
647
|
+
});
|
|
648
|
+
this._requests.set(fetch.request_id, {
|
|
649
|
+
kind: "fetch",
|
|
650
|
+
// The receiver of a FETCH serves it, so the peer publishes the objects.
|
|
651
|
+
publisherSide: direction === "outbound" ? "remote" : "local",
|
|
652
|
+
outstandingUpdates: 0
|
|
653
|
+
});
|
|
654
|
+
this._pendingFetches.push(fetch.request_id);
|
|
655
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
656
|
+
}
|
|
657
|
+
handleFetchOk(streamRequestId, sideEffects) {
|
|
658
|
+
const err = this.requireReady("fetch_ok");
|
|
659
|
+
if (err) return { ok: false, violation: err };
|
|
660
|
+
const requestId = streamRequestId ?? this._pendingFetches.shift();
|
|
661
|
+
if (requestId === void 0) {
|
|
662
|
+
return {
|
|
663
|
+
ok: false,
|
|
664
|
+
violation: violation(
|
|
665
|
+
"UNEXPECTED_MESSAGE",
|
|
666
|
+
"FETCH_OK with no pending fetch",
|
|
667
|
+
this._phase,
|
|
668
|
+
"fetch_ok"
|
|
669
|
+
)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (streamRequestId !== void 0) {
|
|
673
|
+
const kind = this._requests.get(requestId)?.kind;
|
|
674
|
+
if (kind !== void 0 && kind !== "fetch") {
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
violation: violation(
|
|
678
|
+
"STATE_VIOLATION",
|
|
679
|
+
`FETCH_OK on request ${requestId}, which is a ${kind}; a fill fetch stream has no FETCH_OK`,
|
|
680
|
+
this._phase,
|
|
681
|
+
"fetch_ok"
|
|
682
|
+
)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
this.dropPending(this._pendingFetches, requestId);
|
|
686
|
+
}
|
|
687
|
+
const existing = this._fetches.get(requestId);
|
|
688
|
+
if (existing && existing.phase === "pending") {
|
|
689
|
+
this._fetches.set(requestId, { ...existing, phase: "active" });
|
|
690
|
+
sideEffects.push({ type: "fetch-activated", requestId });
|
|
691
|
+
}
|
|
692
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
693
|
+
}
|
|
694
|
+
handleRequestError(streamRequestId, sideEffects) {
|
|
695
|
+
const err = this.requireReady("request_error");
|
|
696
|
+
if (err) return { ok: false, violation: err };
|
|
697
|
+
if (streamRequestId !== void 0) {
|
|
698
|
+
this.restoreUpdateCredit(streamRequestId);
|
|
699
|
+
const sub = this._subscriptions.get(streamRequestId);
|
|
700
|
+
if (sub !== void 0 && sub.phase === "pending") {
|
|
701
|
+
this._subscriptions.set(streamRequestId, { ...sub, phase: "error" });
|
|
702
|
+
this.dropPending(this._pendingSubscribes, streamRequestId);
|
|
703
|
+
sideEffects.push({
|
|
704
|
+
type: "subscription-ended",
|
|
705
|
+
subscribeId: streamRequestId,
|
|
706
|
+
reason: "request_error"
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
const pub = this._publishes.get(streamRequestId);
|
|
710
|
+
if (pub !== void 0 && pub.phase === "pending") {
|
|
711
|
+
this._publishes.set(streamRequestId, { ...pub, phase: "error" });
|
|
712
|
+
this.dropPending(this._pendingPublishes, streamRequestId);
|
|
713
|
+
sideEffects.push({
|
|
714
|
+
type: "publish-ended",
|
|
715
|
+
requestId: streamRequestId,
|
|
716
|
+
reason: "request_error"
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const f = this._fetches.get(streamRequestId);
|
|
720
|
+
if (f !== void 0 && f.phase === "pending") {
|
|
721
|
+
this._fetches.set(streamRequestId, { ...f, phase: "error" });
|
|
722
|
+
this.dropPending(this._pendingFetches, streamRequestId);
|
|
723
|
+
sideEffects.push({
|
|
724
|
+
type: "fetch-ended",
|
|
725
|
+
requestId: streamRequestId,
|
|
726
|
+
reason: "request_error"
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
730
|
+
}
|
|
731
|
+
const subId = this.dequeuePending(this._pendingSubscribes, this._subscriptions);
|
|
732
|
+
if (subId !== void 0) {
|
|
733
|
+
const sub = this._subscriptions.get(subId);
|
|
734
|
+
this._subscriptions.set(subId, { ...sub, phase: "error" });
|
|
735
|
+
sideEffects.push({
|
|
736
|
+
type: "subscription-ended",
|
|
737
|
+
subscribeId: subId,
|
|
738
|
+
reason: "request_error"
|
|
739
|
+
});
|
|
740
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
741
|
+
}
|
|
742
|
+
const pubId = this.dequeuePending(this._pendingPublishes, this._publishes);
|
|
743
|
+
if (pubId !== void 0) {
|
|
744
|
+
const pub = this._publishes.get(pubId);
|
|
745
|
+
this._publishes.set(pubId, { ...pub, phase: "error" });
|
|
746
|
+
sideEffects.push({
|
|
747
|
+
type: "publish-ended",
|
|
748
|
+
requestId: pubId,
|
|
749
|
+
reason: "request_error"
|
|
750
|
+
});
|
|
751
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
752
|
+
}
|
|
753
|
+
const fetchId = this.dequeuePending(this._pendingFetches, this._fetches);
|
|
754
|
+
if (fetchId !== void 0) {
|
|
755
|
+
const f = this._fetches.get(fetchId);
|
|
756
|
+
this._fetches.set(fetchId, { ...f, phase: "error" });
|
|
757
|
+
sideEffects.push({
|
|
758
|
+
type: "fetch-ended",
|
|
759
|
+
requestId: fetchId,
|
|
760
|
+
reason: "request_error"
|
|
761
|
+
});
|
|
762
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
763
|
+
}
|
|
764
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
765
|
+
}
|
|
766
|
+
handleRequestOk(streamRequestId, sideEffects) {
|
|
767
|
+
const err = this.requireReady("request_ok");
|
|
768
|
+
if (err) return { ok: false, violation: err };
|
|
769
|
+
if (streamRequestId !== void 0) {
|
|
770
|
+
this.restoreUpdateCredit(streamRequestId);
|
|
771
|
+
const pub = this._publishes.get(streamRequestId);
|
|
772
|
+
if (pub !== void 0 && pub.phase === "pending") {
|
|
773
|
+
this._publishes.set(streamRequestId, { ...pub, phase: "active" });
|
|
774
|
+
this.dropPending(this._pendingPublishes, streamRequestId);
|
|
775
|
+
sideEffects.push({ type: "publish-activated", requestId: streamRequestId });
|
|
776
|
+
const sub = this._subscriptions.get(streamRequestId);
|
|
777
|
+
if (sub !== void 0 && sub.phase === "pending") {
|
|
778
|
+
this._subscriptions.set(streamRequestId, { ...sub, phase: "active" });
|
|
779
|
+
sideEffects.push({ type: "subscription-activated", subscribeId: streamRequestId });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
783
|
+
}
|
|
784
|
+
const pubId = this.dequeuePending(this._pendingPublishes, this._publishes);
|
|
785
|
+
if (pubId !== void 0) {
|
|
786
|
+
const pub = this._publishes.get(pubId);
|
|
787
|
+
this._publishes.set(pubId, { ...pub, phase: "active" });
|
|
788
|
+
sideEffects.push({ type: "publish-activated", requestId: pubId });
|
|
789
|
+
}
|
|
790
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
791
|
+
}
|
|
792
|
+
handleGenericRequest(message, kind, direction, sideEffects) {
|
|
793
|
+
const err = this.requireReady(message.type);
|
|
794
|
+
if (err) return { ok: false, violation: err };
|
|
795
|
+
const requestId = message.request_id;
|
|
796
|
+
if (requestId === void 0) return { ok: true, phase: this._phase, sideEffects };
|
|
797
|
+
const dupErr = this.checkDuplicateRequestId(requestId, message.type);
|
|
798
|
+
if (dupErr) return { ok: false, violation: dupErr };
|
|
799
|
+
this._requestIds.add(requestId);
|
|
800
|
+
this._requests.set(requestId, {
|
|
801
|
+
kind,
|
|
802
|
+
// Only subscriptions have a publisher in the Section 10.10 sense; the
|
|
803
|
+
// value is recorded for symmetry and never consulted for these kinds.
|
|
804
|
+
publisherSide: direction === "outbound" ? "remote" : "local",
|
|
805
|
+
outstandingUpdates: 0
|
|
806
|
+
});
|
|
807
|
+
return { ok: true, phase: this._phase, sideEffects };
|
|
808
|
+
}
|
|
809
|
+
restoreUpdateCredit(requestId) {
|
|
810
|
+
const request = this._requests.get(requestId);
|
|
811
|
+
if (request !== void 0 && request.outstandingUpdates > 0) {
|
|
812
|
+
request.outstandingUpdates -= 1;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
dropPending(queue, requestId) {
|
|
816
|
+
const index = queue.indexOf(requestId);
|
|
817
|
+
if (index >= 0) queue.splice(index, 1);
|
|
818
|
+
}
|
|
819
|
+
dequeuePending(queue, stateMap) {
|
|
820
|
+
while (queue.length > 0) {
|
|
821
|
+
const id = queue[0];
|
|
822
|
+
const state = stateMap.get(id);
|
|
823
|
+
if (state && state.phase === "pending") {
|
|
824
|
+
queue.shift();
|
|
825
|
+
return id;
|
|
826
|
+
}
|
|
827
|
+
queue.shift();
|
|
828
|
+
}
|
|
829
|
+
return void 0;
|
|
830
|
+
}
|
|
831
|
+
handleReadyPhaseMessage(message) {
|
|
832
|
+
const err = this.requireReady(message.type);
|
|
833
|
+
if (err) return { ok: false, violation: err };
|
|
834
|
+
return { ok: true, phase: this._phase, sideEffects: [] };
|
|
835
|
+
}
|
|
836
|
+
reset() {
|
|
837
|
+
this._phase = "idle";
|
|
838
|
+
this._setupDirection = null;
|
|
839
|
+
this._subscriptions.clear();
|
|
840
|
+
this._subscriptionMeta.clear();
|
|
841
|
+
this._publishes.clear();
|
|
842
|
+
this._fetches.clear();
|
|
843
|
+
this._requests.clear();
|
|
844
|
+
this._requestIds.clear();
|
|
845
|
+
this._pendingSubscribes.length = 0;
|
|
846
|
+
this._pendingPublishes.length = 0;
|
|
847
|
+
this._pendingFetches.length = 0;
|
|
848
|
+
this._peerMaxRequestUpdates = 0n;
|
|
849
|
+
this._localMaxRequestUpdates = 0n;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// src/drafts/draft20/session.ts
|
|
854
|
+
function createDraft20SessionState(role) {
|
|
855
|
+
return new Draft20SessionFSM(role);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export {
|
|
859
|
+
CONTROL_MESSAGES,
|
|
860
|
+
CLIENT_ONLY_MESSAGES,
|
|
861
|
+
SERVER_ONLY_MESSAGES,
|
|
862
|
+
BIDIRECTIONAL_MESSAGES,
|
|
863
|
+
REQUEST_STREAM_OPENERS,
|
|
864
|
+
getLegalOutgoing,
|
|
865
|
+
getLegalIncoming,
|
|
866
|
+
Draft20SessionFSM,
|
|
867
|
+
createDraft20SessionState
|
|
868
|
+
};
|