@parity/truapi 0.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.js +320 -0
- package/dist/generated/client.d.ts +243 -0
- package/dist/generated/client.js +1081 -0
- package/dist/generated/index.d.ts +2 -0
- package/dist/generated/index.js +2 -0
- package/dist/generated/types.d.ts +2666 -0
- package/dist/generated/types.js +877 -0
- package/dist/generated/wire-table.d.ts +246 -0
- package/dist/generated/wire-table.js +249 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/playground/codegen/services.d.ts +2 -0
- package/dist/playground/codegen/services.js +456 -0
- package/dist/playground/services-types.d.ts +12 -0
- package/dist/playground/services-types.js +1 -0
- package/dist/scale.d.ts +77 -0
- package/dist/scale.js +121 -0
- package/dist/transport.d.ts +237 -0
- package/dist/transport.js +238 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parity Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @parity/truapi
|
|
2
|
+
|
|
3
|
+
_Typed TypeScript client for products that talk to a TrUAPI host._
|
|
4
|
+
|
|
5
|
+
[](../../../LICENSE)
|
|
6
|
+
[](./package.json)
|
|
7
|
+
|
|
8
|
+
This package gives a product running inside a Polkadot host (Desktop Browser, Triangle webview) a fully typed client for every TrUAPI method. The transport, SCALE codecs, generated types, and generated domain clients are all bundled together.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @parity/truapi
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {
|
|
20
|
+
createClient,
|
|
21
|
+
createMessagePortProvider,
|
|
22
|
+
createTransport,
|
|
23
|
+
type Client,
|
|
24
|
+
type HostAccountGetResponse,
|
|
25
|
+
} from "@parity/truapi";
|
|
26
|
+
|
|
27
|
+
const provider = createMessagePortProvider(port);
|
|
28
|
+
const transport = createTransport(provider);
|
|
29
|
+
const truapi: Client = createClient(transport);
|
|
30
|
+
|
|
31
|
+
const result = await truapi.accountManagement.accountGet({
|
|
32
|
+
productAccountId: { dotNsIdentifier: "my-product.dot", derivationIndex: 0 },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result.isErr()) throw result.error;
|
|
36
|
+
const account: HostAccountGetResponse = result.value;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Request methods take the inner request value directly. The transport adds the wire-level version wrapper and unwraps versioned responses before the generated method returns.
|
|
40
|
+
|
|
41
|
+
## Subscriptions
|
|
42
|
+
|
|
43
|
+
Streaming methods return a small Observable-compatible object:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import type { Subscription, RemoteChainHeadFollowItem } from "@parity/truapi";
|
|
47
|
+
|
|
48
|
+
const sub: Subscription = truapi.chainInteraction
|
|
49
|
+
.chainHeadFollow({ request: { genesisHash, withRuntime: false } })
|
|
50
|
+
.subscribe({
|
|
51
|
+
next(event: RemoteChainHeadFollowItem) {
|
|
52
|
+
console.log(event);
|
|
53
|
+
},
|
|
54
|
+
error(error: Error) {
|
|
55
|
+
console.error(error);
|
|
56
|
+
},
|
|
57
|
+
complete() {
|
|
58
|
+
console.log("stream ended");
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
sub.unsubscribe();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## What's in the package
|
|
66
|
+
|
|
67
|
+
- **Transport providers** for `MessagePort` pipes (used by both webview hosts and iframe hosts).
|
|
68
|
+
- **TrUAPI transport** that handles request, response, subscription, and handshake framing.
|
|
69
|
+
- **Generated domain clients and types** produced from the Rust API contract.
|
|
70
|
+
- **SCALE codec helpers** used by the generated code, also re-exported for direct use.
|
|
71
|
+
|
|
72
|
+
## Wire format
|
|
73
|
+
|
|
74
|
+
Frames are SCALE encoded:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
[requestId: SCALE str][discriminant: u8][payload bytes...]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The discriminant table is generated from Rust `#[wire(request_id = N)]` and `#[wire(start_id = N)]` annotations and is written to `src/generated/wire-table.ts`.
|
|
81
|
+
|
|
82
|
+
## Generated files
|
|
83
|
+
|
|
84
|
+
`src/generated/`, `src/playground/codegen/`, and `test/generated/examples/` are produced by [`truapi-codegen`](../../../rust/crates/truapi-codegen/) from the Rust crate and are ignored by git. Do not edit generated files directly. Run from the repo root:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
./scripts/codegen.sh
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Develop
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install
|
|
94
|
+
npm run build
|
|
95
|
+
npm test
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
On a clean checkout, the first build or test run will generate the ignored TypeScript outputs from the Rust sources, so Rust stable + nightly must be installed locally. `npm test` runs the package's smoke tests under [bun](https://bun.sh/), so bun must also be installed (`curl -fsSL https://bun.sh/install | bash`). The tests load the source `.ts` files directly without a build step.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
[MIT](../../../LICENSE)
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Provider, type Subscription, type TrUApiTransport } from "./transport.js";
|
|
2
|
+
export type { Subscription, TrUApiTransport };
|
|
3
|
+
/**
|
|
4
|
+
* Version overrides used when constructing a transport.
|
|
5
|
+
*/
|
|
6
|
+
export interface CreateTransportOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Highest TrUAPI protocol version exposed by the transport.
|
|
9
|
+
*/
|
|
10
|
+
truapiVersion?: number;
|
|
11
|
+
/**
|
|
12
|
+
* SCALE codec version advertised during host handshake negotiation.
|
|
13
|
+
*/
|
|
14
|
+
codecVersion?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a `TrUApiTransport` on top of a `Provider`, adding request/response
|
|
18
|
+
* correlation and subscription start/receive/stop lifecycle handling.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createTransport(provider: Provider, options?: CreateTransportOptions): TrUApiTransport;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { decodeWireMessage, encodeWireMessage, } from "./transport.js";
|
|
2
|
+
import { indexedTaggedUnion, Result, _void, } from "./scale.js";
|
|
3
|
+
import { TRUAPI_CODEC_VERSION, TRUAPI_VERSION } from "./generated/client.js";
|
|
4
|
+
import * as T from "./generated/types.js";
|
|
5
|
+
import * as W from "./generated/wire-table.js";
|
|
6
|
+
/**
|
|
7
|
+
* Convert a positive protocol version number into the generated version tag
|
|
8
|
+
* used by TrUAPI wire wrappers.
|
|
9
|
+
*/
|
|
10
|
+
function protocolVersionTag(version) {
|
|
11
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
12
|
+
throw new Error(`Invalid TrUAPI protocol version: ${version}`);
|
|
13
|
+
}
|
|
14
|
+
return `V${version}`;
|
|
15
|
+
}
|
|
16
|
+
const HANDSHAKE_WIRE_VERSION = 1;
|
|
17
|
+
/**
|
|
18
|
+
* Build the versioned handshake response codec for the selected wire version.
|
|
19
|
+
*/
|
|
20
|
+
function handshakeResponseCodec(version) {
|
|
21
|
+
return indexedTaggedUnion({
|
|
22
|
+
[protocolVersionTag(version)]: [
|
|
23
|
+
version - 1,
|
|
24
|
+
Result(_void, T.HostHandshakeError),
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Encode a successful host-handshake response payload.
|
|
30
|
+
*/
|
|
31
|
+
function encodeSuccessfulHandshakeResponse(version) {
|
|
32
|
+
return encodeHandshakeResponse(version, {
|
|
33
|
+
tag: protocolVersionTag(version),
|
|
34
|
+
value: {
|
|
35
|
+
success: true,
|
|
36
|
+
value: undefined,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Encode a host-handshake response that reports an unsupported codec version.
|
|
42
|
+
*/
|
|
43
|
+
function encodeUnsupportedHandshakeResponse(version) {
|
|
44
|
+
return encodeHandshakeResponse(version, {
|
|
45
|
+
tag: protocolVersionTag(version),
|
|
46
|
+
value: {
|
|
47
|
+
success: false,
|
|
48
|
+
value: {
|
|
49
|
+
tag: "UnsupportedProtocolVersion",
|
|
50
|
+
value: undefined,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Encode a typed handshake response with the versioned response codec.
|
|
57
|
+
*/
|
|
58
|
+
function encodeHandshakeResponse(version, response) {
|
|
59
|
+
return handshakeResponseCodec(version).enc(response);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check whether a decoded SCALE value has the generated `{ tag, value }`
|
|
63
|
+
* wrapper shape used for versioned wire payloads.
|
|
64
|
+
*/
|
|
65
|
+
function isVersionedWireValue(value) {
|
|
66
|
+
return (typeof value === "object" &&
|
|
67
|
+
value !== null &&
|
|
68
|
+
"tag" in value &&
|
|
69
|
+
"value" in value &&
|
|
70
|
+
typeof value.tag === "string" &&
|
|
71
|
+
/^V\d+$/.test(value.tag));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Return the inner payload from a versioned wire wrapper, or the original
|
|
75
|
+
* value when the payload is already unwrapped.
|
|
76
|
+
*/
|
|
77
|
+
function unwrapVersionedWireValue(value) {
|
|
78
|
+
return isVersionedWireValue(value) ? value.value : value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a `TrUApiTransport` on top of a `Provider`, adding request/response
|
|
82
|
+
* correlation and subscription start/receive/stop lifecycle handling.
|
|
83
|
+
*/
|
|
84
|
+
export function createTransport(provider, options = {}) {
|
|
85
|
+
const truapiVersion = options.truapiVersion ?? TRUAPI_VERSION;
|
|
86
|
+
const codecVersion = options.codecVersion ?? TRUAPI_CODEC_VERSION;
|
|
87
|
+
let idCounter = 0;
|
|
88
|
+
let closedError = null;
|
|
89
|
+
const pending = new Map();
|
|
90
|
+
const subscriptions = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Normalize arbitrary thrown values into `Error` instances.
|
|
93
|
+
*/
|
|
94
|
+
function toError(error) {
|
|
95
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Close the transport once, rejecting pending requests and notifying live
|
|
99
|
+
* subscriptions.
|
|
100
|
+
*/
|
|
101
|
+
function closeWithError(error) {
|
|
102
|
+
const nextError = toError(error);
|
|
103
|
+
if (closedError) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
closedError = nextError;
|
|
107
|
+
for (const [requestId, entry] of pending) {
|
|
108
|
+
pending.delete(requestId);
|
|
109
|
+
entry.reject(nextError);
|
|
110
|
+
}
|
|
111
|
+
for (const [requestId, subscription] of subscriptions) {
|
|
112
|
+
subscriptions.delete(requestId);
|
|
113
|
+
subscription.onClose?.(nextError);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const unsubscribeClose = provider.subscribeClose?.((error) => {
|
|
117
|
+
closeWithError(error);
|
|
118
|
+
});
|
|
119
|
+
const unsubscribeMessage = provider.subscribe((message) => {
|
|
120
|
+
if (closedError) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const decoded = decodeWireMessage(message);
|
|
124
|
+
if (decoded.isErr()) {
|
|
125
|
+
closeWithError(decoded.error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { requestId, payload } = decoded.value;
|
|
129
|
+
if (payload.id === W.SYSTEM_HANDSHAKE.request) {
|
|
130
|
+
// Auto-respond to inbound `host_handshake_request` frames.
|
|
131
|
+
//
|
|
132
|
+
// Legacy hosts shipping `@novasamatech/host-api@0.6.x` (e.g. dotli)
|
|
133
|
+
// initiate their own handshake from the host side at startup and ping
|
|
134
|
+
// the iframe with `host_handshake_request` every 50ms until they see a
|
|
135
|
+
// matching response. The legacy host-api `createTransport` registered
|
|
136
|
+
// an internal handler for this message; preserving that behaviour
|
|
137
|
+
// keeps `@parity/truapi` a drop-in replacement for legacy bridges.
|
|
138
|
+
//
|
|
139
|
+
// Respond with the handshake method's selected wire version. The inner
|
|
140
|
+
// request carries the wire codec version.
|
|
141
|
+
let response;
|
|
142
|
+
try {
|
|
143
|
+
const request = unwrapVersionedWireValue(T.VersionedHostHandshakeRequest.dec(payload.value));
|
|
144
|
+
const requestedCodecVersion = request.codecVersion;
|
|
145
|
+
response =
|
|
146
|
+
requestedCodecVersion === codecVersion
|
|
147
|
+
? encodeSuccessfulHandshakeResponse(HANDSHAKE_WIRE_VERSION)
|
|
148
|
+
: encodeUnsupportedHandshakeResponse(HANDSHAKE_WIRE_VERSION);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
closeWithError(toError(error));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
send({
|
|
156
|
+
requestId,
|
|
157
|
+
payload: {
|
|
158
|
+
id: W.SYSTEM_HANDSHAKE.response,
|
|
159
|
+
value: response,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// provider already closed
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const p = pending.get(requestId);
|
|
169
|
+
if (p) {
|
|
170
|
+
if (payload.id !== p.ids.response) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
pending.delete(requestId);
|
|
174
|
+
try {
|
|
175
|
+
p.resolve(payload.value);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
p.reject(toError(error));
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const subscription = subscriptions.get(requestId);
|
|
183
|
+
if (subscription) {
|
|
184
|
+
if (payload.id === subscription.ids.receive) {
|
|
185
|
+
try {
|
|
186
|
+
subscription.onReceive(payload.value);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// A consumer-side decode/handler error must not tear down the
|
|
190
|
+
// provider's message loop and silently break every other
|
|
191
|
+
// subscription on the same transport. Surface via onClose and
|
|
192
|
+
// drop this subscription; siblings stay alive.
|
|
193
|
+
subscriptions.delete(requestId);
|
|
194
|
+
subscription.onClose?.(toError(error));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (payload.id === subscription.ids.interrupt) {
|
|
198
|
+
subscriptions.delete(requestId);
|
|
199
|
+
subscription.onInterrupt?.(payload.value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
/**
|
|
204
|
+
* Encode and post a protocol message through the underlying provider.
|
|
205
|
+
*/
|
|
206
|
+
function send(message) {
|
|
207
|
+
if (closedError) {
|
|
208
|
+
throw closedError;
|
|
209
|
+
}
|
|
210
|
+
const encoded = encodeWireMessage(message);
|
|
211
|
+
if (encoded.isErr()) {
|
|
212
|
+
closeWithError(encoded.error);
|
|
213
|
+
throw encoded.error;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
provider.postMessage(encoded.value);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
closeWithError(error);
|
|
220
|
+
throw toError(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
truapiVersion,
|
|
225
|
+
codecVersion,
|
|
226
|
+
/**
|
|
227
|
+
* Send one request frame and resolve with the decoded response payload.
|
|
228
|
+
*/
|
|
229
|
+
request({ ids, payload, decodeResponse, }) {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
if (closedError) {
|
|
232
|
+
reject(closedError);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const requestId = `p:${++idCounter}`;
|
|
236
|
+
pending.set(requestId, {
|
|
237
|
+
ids,
|
|
238
|
+
resolve: (response) => resolve(decodeResponse(response)),
|
|
239
|
+
reject,
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
send({
|
|
243
|
+
requestId,
|
|
244
|
+
payload: {
|
|
245
|
+
id: ids.request,
|
|
246
|
+
value: payload,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
pending.delete(requestId);
|
|
252
|
+
reject(toError(error));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
/**
|
|
257
|
+
* Start a raw subscription and route incoming receive/interrupt frames to
|
|
258
|
+
* the supplied callbacks.
|
|
259
|
+
*/
|
|
260
|
+
subscribeRaw({ ids, payload, onReceive, onInterrupt, onClose, }) {
|
|
261
|
+
if (closedError) {
|
|
262
|
+
onClose?.(closedError);
|
|
263
|
+
return { unsubscribe: () => { }, subscriptionId: "" };
|
|
264
|
+
}
|
|
265
|
+
const requestId = `p:${++idCounter}`;
|
|
266
|
+
subscriptions.set(requestId, {
|
|
267
|
+
ids,
|
|
268
|
+
onReceive,
|
|
269
|
+
onInterrupt,
|
|
270
|
+
onClose,
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
send({
|
|
274
|
+
requestId,
|
|
275
|
+
payload: {
|
|
276
|
+
id: ids.start,
|
|
277
|
+
value: payload,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
subscriptions.delete(requestId);
|
|
283
|
+
onClose?.(toError(error));
|
|
284
|
+
return { unsubscribe: () => { }, subscriptionId: requestId };
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
subscriptionId: requestId,
|
|
288
|
+
unsubscribe: () => {
|
|
289
|
+
// Skip the `_stop` frame when the host already terminated the stream
|
|
290
|
+
// via `_interrupt` (which removes the entry from `subscriptions`).
|
|
291
|
+
if (!subscriptions.has(requestId))
|
|
292
|
+
return;
|
|
293
|
+
subscriptions.delete(requestId);
|
|
294
|
+
try {
|
|
295
|
+
send({
|
|
296
|
+
requestId,
|
|
297
|
+
payload: {
|
|
298
|
+
id: ids.stop,
|
|
299
|
+
value: _void.enc(undefined),
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// provider already closed
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
/**
|
|
310
|
+
* Close this transport and detach its provider listeners.
|
|
311
|
+
*/
|
|
312
|
+
dispose() {
|
|
313
|
+
// Idempotent: closeWithError is a no-op once closedError is set, and
|
|
314
|
+
// unsubscribe handles tolerate being called twice.
|
|
315
|
+
closeWithError(new Error("transport disposed"));
|
|
316
|
+
unsubscribeMessage();
|
|
317
|
+
unsubscribeClose?.();
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|