@peerbit/canonical-transport 0.0.0-e209d2e
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 +202 -0
- package/dist/src/index.d.ts +42 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +213 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/protocol.d.ts +67 -0
- package/dist/src/protocol.d.ts.map +1 -0
- package/dist/src/protocol.js +321 -0
- package/dist/src/protocol.js.map +1 -0
- package/package.json +78 -0
- package/src/index.ts +287 -0
- package/src/protocol.ts +136 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { deserialize, serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import {
|
|
3
|
+
CanonicalBootstrapRequest,
|
|
4
|
+
CanonicalChannelClose,
|
|
5
|
+
CanonicalChannelMessage,
|
|
6
|
+
CanonicalControlRequest,
|
|
7
|
+
CanonicalControlResponse,
|
|
8
|
+
CanonicalFrame,
|
|
9
|
+
CanonicalLoadProgramRequest,
|
|
10
|
+
CanonicalSignRequest,
|
|
11
|
+
} from "./protocol.js";
|
|
12
|
+
|
|
13
|
+
export type CanonicalTransport = {
|
|
14
|
+
send: (data: Uint8Array, transfer?: Transferable[]) => void;
|
|
15
|
+
onMessage: (handler: (data: Uint8Array) => void) => () => void;
|
|
16
|
+
close?: () => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CanonicalChannel = {
|
|
20
|
+
send: (data: Uint8Array) => void;
|
|
21
|
+
onMessage: (handler: (data: Uint8Array) => void) => () => void;
|
|
22
|
+
close?: () => void;
|
|
23
|
+
onClose?: (handler: () => void) => () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type CanonicalRpcTransport = {
|
|
27
|
+
send: (data: Uint8Array) => void;
|
|
28
|
+
onMessage: (handler: (data: Uint8Array) => void) => () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const coerceToTransferableUint8Array = (bytes: Uint8Array): Uint8Array => {
|
|
32
|
+
if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
33
|
+
return bytes;
|
|
34
|
+
}
|
|
35
|
+
return bytes.slice();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const toUint8Array = (data: unknown): Uint8Array | undefined => {
|
|
39
|
+
if (data instanceof Uint8Array) return data;
|
|
40
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isMessagePort = (
|
|
45
|
+
value: MessagePort | CanonicalChannel,
|
|
46
|
+
): value is MessagePort => {
|
|
47
|
+
return typeof (value as MessagePort).postMessage === "function";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const createMessagePortTransport = (
|
|
51
|
+
port: MessagePort,
|
|
52
|
+
): CanonicalTransport => {
|
|
53
|
+
port.start();
|
|
54
|
+
return {
|
|
55
|
+
send: (data, transfer) => {
|
|
56
|
+
const transferable = coerceToTransferableUint8Array(data);
|
|
57
|
+
if (transfer && transfer.length > 0) {
|
|
58
|
+
port.postMessage(transferable, transfer);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
port.postMessage(transferable);
|
|
62
|
+
},
|
|
63
|
+
onMessage: (handler) => {
|
|
64
|
+
const onMessage = (ev: MessageEvent) => {
|
|
65
|
+
const data = toUint8Array(ev.data);
|
|
66
|
+
if (data) handler(data);
|
|
67
|
+
};
|
|
68
|
+
port.addEventListener("message", onMessage);
|
|
69
|
+
return () => port.removeEventListener("message", onMessage);
|
|
70
|
+
},
|
|
71
|
+
close: () => {
|
|
72
|
+
try {
|
|
73
|
+
port.close();
|
|
74
|
+
} catch {}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const createRpcTransport = (
|
|
80
|
+
port: MessagePort | CanonicalChannel,
|
|
81
|
+
): CanonicalRpcTransport => {
|
|
82
|
+
if (!isMessagePort(port)) {
|
|
83
|
+
return {
|
|
84
|
+
send: port.send,
|
|
85
|
+
onMessage: port.onMessage,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
port.start();
|
|
90
|
+
return {
|
|
91
|
+
send: (data) => {
|
|
92
|
+
const transferable = coerceToTransferableUint8Array(data);
|
|
93
|
+
port.postMessage(transferable, [transferable.buffer]);
|
|
94
|
+
},
|
|
95
|
+
onMessage: (handler) => {
|
|
96
|
+
const onMessage = (ev: MessageEvent) => {
|
|
97
|
+
const data = toUint8Array(ev.data);
|
|
98
|
+
if (data) handler(data);
|
|
99
|
+
};
|
|
100
|
+
port.addEventListener("message", onMessage);
|
|
101
|
+
return () => port.removeEventListener("message", onMessage);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const WINDOW_MARKER = "__peerbit_canonical_transport__";
|
|
107
|
+
|
|
108
|
+
export const createWindowTransport = (
|
|
109
|
+
targetWindow: Window,
|
|
110
|
+
options?: { targetOrigin?: string; source?: Window; channel?: string },
|
|
111
|
+
): CanonicalTransport => {
|
|
112
|
+
const targetOrigin = options?.targetOrigin ?? "*";
|
|
113
|
+
const channel = options?.channel ?? "peerbit-canonical";
|
|
114
|
+
return {
|
|
115
|
+
send: (data, transfer) => {
|
|
116
|
+
const payload = coerceToTransferableUint8Array(data);
|
|
117
|
+
targetWindow.postMessage(
|
|
118
|
+
{ [WINDOW_MARKER]: true, channel, data: payload },
|
|
119
|
+
targetOrigin,
|
|
120
|
+
transfer as any,
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
onMessage: (handler) => {
|
|
124
|
+
const onMessage = (event: MessageEvent) => {
|
|
125
|
+
if (options?.source && event.source !== options.source) return;
|
|
126
|
+
const msg = event.data as any;
|
|
127
|
+
if (!msg || msg[WINDOW_MARKER] !== true) return;
|
|
128
|
+
if (msg.channel !== channel) return;
|
|
129
|
+
const data = toUint8Array(msg.data);
|
|
130
|
+
if (data) handler(data);
|
|
131
|
+
};
|
|
132
|
+
globalThis.addEventListener("message", onMessage);
|
|
133
|
+
return () => globalThis.removeEventListener("message", onMessage);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type ChannelState = {
|
|
139
|
+
channel: CanonicalChannel;
|
|
140
|
+
handlers: Set<(data: Uint8Array) => void>;
|
|
141
|
+
closeHandlers: Set<() => void>;
|
|
142
|
+
closed: boolean;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export class CanonicalConnection {
|
|
146
|
+
private readonly channels = new Map<number, ChannelState>();
|
|
147
|
+
private readonly controlHandlers = new Set<
|
|
148
|
+
(frame: CanonicalControlRequest | CanonicalControlResponse) => void
|
|
149
|
+
>();
|
|
150
|
+
private readonly activityHandlers = new Set<() => void>();
|
|
151
|
+
private readonly unsubscribe: () => void;
|
|
152
|
+
|
|
153
|
+
constructor(readonly transport: CanonicalTransport) {
|
|
154
|
+
this.unsubscribe = transport.onMessage((data) => {
|
|
155
|
+
this.onMessage(data);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
onControl(
|
|
160
|
+
handler: (
|
|
161
|
+
frame: CanonicalControlRequest | CanonicalControlResponse,
|
|
162
|
+
) => void,
|
|
163
|
+
): () => void {
|
|
164
|
+
this.controlHandlers.add(handler);
|
|
165
|
+
return () => this.controlHandlers.delete(handler);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
onActivity(handler: () => void): () => void {
|
|
169
|
+
this.activityHandlers.add(handler);
|
|
170
|
+
return () => this.activityHandlers.delete(handler);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
sendControl(frame: CanonicalControlRequest | CanonicalControlResponse): void {
|
|
174
|
+
this.sendFrame(frame);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
createChannel(channelId: number): CanonicalChannel {
|
|
178
|
+
const existing = this.channels.get(channelId);
|
|
179
|
+
if (existing) return existing.channel;
|
|
180
|
+
|
|
181
|
+
const handlers = new Set<(data: Uint8Array) => void>();
|
|
182
|
+
const closeHandlers = new Set<() => void>();
|
|
183
|
+
const state: ChannelState = {
|
|
184
|
+
channel: {
|
|
185
|
+
send: (payload) => {
|
|
186
|
+
this.sendFrame(
|
|
187
|
+
new CanonicalChannelMessage({
|
|
188
|
+
channelId,
|
|
189
|
+
payload,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
onMessage: (handler) => {
|
|
194
|
+
handlers.add(handler);
|
|
195
|
+
return () => handlers.delete(handler);
|
|
196
|
+
},
|
|
197
|
+
close: () => {
|
|
198
|
+
this.closeChannel(channelId, true);
|
|
199
|
+
},
|
|
200
|
+
onClose: (handler) => {
|
|
201
|
+
closeHandlers.add(handler);
|
|
202
|
+
return () => closeHandlers.delete(handler);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
handlers,
|
|
206
|
+
closeHandlers,
|
|
207
|
+
closed: false,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
this.channels.set(channelId, state);
|
|
211
|
+
return state.channel;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
releaseChannel(channelId: number): void {
|
|
215
|
+
this.closeChannel(channelId, false);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
close(): void {
|
|
219
|
+
this.unsubscribe();
|
|
220
|
+
const ids = [...this.channels.keys()];
|
|
221
|
+
for (const id of ids) {
|
|
222
|
+
this.closeChannel(id, true);
|
|
223
|
+
}
|
|
224
|
+
this.transport.close?.();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private closeChannel(channelId: number, emitClose: boolean): void {
|
|
228
|
+
const state = this.channels.get(channelId);
|
|
229
|
+
if (!state || state.closed) return;
|
|
230
|
+
state.closed = true;
|
|
231
|
+
this.channels.delete(channelId);
|
|
232
|
+
if (emitClose) {
|
|
233
|
+
try {
|
|
234
|
+
this.sendFrame(new CanonicalChannelClose({ channelId }));
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
for (const handler of state.closeHandlers) {
|
|
238
|
+
handler();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private sendFrame(frame: CanonicalFrame): void {
|
|
243
|
+
const bytes = serialize(frame);
|
|
244
|
+
const transferable = coerceToTransferableUint8Array(bytes);
|
|
245
|
+
this.transport.send(transferable);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private onMessage(data: Uint8Array): void {
|
|
249
|
+
for (const handler of this.activityHandlers) {
|
|
250
|
+
handler();
|
|
251
|
+
}
|
|
252
|
+
const frame = deserialize(data, CanonicalFrame) as CanonicalFrame;
|
|
253
|
+
if (frame instanceof CanonicalChannelMessage) {
|
|
254
|
+
const state = this.channels.get(frame.channelId);
|
|
255
|
+
if (!state || state.closed) return;
|
|
256
|
+
for (const handler of state.handlers) {
|
|
257
|
+
handler(frame.payload);
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (frame instanceof CanonicalChannelClose) {
|
|
263
|
+
this.closeChannel(frame.channelId, false);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
frame instanceof CanonicalControlRequest ||
|
|
269
|
+
frame instanceof CanonicalControlResponse
|
|
270
|
+
) {
|
|
271
|
+
for (const handler of this.controlHandlers) {
|
|
272
|
+
handler(frame);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export {
|
|
279
|
+
CanonicalChannelClose,
|
|
280
|
+
CanonicalChannelMessage,
|
|
281
|
+
CanonicalControlRequest,
|
|
282
|
+
CanonicalControlResponse,
|
|
283
|
+
CanonicalFrame,
|
|
284
|
+
CanonicalSignRequest,
|
|
285
|
+
CanonicalBootstrapRequest,
|
|
286
|
+
CanonicalLoadProgramRequest,
|
|
287
|
+
};
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { field, option, variant, vec } from "@dao-xyz/borsh";
|
|
2
|
+
|
|
3
|
+
export abstract class CanonicalFrame {}
|
|
4
|
+
|
|
5
|
+
@variant(0)
|
|
6
|
+
export class CanonicalControlRequest extends CanonicalFrame {
|
|
7
|
+
@field({ type: "u32" })
|
|
8
|
+
id: number;
|
|
9
|
+
|
|
10
|
+
@field({ type: "string" })
|
|
11
|
+
op: string;
|
|
12
|
+
|
|
13
|
+
@field({ type: option("string") })
|
|
14
|
+
name?: string;
|
|
15
|
+
|
|
16
|
+
@field({ type: option(Uint8Array) })
|
|
17
|
+
payload?: Uint8Array;
|
|
18
|
+
|
|
19
|
+
constructor(properties: {
|
|
20
|
+
id: number;
|
|
21
|
+
op: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
payload?: Uint8Array;
|
|
24
|
+
}) {
|
|
25
|
+
super();
|
|
26
|
+
this.id = properties.id;
|
|
27
|
+
this.op = properties.op;
|
|
28
|
+
this.name = properties.name;
|
|
29
|
+
this.payload = properties.payload;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@variant(1)
|
|
34
|
+
export class CanonicalControlResponse extends CanonicalFrame {
|
|
35
|
+
@field({ type: "u32" })
|
|
36
|
+
id: number;
|
|
37
|
+
|
|
38
|
+
@field({ type: "bool" })
|
|
39
|
+
ok: boolean;
|
|
40
|
+
|
|
41
|
+
@field({ type: option("string") })
|
|
42
|
+
error?: string;
|
|
43
|
+
|
|
44
|
+
@field({ type: option("string") })
|
|
45
|
+
peerId?: string;
|
|
46
|
+
|
|
47
|
+
@field({ type: option("u32") })
|
|
48
|
+
channelId?: number;
|
|
49
|
+
|
|
50
|
+
// Optional op-specific payload (e.g. serialized PublicSignKey / SignatureWithKey).
|
|
51
|
+
@field({ type: option(Uint8Array) })
|
|
52
|
+
payload?: Uint8Array;
|
|
53
|
+
|
|
54
|
+
// Optional op-specific strings (e.g. multiaddrs).
|
|
55
|
+
@field({ type: option(vec("string")) })
|
|
56
|
+
strings?: string[];
|
|
57
|
+
|
|
58
|
+
constructor(properties: {
|
|
59
|
+
id: number;
|
|
60
|
+
ok: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
peerId?: string;
|
|
63
|
+
channelId?: number;
|
|
64
|
+
payload?: Uint8Array;
|
|
65
|
+
strings?: string[];
|
|
66
|
+
}) {
|
|
67
|
+
super();
|
|
68
|
+
this.id = properties.id;
|
|
69
|
+
this.ok = properties.ok;
|
|
70
|
+
this.error = properties.error;
|
|
71
|
+
this.peerId = properties.peerId;
|
|
72
|
+
this.channelId = properties.channelId;
|
|
73
|
+
this.payload = properties.payload;
|
|
74
|
+
this.strings = properties.strings;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@variant(4)
|
|
79
|
+
export class CanonicalSignRequest {
|
|
80
|
+
@field({ type: Uint8Array })
|
|
81
|
+
data: Uint8Array;
|
|
82
|
+
|
|
83
|
+
@field({ type: option("u8") })
|
|
84
|
+
prehash?: number;
|
|
85
|
+
|
|
86
|
+
constructor(properties: { data: Uint8Array; prehash?: number }) {
|
|
87
|
+
this.data = properties.data;
|
|
88
|
+
this.prehash = properties.prehash;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@variant(5)
|
|
93
|
+
export class CanonicalBootstrapRequest {
|
|
94
|
+
@field({ type: vec("string") })
|
|
95
|
+
addresses: string[];
|
|
96
|
+
|
|
97
|
+
constructor(properties?: { addresses?: string[] }) {
|
|
98
|
+
this.addresses = properties?.addresses ?? [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@variant(6)
|
|
103
|
+
export class CanonicalLoadProgramRequest {
|
|
104
|
+
@field({ type: option("u32") })
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
|
|
107
|
+
constructor(properties?: { timeoutMs?: number }) {
|
|
108
|
+
this.timeoutMs = properties?.timeoutMs;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@variant(2)
|
|
113
|
+
export class CanonicalChannelMessage extends CanonicalFrame {
|
|
114
|
+
@field({ type: "u32" })
|
|
115
|
+
channelId: number;
|
|
116
|
+
|
|
117
|
+
@field({ type: Uint8Array })
|
|
118
|
+
payload: Uint8Array;
|
|
119
|
+
|
|
120
|
+
constructor(properties: { channelId: number; payload: Uint8Array }) {
|
|
121
|
+
super();
|
|
122
|
+
this.channelId = properties.channelId;
|
|
123
|
+
this.payload = properties.payload;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@variant(3)
|
|
128
|
+
export class CanonicalChannelClose extends CanonicalFrame {
|
|
129
|
+
@field({ type: "u32" })
|
|
130
|
+
channelId: number;
|
|
131
|
+
|
|
132
|
+
constructor(properties: { channelId: number }) {
|
|
133
|
+
super();
|
|
134
|
+
this.channelId = properties.channelId;
|
|
135
|
+
}
|
|
136
|
+
}
|