@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +16 -4
  3. package/dist/chunk-BMJMY6FX.cjs +868 -0
  4. package/dist/chunk-MAXEHYVB.cjs +2362 -0
  5. package/dist/{chunk-3YAR3I3S.js → chunk-Q7NSUXXB.js} +3 -1
  6. package/dist/{chunk-VWXUT5R2.cjs → chunk-TVSFDQYE.cjs} +3 -1
  7. package/dist/chunk-WX5PU7HF.js +868 -0
  8. package/dist/chunk-YJGOD3Y4.js +2362 -0
  9. package/dist/{codec-BLYAy2OP.d.ts → codec--CVeqGQv.d.ts} +1 -1
  10. package/dist/{codec-DujHf55Z.d.ts → codec-BJ7BHuiZ.d.ts} +1 -1
  11. package/dist/{codec-D47r_-Rl.d.ts → codec-BKsVu3Fr.d.ts} +1 -1
  12. package/dist/{codec-BNS3gS77.d.ts → codec-BMwgZIgG.d.ts} +1 -1
  13. package/dist/{codec-CI0BjmHc.d.ts → codec-BNy5syCU.d.ts} +1 -1
  14. package/dist/{codec-v6Vloc7H.d.ts → codec-BQa6Qsav.d.ts} +1 -1
  15. package/dist/codec-Bym-K2q2.d.cts +49 -0
  16. package/dist/{codec-D2JbI2tp.d.ts → codec-C006F75E.d.ts} +1 -1
  17. package/dist/codec-C5evFHL-.d.ts +49 -0
  18. package/dist/{codec-DQWQSam3.d.cts → codec-C6Mx4bwz.d.cts} +1 -1
  19. package/dist/{codec-DL0njGkL.d.ts → codec-CA6nbbEk.d.ts} +1 -1
  20. package/dist/{codec-D89DWXbv.d.cts → codec-CSBFdPgP.d.cts} +1 -1
  21. package/dist/{codec-DvcJgYKq.d.cts → codec-CXCjya_G.d.cts} +1 -1
  22. package/dist/{codec-hvkJfCF5.d.cts → codec-C_xxsbKy.d.cts} +1 -1
  23. package/dist/{codec-BOyrk_C9.d.cts → codec-Cfo_z5ET.d.cts} +1 -1
  24. package/dist/{codec-xeuGZY77.d.cts → codec-CgZdadPt.d.cts} +2 -2
  25. package/dist/{codec-WwwUipaU.d.ts → codec-Cl73EfRW.d.ts} +1 -1
  26. package/dist/{codec-BOtv3R5j.d.cts → codec-CtvFK4aV.d.cts} +1 -1
  27. package/dist/{codec-DAePx2NL.d.ts → codec-CviAa4lp.d.ts} +2 -2
  28. package/dist/{codec-DJmV04i0.d.ts → codec-D4RvFQmu.d.ts} +1 -1
  29. package/dist/{codec-cou9XBMF.d.cts → codec-DAyiF5sq.d.cts} +1 -1
  30. package/dist/{codec-CyApDKwf.d.cts → codec-DJmqoIsp.d.cts} +1 -1
  31. package/dist/{codec-NMsTK0GX.d.cts → codec-DchsPfr3.d.cts} +1 -1
  32. package/dist/{codec-DplRgIKq.d.ts → codec-Dl_QIXMz.d.ts} +1 -1
  33. package/dist/{codec-N3g1lxQQ.d.cts → codec-DzLvG8Wb.d.cts} +1 -1
  34. package/dist/{codec-D95gT98n.d.cts → codec-DzkdW4kO.d.cts} +1 -1
  35. package/dist/{codec-CWj5iTxA.d.ts → codec-O-1Js7h4.d.ts} +1 -1
  36. package/dist/{codec-BqqoYdh_.d.cts → codec-vddqy0KY.d.cts} +1 -1
  37. package/dist/draft07-session.d.cts +3 -3
  38. package/dist/draft07-session.d.ts +3 -3
  39. package/dist/draft07.d.cts +3 -3
  40. package/dist/draft07.d.ts +3 -3
  41. package/dist/draft08-session.d.cts +3 -3
  42. package/dist/draft08-session.d.ts +3 -3
  43. package/dist/draft08.d.cts +4 -4
  44. package/dist/draft08.d.ts +4 -4
  45. package/dist/draft09-session.d.cts +3 -3
  46. package/dist/draft09-session.d.ts +3 -3
  47. package/dist/draft09.d.cts +4 -4
  48. package/dist/draft09.d.ts +4 -4
  49. package/dist/draft10-session.d.cts +3 -3
  50. package/dist/draft10-session.d.ts +3 -3
  51. package/dist/draft10.d.cts +4 -4
  52. package/dist/draft10.d.ts +4 -4
  53. package/dist/draft11-session.d.cts +3 -3
  54. package/dist/draft11-session.d.ts +3 -3
  55. package/dist/draft11.d.cts +4 -4
  56. package/dist/draft11.d.ts +4 -4
  57. package/dist/draft12-session.d.cts +3 -3
  58. package/dist/draft12-session.d.ts +3 -3
  59. package/dist/draft12.d.cts +4 -4
  60. package/dist/draft12.d.ts +4 -4
  61. package/dist/draft13-session.d.cts +3 -3
  62. package/dist/draft13-session.d.ts +3 -3
  63. package/dist/draft13.d.cts +4 -4
  64. package/dist/draft13.d.ts +4 -4
  65. package/dist/draft14-session.d.cts +3 -3
  66. package/dist/draft14-session.d.ts +3 -3
  67. package/dist/draft14.d.cts +4 -4
  68. package/dist/draft14.d.ts +4 -4
  69. package/dist/draft15-session.d.cts +3 -3
  70. package/dist/draft15-session.d.ts +3 -3
  71. package/dist/draft15.d.cts +4 -4
  72. package/dist/draft15.d.ts +4 -4
  73. package/dist/draft16-session.d.cts +3 -3
  74. package/dist/draft16-session.d.ts +3 -3
  75. package/dist/draft16.d.cts +4 -4
  76. package/dist/draft16.d.ts +4 -4
  77. package/dist/draft17-session.d.cts +3 -3
  78. package/dist/draft17-session.d.ts +3 -3
  79. package/dist/draft17.d.cts +3 -3
  80. package/dist/draft17.d.ts +3 -3
  81. package/dist/draft18-session.d.cts +3 -3
  82. package/dist/draft18-session.d.ts +3 -3
  83. package/dist/draft18.d.cts +3 -3
  84. package/dist/draft18.d.ts +3 -3
  85. package/dist/draft19-session.d.cts +3 -3
  86. package/dist/draft19-session.d.ts +3 -3
  87. package/dist/draft19.cjs +2 -2
  88. package/dist/draft19.d.cts +3 -3
  89. package/dist/draft19.d.ts +3 -3
  90. package/dist/draft19.js +1 -1
  91. package/dist/draft20-session.cjs +8 -0
  92. package/dist/draft20-session.d.cts +195 -0
  93. package/dist/draft20-session.d.ts +195 -0
  94. package/dist/draft20-session.js +8 -0
  95. package/dist/draft20.cjs +217 -0
  96. package/dist/draft20.d.cts +175 -0
  97. package/dist/draft20.d.ts +175 -0
  98. package/dist/draft20.js +217 -0
  99. package/dist/index.cjs +9 -3
  100. package/dist/index.d.cts +32 -17
  101. package/dist/index.d.ts +32 -17
  102. package/dist/index.js +8 -2
  103. package/dist/{session-types-KsqyVp6y.d.cts → session-types-B9idsih-.d.cts} +13 -1
  104. package/dist/{session-types-C0l_-Ups.d.ts → session-types-Dn7b3RZM.d.ts} +13 -1
  105. package/dist/session.cjs +5 -1
  106. package/dist/session.d.cts +5 -3
  107. package/dist/session.d.ts +5 -3
  108. package/dist/session.js +5 -1
  109. package/dist/types-CWCQwuvy.d.cts +359 -0
  110. package/dist/types-CWCQwuvy.d.ts +359 -0
  111. package/dist/{types-CdCSIfvI.d.cts → types-DG5_iKuv.d.cts} +1 -1
  112. package/dist/{types-CdCSIfvI.d.ts → types-DG5_iKuv.d.ts} +1 -1
  113. package/package.json +16 -3
  114. package/src/core/session-types.ts +7 -0
  115. package/src/core/types.ts +1 -0
  116. package/src/drafts/draft19/codec.ts +4 -2
  117. package/src/drafts/draft20/codec.ts +1737 -0
  118. package/src/drafts/draft20/data-streams.ts +1291 -0
  119. package/src/drafts/draft20/error-codes.ts +135 -0
  120. package/src/drafts/draft20/index.ts +165 -0
  121. package/src/drafts/draft20/messages.ts +68 -0
  122. package/src/drafts/draft20/rules.ts +107 -0
  123. package/src/drafts/draft20/session-fsm.ts +1063 -0
  124. package/src/drafts/draft20/session.ts +21 -0
  125. package/src/drafts/draft20/types.ts +455 -0
  126. package/src/index.ts +20 -2
  127. 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
+ };