@dxos/teleport 0.1.14
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 +8 -0
- package/README.md +0 -0
- package/dist/lib/browser/index.mjs +544 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/testing.mjs +621 -0
- package/dist/lib/browser/testing.mjs.map +7 -0
- package/dist/lib/node/index.cjs +580 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/testing.cjs +653 -0
- package/dist/lib/node/testing.cjs.map +7 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/muxing/framer.d.ts +29 -0
- package/dist/types/src/muxing/framer.d.ts.map +1 -0
- package/dist/types/src/muxing/framer.test.d.ts +2 -0
- package/dist/types/src/muxing/framer.test.d.ts.map +1 -0
- package/dist/types/src/muxing/index.d.ts +4 -0
- package/dist/types/src/muxing/index.d.ts.map +1 -0
- package/dist/types/src/muxing/muxer.d.ts +60 -0
- package/dist/types/src/muxing/muxer.d.ts.map +1 -0
- package/dist/types/src/muxing/muxer.test.d.ts +2 -0
- package/dist/types/src/muxing/muxer.test.d.ts.map +1 -0
- package/dist/types/src/muxing/rpc-port.d.ts +11 -0
- package/dist/types/src/muxing/rpc-port.d.ts.map +1 -0
- package/dist/types/src/muxing/rpc-port.test.d.ts +2 -0
- package/dist/types/src/muxing/rpc-port.test.d.ts.map +1 -0
- package/dist/types/src/teleport.d.ts +47 -0
- package/dist/types/src/teleport.d.ts.map +1 -0
- package/dist/types/src/teleport.test.d.ts +2 -0
- package/dist/types/src/teleport.test.d.ts.map +1 -0
- package/dist/types/src/test-extension.d.ts +12 -0
- package/dist/types/src/test-extension.d.ts.map +1 -0
- package/dist/types/src/testing.d.ts +29 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/package.json +47 -0
- package/src/index.ts +6 -0
- package/src/muxing/framer.test.ts +160 -0
- package/src/muxing/framer.ts +132 -0
- package/src/muxing/index.ts +7 -0
- package/src/muxing/muxer.test.ts +185 -0
- package/src/muxing/muxer.ts +301 -0
- package/src/muxing/rpc-port.test.ts +18 -0
- package/src/muxing/rpc-port.ts +15 -0
- package/src/teleport.test.ts +81 -0
- package/src/teleport.ts +277 -0
- package/src/test-extension.ts +65 -0
- package/src/testing.ts +75 -0
- package/testing.d.ts +11 -0
- package/testing.js +5 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { Duplex } from 'node:stream';
|
|
7
|
+
import * as varint from 'varint';
|
|
8
|
+
|
|
9
|
+
import { RpcPort } from './rpc-port';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Framer that turns a stream of binary messages into a framed RpcPort.
|
|
13
|
+
*
|
|
14
|
+
* Buffers are written prefixed by their length encoded as a varint.
|
|
15
|
+
*/
|
|
16
|
+
export class Framer {
|
|
17
|
+
// private readonly _tagBuffer = Buffer.alloc(4)
|
|
18
|
+
private _messageCb?: (msg: Uint8Array) => void;
|
|
19
|
+
private _subscribeCb?: () => void;
|
|
20
|
+
private _buffer?: Buffer; // The rest of the bytes from the previous write call.
|
|
21
|
+
|
|
22
|
+
private readonly _stream = new Duplex({
|
|
23
|
+
objectMode: false,
|
|
24
|
+
read: () => {},
|
|
25
|
+
write: (chunk, encoding, callback) => {
|
|
26
|
+
assert(!this._subscribeCb, 'Internal Framer bug. Concurrent writes detected.');
|
|
27
|
+
|
|
28
|
+
if (this._buffer && this._buffer.length > 0) {
|
|
29
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
30
|
+
} else {
|
|
31
|
+
this._buffer = chunk;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this._messageCb) {
|
|
35
|
+
this._popFrames();
|
|
36
|
+
callback();
|
|
37
|
+
} else {
|
|
38
|
+
this._subscribeCb = () => {
|
|
39
|
+
// Schedule the processing of the chunk after the peer subscribes to the messages.
|
|
40
|
+
this._popFrames();
|
|
41
|
+
this._subscribeCb = undefined;
|
|
42
|
+
callback();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
public readonly port: RpcPort = {
|
|
49
|
+
send: (message) => {
|
|
50
|
+
this._stream.push(encodeLength(message.length));
|
|
51
|
+
this._stream.push(message);
|
|
52
|
+
},
|
|
53
|
+
subscribe: (callback) => {
|
|
54
|
+
assert(!this._messageCb, 'Rpc port already has a message listener.');
|
|
55
|
+
this._messageCb = callback;
|
|
56
|
+
this._subscribeCb?.();
|
|
57
|
+
return () => {
|
|
58
|
+
this._messageCb = undefined;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
get stream(): Duplex {
|
|
64
|
+
return this._stream;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Attempts to pop frames from the buffer and call the message callback.
|
|
69
|
+
*/
|
|
70
|
+
private _popFrames() {
|
|
71
|
+
let offset = 0;
|
|
72
|
+
while (offset < this._buffer!.length) {
|
|
73
|
+
const frame = readFrame(this._buffer!, offset);
|
|
74
|
+
|
|
75
|
+
if (!frame) {
|
|
76
|
+
break; // Couldn't read frame but there are still bytes left in the buffer.
|
|
77
|
+
}
|
|
78
|
+
offset += frame.bytesConsumed;
|
|
79
|
+
// TODO(dmaretskyi): Possible bug if the peer unsubscribes while we're reading frames.
|
|
80
|
+
this._messageCb!(frame.payload);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (offset < this._buffer!.length) {
|
|
84
|
+
// Save the rest of the bytes for the next write call.
|
|
85
|
+
this._buffer = this._buffer!.subarray(offset);
|
|
86
|
+
} else {
|
|
87
|
+
this._buffer = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
destroy() {
|
|
92
|
+
// TODO(dmaretskyi): Call stream.end() instead?
|
|
93
|
+
this._stream.destroy();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Attempts to read a frame from the input buffer.
|
|
99
|
+
*/
|
|
100
|
+
export const readFrame = (buffer: Buffer, offset: number): { payload: Buffer; bytesConsumed: number } | undefined => {
|
|
101
|
+
try {
|
|
102
|
+
const frameLength = varint.decode(buffer, offset);
|
|
103
|
+
const tagLength = varint.decode.bytes;
|
|
104
|
+
|
|
105
|
+
if (buffer.length < offset + tagLength + frameLength) {
|
|
106
|
+
// Not enough bytes to read the frame.
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const payload = buffer.subarray(offset + tagLength, offset + tagLength + frameLength);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
payload,
|
|
114
|
+
bytesConsumed: tagLength + frameLength
|
|
115
|
+
};
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err instanceof RangeError) {
|
|
118
|
+
// Not enough bytes to read the tag.
|
|
119
|
+
return undefined;
|
|
120
|
+
} else {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const encodeLength = (length: number) => {
|
|
127
|
+
const res = varint.encode(length, Buffer.allocUnsafe(4)).subarray(0, varint.encode.bytes);
|
|
128
|
+
if (varint.encode.bytes > 4) {
|
|
129
|
+
throw new Error('Frame too large');
|
|
130
|
+
}
|
|
131
|
+
return res;
|
|
132
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
import { pipeline, Transform } from 'node:stream';
|
|
7
|
+
import waitForExpect from 'wait-for-expect';
|
|
8
|
+
|
|
9
|
+
import { latch, asyncTimeout } from '@dxos/async';
|
|
10
|
+
import { schema } from '@dxos/protocols';
|
|
11
|
+
import { TestService } from '@dxos/protocols/proto/example/testing/rpc';
|
|
12
|
+
import { createProtoRpcPeer } from '@dxos/rpc';
|
|
13
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
14
|
+
|
|
15
|
+
import { Muxer } from './muxer';
|
|
16
|
+
import { RpcPort } from './rpc-port';
|
|
17
|
+
|
|
18
|
+
const setupPeers = () => {
|
|
19
|
+
const peer1 = new Muxer();
|
|
20
|
+
const peer2 = new Muxer();
|
|
21
|
+
|
|
22
|
+
peer1.stream.pipe(peer2.stream).pipe(peer1.stream);
|
|
23
|
+
|
|
24
|
+
const unpipe = () => {
|
|
25
|
+
peer1.stream.unpipe(peer2.stream);
|
|
26
|
+
peer2.stream.unpipe(peer1.stream);
|
|
27
|
+
};
|
|
28
|
+
afterTest(unpipe);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
peer1,
|
|
32
|
+
peer2,
|
|
33
|
+
unpipe
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createRpc = (port: RpcPort, handler: TestService['testCall']) =>
|
|
38
|
+
createProtoRpcPeer({
|
|
39
|
+
requested: {
|
|
40
|
+
TestService: schema.getService('example.testing.rpc.TestService')
|
|
41
|
+
},
|
|
42
|
+
exposed: {
|
|
43
|
+
TestService: schema.getService('example.testing.rpc.TestService')
|
|
44
|
+
},
|
|
45
|
+
handlers: {
|
|
46
|
+
TestService: {
|
|
47
|
+
testCall: handler,
|
|
48
|
+
voidCall: async () => {}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
port
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('Muxer', () => {
|
|
55
|
+
test('rpc calls on 1 port', async () => {
|
|
56
|
+
const { peer1, peer2 } = setupPeers();
|
|
57
|
+
|
|
58
|
+
const [wait, inc] = latch({ count: 2, timeout: 500 });
|
|
59
|
+
|
|
60
|
+
for (const peer of [peer1, peer2]) {
|
|
61
|
+
const client = createRpc(
|
|
62
|
+
peer.createPort('example.extension/rpc', {
|
|
63
|
+
contentType: 'application/x-protobuf; messageType="dxos.rpc.Message"'
|
|
64
|
+
}),
|
|
65
|
+
async ({ data }) => ({ data })
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
setTimeout(async () => {
|
|
69
|
+
await client.open();
|
|
70
|
+
expect(await client.rpc.TestService.testCall({ data: 'test' })).to.deep.eq({ data: 'test' });
|
|
71
|
+
inc();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await wait();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('destroy releases other stream', async () => {
|
|
79
|
+
const { peer1, peer2 } = setupPeers();
|
|
80
|
+
|
|
81
|
+
const promise = asyncTimeout(peer1.close.waitForCount(1), 100);
|
|
82
|
+
|
|
83
|
+
peer2.destroy();
|
|
84
|
+
|
|
85
|
+
await promise;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('two concurrent rpc ports', async () => {
|
|
89
|
+
const { peer1, peer2 } = setupPeers();
|
|
90
|
+
|
|
91
|
+
const [wait, inc] = latch({ count: 4, timeout: 500 });
|
|
92
|
+
|
|
93
|
+
for (const peer of [peer1, peer2]) {
|
|
94
|
+
{
|
|
95
|
+
const client = createRpc(
|
|
96
|
+
peer.createPort('example.extension/rpc1', {
|
|
97
|
+
contentType: 'application/x-protobuf; messageType="dxos.rpc.Message"'
|
|
98
|
+
}),
|
|
99
|
+
async ({ data }) => ({ data: data + '-rpc1' })
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
setTimeout(async () => {
|
|
103
|
+
await client.open();
|
|
104
|
+
expect(await client.rpc.TestService.testCall({ data: 'test' })).to.deep.eq({ data: 'test-rpc1' });
|
|
105
|
+
inc();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
{
|
|
109
|
+
const client = createRpc(
|
|
110
|
+
peer.createPort('example.extension/rpc2', {
|
|
111
|
+
contentType: 'application/x-protobuf; messageType="dxos.rpc.Message"'
|
|
112
|
+
}),
|
|
113
|
+
async ({ data }) => ({ data: data + '-rpc2' })
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
setTimeout(async () => {
|
|
117
|
+
await client.open();
|
|
118
|
+
expect(await client.rpc.TestService.testCall({ data: 'test' })).to.deep.eq({ data: 'test-rpc2' });
|
|
119
|
+
inc();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await wait();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('node.js streams', async () => {
|
|
128
|
+
const { peer1, peer2 } = setupPeers();
|
|
129
|
+
|
|
130
|
+
const stream2 = peer2.createStream('example.extension/stream1', {
|
|
131
|
+
contentType: 'application/octet-stream'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Buffer data before remote peer opens.
|
|
135
|
+
stream2.write('hello');
|
|
136
|
+
|
|
137
|
+
const stream1 = peer1.createStream('example.extension/stream1', {
|
|
138
|
+
contentType: 'application/octet-stream'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pipeline(
|
|
142
|
+
stream1,
|
|
143
|
+
new Transform({
|
|
144
|
+
transform: (chunk, encoding, callback) => {
|
|
145
|
+
callback(null, Buffer.from(Buffer.from(chunk).toString().toUpperCase())); // Make all characters uppercase.
|
|
146
|
+
}
|
|
147
|
+
}),
|
|
148
|
+
stream1,
|
|
149
|
+
() => {}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
let received = '';
|
|
153
|
+
stream2.on('data', (chunk) => {
|
|
154
|
+
received += Buffer.from(chunk).toString();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
stream2.write(' world!');
|
|
158
|
+
|
|
159
|
+
await waitForExpect(() => {
|
|
160
|
+
expect(received).to.eq('HELLO WORLD!');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('destroying muxers destroys open streams', async () => {
|
|
165
|
+
const { peer1, peer2 } = setupPeers();
|
|
166
|
+
|
|
167
|
+
const stream1 = peer1.createStream('example.extension/stream1', {
|
|
168
|
+
contentType: 'application/octet-stream'
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const stream2 = peer2.createStream('example.extension/stream1', {
|
|
172
|
+
contentType: 'application/octet-stream'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const [wait, inc] = latch({ count: 2, timeout: 500 });
|
|
176
|
+
|
|
177
|
+
stream1.once('close', inc);
|
|
178
|
+
stream2.once('close', inc);
|
|
179
|
+
|
|
180
|
+
peer1.destroy();
|
|
181
|
+
// Peer2 should also be destroyed.
|
|
182
|
+
|
|
183
|
+
await wait();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { Duplex } from 'node:stream';
|
|
7
|
+
|
|
8
|
+
import { Event } from '@dxos/async';
|
|
9
|
+
import { failUndefined } from '@dxos/debug';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import { schema } from '@dxos/protocols';
|
|
12
|
+
import { Command } from '@dxos/protocols/proto/dxos/mesh/muxer';
|
|
13
|
+
|
|
14
|
+
import { Framer } from './framer';
|
|
15
|
+
import { RpcPort } from './rpc-port';
|
|
16
|
+
|
|
17
|
+
const codec = schema.getCodecForType('dxos.mesh.muxer.Command');
|
|
18
|
+
|
|
19
|
+
export type CleanupCb = void | (() => void);
|
|
20
|
+
|
|
21
|
+
export type CreateChannelOpts = {
|
|
22
|
+
/**
|
|
23
|
+
* MIME type of the wire content.
|
|
24
|
+
*
|
|
25
|
+
* Examples:
|
|
26
|
+
* - application/octet-stream
|
|
27
|
+
* - application/x-protobuf; messageType="dxos.rpc.Message"
|
|
28
|
+
*/
|
|
29
|
+
contentType?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Channel based multiplexer.
|
|
34
|
+
*
|
|
35
|
+
* Can be used to open a number of channels represented by streams or RPC ports.
|
|
36
|
+
* Performs framing for RPC ports.
|
|
37
|
+
* Will buffer data until the remote peer opens the channel.
|
|
38
|
+
*
|
|
39
|
+
* The API will not advertise channels that as they are opened by the remote peer.
|
|
40
|
+
* A higher level API (could be build on top of this muxer) for channel discovery is required.
|
|
41
|
+
*/
|
|
42
|
+
export class Muxer {
|
|
43
|
+
private readonly _framer = new Framer();
|
|
44
|
+
public readonly stream = this._framer.stream;
|
|
45
|
+
|
|
46
|
+
private readonly _channelsByLocalId = new Map<number, Channel>();
|
|
47
|
+
private readonly _channelsByTag = new Map<string, Channel>();
|
|
48
|
+
|
|
49
|
+
private _nextId = 0;
|
|
50
|
+
private _destroyed = false;
|
|
51
|
+
private _destroying = false;
|
|
52
|
+
|
|
53
|
+
public close = new Event<Error | undefined>();
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
this._framer.port.subscribe((msg) => {
|
|
57
|
+
this._handleCommand(codec.decode(msg));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates a duplex Node.js-style stream.
|
|
63
|
+
* The remote peer is expected to call `createStream` with the same tag.
|
|
64
|
+
* The stream is immediately readable and writable.
|
|
65
|
+
* NOTE: The data will be buffered until the stream is opened remotely with the same tag (may cause a memory leak).
|
|
66
|
+
*/
|
|
67
|
+
createStream(tag: string, opts: CreateChannelOpts = {}): Duplex {
|
|
68
|
+
const channel = this._getOrCreateStream({
|
|
69
|
+
tag,
|
|
70
|
+
contentType: opts.contentType
|
|
71
|
+
});
|
|
72
|
+
assert(!channel.push, `Channel already open: ${tag}`);
|
|
73
|
+
|
|
74
|
+
const stream = new Duplex({
|
|
75
|
+
write: (data, encoding, callback) => {
|
|
76
|
+
this._sendData(channel, data);
|
|
77
|
+
// TODO(dmaretskyi): Should we error if sending data has errored?
|
|
78
|
+
callback();
|
|
79
|
+
},
|
|
80
|
+
read: () => {} // No-op. We will push data when we receive it.
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
channel.push = (data) => {
|
|
84
|
+
stream.push(data);
|
|
85
|
+
};
|
|
86
|
+
channel.destroy = (err) => {
|
|
87
|
+
// TODO(dmaretskyi): Call stream.end() instead?
|
|
88
|
+
stream.destroy(err);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// NOTE: Make sure channel.push is set before sending the command.
|
|
92
|
+
this._sendCommand({
|
|
93
|
+
openChannel: {
|
|
94
|
+
id: channel.id,
|
|
95
|
+
tag: channel.tag,
|
|
96
|
+
contentType: channel.contentType
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return stream;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates an RPC port.
|
|
105
|
+
* The remote peer is expected to call `createPort` with the same tag.
|
|
106
|
+
* The port is immediately usable.
|
|
107
|
+
* NOTE: The data will be buffered until the stream is opened remotely with the same tag (may cause a memory leak).
|
|
108
|
+
*/
|
|
109
|
+
createPort(tag: string, opts: CreateChannelOpts = {}): RpcPort {
|
|
110
|
+
const channel = this._getOrCreateStream({
|
|
111
|
+
tag,
|
|
112
|
+
contentType: opts.contentType
|
|
113
|
+
});
|
|
114
|
+
assert(!channel.push, `Channel already open: ${tag}`);
|
|
115
|
+
|
|
116
|
+
// We need to buffer incoming data until the port is subscribed to.
|
|
117
|
+
let inboundBuffer: Uint8Array[] = [];
|
|
118
|
+
let callback: ((data: Uint8Array) => void) | undefined;
|
|
119
|
+
|
|
120
|
+
channel.push = (data) => {
|
|
121
|
+
if (callback) {
|
|
122
|
+
callback(data);
|
|
123
|
+
} else {
|
|
124
|
+
inboundBuffer.push(data);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const port: RpcPort = {
|
|
129
|
+
send: (data: Uint8Array) => {
|
|
130
|
+
this._sendData(channel, data); // TODO(dmaretskyi): Error propagation?
|
|
131
|
+
|
|
132
|
+
// TODO(dmaretskyi): Debugging.
|
|
133
|
+
// appendFileSync('log.json', JSON.stringify(schema.getCodecForType('dxos.rpc.RpcMessage').decode(data), null, 2) + '\n')
|
|
134
|
+
},
|
|
135
|
+
subscribe: (cb: (data: Uint8Array) => void) => {
|
|
136
|
+
assert(!callback, 'Only one subscriber is allowed');
|
|
137
|
+
callback = cb;
|
|
138
|
+
for (const data of inboundBuffer) {
|
|
139
|
+
cb(data);
|
|
140
|
+
}
|
|
141
|
+
inboundBuffer = [];
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// NOTE: Make sure channel.push is set before sending the command.
|
|
146
|
+
this._sendCommand({
|
|
147
|
+
openChannel: {
|
|
148
|
+
id: channel.id,
|
|
149
|
+
tag: channel.tag,
|
|
150
|
+
contentType: channel.contentType
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return port;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Force-close with optional error.
|
|
159
|
+
*/
|
|
160
|
+
destroy(err?: Error) {
|
|
161
|
+
if (this._destroying) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this._destroying = true;
|
|
165
|
+
|
|
166
|
+
this._sendCommand({
|
|
167
|
+
destroy: {
|
|
168
|
+
error: err?.message
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
this._dispose();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _dispose(err?: Error) {
|
|
175
|
+
if (this._destroyed) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this._destroyed = true;
|
|
180
|
+
this._framer.destroy();
|
|
181
|
+
|
|
182
|
+
for (const channel of this._channelsByTag.values()) {
|
|
183
|
+
channel.destroy?.(err);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.close.emit(err);
|
|
187
|
+
|
|
188
|
+
// Make it easy for GC.
|
|
189
|
+
this._channelsByLocalId.clear();
|
|
190
|
+
this._channelsByTag.clear();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private _handleCommand(cmd: Command) {
|
|
194
|
+
log('Received command', { cmd });
|
|
195
|
+
|
|
196
|
+
if (this._destroyed || this._destroying) {
|
|
197
|
+
log.warn('Received command after destroy');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (cmd.openChannel) {
|
|
202
|
+
const channel = this._getOrCreateStream({
|
|
203
|
+
tag: cmd.openChannel.tag,
|
|
204
|
+
contentType: cmd.openChannel.contentType
|
|
205
|
+
});
|
|
206
|
+
channel.remoteId = cmd.openChannel.id;
|
|
207
|
+
|
|
208
|
+
// Flush any buffered data.
|
|
209
|
+
for (const data of channel.buffer) {
|
|
210
|
+
this._sendCommand({
|
|
211
|
+
data: {
|
|
212
|
+
channelId: channel.remoteId,
|
|
213
|
+
data
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
channel.buffer = [];
|
|
218
|
+
} else if (cmd.data) {
|
|
219
|
+
const stream = this._channelsByLocalId.get(cmd.data.channelId) ?? failUndefined();
|
|
220
|
+
if (!stream.push) {
|
|
221
|
+
log.warn('Received data for channel before it was opened', { tag: stream.tag });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
stream.push(cmd.data.data);
|
|
225
|
+
} else if (cmd.destroy) {
|
|
226
|
+
this._dispose();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private _sendCommand(cmd: Command) {
|
|
231
|
+
Promise.resolve(this._framer.port.send(codec.encode(cmd))).catch((err) => {
|
|
232
|
+
this.destroy(err);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private _getOrCreateStream(params: CreateChannelInternalParams): Channel {
|
|
237
|
+
let channel = this._channelsByTag.get(params.tag);
|
|
238
|
+
if (!channel) {
|
|
239
|
+
channel = {
|
|
240
|
+
id: this._nextId++,
|
|
241
|
+
remoteId: null,
|
|
242
|
+
tag: params.tag,
|
|
243
|
+
contentType: params.contentType,
|
|
244
|
+
buffer: [],
|
|
245
|
+
push: null,
|
|
246
|
+
destroy: null
|
|
247
|
+
};
|
|
248
|
+
this._channelsByTag.set(channel.tag, channel);
|
|
249
|
+
this._channelsByLocalId.set(channel.id, channel);
|
|
250
|
+
}
|
|
251
|
+
return channel;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private _sendData(channel: Channel, data: Uint8Array) {
|
|
255
|
+
if (channel.remoteId === null) {
|
|
256
|
+
// Remote side has not opened the channel yet.
|
|
257
|
+
channel.buffer.push(data);
|
|
258
|
+
} else {
|
|
259
|
+
this._sendCommand({
|
|
260
|
+
data: {
|
|
261
|
+
channelId: channel.remoteId,
|
|
262
|
+
data
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type Channel = {
|
|
270
|
+
/**
|
|
271
|
+
* Our local channel ID.
|
|
272
|
+
* Incoming Data commands will have this ID.
|
|
273
|
+
*/
|
|
274
|
+
id: number;
|
|
275
|
+
tag: string;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Remote id is set when we receive an OpenChannel command.
|
|
279
|
+
* The originating Data commands should carry this id.
|
|
280
|
+
*/
|
|
281
|
+
remoteId: null | number;
|
|
282
|
+
|
|
283
|
+
contentType?: string;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Send buffer.
|
|
287
|
+
*/
|
|
288
|
+
buffer: Uint8Array[];
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Set when we initialize a NodeJS stream or an RPC port consuming the channel.
|
|
292
|
+
*/
|
|
293
|
+
push: null | ((data: Uint8Array) => void);
|
|
294
|
+
|
|
295
|
+
destroy: null | ((err?: Error) => void);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
type CreateChannelInternalParams = {
|
|
299
|
+
tag: string;
|
|
300
|
+
contentType?: string;
|
|
301
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as rpc from '@dxos/rpc';
|
|
6
|
+
import { test } from '@dxos/test';
|
|
7
|
+
|
|
8
|
+
import { RpcPort } from './rpc-port';
|
|
9
|
+
|
|
10
|
+
// This test will break at compile time if the interface changes.
|
|
11
|
+
test('RpcPort type is assignable to type from @dxos/rpc package', () => {
|
|
12
|
+
{
|
|
13
|
+
const _port: RpcPort = {} as rpc.RpcPort;
|
|
14
|
+
}
|
|
15
|
+
{
|
|
16
|
+
const _port: rpc.RpcPort = {} as RpcPort;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { MaybePromise } from '@dxos/util';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface for a transport-agnostic port to send/receive binary messages.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: Copied from @dxos/rpc to avoid dependency. Structural typing should still work.
|
|
11
|
+
*/
|
|
12
|
+
export interface RpcPort {
|
|
13
|
+
send: (msg: Uint8Array) => MaybePromise<void>;
|
|
14
|
+
subscribe: (cb: (msg: Uint8Array) => void) => (() => void) | void;
|
|
15
|
+
}
|