@moqtap/codec 0.4.3 → 0.6.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 (133) hide show
  1. package/dist/chunk-62LYLROG.js +2095 -0
  2. package/dist/chunk-A33U4WVS.cjs +1993 -0
  3. package/dist/chunk-A655PHFX.cjs +446 -0
  4. package/dist/chunk-CQ65LKPQ.cjs +446 -0
  5. package/dist/chunk-DLSGOZ6P.js +1993 -0
  6. package/dist/chunk-IWYKDCXP.js +446 -0
  7. package/dist/chunk-MSG2FXJ2.cjs +2095 -0
  8. package/dist/chunk-O5DQ44TM.js +446 -0
  9. package/dist/{codec-C7UzTYSS.d.cts → codec-B1snaM9L.d.cts} +1 -1
  10. package/dist/{codec-ZwV-Njt6.d.ts → codec-BLYAy2OP.d.ts} +1 -1
  11. package/dist/{codec-B2lYE0YA.d.ts → codec-BNS3gS77.d.ts} +1 -1
  12. package/dist/{codec-BbbLDSKA.d.cts → codec-BOtv3R5j.d.cts} +1 -1
  13. package/dist/{codec-DDUBJGn0.d.cts → codec-BOyrk_C9.d.cts} +1 -1
  14. package/dist/{codec-BWQ5Jlap.d.cts → codec-BqqoYdh_.d.cts} +1 -1
  15. package/dist/{codec-3Ppy_5Gd.d.ts → codec-CI0BjmHc.d.ts} +1 -1
  16. package/dist/{codec-DftaRuVz.d.ts → codec-CWj5iTxA.d.ts} +1 -1
  17. package/dist/{codec-B_QST4y1.d.cts → codec-CyApDKwf.d.cts} +1 -1
  18. package/dist/{codec-BFg5xnrJ.d.ts → codec-D2JbI2tp.d.ts} +1 -1
  19. package/dist/{codec-DFU-6Rax.d.ts → codec-D47r_-Rl.d.ts} +1 -1
  20. package/dist/{codec-Db-0pk38.d.ts → codec-D49OofBr.d.ts} +1 -1
  21. package/dist/codec-D89DWXbv.d.cts +30 -0
  22. package/dist/{codec-R9TJCq-E.d.cts → codec-D95gT98n.d.cts} +1 -1
  23. package/dist/codec-DAePx2NL.d.ts +30 -0
  24. package/dist/{codec-CpZrZ3ok.d.ts → codec-DJmV04i0.d.ts} +1 -1
  25. package/dist/{codec-Va_wZCF_.d.ts → codec-DL0njGkL.d.ts} +1 -1
  26. package/dist/{codec-CFWQY9Lj.d.cts → codec-DQWQSam3.d.cts} +1 -1
  27. package/dist/{codec-wtt6WF1K.d.ts → codec-DplRgIKq.d.ts} +1 -1
  28. package/dist/{codec-CewFCsEy.d.ts → codec-DujHf55Z.d.ts} +1 -1
  29. package/dist/{codec-j819z_hL.d.cts → codec-DvcJgYKq.d.cts} +1 -1
  30. package/dist/{codec-C8aSvE41.d.cts → codec-NMsTK0GX.d.cts} +1 -1
  31. package/dist/codec-WwwUipaU.d.ts +30 -0
  32. package/dist/{codec-CAbm9d_n.d.cts → codec-cou9XBMF.d.cts} +1 -1
  33. package/dist/{codec-lqDowbyT.d.cts → codec-hvkJfCF5.d.cts} +1 -1
  34. package/dist/codec-xeuGZY77.d.cts +30 -0
  35. package/dist/draft07-session.d.cts +3 -3
  36. package/dist/draft07-session.d.ts +3 -3
  37. package/dist/draft07.d.cts +3 -3
  38. package/dist/draft07.d.ts +3 -3
  39. package/dist/draft08-session.d.cts +3 -3
  40. package/dist/draft08-session.d.ts +3 -3
  41. package/dist/draft08.d.cts +4 -4
  42. package/dist/draft08.d.ts +4 -4
  43. package/dist/draft09-session.d.cts +3 -3
  44. package/dist/draft09-session.d.ts +3 -3
  45. package/dist/draft09.d.cts +4 -4
  46. package/dist/draft09.d.ts +4 -4
  47. package/dist/draft10-session.d.cts +3 -3
  48. package/dist/draft10-session.d.ts +3 -3
  49. package/dist/draft10.d.cts +4 -4
  50. package/dist/draft10.d.ts +4 -4
  51. package/dist/draft11-session.d.cts +3 -3
  52. package/dist/draft11-session.d.ts +3 -3
  53. package/dist/draft11.d.cts +4 -4
  54. package/dist/draft11.d.ts +4 -4
  55. package/dist/draft12-session.d.cts +3 -3
  56. package/dist/draft12-session.d.ts +3 -3
  57. package/dist/draft12.d.cts +4 -4
  58. package/dist/draft12.d.ts +4 -4
  59. package/dist/draft13-session.d.cts +3 -3
  60. package/dist/draft13-session.d.ts +3 -3
  61. package/dist/draft13.d.cts +4 -4
  62. package/dist/draft13.d.ts +4 -4
  63. package/dist/draft14-session.d.cts +3 -3
  64. package/dist/draft14-session.d.ts +3 -3
  65. package/dist/draft14.d.cts +4 -4
  66. package/dist/draft14.d.ts +4 -4
  67. package/dist/draft15-session.d.cts +3 -3
  68. package/dist/draft15-session.d.ts +3 -3
  69. package/dist/draft15.d.cts +4 -4
  70. package/dist/draft15.d.ts +4 -4
  71. package/dist/draft16-session.d.cts +3 -3
  72. package/dist/draft16-session.d.ts +3 -3
  73. package/dist/draft16.d.cts +4 -4
  74. package/dist/draft16.d.ts +4 -4
  75. package/dist/draft17-session.d.cts +3 -3
  76. package/dist/draft17-session.d.ts +3 -3
  77. package/dist/draft17.d.cts +3 -3
  78. package/dist/draft17.d.ts +3 -3
  79. package/dist/draft18-session.cjs +8 -0
  80. package/dist/draft18-session.d.cts +50 -0
  81. package/dist/draft18-session.d.ts +50 -0
  82. package/dist/draft18-session.js +8 -0
  83. package/dist/draft18.cjs +179 -0
  84. package/dist/draft18.d.cts +126 -0
  85. package/dist/draft18.d.ts +126 -0
  86. package/dist/draft18.js +179 -0
  87. package/dist/draft19-session.cjs +8 -0
  88. package/dist/draft19-session.d.cts +50 -0
  89. package/dist/draft19-session.d.ts +50 -0
  90. package/dist/draft19-session.js +8 -0
  91. package/dist/draft19.cjs +189 -0
  92. package/dist/draft19.d.cts +130 -0
  93. package/dist/draft19.d.ts +130 -0
  94. package/dist/draft19.js +189 -0
  95. package/dist/index.cjs +13 -1
  96. package/dist/index.d.cts +24 -14
  97. package/dist/index.d.ts +24 -14
  98. package/dist/index.js +13 -1
  99. package/dist/{session-types-X-IYEGoG.d.ts → session-types-C0l_-Ups.d.ts} +1 -1
  100. package/dist/{session-types-C7UONex5.d.cts → session-types-KsqyVp6y.d.cts} +1 -1
  101. package/dist/session.cjs +9 -1
  102. package/dist/session.d.cts +7 -3
  103. package/dist/session.d.ts +7 -3
  104. package/dist/session.js +9 -1
  105. package/dist/{types-DuIO7cWK.d.cts → types-CdCSIfvI.d.cts} +1 -1
  106. package/dist/{types-DuIO7cWK.d.ts → types-CdCSIfvI.d.ts} +1 -1
  107. package/dist/types-Cqp10JtB.d.cts +249 -0
  108. package/dist/types-Cqp10JtB.d.ts +249 -0
  109. package/dist/types-zHgCS6o5.d.cts +265 -0
  110. package/dist/types-zHgCS6o5.d.ts +265 -0
  111. package/package.json +26 -2
  112. package/src/core/types.ts +14 -1
  113. package/src/drafts/draft17/error-codes.ts +72 -0
  114. package/src/drafts/draft18/codec.ts +1310 -0
  115. package/src/drafts/draft18/data-streams.ts +996 -0
  116. package/src/drafts/draft18/error-codes.ts +75 -0
  117. package/src/drafts/draft18/index.ts +122 -0
  118. package/src/drafts/draft18/messages.ts +56 -0
  119. package/src/drafts/draft18/rules.ts +85 -0
  120. package/src/drafts/draft18/session-fsm.ts +451 -0
  121. package/src/drafts/draft18/session.ts +15 -0
  122. package/src/drafts/draft18/types.ts +347 -0
  123. package/src/drafts/draft19/codec.ts +1425 -0
  124. package/src/drafts/draft19/data-streams.ts +996 -0
  125. package/src/drafts/draft19/error-codes.ts +78 -0
  126. package/src/drafts/draft19/index.ts +126 -0
  127. package/src/drafts/draft19/messages.ts +58 -0
  128. package/src/drafts/draft19/rules.ts +85 -0
  129. package/src/drafts/draft19/session-fsm.ts +451 -0
  130. package/src/drafts/draft19/session.ts +15 -0
  131. package/src/drafts/draft19/types.ts +368 -0
  132. package/src/index.ts +17 -1
  133. package/src/session.ts +8 -0
@@ -0,0 +1,451 @@
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 { getLegalIncoming, getLegalOutgoing } from './rules.js'
13
+ import type { Draft18Message, Draft18MessageType } from './types.js'
14
+
15
+ function violation(
16
+ code: ProtocolViolation<Draft18MessageType>['code'],
17
+ message: string,
18
+ currentPhase: SessionPhase,
19
+ offendingMessage: Draft18MessageType,
20
+ ): ProtocolViolation<Draft18MessageType> {
21
+ return { code, message, currentPhase, offendingMessage }
22
+ }
23
+
24
+ export class Draft18SessionFSM {
25
+ private _phase: SessionPhase = 'idle'
26
+ private _role: 'client' | 'server'
27
+ private _setupDirection: 'inbound' | 'outbound' | null = null
28
+ private _subscriptions = new Map<bigint, SubscriptionState>()
29
+ private _publishes = new Map<bigint, PublishState>()
30
+ private _fetches = new Map<bigint, FetchState>()
31
+ private _requestIds = new Set<bigint>()
32
+ private _pendingSubscribes: bigint[] = []
33
+ private _pendingPublishes: bigint[] = []
34
+ private _pendingFetches: bigint[] = []
35
+
36
+ constructor(role: 'client' | 'server') {
37
+ this._role = role
38
+ }
39
+
40
+ get phase(): SessionPhase {
41
+ return this._phase
42
+ }
43
+ get role(): 'client' | 'server' {
44
+ return this._role
45
+ }
46
+ get subscriptions(): ReadonlyMap<bigint, SubscriptionState> {
47
+ return this._subscriptions
48
+ }
49
+ get announces(): ReadonlyMap<string, AnnounceState> {
50
+ return new Map()
51
+ }
52
+ get publishes(): ReadonlyMap<bigint, PublishState> {
53
+ return this._publishes
54
+ }
55
+ get fetches(): ReadonlyMap<bigint, FetchState> {
56
+ return this._fetches
57
+ }
58
+
59
+ get legalOutgoing(): ReadonlySet<Draft18MessageType> {
60
+ return getLegalOutgoing(this._phase, this._role)
61
+ }
62
+
63
+ get legalIncoming(): ReadonlySet<Draft18MessageType> {
64
+ return getLegalIncoming(this._phase, this._role)
65
+ }
66
+
67
+ private checkDuplicateRequestId(
68
+ requestId: bigint,
69
+ msgType: Draft18MessageType,
70
+ ): ProtocolViolation<Draft18MessageType> | null {
71
+ if (this._requestIds.has(requestId)) {
72
+ return violation(
73
+ 'DUPLICATE_REQUEST_ID',
74
+ `Request ID ${requestId} already in use`,
75
+ this._phase,
76
+ msgType,
77
+ )
78
+ }
79
+ return null
80
+ }
81
+
82
+ validateOutgoing(message: Draft18Message): ValidationResult<Draft18MessageType> {
83
+ if (!this.legalOutgoing.has(message.type)) {
84
+ return {
85
+ ok: false,
86
+ violation: violation(
87
+ this._phase === 'idle' || this._phase === 'setup'
88
+ ? 'MESSAGE_BEFORE_SETUP'
89
+ : 'UNEXPECTED_MESSAGE',
90
+ `Cannot send ${message.type} in phase ${this._phase}`,
91
+ this._phase,
92
+ message.type,
93
+ ),
94
+ }
95
+ }
96
+ return { ok: true }
97
+ }
98
+
99
+ receive(message: Draft18Message): TransitionResult<Draft18MessageType> {
100
+ return this.applyTransition(message, 'inbound')
101
+ }
102
+
103
+ send(message: Draft18Message): TransitionResult<Draft18MessageType> {
104
+ return this.applyTransition(message, 'outbound')
105
+ }
106
+
107
+ private applyTransition(
108
+ message: Draft18Message,
109
+ direction: 'inbound' | 'outbound',
110
+ ): TransitionResult<Draft18MessageType> {
111
+ const sideEffects: SideEffect[] = []
112
+
113
+ switch (message.type) {
114
+ case 'setup':
115
+ return this.handleSetup(direction)
116
+ case 'goaway':
117
+ return this.handleGoAway(message, sideEffects)
118
+
119
+ case 'subscribe':
120
+ return this.handleSubscribe(message, sideEffects)
121
+ case 'subscribe_ok':
122
+ return this.handleSubscribeOk(message, sideEffects)
123
+ case 'request_update':
124
+ return this.handleRequestUpdate(message, sideEffects)
125
+
126
+ case 'publish':
127
+ return this.handlePublish(message, sideEffects)
128
+ case 'publish_done':
129
+ return this.handlePublishDone(message, sideEffects)
130
+
131
+ case 'fetch':
132
+ return this.handleFetch(message, sideEffects)
133
+ case 'fetch_ok':
134
+ return this.handleFetchOk(sideEffects)
135
+
136
+ case 'request_ok':
137
+ return this.handleRequestOk(sideEffects)
138
+ case 'request_error':
139
+ return this.handleRequestError(sideEffects)
140
+
141
+ default:
142
+ return this.handleReadyPhaseMessage(message)
143
+ }
144
+ }
145
+
146
+ private handleSetup(direction: 'inbound' | 'outbound'): TransitionResult<Draft18MessageType> {
147
+ if (this._phase === 'idle') {
148
+ this._setupDirection = direction
149
+ this._phase = 'setup'
150
+ return { ok: true, phase: this._phase, sideEffects: [] }
151
+ }
152
+ if (this._phase === 'setup') {
153
+ if (direction === this._setupDirection) {
154
+ return {
155
+ ok: false,
156
+ violation: violation(
157
+ 'SETUP_VIOLATION',
158
+ `Second SETUP must be ${this._setupDirection === 'inbound' ? 'outbound' : 'inbound'}, got ${direction}`,
159
+ this._phase,
160
+ 'setup',
161
+ ),
162
+ }
163
+ }
164
+ this._phase = 'ready'
165
+ return {
166
+ ok: true,
167
+ phase: this._phase,
168
+ sideEffects: [{ type: 'session-ready' }],
169
+ }
170
+ }
171
+ return {
172
+ ok: false,
173
+ violation: violation(
174
+ 'SETUP_VIOLATION',
175
+ 'SETUP not valid in current phase',
176
+ this._phase,
177
+ 'setup',
178
+ ),
179
+ }
180
+ }
181
+
182
+ private handleGoAway(
183
+ message: Draft18Message,
184
+ sideEffects: SideEffect[],
185
+ ): TransitionResult<Draft18MessageType> {
186
+ if (this._phase !== 'ready' && this._phase !== 'draining') {
187
+ return {
188
+ ok: false,
189
+ violation: violation(
190
+ 'UNEXPECTED_MESSAGE',
191
+ `GOAWAY not valid in phase ${this._phase}`,
192
+ this._phase,
193
+ 'goaway',
194
+ ),
195
+ }
196
+ }
197
+ this._phase = 'draining'
198
+ const goaway = message as import('./types.js').Draft18GoAway
199
+ sideEffects.push({
200
+ type: 'session-draining',
201
+ goAwayUri: goaway.new_session_uri,
202
+ })
203
+ return { ok: true, phase: this._phase, sideEffects }
204
+ }
205
+
206
+ private requireReady(msgType: Draft18MessageType): ProtocolViolation<Draft18MessageType> | null {
207
+ if (this._phase !== 'ready' && this._phase !== 'draining') {
208
+ return violation(
209
+ this._phase === 'idle' || this._phase === 'setup'
210
+ ? 'MESSAGE_BEFORE_SETUP'
211
+ : 'UNEXPECTED_MESSAGE',
212
+ `${msgType} requires ready phase, current: ${this._phase}`,
213
+ this._phase,
214
+ msgType,
215
+ )
216
+ }
217
+ return null
218
+ }
219
+
220
+ private handleSubscribe(
221
+ message: Draft18Message,
222
+ sideEffects: SideEffect[],
223
+ ): TransitionResult<Draft18MessageType> {
224
+ const err = this.requireReady(message.type)
225
+ if (err) return { ok: false, violation: err }
226
+ const sub = message as import('./types.js').Draft18Subscribe
227
+ const dupErr = this.checkDuplicateRequestId(sub.request_id, message.type)
228
+ if (dupErr) return { ok: false, violation: dupErr }
229
+ this._requestIds.add(sub.request_id)
230
+ this._subscriptions.set(sub.request_id, {
231
+ subscribeId: sub.request_id,
232
+ phase: 'pending',
233
+ trackNamespace: sub.track_namespace,
234
+ trackName: sub.track_name,
235
+ })
236
+ this._pendingSubscribes.push(sub.request_id)
237
+ return { ok: true, phase: this._phase, sideEffects }
238
+ }
239
+
240
+ private handleSubscribeOk(
241
+ _message: Draft18Message,
242
+ sideEffects: SideEffect[],
243
+ ): TransitionResult<Draft18MessageType> {
244
+ const err = this.requireReady('subscribe_ok')
245
+ if (err) return { ok: false, violation: err }
246
+ const requestId = this._pendingSubscribes.shift()
247
+ if (requestId === undefined) {
248
+ return {
249
+ ok: false,
250
+ violation: violation(
251
+ 'UNEXPECTED_MESSAGE',
252
+ 'SUBSCRIBE_OK with no pending subscribe',
253
+ this._phase,
254
+ 'subscribe_ok',
255
+ ),
256
+ }
257
+ }
258
+ const existing = this._subscriptions.get(requestId)
259
+ if (existing && existing.phase === 'pending') {
260
+ this._subscriptions.set(requestId, { ...existing, phase: 'active' })
261
+ sideEffects.push({
262
+ type: 'subscription-activated',
263
+ subscribeId: requestId,
264
+ })
265
+ }
266
+ return { ok: true, phase: this._phase, sideEffects }
267
+ }
268
+
269
+ private handleRequestUpdate(
270
+ message: Draft18Message,
271
+ sideEffects: SideEffect[],
272
+ ): TransitionResult<Draft18MessageType> {
273
+ const err = this.requireReady(message.type)
274
+ if (err) return { ok: false, violation: err }
275
+ const update = message as import('./types.js').Draft18RequestUpdate
276
+ const dupErr = this.checkDuplicateRequestId(update.request_id, message.type)
277
+ if (dupErr) return { ok: false, violation: dupErr }
278
+ this._requestIds.add(update.request_id)
279
+ return { ok: true, phase: this._phase, sideEffects }
280
+ }
281
+
282
+ private handlePublish(
283
+ message: Draft18Message,
284
+ sideEffects: SideEffect[],
285
+ ): TransitionResult<Draft18MessageType> {
286
+ const err = this.requireReady(message.type)
287
+ if (err) return { ok: false, violation: err }
288
+ const pub = message as import('./types.js').Draft18Publish
289
+ const dupErr = this.checkDuplicateRequestId(pub.request_id, message.type)
290
+ if (dupErr) return { ok: false, violation: dupErr }
291
+ this._requestIds.add(pub.request_id)
292
+ this._publishes.set(pub.request_id, {
293
+ requestId: pub.request_id,
294
+ phase: 'pending',
295
+ })
296
+ this._pendingPublishes.push(pub.request_id)
297
+ return { ok: true, phase: this._phase, sideEffects }
298
+ }
299
+
300
+ private handlePublishDone(
301
+ _message: Draft18Message,
302
+ sideEffects: SideEffect[],
303
+ ): TransitionResult<Draft18MessageType> {
304
+ const err = this.requireReady('publish_done')
305
+ if (err) return { ok: false, violation: err }
306
+ // PUBLISH_DONE has no request_id; dequeue oldest active publish
307
+ for (const [reqId, pub] of this._publishes) {
308
+ if (pub.phase === 'active') {
309
+ this._publishes.set(reqId, { ...pub, phase: 'done' })
310
+ sideEffects.push({
311
+ type: 'publish-ended',
312
+ requestId: reqId,
313
+ reason: 'done',
314
+ })
315
+ break
316
+ }
317
+ }
318
+ return { ok: true, phase: this._phase, sideEffects }
319
+ }
320
+
321
+ private handleFetch(
322
+ message: Draft18Message,
323
+ sideEffects: SideEffect[],
324
+ ): TransitionResult<Draft18MessageType> {
325
+ const err = this.requireReady(message.type)
326
+ if (err) return { ok: false, violation: err }
327
+ const fetch = message as import('./types.js').Draft18Fetch
328
+ const dupErr = this.checkDuplicateRequestId(fetch.request_id, message.type)
329
+ if (dupErr) return { ok: false, violation: dupErr }
330
+ this._requestIds.add(fetch.request_id)
331
+ this._fetches.set(fetch.request_id, {
332
+ requestId: fetch.request_id,
333
+ phase: 'pending',
334
+ })
335
+ this._pendingFetches.push(fetch.request_id)
336
+ return { ok: true, phase: this._phase, sideEffects }
337
+ }
338
+
339
+ private handleFetchOk(sideEffects: SideEffect[]): TransitionResult<Draft18MessageType> {
340
+ const err = this.requireReady('fetch_ok')
341
+ if (err) return { ok: false, violation: err }
342
+ const requestId = this._pendingFetches.shift()
343
+ if (requestId === undefined) {
344
+ return {
345
+ ok: false,
346
+ violation: violation(
347
+ 'UNEXPECTED_MESSAGE',
348
+ 'FETCH_OK with no pending fetch',
349
+ this._phase,
350
+ 'fetch_ok',
351
+ ),
352
+ }
353
+ }
354
+ const existing = this._fetches.get(requestId)
355
+ if (existing && existing.phase === 'pending') {
356
+ this._fetches.set(requestId, { ...existing, phase: 'active' })
357
+ sideEffects.push({ type: 'fetch-activated', requestId })
358
+ }
359
+ return { ok: true, phase: this._phase, sideEffects }
360
+ }
361
+
362
+ private handleRequestError(sideEffects: SideEffect[]): TransitionResult<Draft18MessageType> {
363
+ const err = this.requireReady('request_error')
364
+ if (err) return { ok: false, violation: err }
365
+ // REQUEST_ERROR can target any pending request — try FIFO dequeue from each queue
366
+ const subId = this.dequeuePending(this._pendingSubscribes, this._subscriptions)
367
+ if (subId !== undefined) {
368
+ const sub = this._subscriptions.get(subId)!
369
+ this._subscriptions.set(subId, { ...sub, phase: 'error' })
370
+ sideEffects.push({
371
+ type: 'subscription-ended',
372
+ subscribeId: subId,
373
+ reason: 'request_error',
374
+ })
375
+ return { ok: true, phase: this._phase, sideEffects }
376
+ }
377
+ const pubId = this.dequeuePending(this._pendingPublishes, this._publishes)
378
+ if (pubId !== undefined) {
379
+ const pub = this._publishes.get(pubId)!
380
+ this._publishes.set(pubId, { ...pub, phase: 'error' })
381
+ sideEffects.push({
382
+ type: 'publish-ended',
383
+ requestId: pubId,
384
+ reason: 'request_error',
385
+ })
386
+ return { ok: true, phase: this._phase, sideEffects }
387
+ }
388
+ const fetchId = this.dequeuePending(this._pendingFetches, this._fetches)
389
+ if (fetchId !== undefined) {
390
+ const f = this._fetches.get(fetchId)!
391
+ this._fetches.set(fetchId, { ...f, phase: 'error' })
392
+ sideEffects.push({
393
+ type: 'fetch-ended',
394
+ requestId: fetchId,
395
+ reason: 'request_error',
396
+ })
397
+ return { ok: true, phase: this._phase, sideEffects }
398
+ }
399
+ // Could be for subscribe_namespace, subscribe_tracks, publish_namespace, track_status — allow through
400
+ return { ok: true, phase: this._phase, sideEffects }
401
+ }
402
+
403
+ private handleRequestOk(sideEffects: SideEffect[]): TransitionResult<Draft18MessageType> {
404
+ const err = this.requireReady('request_ok')
405
+ if (err) return { ok: false, violation: err }
406
+ // REQUEST_OK is the unified response (also serves as PUBLISH_OK alias) — try FIFO
407
+ // dequeue from each queue. Falls through for namespace-scoped requests.
408
+ const pubId = this.dequeuePending(this._pendingPublishes, this._publishes)
409
+ if (pubId !== undefined) {
410
+ const pub = this._publishes.get(pubId)!
411
+ this._publishes.set(pubId, { ...pub, phase: 'active' })
412
+ sideEffects.push({ type: 'publish-activated', requestId: pubId })
413
+ }
414
+ return { ok: true, phase: this._phase, sideEffects }
415
+ }
416
+
417
+ private dequeuePending(
418
+ queue: bigint[],
419
+ stateMap: ReadonlyMap<bigint, { phase: string }>,
420
+ ): bigint | undefined {
421
+ while (queue.length > 0) {
422
+ const id = queue[0]!
423
+ const state = stateMap.get(id)
424
+ if (state && state.phase === 'pending') {
425
+ queue.shift()
426
+ return id
427
+ }
428
+ // Skip non-pending entries (already resolved by type-specific handler)
429
+ queue.shift()
430
+ }
431
+ return undefined
432
+ }
433
+
434
+ private handleReadyPhaseMessage(message: Draft18Message): TransitionResult<Draft18MessageType> {
435
+ const err = this.requireReady(message.type)
436
+ if (err) return { ok: false, violation: err }
437
+ return { ok: true, phase: this._phase, sideEffects: [] }
438
+ }
439
+
440
+ reset(): void {
441
+ this._phase = 'idle'
442
+ this._setupDirection = null
443
+ this._subscriptions.clear()
444
+ this._publishes.clear()
445
+ this._fetches.clear()
446
+ this._requestIds.clear()
447
+ this._pendingSubscribes.length = 0
448
+ this._pendingPublishes.length = 0
449
+ this._pendingFetches.length = 0
450
+ }
451
+ }
@@ -0,0 +1,15 @@
1
+ import { Draft18SessionFSM } from './session-fsm.js'
2
+
3
+ export type {
4
+ ProtocolViolation,
5
+ SessionPhase,
6
+ SideEffect,
7
+ TransitionResult,
8
+ ValidationResult,
9
+ } from '../../core/session-types.js'
10
+
11
+ export function createDraft18SessionState(role: 'client' | 'server'): Draft18SessionFSM {
12
+ return new Draft18SessionFSM(role)
13
+ }
14
+
15
+ export { Draft18SessionFSM }