@moqtap/codec 0.1.1 → 0.2.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 (252) hide show
  1. package/README.md +99 -95
  2. package/dist/chunk-4RIXXEII.js +1275 -0
  3. package/dist/chunk-4XYGE53S.cjs +698 -0
  4. package/dist/chunk-4YJANAXU.cjs +1109 -0
  5. package/dist/chunk-6AEHWULA.cjs +641 -0
  6. package/dist/chunk-7DUBLRXC.js +680 -0
  7. package/dist/{chunk-YBZD3DU5.cjs → chunk-7IVGHMKJ.cjs} +44 -42
  8. package/dist/chunk-BISI45MN.cjs +680 -0
  9. package/dist/{chunk-NLYTRGXA.cjs → chunk-BPNL5YFQ.cjs} +5 -5
  10. package/dist/chunk-CXDHOMHG.js +1097 -0
  11. package/dist/chunk-DUBCL3WF.cjs +749 -0
  12. package/dist/chunk-DWK5ZQZ4.js +642 -0
  13. package/dist/chunk-E6E3NQYU.js +680 -0
  14. package/dist/chunk-EFM5T7OM.js +698 -0
  15. package/dist/chunk-ENURAVHI.cjs +680 -0
  16. package/dist/{chunk-Z66WDWHI.js → chunk-FXZ2MYKJ.js} +127 -1
  17. package/dist/chunk-G26SJ6XS.cjs +1341 -0
  18. package/dist/chunk-G7GI7LJA.js +737 -0
  19. package/dist/chunk-GXEW4COZ.cjs +737 -0
  20. package/dist/chunk-HSVYF6XX.cjs +1361 -0
  21. package/dist/chunk-IBVM4DMJ.cjs +1097 -0
  22. package/dist/chunk-IV2H5CFI.cjs +1275 -0
  23. package/dist/chunk-IV2HRJVT.js +1198 -0
  24. package/dist/chunk-JSQM2MG3.js +680 -0
  25. package/dist/chunk-K4OLITS2.cjs +1055 -0
  26. package/dist/{chunk-UOBWHJA5.js → chunk-KFTCU2P6.js} +2 -3
  27. package/dist/chunk-LH4NTURO.js +1361 -0
  28. package/dist/{chunk-YTXLWKOR.js → chunk-MFAP7R6L.js} +1 -1
  29. package/dist/chunk-NGVE2RZT.js +1097 -0
  30. package/dist/chunk-NUX5BHWO.js +1341 -0
  31. package/dist/chunk-PJRA2TQ5.js +1055 -0
  32. package/dist/chunk-RVJAGE4S.cjs +1198 -0
  33. package/dist/{chunk-QYG6KGOV.cjs → chunk-RWQ43Z4F.cjs} +2 -3
  34. package/dist/chunk-RZHAPEXO.js +749 -0
  35. package/dist/chunk-ST24APEO.js +1109 -0
  36. package/dist/chunk-SYHW3FLI.cjs +642 -0
  37. package/dist/chunk-TLYNOOQP.cjs +432 -0
  38. package/dist/{chunk-IXHOBNXA.js → chunk-TMNGRIPL.js} +43 -41
  39. package/dist/chunk-UNS34PTA.cjs +680 -0
  40. package/dist/chunk-UR6JKS56.js +432 -0
  41. package/dist/{chunk-NPWHHWXT.cjs → chunk-UYXTY6ZQ.cjs} +127 -1
  42. package/dist/chunk-XUUCOLWU.cjs +1097 -0
  43. package/dist/chunk-YG5KJESI.js +641 -0
  44. package/dist/chunk-ZBKE2QRQ.js +1401 -0
  45. package/dist/chunk-ZSPO2GF2.cjs +1401 -0
  46. package/dist/codec-95k8CAu5.d.cts +35 -0
  47. package/dist/codec-AFuOxfsO.d.ts +60 -0
  48. package/dist/codec-B-UJ5Iow.d.cts +75 -0
  49. package/dist/codec-BC5jfvMb.d.ts +35 -0
  50. package/dist/codec-BECYPfY8.d.ts +35 -0
  51. package/dist/codec-BsPU1vNC.d.ts +39 -0
  52. package/dist/codec-BvpuF-6u.d.cts +39 -0
  53. package/dist/codec-C8jZI5Cx.d.cts +39 -0
  54. package/dist/codec-CAevkgf5.d.cts +33 -0
  55. package/dist/codec-CSUqCrRs.d.ts +39 -0
  56. package/dist/codec-C_HMXNK_.d.ts +33 -0
  57. package/dist/{codec-Bvr7rFtj.d.cts → codec-CpuvYTSV.d.cts} +1 -1
  58. package/dist/codec-D0x8-SCw.d.cts +35 -0
  59. package/dist/codec-D7ARhpG1.d.ts +75 -0
  60. package/dist/codec-DNAUGshO.d.cts +60 -0
  61. package/dist/codec-DPx_QNn0.d.ts +31 -0
  62. package/dist/{codec-B2mc2g3i.d.ts → codec-DRhCx_hw.d.ts} +1 -1
  63. package/dist/codec-Db7YPe3l.d.ts +31 -0
  64. package/dist/codec-axkJpb7D.d.cts +31 -0
  65. package/dist/codec-ujAbFaep.d.cts +31 -0
  66. package/dist/draft10-session.cjs +6 -0
  67. package/dist/draft10-session.d.cts +8 -0
  68. package/dist/draft10-session.d.ts +8 -0
  69. package/dist/draft10-session.js +6 -0
  70. package/dist/draft10.cjs +115 -0
  71. package/dist/draft10.d.cts +95 -0
  72. package/dist/draft10.d.ts +95 -0
  73. package/dist/draft10.js +115 -0
  74. package/dist/draft11-session.cjs +6 -0
  75. package/dist/draft11-session.d.cts +8 -0
  76. package/dist/draft11-session.d.ts +8 -0
  77. package/dist/draft11-session.js +6 -0
  78. package/dist/draft11.cjs +109 -0
  79. package/dist/draft11.d.cts +99 -0
  80. package/dist/draft11.d.ts +99 -0
  81. package/dist/draft11.js +109 -0
  82. package/dist/draft12-session.cjs +6 -0
  83. package/dist/draft12-session.d.cts +8 -0
  84. package/dist/draft12-session.d.ts +8 -0
  85. package/dist/draft12-session.js +6 -0
  86. package/dist/draft12.cjs +117 -0
  87. package/dist/draft12.d.cts +106 -0
  88. package/dist/draft12.d.ts +106 -0
  89. package/dist/draft12.js +117 -0
  90. package/dist/draft13-session.cjs +6 -0
  91. package/dist/draft13-session.d.cts +8 -0
  92. package/dist/draft13-session.d.ts +8 -0
  93. package/dist/draft13-session.js +6 -0
  94. package/dist/draft13.cjs +119 -0
  95. package/dist/draft13.d.cts +108 -0
  96. package/dist/draft13.d.ts +108 -0
  97. package/dist/draft13.js +119 -0
  98. package/dist/draft14-session.cjs +2 -2
  99. package/dist/draft14-session.d.cts +3 -3
  100. package/dist/draft14-session.d.ts +3 -3
  101. package/dist/draft14-session.js +1 -1
  102. package/dist/draft14.cjs +3 -3
  103. package/dist/draft14.d.cts +16 -4
  104. package/dist/draft14.d.ts +16 -4
  105. package/dist/draft14.js +2 -2
  106. package/dist/draft15-session.cjs +6 -0
  107. package/dist/draft15-session.d.cts +8 -0
  108. package/dist/draft15-session.d.ts +8 -0
  109. package/dist/draft15-session.js +6 -0
  110. package/dist/draft15.cjs +111 -0
  111. package/dist/draft15.d.cts +93 -0
  112. package/dist/draft15.d.ts +93 -0
  113. package/dist/draft15.js +111 -0
  114. package/dist/draft16-session.cjs +6 -0
  115. package/dist/draft16-session.d.cts +8 -0
  116. package/dist/draft16-session.d.ts +8 -0
  117. package/dist/draft16-session.js +6 -0
  118. package/dist/draft16.cjs +113 -0
  119. package/dist/draft16.d.cts +94 -0
  120. package/dist/draft16.d.ts +94 -0
  121. package/dist/draft16.js +113 -0
  122. package/dist/draft17-session.cjs +8 -0
  123. package/dist/draft17-session.d.cts +51 -0
  124. package/dist/draft17-session.d.ts +51 -0
  125. package/dist/draft17-session.js +8 -0
  126. package/dist/draft17.cjs +99 -0
  127. package/dist/draft17.d.cts +40 -0
  128. package/dist/draft17.d.ts +40 -0
  129. package/dist/draft17.js +99 -0
  130. package/dist/draft7-session.cjs +3 -3
  131. package/dist/draft7-session.d.cts +3 -3
  132. package/dist/draft7-session.d.ts +3 -3
  133. package/dist/draft7-session.js +2 -2
  134. package/dist/draft7.cjs +4 -4
  135. package/dist/draft7.d.cts +2 -2
  136. package/dist/draft7.d.ts +2 -2
  137. package/dist/draft7.js +2 -2
  138. package/dist/draft8-session.cjs +6 -0
  139. package/dist/draft8-session.d.cts +8 -0
  140. package/dist/draft8-session.d.ts +8 -0
  141. package/dist/draft8-session.js +6 -0
  142. package/dist/draft8.cjs +115 -0
  143. package/dist/draft8.d.cts +95 -0
  144. package/dist/draft8.d.ts +95 -0
  145. package/dist/draft8.js +115 -0
  146. package/dist/draft9-session.cjs +6 -0
  147. package/dist/draft9-session.d.cts +8 -0
  148. package/dist/draft9-session.d.ts +8 -0
  149. package/dist/draft9-session.js +6 -0
  150. package/dist/draft9.cjs +115 -0
  151. package/dist/draft9.d.cts +95 -0
  152. package/dist/draft9.d.ts +95 -0
  153. package/dist/draft9.js +115 -0
  154. package/dist/index.cjs +76 -4
  155. package/dist/index.d.cts +68 -5
  156. package/dist/index.d.ts +68 -5
  157. package/dist/index.js +76 -4
  158. package/dist/{session-types-DFjMk4HH.d.ts → session-types-CJIFbTPd.d.ts} +1 -1
  159. package/dist/{session-types-DW1RSZX_.d.cts → session-types-Cbq8IGCP.d.cts} +1 -1
  160. package/dist/session.cjs +5 -5
  161. package/dist/session.d.cts +3 -3
  162. package/dist/session.d.ts +3 -3
  163. package/dist/session.js +5 -5
  164. package/dist/types-4VxSL2Ho.d.cts +261 -0
  165. package/dist/types-4VxSL2Ho.d.ts +261 -0
  166. package/dist/types-B2afJZM-.d.cts +236 -0
  167. package/dist/types-B2afJZM-.d.ts +236 -0
  168. package/dist/types-Bg6QYNVt.d.cts +290 -0
  169. package/dist/types-Bg6QYNVt.d.ts +290 -0
  170. package/dist/types-C_1HrqBl.d.cts +306 -0
  171. package/dist/types-C_1HrqBl.d.ts +306 -0
  172. package/dist/types-Cw4WE9dh.d.cts +261 -0
  173. package/dist/types-Cw4WE9dh.d.ts +261 -0
  174. package/dist/types-D5gNQiDj.d.cts +261 -0
  175. package/dist/types-D5gNQiDj.d.ts +261 -0
  176. package/dist/types-DqCDFqgB.d.cts +230 -0
  177. package/dist/types-DqCDFqgB.d.ts +230 -0
  178. package/dist/types-ERexTpT8.d.cts +217 -0
  179. package/dist/types-ERexTpT8.d.ts +217 -0
  180. package/dist/{types-DPYE49t0.d.cts → types-QNXoxC9Y.d.cts} +2 -7
  181. package/dist/{types-DPYE49t0.d.ts → types-QNXoxC9Y.d.ts} +2 -7
  182. package/dist/types-r-CasCf1.d.cts +262 -0
  183. package/dist/types-r-CasCf1.d.ts +262 -0
  184. package/package.json +110 -2
  185. package/src/core/types.ts +24 -7
  186. package/src/drafts/draft07/codec.ts +49 -41
  187. package/src/drafts/draft07/rules.ts +1 -3
  188. package/src/drafts/draft08/codec.ts +1254 -0
  189. package/src/drafts/draft08/index.ts +125 -0
  190. package/src/drafts/draft08/messages.ts +72 -0
  191. package/src/drafts/draft08/rules.ts +91 -0
  192. package/src/drafts/draft08/session-fsm.ts +718 -0
  193. package/src/drafts/draft08/session.ts +26 -0
  194. package/src/drafts/draft08/types.ts +377 -0
  195. package/src/drafts/draft09/codec.ts +1235 -0
  196. package/src/drafts/draft09/index.ts +125 -0
  197. package/src/drafts/draft09/messages.ts +72 -0
  198. package/src/drafts/draft09/rules.ts +91 -0
  199. package/src/drafts/draft09/session-fsm.ts +718 -0
  200. package/src/drafts/draft09/session.ts +26 -0
  201. package/src/drafts/draft09/types.ts +376 -0
  202. package/src/drafts/draft10/codec.ts +1235 -0
  203. package/src/drafts/draft10/index.ts +125 -0
  204. package/src/drafts/draft10/messages.ts +72 -0
  205. package/src/drafts/draft10/rules.ts +91 -0
  206. package/src/drafts/draft10/session-fsm.ts +718 -0
  207. package/src/drafts/draft10/session.ts +26 -0
  208. package/src/drafts/draft10/types.ts +376 -0
  209. package/src/drafts/draft11/codec.ts +1198 -0
  210. package/src/drafts/draft11/index.ts +123 -0
  211. package/src/drafts/draft11/messages.ts +71 -0
  212. package/src/drafts/draft11/rules.ts +100 -0
  213. package/src/drafts/draft11/session-fsm.ts +758 -0
  214. package/src/drafts/draft11/session.ts +26 -0
  215. package/src/drafts/draft11/types.ts +375 -0
  216. package/src/drafts/draft12/codec.ts +1354 -0
  217. package/src/drafts/draft12/index.ts +130 -0
  218. package/src/drafts/draft12/messages.ts +84 -0
  219. package/src/drafts/draft12/rules.ts +106 -0
  220. package/src/drafts/draft12/session-fsm.ts +805 -0
  221. package/src/drafts/draft12/session.ts +26 -0
  222. package/src/drafts/draft12/types.ts +414 -0
  223. package/src/drafts/draft13/codec.ts +1438 -0
  224. package/src/drafts/draft13/index.ts +132 -0
  225. package/src/drafts/draft13/messages.ts +86 -0
  226. package/src/drafts/draft13/rules.ts +108 -0
  227. package/src/drafts/draft13/session-fsm.ts +819 -0
  228. package/src/drafts/draft13/session.ts +26 -0
  229. package/src/drafts/draft13/types.ts +433 -0
  230. package/src/drafts/draft14/session-fsm.ts +182 -1
  231. package/src/drafts/draft15/codec.ts +1661 -0
  232. package/src/drafts/draft15/index.ts +121 -0
  233. package/src/drafts/draft15/messages.ts +64 -0
  234. package/src/drafts/draft15/rules.ts +95 -0
  235. package/src/drafts/draft15/session-fsm.ts +687 -0
  236. package/src/drafts/draft15/session.ts +26 -0
  237. package/src/drafts/draft15/types.ts +336 -0
  238. package/src/drafts/draft16/codec.ts +1623 -0
  239. package/src/drafts/draft16/index.ts +123 -0
  240. package/src/drafts/draft16/messages.ts +67 -0
  241. package/src/drafts/draft16/rules.ts +96 -0
  242. package/src/drafts/draft16/session-fsm.ts +682 -0
  243. package/src/drafts/draft16/session.ts +26 -0
  244. package/src/drafts/draft16/types.ts +354 -0
  245. package/src/drafts/draft17/codec.ts +1621 -0
  246. package/src/drafts/draft17/index.ts +105 -0
  247. package/src/drafts/draft17/messages.ts +53 -0
  248. package/src/drafts/draft17/rules.ts +85 -0
  249. package/src/drafts/draft17/session-fsm.ts +437 -0
  250. package/src/drafts/draft17/session.ts +15 -0
  251. package/src/drafts/draft17/types.ts +310 -0
  252. package/src/index.ts +217 -2
@@ -0,0 +1,687 @@
1
+ import type {
2
+ AnnounceState,
3
+ FetchState,
4
+ ProtocolViolation,
5
+ PublishState,
6
+ SessionPhase,
7
+ SideEffect,
8
+ SubscriptionState,
9
+ TransitionResult,
10
+ ValidationResult,
11
+ } from "../../core/session-types.js";
12
+ import {
13
+ CLIENT_ONLY_MESSAGES,
14
+ getLegalIncoming,
15
+ getLegalOutgoing,
16
+ SERVER_ONLY_MESSAGES,
17
+ } from "./rules.js";
18
+ import type { Draft15Message, Draft15MessageType } from "./types.js";
19
+
20
+ function violation(
21
+ code: ProtocolViolation<Draft15MessageType>["code"],
22
+ message: string,
23
+ currentPhase: SessionPhase,
24
+ offendingMessage: Draft15MessageType,
25
+ ): ProtocolViolation<Draft15MessageType> {
26
+ return { code, message, currentPhase, offendingMessage };
27
+ }
28
+
29
+ export class Draft15SessionFSM {
30
+ private _phase: SessionPhase = "idle";
31
+ private _role: "client" | "server";
32
+ private _subscriptions = new Map<bigint, SubscriptionState>();
33
+ private _publishes = new Map<bigint, PublishState>();
34
+ private _fetches = new Map<bigint, FetchState>();
35
+ private _requestIds = new Set<bigint>();
36
+
37
+ constructor(role: "client" | "server") {
38
+ this._role = role;
39
+ }
40
+
41
+ get phase(): SessionPhase {
42
+ return this._phase;
43
+ }
44
+ get role(): "client" | "server" {
45
+ return this._role;
46
+ }
47
+ get subscriptions(): ReadonlyMap<bigint, SubscriptionState> {
48
+ return this._subscriptions;
49
+ }
50
+ get announces(): ReadonlyMap<string, AnnounceState> {
51
+ return new Map();
52
+ }
53
+ get publishes(): ReadonlyMap<bigint, PublishState> {
54
+ return this._publishes;
55
+ }
56
+ get fetches(): ReadonlyMap<bigint, FetchState> {
57
+ return this._fetches;
58
+ }
59
+
60
+ get legalOutgoing(): ReadonlySet<Draft15MessageType> {
61
+ return getLegalOutgoing(this._phase, this._role);
62
+ }
63
+
64
+ get legalIncoming(): ReadonlySet<Draft15MessageType> {
65
+ return getLegalIncoming(this._phase, this._role);
66
+ }
67
+
68
+ private checkRole(
69
+ message: Draft15Message,
70
+ direction: "inbound" | "outbound",
71
+ ): ProtocolViolation<Draft15MessageType> | null {
72
+ const senderRole =
73
+ direction === "outbound" ? this._role : this._role === "client" ? "server" : "client";
74
+
75
+ if (CLIENT_ONLY_MESSAGES.has(message.type) && senderRole !== "client") {
76
+ return violation(
77
+ "ROLE_VIOLATION",
78
+ `${message.type} can only be sent by client`,
79
+ this._phase,
80
+ message.type,
81
+ );
82
+ }
83
+ if (SERVER_ONLY_MESSAGES.has(message.type) && senderRole !== "server") {
84
+ return violation(
85
+ "ROLE_VIOLATION",
86
+ `${message.type} can only be sent by server`,
87
+ this._phase,
88
+ message.type,
89
+ );
90
+ }
91
+ return null;
92
+ }
93
+
94
+ private checkDuplicateRequestId(
95
+ requestId: bigint,
96
+ msgType: Draft15MessageType,
97
+ ): ProtocolViolation<Draft15MessageType> | null {
98
+ if (this._requestIds.has(requestId)) {
99
+ return violation(
100
+ "DUPLICATE_REQUEST_ID",
101
+ `Request ID ${requestId} already in use`,
102
+ this._phase,
103
+ msgType,
104
+ );
105
+ }
106
+ return null;
107
+ }
108
+
109
+ private checkKnownRequestId(
110
+ requestId: bigint,
111
+ msgType: Draft15MessageType,
112
+ ): ProtocolViolation<Draft15MessageType> | null {
113
+ if (!this._requestIds.has(requestId)) {
114
+ return violation(
115
+ "UNKNOWN_REQUEST_ID",
116
+ `No request with ID ${requestId}`,
117
+ this._phase,
118
+ msgType,
119
+ );
120
+ }
121
+ return null;
122
+ }
123
+
124
+ validateOutgoing(message: Draft15Message): ValidationResult<Draft15MessageType> {
125
+ const roleViolation = this.checkRole(message, "outbound");
126
+ if (roleViolation) return { ok: false, violation: roleViolation };
127
+
128
+ if (!this.legalOutgoing.has(message.type)) {
129
+ return {
130
+ ok: false,
131
+ violation: violation(
132
+ this._phase === "idle" || this._phase === "setup"
133
+ ? "MESSAGE_BEFORE_SETUP"
134
+ : "UNEXPECTED_MESSAGE",
135
+ `Cannot send ${message.type} in phase ${this._phase}`,
136
+ this._phase,
137
+ message.type,
138
+ ),
139
+ };
140
+ }
141
+ return { ok: true };
142
+ }
143
+
144
+ receive(message: Draft15Message): TransitionResult<Draft15MessageType> {
145
+ const roleViolation = this.checkRole(message, "inbound");
146
+ if (roleViolation) return { ok: false, violation: roleViolation };
147
+ return this.applyTransition(message, "inbound");
148
+ }
149
+
150
+ send(message: Draft15Message): TransitionResult<Draft15MessageType> {
151
+ const roleViolation = this.checkRole(message, "outbound");
152
+ if (roleViolation) return { ok: false, violation: roleViolation };
153
+ return this.applyTransition(message, "outbound");
154
+ }
155
+
156
+ private applyTransition(
157
+ message: Draft15Message,
158
+ direction: "inbound" | "outbound",
159
+ ): TransitionResult<Draft15MessageType> {
160
+ const sideEffects: SideEffect[] = [];
161
+
162
+ switch (message.type) {
163
+ case "client_setup":
164
+ return this.handleClientSetup(direction);
165
+ case "server_setup":
166
+ return this.handleServerSetup(direction);
167
+ case "goaway":
168
+ return this.handleGoAway(message, sideEffects);
169
+
170
+ // Subscribe lifecycle
171
+ case "subscribe":
172
+ return this.handleSubscribe(message, sideEffects);
173
+ case "subscribe_ok":
174
+ return this.handleSubscribeOk(message, sideEffects);
175
+ case "subscribe_update":
176
+ return this.handleSubscribeUpdate(message, sideEffects);
177
+ case "unsubscribe":
178
+ return this.handleUnsubscribe(message, sideEffects);
179
+
180
+ // Publish lifecycle
181
+ case "publish":
182
+ return this.handlePublish(message, sideEffects);
183
+ case "publish_ok":
184
+ return this.handlePublishOk(message, sideEffects);
185
+ case "publish_done":
186
+ return this.handlePublishDone(message, sideEffects);
187
+
188
+ // Fetch lifecycle
189
+ case "fetch":
190
+ return this.handleFetch(message, sideEffects);
191
+ case "fetch_ok":
192
+ return this.handleFetchOk(message, sideEffects);
193
+ case "fetch_cancel":
194
+ return this.handleFetchCancel(message, sideEffects);
195
+
196
+ // Consolidated responses — REQUEST_ERROR can end any pending request
197
+ case "request_ok":
198
+ return this.handleRequestOk(message, sideEffects);
199
+ case "request_error":
200
+ return this.handleRequestError(message, sideEffects);
201
+
202
+ // Namespace and track status — register request IDs
203
+ case "publish_namespace":
204
+ return this.handlePublishNamespace(message, sideEffects);
205
+ case "subscribe_namespace":
206
+ return this.handleSubscribeNamespace(message, sideEffects);
207
+ case "track_status":
208
+ return this.handleTrackStatus(message, sideEffects);
209
+
210
+ // Other ready-phase messages
211
+ default:
212
+ return this.handleReadyPhaseMessage(message);
213
+ }
214
+ }
215
+
216
+ private handleClientSetup(
217
+ direction: "inbound" | "outbound",
218
+ ): TransitionResult<Draft15MessageType> {
219
+ if (this._phase !== "idle") {
220
+ return {
221
+ ok: false,
222
+ violation: violation(
223
+ "SETUP_VIOLATION",
224
+ "CLIENT_SETUP already sent/received",
225
+ this._phase,
226
+ "client_setup",
227
+ ),
228
+ };
229
+ }
230
+ if (direction === "outbound" && this._role !== "client") {
231
+ return {
232
+ ok: false,
233
+ violation: violation(
234
+ "ROLE_VIOLATION",
235
+ "Only client can send CLIENT_SETUP",
236
+ this._phase,
237
+ "client_setup",
238
+ ),
239
+ };
240
+ }
241
+ this._phase = "setup";
242
+ return { ok: true, phase: this._phase, sideEffects: [] };
243
+ }
244
+
245
+ private handleServerSetup(
246
+ direction: "inbound" | "outbound",
247
+ ): TransitionResult<Draft15MessageType> {
248
+ if (this._phase !== "setup") {
249
+ return {
250
+ ok: false,
251
+ violation: violation(
252
+ "SETUP_VIOLATION",
253
+ "SERVER_SETUP before CLIENT_SETUP",
254
+ this._phase,
255
+ "server_setup",
256
+ ),
257
+ };
258
+ }
259
+ if (direction === "outbound" && this._role !== "server") {
260
+ return {
261
+ ok: false,
262
+ violation: violation(
263
+ "ROLE_VIOLATION",
264
+ "Only server can send SERVER_SETUP",
265
+ this._phase,
266
+ "server_setup",
267
+ ),
268
+ };
269
+ }
270
+ this._phase = "ready";
271
+ return { ok: true, phase: this._phase, sideEffects: [{ type: "session-ready" }] };
272
+ }
273
+
274
+ private handleGoAway(
275
+ message: Draft15Message,
276
+ sideEffects: SideEffect[],
277
+ ): TransitionResult<Draft15MessageType> {
278
+ if (this._phase !== "ready" && this._phase !== "draining") {
279
+ return {
280
+ ok: false,
281
+ violation: violation(
282
+ "UNEXPECTED_MESSAGE",
283
+ `GOAWAY not valid in phase ${this._phase}`,
284
+ this._phase,
285
+ "goaway",
286
+ ),
287
+ };
288
+ }
289
+ this._phase = "draining";
290
+ const goaway = message as import("./types.js").Draft15GoAway;
291
+ sideEffects.push({ type: "session-draining", goAwayUri: goaway.new_session_uri });
292
+ return { ok: true, phase: this._phase, sideEffects };
293
+ }
294
+
295
+ private requireReady(msgType: Draft15MessageType): ProtocolViolation<Draft15MessageType> | null {
296
+ if (this._phase !== "ready" && this._phase !== "draining") {
297
+ return violation(
298
+ this._phase === "idle" || this._phase === "setup"
299
+ ? "MESSAGE_BEFORE_SETUP"
300
+ : "UNEXPECTED_MESSAGE",
301
+ `${msgType} requires ready phase, current: ${this._phase}`,
302
+ this._phase,
303
+ msgType,
304
+ );
305
+ }
306
+ return null;
307
+ }
308
+
309
+ private handleSubscribe(
310
+ message: Draft15Message,
311
+ sideEffects: SideEffect[],
312
+ ): TransitionResult<Draft15MessageType> {
313
+ const err = this.requireReady(message.type);
314
+ if (err) return { ok: false, violation: err };
315
+ const sub = message as import("./types.js").Draft15Subscribe;
316
+ const dupErr = this.checkDuplicateRequestId(sub.request_id, message.type);
317
+ if (dupErr) return { ok: false, violation: dupErr };
318
+ this._requestIds.add(sub.request_id);
319
+ this._subscriptions.set(sub.request_id, {
320
+ subscribeId: sub.request_id,
321
+ phase: "pending",
322
+ trackNamespace: sub.track_namespace,
323
+ trackName: sub.track_name,
324
+ });
325
+ return { ok: true, phase: this._phase, sideEffects };
326
+ }
327
+
328
+ private handleSubscribeOk(
329
+ message: Draft15Message,
330
+ sideEffects: SideEffect[],
331
+ ): TransitionResult<Draft15MessageType> {
332
+ const err = this.requireReady(message.type);
333
+ if (err) return { ok: false, violation: err };
334
+ const ok = message as import("./types.js").Draft15SubscribeOk;
335
+ const idErr = this.checkKnownRequestId(ok.request_id, message.type);
336
+ if (idErr) return { ok: false, violation: idErr };
337
+ const existing = this._subscriptions.get(ok.request_id);
338
+ if (!existing)
339
+ return {
340
+ ok: false,
341
+ violation: violation(
342
+ "UNKNOWN_REQUEST_ID",
343
+ `No subscription with request ID ${ok.request_id}`,
344
+ this._phase,
345
+ message.type,
346
+ ),
347
+ };
348
+ if (existing.phase !== "pending")
349
+ return {
350
+ ok: false,
351
+ violation: violation(
352
+ "STATE_VIOLATION",
353
+ `Subscription ${ok.request_id} is ${existing.phase}, not pending`,
354
+ this._phase,
355
+ message.type,
356
+ ),
357
+ };
358
+ this._subscriptions.set(ok.request_id, { ...existing, phase: "active" });
359
+ sideEffects.push({ type: "subscription-activated", subscribeId: ok.request_id });
360
+ return { ok: true, phase: this._phase, sideEffects };
361
+ }
362
+
363
+ private handleSubscribeUpdate(
364
+ message: Draft15Message,
365
+ sideEffects: SideEffect[],
366
+ ): TransitionResult<Draft15MessageType> {
367
+ const err = this.requireReady(message.type);
368
+ if (err) return { ok: false, violation: err };
369
+ const update = message as import("./types.js").Draft15SubscribeUpdate;
370
+ // subscribe_update now uses its own request_id + subscription_request_id
371
+ const dupErr = this.checkDuplicateRequestId(update.request_id, message.type);
372
+ if (dupErr) return { ok: false, violation: dupErr };
373
+ this._requestIds.add(update.request_id);
374
+ const existing = this._subscriptions.get(update.subscription_request_id);
375
+ if (!existing)
376
+ return {
377
+ ok: false,
378
+ violation: violation(
379
+ "UNKNOWN_REQUEST_ID",
380
+ `No subscription with request ID ${update.subscription_request_id}`,
381
+ this._phase,
382
+ message.type,
383
+ ),
384
+ };
385
+ if (existing.phase !== "active")
386
+ return {
387
+ ok: false,
388
+ violation: violation(
389
+ "STATE_VIOLATION",
390
+ `Subscription ${update.subscription_request_id} is ${existing.phase}, not active`,
391
+ this._phase,
392
+ message.type,
393
+ ),
394
+ };
395
+ return { ok: true, phase: this._phase, sideEffects };
396
+ }
397
+
398
+ private handleUnsubscribe(
399
+ message: Draft15Message,
400
+ sideEffects: SideEffect[],
401
+ ): TransitionResult<Draft15MessageType> {
402
+ const err = this.requireReady(message.type);
403
+ if (err) return { ok: false, violation: err };
404
+ const unsub = message as import("./types.js").Draft15Unsubscribe;
405
+ const idErr = this.checkKnownRequestId(unsub.request_id, message.type);
406
+ if (idErr) return { ok: false, violation: idErr };
407
+ const existing = this._subscriptions.get(unsub.request_id);
408
+ if (!existing)
409
+ return {
410
+ ok: false,
411
+ violation: violation(
412
+ "UNKNOWN_REQUEST_ID",
413
+ `No subscription with request ID ${unsub.request_id}`,
414
+ this._phase,
415
+ message.type,
416
+ ),
417
+ };
418
+ this._subscriptions.set(unsub.request_id, { ...existing, phase: "done" });
419
+ sideEffects.push({
420
+ type: "subscription-ended",
421
+ subscribeId: unsub.request_id,
422
+ reason: "unsubscribed",
423
+ });
424
+ return { ok: true, phase: this._phase, sideEffects };
425
+ }
426
+
427
+ private handlePublish(
428
+ message: Draft15Message,
429
+ sideEffects: SideEffect[],
430
+ ): TransitionResult<Draft15MessageType> {
431
+ const err = this.requireReady(message.type);
432
+ if (err) return { ok: false, violation: err };
433
+ const pub = message as import("./types.js").Draft15Publish;
434
+ const dupErr = this.checkDuplicateRequestId(pub.request_id, message.type);
435
+ if (dupErr) return { ok: false, violation: dupErr };
436
+ this._requestIds.add(pub.request_id);
437
+ this._publishes.set(pub.request_id, { requestId: pub.request_id, phase: "pending" });
438
+ return { ok: true, phase: this._phase, sideEffects };
439
+ }
440
+
441
+ private handlePublishOk(
442
+ message: Draft15Message,
443
+ sideEffects: SideEffect[],
444
+ ): TransitionResult<Draft15MessageType> {
445
+ const err = this.requireReady(message.type);
446
+ if (err) return { ok: false, violation: err };
447
+ const ok = message as import("./types.js").Draft15PublishOk;
448
+ const idErr = this.checkKnownRequestId(ok.request_id, message.type);
449
+ if (idErr) return { ok: false, violation: idErr };
450
+ const existing = this._publishes.get(ok.request_id);
451
+ if (!existing)
452
+ return {
453
+ ok: false,
454
+ violation: violation(
455
+ "UNKNOWN_REQUEST_ID",
456
+ `No publish with request ID ${ok.request_id}`,
457
+ this._phase,
458
+ message.type,
459
+ ),
460
+ };
461
+ if (existing.phase !== "pending")
462
+ return {
463
+ ok: false,
464
+ violation: violation(
465
+ "STATE_VIOLATION",
466
+ `Publish ${ok.request_id} is ${existing.phase}, not pending`,
467
+ this._phase,
468
+ message.type,
469
+ ),
470
+ };
471
+ this._publishes.set(ok.request_id, { ...existing, phase: "active" });
472
+ sideEffects.push({ type: "publish-activated", requestId: ok.request_id });
473
+ return { ok: true, phase: this._phase, sideEffects };
474
+ }
475
+
476
+ private handlePublishDone(
477
+ message: Draft15Message,
478
+ sideEffects: SideEffect[],
479
+ ): TransitionResult<Draft15MessageType> {
480
+ const err = this.requireReady(message.type);
481
+ if (err) return { ok: false, violation: err };
482
+ const done = message as import("./types.js").Draft15PublishDone;
483
+ const idErr = this.checkKnownRequestId(done.request_id, message.type);
484
+ if (idErr) return { ok: false, violation: idErr };
485
+ const existing = this._publishes.get(done.request_id);
486
+ if (!existing)
487
+ return {
488
+ ok: false,
489
+ violation: violation(
490
+ "UNKNOWN_REQUEST_ID",
491
+ `No publish with request ID ${done.request_id}`,
492
+ this._phase,
493
+ message.type,
494
+ ),
495
+ };
496
+ this._publishes.set(done.request_id, { ...existing, phase: "done" });
497
+ sideEffects.push({
498
+ type: "publish-ended",
499
+ requestId: done.request_id,
500
+ reason: done.reason_phrase,
501
+ });
502
+ return { ok: true, phase: this._phase, sideEffects };
503
+ }
504
+
505
+ private handleFetch(
506
+ message: Draft15Message,
507
+ sideEffects: SideEffect[],
508
+ ): TransitionResult<Draft15MessageType> {
509
+ const err = this.requireReady(message.type);
510
+ if (err) return { ok: false, violation: err };
511
+ const fetch = message as import("./types.js").Draft15Fetch;
512
+ const dupErr = this.checkDuplicateRequestId(fetch.request_id, message.type);
513
+ if (dupErr) return { ok: false, violation: dupErr };
514
+ this._requestIds.add(fetch.request_id);
515
+ this._fetches.set(fetch.request_id, { requestId: fetch.request_id, phase: "pending" });
516
+ return { ok: true, phase: this._phase, sideEffects };
517
+ }
518
+
519
+ private handleFetchOk(
520
+ message: Draft15Message,
521
+ sideEffects: SideEffect[],
522
+ ): TransitionResult<Draft15MessageType> {
523
+ const err = this.requireReady(message.type);
524
+ if (err) return { ok: false, violation: err };
525
+ const ok = message as import("./types.js").Draft15FetchOk;
526
+ const idErr = this.checkKnownRequestId(ok.request_id, message.type);
527
+ if (idErr) return { ok: false, violation: idErr };
528
+ const existing = this._fetches.get(ok.request_id);
529
+ if (!existing)
530
+ return {
531
+ ok: false,
532
+ violation: violation(
533
+ "UNKNOWN_REQUEST_ID",
534
+ `No fetch with request ID ${ok.request_id}`,
535
+ this._phase,
536
+ message.type,
537
+ ),
538
+ };
539
+ if (existing.phase !== "pending")
540
+ return {
541
+ ok: false,
542
+ violation: violation(
543
+ "STATE_VIOLATION",
544
+ `Fetch ${ok.request_id} is ${existing.phase}, not pending`,
545
+ this._phase,
546
+ message.type,
547
+ ),
548
+ };
549
+ this._fetches.set(ok.request_id, { ...existing, phase: "active" });
550
+ sideEffects.push({ type: "fetch-activated", requestId: ok.request_id });
551
+ return { ok: true, phase: this._phase, sideEffects };
552
+ }
553
+
554
+ private handleFetchCancel(
555
+ message: Draft15Message,
556
+ sideEffects: SideEffect[],
557
+ ): TransitionResult<Draft15MessageType> {
558
+ const err = this.requireReady(message.type);
559
+ if (err) return { ok: false, violation: err };
560
+ const cancel = message as import("./types.js").Draft15FetchCancel;
561
+ const idErr = this.checkKnownRequestId(cancel.request_id, message.type);
562
+ if (idErr) return { ok: false, violation: idErr };
563
+ const existing = this._fetches.get(cancel.request_id);
564
+ if (!existing)
565
+ return {
566
+ ok: false,
567
+ violation: violation(
568
+ "UNKNOWN_REQUEST_ID",
569
+ `No fetch with request ID ${cancel.request_id}`,
570
+ this._phase,
571
+ message.type,
572
+ ),
573
+ };
574
+ this._fetches.set(cancel.request_id, { ...existing, phase: "cancelled" });
575
+ sideEffects.push({ type: "fetch-ended", requestId: cancel.request_id, reason: "cancelled" });
576
+ return { ok: true, phase: this._phase, sideEffects };
577
+ }
578
+
579
+ // REQUEST_ERROR replaces subscribe_error, publish_error, fetch_error, etc.
580
+ private handleRequestError(
581
+ message: Draft15Message,
582
+ sideEffects: SideEffect[],
583
+ ): TransitionResult<Draft15MessageType> {
584
+ const err = this.requireReady(message.type);
585
+ if (err) return { ok: false, violation: err };
586
+ const reqErr = message as import("./types.js").Draft15RequestError;
587
+ // REQUEST_ERROR can target any pending request — try to find it
588
+ const sub = this._subscriptions.get(reqErr.request_id);
589
+ if (sub && sub.phase === "pending") {
590
+ this._subscriptions.set(reqErr.request_id, { ...sub, phase: "error" });
591
+ sideEffects.push({
592
+ type: "subscription-ended",
593
+ subscribeId: reqErr.request_id,
594
+ reason: reqErr.reason_phrase,
595
+ });
596
+ return { ok: true, phase: this._phase, sideEffects };
597
+ }
598
+ const pub = this._publishes.get(reqErr.request_id);
599
+ if (pub && pub.phase === "pending") {
600
+ this._publishes.set(reqErr.request_id, { ...pub, phase: "error" });
601
+ sideEffects.push({
602
+ type: "publish-ended",
603
+ requestId: reqErr.request_id,
604
+ reason: reqErr.reason_phrase,
605
+ });
606
+ return { ok: true, phase: this._phase, sideEffects };
607
+ }
608
+ const fetch = this._fetches.get(reqErr.request_id);
609
+ if (fetch && fetch.phase === "pending") {
610
+ this._fetches.set(reqErr.request_id, { ...fetch, phase: "error" });
611
+ sideEffects.push({
612
+ type: "fetch-ended",
613
+ requestId: reqErr.request_id,
614
+ reason: reqErr.reason_phrase,
615
+ });
616
+ return { ok: true, phase: this._phase, sideEffects };
617
+ }
618
+ // Could also be for subscribe_namespace, publish_namespace, track_status — allow through
619
+ return { ok: true, phase: this._phase, sideEffects };
620
+ }
621
+
622
+ // REQUEST_OK replaces subscribe_namespace_ok, publish_namespace_ok, track_status_ok
623
+ private handleRequestOk(
624
+ message: Draft15Message,
625
+ sideEffects: SideEffect[],
626
+ ): TransitionResult<Draft15MessageType> {
627
+ const err = this.requireReady(message.type);
628
+ if (err) return { ok: false, violation: err };
629
+ // REQUEST_OK is a generic success response — allow through
630
+ return { ok: true, phase: this._phase, sideEffects };
631
+ }
632
+
633
+ // ─── Namespace and track status request ID tracking ───
634
+
635
+ private handlePublishNamespace(
636
+ message: Draft15Message,
637
+ sideEffects: SideEffect[],
638
+ ): TransitionResult<Draft15MessageType> {
639
+ const err = this.requireReady(message.type);
640
+ if (err) return { ok: false, violation: err };
641
+ const pn = message as import("./types.js").Draft15PublishNamespace;
642
+ const dupErr = this.checkDuplicateRequestId(pn.request_id, message.type);
643
+ if (dupErr) return { ok: false, violation: dupErr };
644
+ this._requestIds.add(pn.request_id);
645
+ return { ok: true, phase: this._phase, sideEffects };
646
+ }
647
+
648
+ private handleSubscribeNamespace(
649
+ message: Draft15Message,
650
+ sideEffects: SideEffect[],
651
+ ): TransitionResult<Draft15MessageType> {
652
+ const err = this.requireReady(message.type);
653
+ if (err) return { ok: false, violation: err };
654
+ const sn = message as import("./types.js").Draft15SubscribeNamespace;
655
+ const dupErr = this.checkDuplicateRequestId(sn.request_id, message.type);
656
+ if (dupErr) return { ok: false, violation: dupErr };
657
+ this._requestIds.add(sn.request_id);
658
+ return { ok: true, phase: this._phase, sideEffects };
659
+ }
660
+
661
+ private handleTrackStatus(
662
+ message: Draft15Message,
663
+ sideEffects: SideEffect[],
664
+ ): TransitionResult<Draft15MessageType> {
665
+ const err = this.requireReady(message.type);
666
+ if (err) return { ok: false, violation: err };
667
+ const ts = message as import("./types.js").Draft15TrackStatus;
668
+ const dupErr = this.checkDuplicateRequestId(ts.request_id, message.type);
669
+ if (dupErr) return { ok: false, violation: dupErr };
670
+ this._requestIds.add(ts.request_id);
671
+ return { ok: true, phase: this._phase, sideEffects };
672
+ }
673
+
674
+ private handleReadyPhaseMessage(message: Draft15Message): TransitionResult<Draft15MessageType> {
675
+ const err = this.requireReady(message.type);
676
+ if (err) return { ok: false, violation: err };
677
+ return { ok: true, phase: this._phase, sideEffects: [] };
678
+ }
679
+
680
+ reset(): void {
681
+ this._phase = "idle";
682
+ this._subscriptions.clear();
683
+ this._publishes.clear();
684
+ this._fetches.clear();
685
+ this._requestIds.clear();
686
+ }
687
+ }
@@ -0,0 +1,26 @@
1
+ import type { SessionState, SessionStateOptions } from "../../core/session-types.js";
2
+ import { Draft15SessionFSM } from "./session-fsm.js";
3
+ import type { Draft15Message, Draft15MessageType } from "./types.js";
4
+
5
+ export function createDraft15SessionState(
6
+ options: SessionStateOptions,
7
+ ): SessionState<Draft15Message, Draft15MessageType> {
8
+ return new Draft15SessionFSM(options.role);
9
+ }
10
+
11
+ export type {
12
+ FetchPhase,
13
+ FetchState,
14
+ ProtocolViolation,
15
+ ProtocolViolationCode,
16
+ PublishPhase,
17
+ PublishState,
18
+ SessionPhase,
19
+ SessionState,
20
+ SessionStateOptions,
21
+ SideEffect,
22
+ SubscriptionPhase,
23
+ SubscriptionState,
24
+ TransitionResult,
25
+ ValidationResult,
26
+ } from "../../core/session-types.js";