@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.
- package/dist/chunk-62LYLROG.js +2095 -0
- package/dist/chunk-A33U4WVS.cjs +1993 -0
- package/dist/chunk-A655PHFX.cjs +446 -0
- package/dist/chunk-CQ65LKPQ.cjs +446 -0
- package/dist/chunk-DLSGOZ6P.js +1993 -0
- package/dist/chunk-IWYKDCXP.js +446 -0
- package/dist/chunk-MSG2FXJ2.cjs +2095 -0
- package/dist/chunk-O5DQ44TM.js +446 -0
- package/dist/{codec-C7UzTYSS.d.cts → codec-B1snaM9L.d.cts} +1 -1
- package/dist/{codec-ZwV-Njt6.d.ts → codec-BLYAy2OP.d.ts} +1 -1
- package/dist/{codec-B2lYE0YA.d.ts → codec-BNS3gS77.d.ts} +1 -1
- package/dist/{codec-BbbLDSKA.d.cts → codec-BOtv3R5j.d.cts} +1 -1
- package/dist/{codec-DDUBJGn0.d.cts → codec-BOyrk_C9.d.cts} +1 -1
- package/dist/{codec-BWQ5Jlap.d.cts → codec-BqqoYdh_.d.cts} +1 -1
- package/dist/{codec-3Ppy_5Gd.d.ts → codec-CI0BjmHc.d.ts} +1 -1
- package/dist/{codec-DftaRuVz.d.ts → codec-CWj5iTxA.d.ts} +1 -1
- package/dist/{codec-B_QST4y1.d.cts → codec-CyApDKwf.d.cts} +1 -1
- package/dist/{codec-BFg5xnrJ.d.ts → codec-D2JbI2tp.d.ts} +1 -1
- package/dist/{codec-DFU-6Rax.d.ts → codec-D47r_-Rl.d.ts} +1 -1
- package/dist/{codec-Db-0pk38.d.ts → codec-D49OofBr.d.ts} +1 -1
- package/dist/codec-D89DWXbv.d.cts +30 -0
- package/dist/{codec-R9TJCq-E.d.cts → codec-D95gT98n.d.cts} +1 -1
- package/dist/codec-DAePx2NL.d.ts +30 -0
- package/dist/{codec-CpZrZ3ok.d.ts → codec-DJmV04i0.d.ts} +1 -1
- package/dist/{codec-Va_wZCF_.d.ts → codec-DL0njGkL.d.ts} +1 -1
- package/dist/{codec-CFWQY9Lj.d.cts → codec-DQWQSam3.d.cts} +1 -1
- package/dist/{codec-wtt6WF1K.d.ts → codec-DplRgIKq.d.ts} +1 -1
- package/dist/{codec-CewFCsEy.d.ts → codec-DujHf55Z.d.ts} +1 -1
- package/dist/{codec-j819z_hL.d.cts → codec-DvcJgYKq.d.cts} +1 -1
- package/dist/{codec-C8aSvE41.d.cts → codec-NMsTK0GX.d.cts} +1 -1
- package/dist/codec-WwwUipaU.d.ts +30 -0
- package/dist/{codec-CAbm9d_n.d.cts → codec-cou9XBMF.d.cts} +1 -1
- package/dist/{codec-lqDowbyT.d.cts → codec-hvkJfCF5.d.cts} +1 -1
- package/dist/codec-xeuGZY77.d.cts +30 -0
- package/dist/draft07-session.d.cts +3 -3
- package/dist/draft07-session.d.ts +3 -3
- package/dist/draft07.d.cts +3 -3
- package/dist/draft07.d.ts +3 -3
- package/dist/draft08-session.d.cts +3 -3
- package/dist/draft08-session.d.ts +3 -3
- package/dist/draft08.d.cts +4 -4
- package/dist/draft08.d.ts +4 -4
- package/dist/draft09-session.d.cts +3 -3
- package/dist/draft09-session.d.ts +3 -3
- package/dist/draft09.d.cts +4 -4
- package/dist/draft09.d.ts +4 -4
- package/dist/draft10-session.d.cts +3 -3
- package/dist/draft10-session.d.ts +3 -3
- package/dist/draft10.d.cts +4 -4
- package/dist/draft10.d.ts +4 -4
- package/dist/draft11-session.d.cts +3 -3
- package/dist/draft11-session.d.ts +3 -3
- package/dist/draft11.d.cts +4 -4
- package/dist/draft11.d.ts +4 -4
- package/dist/draft12-session.d.cts +3 -3
- package/dist/draft12-session.d.ts +3 -3
- package/dist/draft12.d.cts +4 -4
- package/dist/draft12.d.ts +4 -4
- package/dist/draft13-session.d.cts +3 -3
- package/dist/draft13-session.d.ts +3 -3
- package/dist/draft13.d.cts +4 -4
- package/dist/draft13.d.ts +4 -4
- package/dist/draft14-session.d.cts +3 -3
- package/dist/draft14-session.d.ts +3 -3
- package/dist/draft14.d.cts +4 -4
- package/dist/draft14.d.ts +4 -4
- package/dist/draft15-session.d.cts +3 -3
- package/dist/draft15-session.d.ts +3 -3
- package/dist/draft15.d.cts +4 -4
- package/dist/draft15.d.ts +4 -4
- package/dist/draft16-session.d.cts +3 -3
- package/dist/draft16-session.d.ts +3 -3
- package/dist/draft16.d.cts +4 -4
- package/dist/draft16.d.ts +4 -4
- package/dist/draft17-session.d.cts +3 -3
- package/dist/draft17-session.d.ts +3 -3
- package/dist/draft17.d.cts +3 -3
- package/dist/draft17.d.ts +3 -3
- package/dist/draft18-session.cjs +8 -0
- package/dist/draft18-session.d.cts +50 -0
- package/dist/draft18-session.d.ts +50 -0
- package/dist/draft18-session.js +8 -0
- package/dist/draft18.cjs +179 -0
- package/dist/draft18.d.cts +126 -0
- package/dist/draft18.d.ts +126 -0
- package/dist/draft18.js +179 -0
- package/dist/draft19-session.cjs +8 -0
- package/dist/draft19-session.d.cts +50 -0
- package/dist/draft19-session.d.ts +50 -0
- package/dist/draft19-session.js +8 -0
- package/dist/draft19.cjs +189 -0
- package/dist/draft19.d.cts +130 -0
- package/dist/draft19.d.ts +130 -0
- package/dist/draft19.js +189 -0
- package/dist/index.cjs +13 -1
- package/dist/index.d.cts +24 -14
- package/dist/index.d.ts +24 -14
- package/dist/index.js +13 -1
- package/dist/{session-types-X-IYEGoG.d.ts → session-types-C0l_-Ups.d.ts} +1 -1
- package/dist/{session-types-C7UONex5.d.cts → session-types-KsqyVp6y.d.cts} +1 -1
- package/dist/session.cjs +9 -1
- package/dist/session.d.cts +7 -3
- package/dist/session.d.ts +7 -3
- package/dist/session.js +9 -1
- package/dist/{types-DuIO7cWK.d.cts → types-CdCSIfvI.d.cts} +1 -1
- package/dist/{types-DuIO7cWK.d.ts → types-CdCSIfvI.d.ts} +1 -1
- package/dist/types-Cqp10JtB.d.cts +249 -0
- package/dist/types-Cqp10JtB.d.ts +249 -0
- package/dist/types-zHgCS6o5.d.cts +265 -0
- package/dist/types-zHgCS6o5.d.ts +265 -0
- package/package.json +26 -2
- package/src/core/types.ts +14 -1
- package/src/drafts/draft17/error-codes.ts +72 -0
- package/src/drafts/draft18/codec.ts +1310 -0
- package/src/drafts/draft18/data-streams.ts +996 -0
- package/src/drafts/draft18/error-codes.ts +75 -0
- package/src/drafts/draft18/index.ts +122 -0
- package/src/drafts/draft18/messages.ts +56 -0
- package/src/drafts/draft18/rules.ts +85 -0
- package/src/drafts/draft18/session-fsm.ts +451 -0
- package/src/drafts/draft18/session.ts +15 -0
- package/src/drafts/draft18/types.ts +347 -0
- package/src/drafts/draft19/codec.ts +1425 -0
- package/src/drafts/draft19/data-streams.ts +996 -0
- package/src/drafts/draft19/error-codes.ts +78 -0
- package/src/drafts/draft19/index.ts +126 -0
- package/src/drafts/draft19/messages.ts +58 -0
- package/src/drafts/draft19/rules.ts +85 -0
- package/src/drafts/draft19/session-fsm.ts +451 -0
- package/src/drafts/draft19/session.ts +15 -0
- package/src/drafts/draft19/types.ts +368 -0
- package/src/index.ts +17 -1
- 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 }
|