@mcpwarp/ws-mixer 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/CHANGELOG.md +357 -0
- package/LICENSE +202 -0
- package/README.md +431 -0
- package/dist/client.d.ts +453 -0
- package/dist/conn.d.ts +328 -0
- package/dist/control.d.ts +74 -0
- package/dist/errors.d.ts +96 -0
- package/dist/frame.d.ts +52 -0
- package/dist/index.cjs +2866 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2806 -0
- package/dist/index.js.map +1 -0
- package/dist/stream.d.ts +124 -0
- package/dist/util.d.ts +3 -0
- package/package.json +59 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@mcpwarp/ws-mixer` (the JS/TypeScript client SDK) are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.6.0 - 2026-09-26
|
|
6
|
+
|
|
7
|
+
- A stream write that fails outside stream termination now reports that error to its write callback,
|
|
8
|
+
so a promisified/awaited write rejects. Previously, with no `'error'` listener attached, the
|
|
9
|
+
callback was called with no error — the write reported success even though its bytes were never
|
|
10
|
+
sent.
|
|
11
|
+
- On a stream whose peer CLOSE already arrived, a write whose socket send fails before the
|
|
12
|
+
connection's teardown reaches the stream is held, the same way as 0.4.0's held writes: the
|
|
13
|
+
buffered data, `'end'` and `'close'` are still delivered, and the callback is settled once the
|
|
14
|
+
read side has emitted `'end'` (or the stream is destroyed first) — with the error of whatever
|
|
15
|
+
tore the stream down first (connection teardown, or the app's own `destroy()`/`reset()`),
|
|
16
|
+
otherwise the send's own error. This no longer depends on teardown reaching the stream, so it
|
|
17
|
+
also holds after the app's own `closeWrite()`. If `'end'` had already fired, the callback gets
|
|
18
|
+
the send's error immediately.
|
|
19
|
+
With an `'error'` listener attached, 0.5.0 instead failed the callback immediately, which
|
|
20
|
+
errored the read side and blocked `'end'`; that is fixed too.
|
|
21
|
+
- Otherwise the callback gets the error immediately. This includes a write rejected after the
|
|
22
|
+
app's own `closeWrite()`: for a stream with no `'error'` listener it now also marks the stream
|
|
23
|
+
errored (`stream.errored` carries the error), same as before with a listener. If nothing is
|
|
24
|
+
listening, the SDK's internal no-op `'error'` listener is attached first, so the failure never
|
|
25
|
+
crashes the process.
|
|
26
|
+
- CI: a new `pair-go-js` conformance job runs the go->js pair matrix (`--mode pair`) against
|
|
27
|
+
ws-mixer-go at `goserver.pin`, with a `go->js: 10` floor in `conformance/COUNTS.json` (all 10 pair
|
|
28
|
+
scenarios pass at spec v0.4.0 / ws-mixer-go v0.6.0). `spec.pin` is v0.4.1 in this release, whose
|
|
29
|
+
runner enforces COUNTS on `--sdk`/`--mode`-filtered runs, so the floor is live.
|
|
30
|
+
- Docs: nearly all comments (84 of 88 references) now cite the split spec docs (`WIRE.md` section
|
|
31
|
+
2.x, `CLIENT-SDK.md` rows, and this repo's `docs/DESIGN.md`) instead of the old monorepo
|
|
32
|
+
`OVERVIEW.md` section numbers. The remaining four point at material that no longer exists in any
|
|
33
|
+
spec doc and are left as-is (`conn.ts`, `control.test.ts` ×2, `reconnect.test.ts`).
|
|
34
|
+
Comment-only; no behaviour change.
|
|
35
|
+
|
|
36
|
+
## 0.5.0 - 2026-09-25
|
|
37
|
+
|
|
38
|
+
- **Breaking:** `MixerClient.close()`'s options no longer take a `code` — `close({message})` always
|
|
39
|
+
closes the connection with `APPLICATION_CLOSE` (`0x0e`/WS close `4014`), never a caller-chosen
|
|
40
|
+
code. WIRE.md section 2.8 makes `APPLICATION_CLOSE` the only code an application may close a
|
|
41
|
+
*connection* with, so a caller-supplied code could previously close a healthy connection with a
|
|
42
|
+
protocol-fault code and produce an `errorCode`/`errorName` pair outside that table (D-2026-09-25-01).
|
|
43
|
+
Which path runs is chosen by whether `message` is present, not by any `code`: `close({message})`
|
|
44
|
+
is the application close, and `close()`/`close({})`/`message: undefined` is the default graceful
|
|
45
|
+
drain-then-close. Passing a `code` key at all (e.g. a plain-JS caller still on
|
|
46
|
+
`close({code: 14})`, with or without `message`) now throws a `TypeError` synchronously, before
|
|
47
|
+
any close is attempted, instead of silently taking either path. The `[0, 999]` `RangeError`
|
|
48
|
+
validation is gone along with `code`.
|
|
49
|
+
- Conformance adapter: the `close` command's `code` now accepts only `0`/absent (graceful close) or
|
|
50
|
+
`14` (application close via the new `close({message})`) — any other value replies with a
|
|
51
|
+
command-error, `"close: only 0 or 14"`.
|
|
52
|
+
|
|
53
|
+
## 0.4.0 - 2026-09-25
|
|
54
|
+
|
|
55
|
+
- New `TokenUnavailableError` (exported from the package root): a token provider that throws or
|
|
56
|
+
rejects with an instance of it (directly, or wrapped via `cause`) now gets treated like a failed
|
|
57
|
+
dial instead of the fatal-by-default verdict every other provider throw/reject still gets — a
|
|
58
|
+
non-fatal `{phase: "dial", fatal: false}` report, normal full-jitter backoff (it counts as a
|
|
59
|
+
failed attempt: `reconnect.maxAttempts` applies, the stability rule is unaffected), and the
|
|
60
|
+
provider's error surfaced verbatim as `cause`, same as before. Detection is `instanceof
|
|
61
|
+
TokenUnavailableError` only, deliberately not duck-typed on any property or method, so an
|
|
62
|
+
unrelated library's error can never accidentally turn a genuinely fatal provider failure into an
|
|
63
|
+
endless retry loop. On the one-time pre-`welcome` refresh-retry's own provider call, a marked
|
|
64
|
+
failure leaves the retry budget spent and takes this same non-fatal path rather than going fatal
|
|
65
|
+
or granting a second refresh. On the very first connect, a marked failure no longer rejects
|
|
66
|
+
`start()`/`connect()` — same as a first-dial network error.
|
|
67
|
+
- A pre-`welcome` token rejection -- an HTTP 401 on the upgrade, or a handshake-phase
|
|
68
|
+
`UNAUTHORIZED` (`4011`, with or without a preceding `error{}`) -- now gets the same one-time
|
|
69
|
+
refresh-retry treatment in *either* place, as ONE shared budget: when `token` is a provider
|
|
70
|
+
function, the SDK calls it again and retries the dial immediately (no backoff); a second
|
|
71
|
+
rejection, in either shape (`401` then `4011`, or `4011` then `401`), is fatal. A retry dial that
|
|
72
|
+
instead fails for an unrelated, non-auth reason (a network error, an HTTP 5xx, a transport death
|
|
73
|
+
before `welcome`) is *not* treated as a second rejection -- it takes the ordinary recoverable path
|
|
74
|
+
(non-fatal report, normal backoff) -- but the budget stays spent regardless, so a genuine
|
|
75
|
+
rejection on some later cycle, before the client ever goes stable, is still fatal with no further
|
|
76
|
+
refresh. Previously only the dial's own HTTP 401 got this retry -- a handshake-phase `4011` was
|
|
77
|
+
always immediately fatal, even with a token provider present. A **static string** `token` is
|
|
78
|
+
unaffected: still fatal on the very first rejection of any of the three forms, since there is
|
|
79
|
+
nothing to refresh (this was already true for HTTP 401 with a static token; it now also
|
|
80
|
+
explicitly applies to the two handshake-phase forms, which previously had no retry concept at all
|
|
81
|
+
to skip). Also fixed: this refresh-retry is now unconditional on `reconnect.maxAttempts`/
|
|
82
|
+
reconnect being disabled (it applies to the very first connect too -- one immediate redial that
|
|
83
|
+
completes the initial connection is not itself "a reconnect"), matching `ws-mixer-go`'s
|
|
84
|
+
`dialAndHandshake`; it likewise does not itself increment the attempt counter.
|
|
85
|
+
- The refresh-retry budget above is a client-level flag (not local to one `connectOnce()` call), and
|
|
86
|
+
re-arms only once the connection reaches stability (see `stableAfter` below), never merely on the
|
|
87
|
+
next redial -- otherwise a server that welcomes and then closes shortly after could make the
|
|
88
|
+
client hit the token endpoint on every single reconnect cycle forever.
|
|
89
|
+
- New `reconnect.stableAfter` option (default `10000`ms, next to `base`/`cap`/`connectTimeout`/
|
|
90
|
+
`maxAttempts`; must be a finite number `>= 0`, or the constructor throws a `RangeError`
|
|
91
|
+
synchronously, same as `close()`'s `code` validation -- `0` is legal and reproduces the pre-0.4
|
|
92
|
+
"reset on `welcome`" behaviour): how long a connection must stay up past `welcome` before it's
|
|
93
|
+
considered stable (WIRE.md/OVERVIEW.md section 2.9's `stable`). Fixed: the backoff attempt
|
|
94
|
+
counter, and every once-only retry budget re-armed alongside it (4013 KEEPALIVE_TIMEOUT's one-shot
|
|
95
|
+
immediate retry; the pre-`welcome` token refresh-retry above), now reset only once a connection
|
|
96
|
+
reaches stability -- **not** on `welcome` itself, as before. Fixed alongside it: 4013's one-shot
|
|
97
|
+
budget no longer un-spends itself on the very next 4013 (a leftover pre-`stableAfter` line reset
|
|
98
|
+
the flag back to "unused" immediately after falling back to normal backoff, so four 4013s in a row
|
|
99
|
+
with no intervening stable connection cycled immediate/backoff/immediate/backoff... forever instead
|
|
100
|
+
of only the first ever being immediate). A server that welcomes and then immediately closes no
|
|
101
|
+
longer resets backoff every cycle (which turned it into a redial-roughly-once-a-second loop
|
|
102
|
+
instead of actually backing off): `4009`/`4014`'s own "start backoff at the cap" treatment is
|
|
103
|
+
unaffected (still an explicit "refused on purpose, back off hard" rule, not a workaround for the
|
|
104
|
+
counter's reset timing). Stability is tracked per connection: a `drain` hand-over's retired
|
|
105
|
+
connection ending, however long after its replacement's own `welcome`, never resets or clears the
|
|
106
|
+
replacement's own stability timer. `reconnect.maxAttempts`'s doc is updated to match: it now
|
|
107
|
+
counts consecutive reconnect attempts *without* a stable connection in between.
|
|
108
|
+
- Fixed: `giveUp` (the `reconnect.maxAttempts`-exhaustion path) now mirrors `goFatal` -- it clears
|
|
109
|
+
the stability timer and detaches-then-fails every live/in-flight conn (`conn`, `dialingConn`,
|
|
110
|
+
`retiringConn`, which a drain hand-over can leave all pointing at the very same live connection)
|
|
111
|
+
before reporting. Previously, exhaustion during (for example) a drain hand-over's parallel dial
|
|
112
|
+
failing could leave the still-live predecessor connection running -- socket, ping/watchdog timers,
|
|
113
|
+
and the now-orphaned stability timer all still ticking 10s past a client that already reports
|
|
114
|
+
itself `closed`.
|
|
115
|
+
- Fixed: a transport failure between the 101 upgrade and `welcome` no longer fabricates a
|
|
116
|
+
semantically-meaningless ws-mixer close code (`wsCode: 4002`, "as if" this side had itself closed
|
|
117
|
+
with `INTERNAL_ERROR`) depending on whether `ws`'s `'error'` or `'close'` event happened to be
|
|
118
|
+
delivered first. `'close'` is now the sole reporter for this window (measured against real `ws`
|
|
119
|
+
8.21: a TCP reset/half-close/peer close frame delivers a bare `'close'` with no `'error'` at all;
|
|
120
|
+
a protocol-level failure `ws` itself detects delivers `'error'` immediately followed by `'close'`
|
|
121
|
+
a macrotask or more later -- `'close'` never races `'error'`, it always follows it, if it comes at
|
|
122
|
+
all); `'error'` only records its message for `'close'`'s report to fall back on when the close
|
|
123
|
+
itself carries no reason. The report now always carries the close code `ws` actually delivers
|
|
124
|
+
(`1006` for a genuine abnormal closure, or whatever real close code it sent), never a fabricated
|
|
125
|
+
one. If `'error'` fires and `'close'` never follows at all, nothing here waits on it: the existing
|
|
126
|
+
hello/welcome timeout (`wsCode` `4001`) is unaffected and is what eventually reports it, as before
|
|
127
|
+
-- deliberately the only fallback for that case. The connected phase was checked for the same
|
|
128
|
+
class of bug and does not have it: an `'error'` there never produces a report by itself (see the
|
|
129
|
+
`message` bullet below for the one real behaviour change to the connected phase's report shape).
|
|
130
|
+
- Fixed: a second `drain` on the same connection (before its first parallel reconnect resolves) no
|
|
131
|
+
longer starts a second, redundant parallel dial -- mirrors `ws-mixer-go`, which already ignores a
|
|
132
|
+
repeat drain the same way.
|
|
133
|
+
- `conformance/adapter/adapter.mjs`: `disconnected.error_name` now prefers the SDK's own
|
|
134
|
+
`DisconnectReason.errorName` (which already derives the wire name from a bare 4xxx close code, not
|
|
135
|
+
only from an explicit `error{}`) over re-deriving it from `errorCode` through the adapter's own
|
|
136
|
+
smaller name table.
|
|
137
|
+
- A disconnect whose close frame carried no reason (from the peer, or a `ws`-level abnormal closure)
|
|
138
|
+
now reports `message: "socket closed with code N"` instead of an empty string, in both the
|
|
139
|
+
connected and handshake phases. Previously that fallback text only ever reached the internal
|
|
140
|
+
`WsMixerError` used to reject in-flight sends and fail the handshake promise; the *emitted*
|
|
141
|
+
`'close'` event (and so `DisconnectPayload.message`) carried the bare, possibly-empty close
|
|
142
|
+
reason directly. A non-empty `message` is strictly more useful (and matches `ws-mixer-go`, which
|
|
143
|
+
reports `"peer closed with code N"` for the same case) -- consumers that want "nothing, if the
|
|
144
|
+
peer sent nothing" specifically should keep preferring `closeReason` over `message`, as the
|
|
145
|
+
`closeReason` field's own doc already recommends.
|
|
146
|
+
- Confirmed (no behavior change): `MixerClient.close()`/`close({code,message})` already stop
|
|
147
|
+
reconnecting in every state, and clear the stability timer alongside the existing backoff timer.
|
|
148
|
+
Precisely what's reported: one non-fatal disconnect when a connection had actually been
|
|
149
|
+
established for this cycle; nothing at all when none had (dialing, mid-handshake, or backing off)
|
|
150
|
+
-- exactly like plain `close()` in those same windows, since there is no connection to send
|
|
151
|
+
`error{}`/a WS close over in the first place.
|
|
152
|
+
- **Behavior change** (D-2026-09-20-09, cross-SDK alignment with `ws-mixer-go`): `errorCode`/
|
|
153
|
+
`errorName` are no longer synthesised for anything that isn't a ws-mixer wire close code. An HTTP
|
|
154
|
+
`401`/`403` upgrade rejection previously carried `errorCode: UNAUTHORIZED` -- it no longer does;
|
|
155
|
+
`httpStatus` alone still identifies it (`404`/`429`/`5xx` were never affected, they never carried
|
|
156
|
+
one). Conversely, a **connected**-phase bare close (no preceding `error{}`) in ws-mixer's private-use
|
|
157
|
+
range `4001`-`4999` now derives both `errorCode`/`errorName` from the wire code the same way the
|
|
158
|
+
dial/handshake phases already did -- previously only `errorName` was derived there, `errorCode`
|
|
159
|
+
stayed `undefined`. The derivation itself is unchanged: `errorCode = wsCode - 4000`, `errorName`
|
|
160
|
+
from the WIRE.md §2.8 table (unknown -> `INTERNAL_ERROR`), restricted to `4001`-`4999` (not `4000`,
|
|
161
|
+
which is never legitimately on the wire -- `NO_ERROR` closes as `1000`). Also fixed as part of the
|
|
162
|
+
same alignment: the `'fatal'` event's `WsMixerError.code` now follows this same rule instead of its
|
|
163
|
+
own, separately-stale `info.errorCode` read -- for an HTTP `401`/`403` it is now `INTERNAL_ERROR`
|
|
164
|
+
(no ws-mixer code exists for an upgrade rejection) where it used to be `UNAUTHORIZED`, and for a
|
|
165
|
+
bare `4010`/`4011` close it is now the derived `UNSUPPORTED`/`UNAUTHORIZED` (previously
|
|
166
|
+
`INTERNAL_ERROR`, mismatching the disconnect reason's own `errorCode` for that same close).
|
|
167
|
+
Consumers that need to distinguish *why* a fatal happened should branch on the disconnect reason's
|
|
168
|
+
`httpStatus`/`wsCode`/`errorCode`, not on the fatal error's `code` alone.
|
|
169
|
+
- **Fixed (blocker):** a stream `OPEN`, `app`, or `drain` event already queued for delivery when the
|
|
170
|
+
connection ended could be silently dropped instead of delivered, whenever an async
|
|
171
|
+
`onStream`/`onApp`/`onDrain` handler was still in flight at that exact moment (`error{}` is always
|
|
172
|
+
the *last* message on the wire, so anything queued ahead of it genuinely arrived before the
|
|
173
|
+
connection ended and is owed delivery — CLIENT-SDK.md's "Handler delivery" row). The delivery loop
|
|
174
|
+
now finishes flushing whatever was already queued even after the connection has torn down, instead
|
|
175
|
+
of exiting as soon as `this.closed` flips true out from under an in-flight `await`. **Behaviour
|
|
176
|
+
change:** `onStream`/`onApp`/`onDrain` (and the `'stream'`/`'app'`/`'drain'` events) may now fire
|
|
177
|
+
shortly after `MixerClient.close()`/`MixerConn.close()`'s own promise has already resolved, for an
|
|
178
|
+
event that arrived before that close. A `drain` delivered this way never starts a reconnect for a
|
|
179
|
+
connection that is no longer the live one: on an already closing/closed client the existing
|
|
180
|
+
`!this.closing && this.state !== "closed"` guard already covered it, but a `drain` queued behind a
|
|
181
|
+
blocked handler on a conn that then dies for an *unrelated* reason (its own 'close' handler already
|
|
182
|
+
ran synchronously and replaced/cleared `this.conn`, and already scheduled the real reconnect) needed
|
|
183
|
+
a further `this.conn === conn` guard -- without it, the flushed `drain` would still see
|
|
184
|
+
`!this.closing && this.state !== "closed" && !this.drainReconnectScheduled` all true and spawn a
|
|
185
|
+
second, parallel reconnect against the already-dead conn, latching `drainReconnectScheduled` and
|
|
186
|
+
wrongly suppressing the next legitimate close-driven reconnect. Nothing can be newly enqueued once
|
|
187
|
+
the connection is closed (defensive backstop, in addition to the architectural invariant that the
|
|
188
|
+
read path always stops before teardown), so the flush is always bounded and terminates.
|
|
189
|
+
- **Fixed:** an open stream whose connection died abnormally (a `1006` tunnel death, or any
|
|
190
|
+
connection-level failure) with no `'error'` listener attached used to end *cleanly* — `'end'`
|
|
191
|
+
then `'close'`, with `stream.errored` never set — indistinguishable from the peer's own response
|
|
192
|
+
legitimately finishing (CLIENT-SDK.md's "Stream teardown on disconnect" row; WIRE.md section 2.9
|
|
193
|
+
requires the opposite: `io.ErrUnexpectedEOF`-equivalent, unless that stream's own `CLOSE` had
|
|
194
|
+
already arrived). **Behaviour change**, and it now depends on whether the peer's own `CLOSE` for
|
|
195
|
+
*that stream* had already arrived when the connection died, per WIRE.md's "`CLOSE` preserves
|
|
196
|
+
buffered data; `RESET` discards it" rule:
|
|
197
|
+
- `CLOSE` already arrived: the response DID complete, independent of the connection dying. Node's
|
|
198
|
+
`push(null)` doesn't discard what's still buffered — it marks EOF, and Node delivers the buffered
|
|
199
|
+
bytes to the consumer and only then emits `'end'`. `stream.errored` stays `null` and no `'error'`
|
|
200
|
+
event fires. Only the **write** side fails: a write already pending, or started afterward, fails
|
|
201
|
+
promptly (its callback receives the connection's error) instead of hanging or silently
|
|
202
|
+
succeeding.
|
|
203
|
+
- Otherwise (a peer `RESET`, or the connection ending any other way, with this stream's `CLOSE`
|
|
204
|
+
never having arrived): the stream always ends with an error, never a false clean end —
|
|
205
|
+
`stream.errored` carries it, `'close'` fires, and (for a consumer that *did* attach `'error'`)
|
|
206
|
+
that listener receives it, but `'end'` is never emitted for this case; a read or write already
|
|
207
|
+
pending, or started afterward, fails promptly. A pending write's callback now also receives the
|
|
208
|
+
real error instead of being reported as having succeeded, in both cases above. A `RESET` always
|
|
209
|
+
discards buffered data and errors, even if this stream's `CLOSE` had *also* already arrived
|
|
210
|
+
(`RESET` always wins). A stream already fully, cleanly closed before the connection died (both
|
|
211
|
+
directions' `CLOSE` already exchanged) is unaffected — it already ended cleanly and is no longer
|
|
212
|
+
tracked by the connection at all. `MixerClient`'s class doc comment and README already described
|
|
213
|
+
the intended behaviour; the code now actually matches it, with no remaining gap against
|
|
214
|
+
CLIENT-SDK.md's "data already buffered ... still delivered first" rule.
|
|
215
|
+
- **Fixed:** a stream whose peer `CLOSE` had already arrived when the connection died -- the
|
|
216
|
+
cleanly-ending case in the bullet above -- never actually emitted `'close'` (never got
|
|
217
|
+
destroyed): `push(null)` delivered the buffered data and `'end'` fired, but nothing then called
|
|
218
|
+
`destroy()`. It now does: buffered data delivered, `'end'`, then `destroy()`/`'close'`, same as
|
|
219
|
+
a stream that ends any other way.
|
|
220
|
+
- **Fixed:** in that same state, a write already pending (or started afterward) had its error
|
|
221
|
+
routed through Node's ordinary Writable error path, which also marks the readable side
|
|
222
|
+
`errored` and permanently blocks `'end'` -- a consumer draining `data`/`end` on the stream could
|
|
223
|
+
hang forever waiting behind a write it never awaited. Affected write callbacks are now held and
|
|
224
|
+
only settled with the terminal error once the read side has finished delivering buffered data
|
|
225
|
+
and emitted `'end'`, or the stream is destroyed some other way (app `destroy()`, `reset()`). An
|
|
226
|
+
app that awaits a write's callback without ever reading this stream will wait until it does one
|
|
227
|
+
or the other.
|
|
228
|
+
- **Fixed:** a fatal close (`UNSUPPORTED`/`UNAUTHORIZED`) on the retiring connection during a
|
|
229
|
+
`drain` hand-over's parallel dial didn't cancel that in-flight dial: the client emitted
|
|
230
|
+
`'fatal'` and then flipped back to `"connected"` once the dial's own `welcome` landed, and with
|
|
231
|
+
`UNAUTHORIZED` it could also redial again via the pre-`welcome` token-refresh-retry loop. The
|
|
232
|
+
dial is now cancelled (`NO_ERROR`/"client closing") as part of going fatal, and the client stays
|
|
233
|
+
closed.
|
|
234
|
+
- `conformance/adapter/adapter.mjs` now reports the SDK's real `SDK_VERSION` in `ready`/`hello`
|
|
235
|
+
instead of a stale hardcoded `"0.1.0"`.
|
|
236
|
+
|
|
237
|
+
## 0.3.1 - 2026-09-20
|
|
238
|
+
|
|
239
|
+
- `DisconnectReason` gains `closeReason`: the reason field of the close frame *received from the
|
|
240
|
+
peer*, verbatim -- never this side's own outgoing reason. Absent when this side initiated the
|
|
241
|
+
close (a peer's echo carries no information), when no close frame was observed at all (an
|
|
242
|
+
abnormal closure), or when the SDK closed on a peer's `error{}` without reading whatever close
|
|
243
|
+
frame follows it. `message` is unchanged and still carries the human-readable text in every case.
|
|
244
|
+
- Fixed: a **handshake-phase** close now classifies (and, for `4010`/`4011`, goes fatal) from the
|
|
245
|
+
ws-mixer wire code the same way a connected-phase one already did, covering two previously-wrong
|
|
246
|
+
cases. (1) A bare close carrying a ws-mixer wire code before `welcome` previously reported
|
|
247
|
+
`errorName: "INTERNAL_ERROR"` and `fatal: false` -- silently retrying forever against a server
|
|
248
|
+
that will never accept the retry. (2) The normative pre-`welcome` auth-rejection shape
|
|
249
|
+
(`error{code:11 UNAUTHORIZED}` + close `4011`, before `welcome` -- OVERVIEW.md section 3.4's
|
|
250
|
+
Authenticate hook, `spec/fixtures/sequences/auth_failure.json`) was previously treated as a
|
|
251
|
+
*local* protocol violation: the client replied with its own `error{PROTOCOL_ERROR}` (forbidden by
|
|
252
|
+
WIRE.md section 2.7 -- a peer that receives `error` must never reply), closed with its own `4001`
|
|
253
|
+
instead of `4011`, discarded the server's real reason, and retried forever. A revoked token is now
|
|
254
|
+
what it should always have been: one fatal, non-retried disconnect. The client's own hello/welcome
|
|
255
|
+
timeout now also reports its locally-generated `wsCode: 4001` (previously omitted).
|
|
256
|
+
- New public API: `MixerClient.close({ code, message })` (CLIENT-SDK.md's "Application close" row)
|
|
257
|
+
performs `error{code, message}` + WS close `4000+code` (message truncated to 123 UTF-8 bytes on a
|
|
258
|
+
character boundary) + close the socket, instead of the default graceful drain -- e.g. to close
|
|
259
|
+
with the new `APPLICATION_CLOSE` code below. `code` must be an integer in `[0, 999]`, or `close()`
|
|
260
|
+
throws a `RangeError` synchronously. `close()` with no arguments is unchanged. Fixed: `code`/
|
|
261
|
+
`message` now also apply when `close()` is called while a `drain`-triggered replacement dial or an
|
|
262
|
+
in-flight (pre-`welcome`) handshake is still outstanding -- both previously always sent
|
|
263
|
+
`error{NO_ERROR, "client closing"}` regardless of what was passed.
|
|
264
|
+
- New named error code: `APPLICATION_CLOSE` (`0x0e`, WS close `4014`). Connection-level only, never
|
|
265
|
+
emitted by ws-mixer itself -- reserved for the application above to close a connection for its
|
|
266
|
+
own reason (see `close({ code, message })` above). A **connected-phase** `4014` gets `4009`'s own
|
|
267
|
+
"start at the cap" backoff treatment (WIRE.md section 2.9), not plain full-jitter backoff: it is
|
|
268
|
+
by nature sent *after* `welcome` already reset the attempt counter, so plain backoff would redial
|
|
269
|
+
roughly once a second forever instead of climbing. A handshake-phase `4014` is unaffected (its
|
|
270
|
+
attempt counter still climbs normally).
|
|
271
|
+
- Fixed: closing with an error code whose `4000 + code` isn't a legal WS close code (any
|
|
272
|
+
`code > 999`, reachable off the wire whenever a peer sends `error{code >= 0x1000_0000}`) now sends
|
|
273
|
+
WS close `4002` (`INTERNAL_ERROR`'s close code) on the wire instead of throwing inside `ws` and
|
|
274
|
+
falling back to `terminate()` -- a bare abnormal closure (`1006`) that told the peer nothing at
|
|
275
|
+
all. `errorCode` and the reported `wsCode` stay the real, semantic `4000+code` either way; only
|
|
276
|
+
the bytes actually put on the wire are clamped (spec `WIRE.md` section 2.8 / decision
|
|
277
|
+
`D-2026-09-20-03`).
|
|
278
|
+
- Fixed: `SDK_VERSION` -- sent on the wire in `hello.agent.sdk_version` and the dial's `User-Agent`
|
|
279
|
+
header -- was hard-coded to `"0.2.0"`, two releases stale. Now kept in sync with `package.json`'s
|
|
280
|
+
version by hand, enforced by `test/version.test.ts` so a future release bump that forgets it fails
|
|
281
|
+
CI instead of silently drifting again.
|
|
282
|
+
- `conformance/adapter/adapter.mjs`: `disconnected` now always includes `phase`, and includes
|
|
283
|
+
`close_reason` when the SDK reports one; `close` now forwards a non-zero numeric `code`/`message`
|
|
284
|
+
to the new application-close API (`code: 0`/absent stays the existing graceful drain-then-close,
|
|
285
|
+
so `graceful_close.json`'s WIRE.md section 2.10 step 14 drain sequence keeps being exercised); the
|
|
286
|
+
RESET code-name table gained `14: "APPLICATION_CLOSE"`; `connect`'s `reconnect.baseMs`/
|
|
287
|
+
`reconnect.capMs` (when positive) now override the SDK's default backoff `base`/`cap`, so a
|
|
288
|
+
scenario needing a fast connected-phase `4014` at-cap reconnect isn't stuck behind the SDK's 60s
|
|
289
|
+
default cap.
|
|
290
|
+
|
|
291
|
+
## 0.3.0 - 2026-09-04
|
|
292
|
+
|
|
293
|
+
Repo split from the `mcpwarp/ws-mixer` monorepo into its own repo,
|
|
294
|
+
`mcpwarp/ws-mixer-js`, per MIGRATION.md section 4.1.
|
|
295
|
+
|
|
296
|
+
- Licensed Apache-2.0 (previously `UNLICENSED` in the monorepo).
|
|
297
|
+
- The package now publishes to the public npm registry (`registry.npmjs.org`) instead of a
|
|
298
|
+
private registry.
|
|
299
|
+
- Spec fixtures are no longer vendored from the monorepo's `spec/` directory;
|
|
300
|
+
they are now fetched from `ws-mixer-spec` at the tag pinned in `spec.pin`
|
|
301
|
+
(`npm run fetch-spec`, into the gitignored `.spec/`).
|
|
302
|
+
|
|
303
|
+
## 0.2.0
|
|
304
|
+
|
|
305
|
+
Per OVERVIEW.md section 4.0 ("Client SDK requirements") and the decision log entry dated
|
|
306
|
+
2026-08-27.
|
|
307
|
+
|
|
308
|
+
- `token` may now be a provider callback (`() => string | Promise<string>`), not just a static
|
|
309
|
+
string. It is called fresh on every dial -- initial connect and every reconnect -- never cached.
|
|
310
|
+
- HTTP `401` on the upgrade now gets exactly one immediate refresh-retry when `token` is a
|
|
311
|
+
provider: the SDK calls it again and redials right away, no backoff, though the retry still
|
|
312
|
+
counts toward `reconnect.maxAttempts`. A second `401` is fatal. A static string `token` is
|
|
313
|
+
unchanged: fatal on the first `401`.
|
|
314
|
+
- A token provider that throws or rejects is fatal; the error is surfaced verbatim via the new
|
|
315
|
+
`DisconnectReason.cause` field.
|
|
316
|
+
- The disconnect reason reported to `onDisconnect` and the `'close'` event is now a `DisconnectReason`:
|
|
317
|
+
adds `phase` (`"dial"` | `"handshake"` | `"connected"`), `errorName`, `httpStatus`, and `cause`
|
|
318
|
+
alongside the existing `wsCode`/`errorCode`/`fatal`/`message`. Every disconnect -- not just fatal
|
|
319
|
+
or maxAttempts-exhaustion ones -- is now reported, including a recoverable dial failure and a
|
|
320
|
+
non-fatal handshake failure, which previously went unreported.
|
|
321
|
+
- Fixed: a disconnect that also exhausted `reconnect.maxAttempts` (or hit the dial-side 401
|
|
322
|
+
refresh-retry ceiling) previously fired `onDisconnect` **twice** -- once for the failure, once
|
|
323
|
+
for giving up. It's now exactly one report per disconnect, `fatal: true` with the exhaustion
|
|
324
|
+
message, carrying the underlying failure's `wsCode`/`errorCode`/`httpStatus`/`cause`.
|
|
325
|
+
- Fixed: the dial's per-attempt failure context (phase, HTTP status, provider `cause`) is now
|
|
326
|
+
threaded through each dial attempt instead of stored on shared mutable client fields, so a
|
|
327
|
+
concurrent old-connection close can no longer clobber a new dial's own disconnect report.
|
|
328
|
+
- Fixed: an `unexpected-response` during the dial (HTTP 401/403/404/429) now drains and destroys
|
|
329
|
+
the response, destroys the request, and terminates the socket -- `ws` skips its own cleanup once
|
|
330
|
+
a listener is attached, and this SDK's listener wasn't doing that cleanup itself.
|
|
331
|
+
- A `drain` while `reconnect.maxAttempts` is `0` no longer starts a parallel reconnect the client
|
|
332
|
+
is configured never to complete, and no longer treats the `drain` itself as an immediate fatal
|
|
333
|
+
event either: per OVERVIEW.md section 2.9, the connection is left alone, in-flight streams finish
|
|
334
|
+
normally, and only once the server's own deadline closes the socket with `4012` is that reported
|
|
335
|
+
-- once -- as the terminal `fatal: true` disconnect (`message: "drained; reconnect disabled"`).
|
|
336
|
+
- Fixed: a superseded (drained) connection's own teardown, once its replacement's `welcome` lands,
|
|
337
|
+
no longer fires a spurious `onDisconnect`/`'close'` -- the client is still connected throughout,
|
|
338
|
+
via the replacement. Its in-flight streams now get a stream-scoped `StreamError(CANCEL,
|
|
339
|
+
"connection drained")` instead of the connection-level error, via `MixerConn.fail()`'s new
|
|
340
|
+
optional per-stream error override.
|
|
341
|
+
- `MixerStream.reset()` now actually sets `resetCode`, as its docs already promised. Added
|
|
342
|
+
internal `abort()`, used for SDK-detected stream violations (mirrors a peer-sent RESET): unlike
|
|
343
|
+
the app-facing `reset()`, `abort()` also emits `'reset'`, since in both cases the stream's owner
|
|
344
|
+
didn't choose the teardown.
|
|
345
|
+
- Per OVERVIEW.md section 2.9's Drain and the 2026-08-27 decision log: an `OPEN` received above a
|
|
346
|
+
drain's `last_stream_id` is now connection-fatal `PROTOCOL_ERROR` (WS close `4001`), not a
|
|
347
|
+
stream-scoped `REFUSED_STREAM` -- the server already promised not to send one, so this is it
|
|
348
|
+
breaking that promise, not a benign race. `stats().drainRefusedOpens` is renamed
|
|
349
|
+
`stats().drainViolations` to match.
|
|
350
|
+
- New public exports: `TokenProvider`, `DisconnectReason`, `DisconnectPhase`, `DisconnectPayload`.
|
|
351
|
+
|
|
352
|
+
## 0.1.0
|
|
353
|
+
|
|
354
|
+
Initial release: `connect()`, `MixerClient`'s reconnect/backoff state machine (OVERVIEW.md section
|
|
355
|
+
2.9 -- full-jitter backoff, `drain`/`4012` handling, `4013`/`4009` special-cased retries, fatal-close
|
|
356
|
+
handling), `MixerConn` (handshake, keepalive, flow control, the control-priority + round-robin
|
|
357
|
+
write scheduler), `MixerStream` (`stream.Duplex`), and the frame/control wire codecs.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 Anatoly Tarnavsky
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|