starpc 0.24.0 → 0.25.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.
@@ -0,0 +1,38 @@
1
+ import type { Sink, Source, Duplex } from 'it-stream-types';
2
+ export interface ChannelStreamMessage<T> {
3
+ from: string;
4
+ ack?: boolean;
5
+ opened?: boolean;
6
+ closed?: boolean;
7
+ error?: Error;
8
+ data?: T;
9
+ }
10
+ export type ChannelPort = MessagePort | {
11
+ tx: BroadcastChannel;
12
+ rx: BroadcastChannel;
13
+ };
14
+ export declare class ChannelStream<T> implements Duplex<AsyncGenerator<T>, Source<T>, Promise<void>> {
15
+ readonly channel: ChannelPort;
16
+ sink: Sink<Source<T>, Promise<void>>;
17
+ source: AsyncGenerator<T>;
18
+ private readonly _source;
19
+ private readonly localId;
20
+ private localOpen;
21
+ private remoteOpen;
22
+ readonly waitRemoteOpen: Promise<void>;
23
+ private _remoteOpen?;
24
+ private remoteAck;
25
+ readonly waitRemoteAck: Promise<void>;
26
+ private _remoteAck?;
27
+ get isAcked(): boolean;
28
+ get isOpen(): boolean;
29
+ constructor(localId: string, channel: ChannelPort, remoteOpen: boolean);
30
+ private postMessage;
31
+ close(error?: Error): void;
32
+ private onLocalOpened;
33
+ private onRemoteAcked;
34
+ private onRemoteOpened;
35
+ private _createSink;
36
+ private onMessage;
37
+ }
38
+ export declare function newBroadcastChannelStream<T>(id: string, readName: string, writeName: string, remoteOpen: boolean): ChannelStream<T>;
@@ -0,0 +1,164 @@
1
+ import { pushable } from 'it-pushable';
2
+ // ChannelStream implements a Stream over a BroadcastChannel duplex or MessagePort.
3
+ //
4
+ // NOTE: there is no way to tell if a BroadcastChannel or MessagePort is closed.
5
+ // This implementation sends a "closed" message when close() is called.
6
+ // However: if the remote is removed w/o closing cleanly, the stream will be left open!
7
+ export class ChannelStream {
8
+ // isAcked checks if the stream is acknowledged by the remote.
9
+ get isAcked() {
10
+ return this.remoteAck || false;
11
+ }
12
+ // isOpen checks if the stream is opened by the remote.
13
+ get isOpen() {
14
+ return this.remoteOpen || false;
15
+ }
16
+ // remoteOpen indicates if we know the remote has already opened the stream.
17
+ constructor(localId, channel, remoteOpen) {
18
+ this.localId = localId;
19
+ this.channel = channel;
20
+ this.sink = this._createSink();
21
+ this.localOpen = false;
22
+ this.remoteAck = remoteOpen;
23
+ this.remoteOpen = remoteOpen;
24
+ if (remoteOpen) {
25
+ this.waitRemoteOpen = Promise.resolve();
26
+ this.waitRemoteAck = Promise.resolve();
27
+ }
28
+ else {
29
+ this.waitRemoteOpen = new Promise((resolve, reject) => {
30
+ this._remoteOpen = (err) => {
31
+ if (err) {
32
+ reject(err);
33
+ }
34
+ else {
35
+ resolve();
36
+ }
37
+ };
38
+ });
39
+ this.waitRemoteOpen.catch(() => { });
40
+ this.waitRemoteAck = new Promise((resolve, reject) => {
41
+ this._remoteAck = (err) => {
42
+ if (err) {
43
+ reject(err);
44
+ }
45
+ else {
46
+ resolve();
47
+ }
48
+ };
49
+ });
50
+ this.waitRemoteAck.catch(() => { });
51
+ }
52
+ const source = pushable({ objectMode: true });
53
+ this.source = source;
54
+ this._source = source;
55
+ const onMessage = this.onMessage.bind(this);
56
+ if (channel instanceof MessagePort) {
57
+ // MessagePort
58
+ channel.onmessage = onMessage;
59
+ channel.start();
60
+ }
61
+ else {
62
+ // BroadcastChannel
63
+ channel.rx.onmessage = onMessage;
64
+ }
65
+ this.postMessage({ ack: true });
66
+ }
67
+ // postMessage writes a message to the stream.
68
+ postMessage(msg) {
69
+ msg.from = this.localId;
70
+ if (this.channel instanceof MessagePort) {
71
+ this.channel.postMessage(msg);
72
+ }
73
+ else {
74
+ this.channel.tx.postMessage(msg);
75
+ }
76
+ }
77
+ // close closes the broadcast channels.
78
+ close(error) {
79
+ // write a message to indicate the stream is now closed.
80
+ this.postMessage({ closed: true, error });
81
+ // close channels
82
+ if (this.channel instanceof MessagePort) {
83
+ this.channel.close();
84
+ }
85
+ else {
86
+ this.channel.tx.close();
87
+ this.channel.rx.close();
88
+ }
89
+ if (!this.remoteOpen && this._remoteOpen) {
90
+ this._remoteOpen(error || new Error('closed'));
91
+ }
92
+ if (!this.remoteAck && this._remoteAck) {
93
+ this._remoteAck(error || new Error('closed'));
94
+ }
95
+ }
96
+ // onLocalOpened indicates the local side has opened the read stream.
97
+ onLocalOpened() {
98
+ if (!this.localOpen) {
99
+ this.localOpen = true;
100
+ this.postMessage({ opened: true });
101
+ }
102
+ }
103
+ // onRemoteAcked indicates the remote side has acked the stream.
104
+ onRemoteAcked() {
105
+ if (!this.remoteAck) {
106
+ this.remoteAck = true;
107
+ if (this._remoteAck) {
108
+ this._remoteAck();
109
+ }
110
+ }
111
+ }
112
+ // onRemoteOpened indicates the remote side has opened the read stream.
113
+ onRemoteOpened() {
114
+ if (!this.remoteOpen) {
115
+ this.remoteOpen = true;
116
+ if (this._remoteOpen) {
117
+ this._remoteOpen();
118
+ }
119
+ }
120
+ }
121
+ _createSink() {
122
+ return async (source) => {
123
+ // make sure the remote is open before we send any data.
124
+ await this.waitRemoteAck;
125
+ this.onLocalOpened();
126
+ await this.waitRemoteOpen;
127
+ try {
128
+ for await (const msg of source) {
129
+ this.postMessage({ data: msg });
130
+ }
131
+ this.postMessage({ closed: true });
132
+ }
133
+ catch (error) {
134
+ this.postMessage({ closed: true, error: error });
135
+ }
136
+ };
137
+ }
138
+ onMessage(ev) {
139
+ const msg = ev.data;
140
+ if (!msg || msg.from === this.localId || !msg.from) {
141
+ return;
142
+ }
143
+ if (msg.ack || msg.opened) {
144
+ this.onRemoteAcked();
145
+ }
146
+ if (msg.opened) {
147
+ this.onRemoteOpened();
148
+ }
149
+ const { data, closed, error: err } = msg;
150
+ if (data) {
151
+ this._source.push(data);
152
+ }
153
+ if (err) {
154
+ this._source.end(err);
155
+ }
156
+ else if (closed) {
157
+ this._source.end();
158
+ }
159
+ }
160
+ }
161
+ // newBroadcastChannelStream constructs a ChannelStream with a channel name.
162
+ export function newBroadcastChannelStream(id, readName, writeName, remoteOpen) {
163
+ return new ChannelStream(id, { tx: new BroadcastChannel(writeName), rx: new BroadcastChannel(readName) }, remoteOpen);
164
+ }
@@ -8,6 +8,7 @@ export { Handler, InvokeFn, MethodMap, StaticHandler, createHandler, } from './h
8
8
  export { MethodProto, createInvokeFn } from './invoker.js';
9
9
  export { Packet, CallStart, CallData } from './rpcproto.pb.js';
10
10
  export { Mux, StaticMux, LookupMethod, createMux } from './mux.js';
11
+ export { ChannelStreamMessage, ChannelPort, ChannelStream, newBroadcastChannelStream, } from './channel.js';
11
12
  export { BroadcastChannelDuplex, BroadcastChannelConn, newBroadcastChannelDuplex, } from './broadcast-channel.js';
12
13
  export { MessagePortDuplex, MessagePortConn, newMessagePortDuplex, } from './message-port.js';
13
14
  export { MessageDefinition, DecodeMessageTransform, buildDecodeMessageTransform, EncodeMessageTransform, buildEncodeMessageTransform, memoProto, memoProtoDecode, } from './message.js';
@@ -7,6 +7,7 @@ export { StaticHandler, createHandler, } from './handler.js';
7
7
  export { createInvokeFn } from './invoker.js';
8
8
  export { Packet, CallStart, CallData } from './rpcproto.pb.js';
9
9
  export { StaticMux, createMux } from './mux.js';
10
+ export { ChannelStream, newBroadcastChannelStream, } from './channel.js';
10
11
  export { BroadcastChannelDuplex, BroadcastChannelConn, newBroadcastChannelDuplex, } from './broadcast-channel.js';
11
12
  export { MessagePortDuplex, MessagePortConn, newMessagePortDuplex, } from './message-port.js';
12
13
  export { buildDecodeMessageTransform, buildEncodeMessageTransform, memoProto, memoProtoDecode, } from './message.js';
package/go.mod CHANGED
@@ -9,7 +9,7 @@ require (
9
9
  )
10
10
 
11
11
  require (
12
- github.com/aperturerobotics/util v1.13.3 // latest
12
+ github.com/aperturerobotics/util v1.13.5 // latest
13
13
  github.com/libp2p/go-libp2p v0.32.2 // latest
14
14
  github.com/libp2p/go-yamux/v4 v4.0.2-0.20240206065824-7222fbc3459d // master
15
15
  github.com/sirupsen/logrus v1.9.3 // latest
package/go.sum CHANGED
@@ -2,6 +2,8 @@ github.com/aperturerobotics/util v1.13.2 h1:MTe8MO+Tfo9d3Z4Zi6Akm//rIymy5gyoAOIE
2
2
  github.com/aperturerobotics/util v1.13.2/go.mod h1:d84OAQAGXCpl7JOBstnal91Lm6nKgk+vBgtHPgxBYrQ=
3
3
  github.com/aperturerobotics/util v1.13.3 h1:N4JXKYql0R/A2hMuV79SZ2vaftf8tiN8DUJ8sGc7Rso=
4
4
  github.com/aperturerobotics/util v1.13.3/go.mod h1:JdziNd9tR6lWqc9bSMIe1At8Gagrg986rmtZuPCv6+w=
5
+ github.com/aperturerobotics/util v1.13.5 h1:g8Q9VKBUYR5Nu5Ee/ZygVbe0JkGXeNq1fBkT0ipLBMY=
6
+ github.com/aperturerobotics/util v1.13.5/go.mod h1:8AfpGb9RJqUItLBb5ec3sprpl9swYyHlgOw0HzkE+S8=
5
7
  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6
8
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7
9
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starpc",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Streaming protobuf RPC service protocol over any two-way channel.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -0,0 +1,230 @@
1
+ import type { Sink, Source, Duplex } from 'it-stream-types'
2
+ import { pushable, Pushable } from 'it-pushable'
3
+
4
+ // ChannelStreamMessage is a message sent over the stream.
5
+ export interface ChannelStreamMessage<T> {
6
+ // from indicates who sent the message.
7
+ from: string
8
+ // ack indicates a remote joined the stream.
9
+ ack?: boolean
10
+ // opened indicates the remote has opened the stream.
11
+ opened?: boolean
12
+ // closed indicates the stream is closed.
13
+ closed?: boolean
14
+ // error indicates the stream has an error.
15
+ error?: Error
16
+ // data is any message data.
17
+ data?: T
18
+ }
19
+
20
+ // Channel represents a channel we can open a stream over.
21
+ export type ChannelPort = MessagePort | { tx: BroadcastChannel; rx: BroadcastChannel }
22
+
23
+ // ChannelStream implements a Stream over a BroadcastChannel duplex or MessagePort.
24
+ //
25
+ // NOTE: there is no way to tell if a BroadcastChannel or MessagePort is closed.
26
+ // This implementation sends a "closed" message when close() is called.
27
+ // However: if the remote is removed w/o closing cleanly, the stream will be left open!
28
+ export class ChannelStream<T>
29
+ implements Duplex<AsyncGenerator<T>, Source<T>, Promise<void>>
30
+ {
31
+ // channel is the read/write channel.
32
+ public readonly channel: ChannelPort
33
+ // sink is the sink for incoming messages.
34
+ public sink: Sink<Source<T>, Promise<void>>
35
+ // source is the source for outgoing messages.
36
+ public source: AsyncGenerator<T>
37
+ // _source emits incoming data to the source.
38
+ private readonly _source: {
39
+ push: (val: T) => void
40
+ end: (err?: Error) => void
41
+ }
42
+ // localId is the local identifier
43
+ private readonly localId: string
44
+ // localOpen indicates the local side has opened the stream.
45
+ private localOpen: boolean
46
+ // remoteOpen indicates the remote side has opened the stream.
47
+ private remoteOpen: boolean
48
+ // waitRemoteOpen indicates the remote side has opened the stream.
49
+ public readonly waitRemoteOpen: Promise<void>
50
+ // _remoteOpen fulfills the waitRemoteOpen promise.
51
+ private _remoteOpen?: (err?: Error) => void
52
+ // remoteAck indicates the remote side has acked the stream.
53
+ private remoteAck: boolean
54
+ // waitRemoteAck indicates the remote side has opened the stream.
55
+ public readonly waitRemoteAck: Promise<void>
56
+ // _remoteAck fulfills the waitRemoteAck promise.
57
+ private _remoteAck?: (err?: Error) => void
58
+
59
+ // isAcked checks if the stream is acknowledged by the remote.
60
+ public get isAcked() {
61
+ return this.remoteAck || false
62
+ }
63
+
64
+ // isOpen checks if the stream is opened by the remote.
65
+ public get isOpen() {
66
+ return this.remoteOpen || false
67
+ }
68
+
69
+ // remoteOpen indicates if we know the remote has already opened the stream.
70
+ constructor(localId: string, channel: ChannelPort, remoteOpen: boolean) {
71
+ this.localId = localId
72
+ this.channel = channel
73
+ this.sink = this._createSink()
74
+
75
+ this.localOpen = false
76
+ this.remoteAck = remoteOpen
77
+ this.remoteOpen = remoteOpen
78
+ if (remoteOpen) {
79
+ this.waitRemoteOpen = Promise.resolve()
80
+ this.waitRemoteAck = Promise.resolve()
81
+ } else {
82
+ this.waitRemoteOpen = new Promise<void>((resolve, reject) => {
83
+ this._remoteOpen = (err?: Error) => {
84
+ if (err) {
85
+ reject(err)
86
+ } else {
87
+ resolve()
88
+ }
89
+ }
90
+ })
91
+ this.waitRemoteOpen.catch(() => {})
92
+ this.waitRemoteAck = new Promise<void>((resolve, reject) => {
93
+ this._remoteAck = (err?: Error) => {
94
+ if (err) {
95
+ reject(err)
96
+ } else {
97
+ resolve()
98
+ }
99
+ }
100
+ })
101
+ this.waitRemoteAck.catch(() => {})
102
+ }
103
+
104
+ const source: Pushable<T> = pushable({ objectMode: true })
105
+ this.source = source
106
+ this._source = source
107
+
108
+ const onMessage = this.onMessage.bind(this)
109
+ if (channel instanceof MessagePort) {
110
+ // MessagePort
111
+ channel.onmessage = onMessage
112
+ channel.start()
113
+ } else {
114
+ // BroadcastChannel
115
+ channel.rx.onmessage = onMessage
116
+ }
117
+ this.postMessage({ ack: true })
118
+ }
119
+
120
+ // postMessage writes a message to the stream.
121
+ private postMessage(msg: Partial<ChannelStreamMessage<T>>) {
122
+ msg.from = this.localId
123
+ if (this.channel instanceof MessagePort) {
124
+ this.channel.postMessage(msg)
125
+ } else {
126
+ this.channel.tx.postMessage(msg)
127
+ }
128
+ }
129
+
130
+ // close closes the broadcast channels.
131
+ public close(error?: Error) {
132
+ // write a message to indicate the stream is now closed.
133
+ this.postMessage({ closed: true, error })
134
+ // close channels
135
+ if (this.channel instanceof MessagePort) {
136
+ this.channel.close()
137
+ } else {
138
+ this.channel.tx.close()
139
+ this.channel.rx.close()
140
+ }
141
+ if (!this.remoteOpen && this._remoteOpen) {
142
+ this._remoteOpen(error || new Error('closed'))
143
+ }
144
+ if (!this.remoteAck && this._remoteAck) {
145
+ this._remoteAck(error || new Error('closed'))
146
+ }
147
+ }
148
+
149
+ // onLocalOpened indicates the local side has opened the read stream.
150
+ private onLocalOpened() {
151
+ if (!this.localOpen) {
152
+ this.localOpen = true
153
+ this.postMessage({ opened: true })
154
+ }
155
+ }
156
+
157
+ // onRemoteAcked indicates the remote side has acked the stream.
158
+ private onRemoteAcked() {
159
+ if (!this.remoteAck) {
160
+ this.remoteAck = true
161
+ if (this._remoteAck) {
162
+ this._remoteAck()
163
+ }
164
+ }
165
+ }
166
+
167
+ // onRemoteOpened indicates the remote side has opened the read stream.
168
+ private onRemoteOpened() {
169
+ if (!this.remoteOpen) {
170
+ this.remoteOpen = true
171
+ if (this._remoteOpen) {
172
+ this._remoteOpen()
173
+ }
174
+ }
175
+ }
176
+
177
+ private _createSink(): Sink<Source<T>, Promise<void>> {
178
+ return async (source: Source<T>) => {
179
+ // make sure the remote is open before we send any data.
180
+ await this.waitRemoteAck
181
+ this.onLocalOpened()
182
+ await this.waitRemoteOpen
183
+
184
+ try {
185
+ for await (const msg of source) {
186
+ this.postMessage({ data: msg })
187
+ }
188
+ this.postMessage({ closed: true })
189
+ } catch (error) {
190
+ this.postMessage({ closed: true, error: error as Error })
191
+ }
192
+ }
193
+ }
194
+
195
+ private onMessage(ev: MessageEvent<ChannelStreamMessage<T>>) {
196
+ const msg = ev.data
197
+ if (!msg || msg.from === this.localId || !msg.from) {
198
+ return
199
+ }
200
+ if (msg.ack || msg.opened) {
201
+ this.onRemoteAcked()
202
+ }
203
+ if (msg.opened) {
204
+ this.onRemoteOpened()
205
+ }
206
+ const { data, closed, error: err } = msg
207
+ if (data) {
208
+ this._source.push(data)
209
+ }
210
+ if (err) {
211
+ this._source.end(err)
212
+ } else if (closed) {
213
+ this._source.end()
214
+ }
215
+ }
216
+ }
217
+
218
+ // newBroadcastChannelStream constructs a ChannelStream with a channel name.
219
+ export function newBroadcastChannelStream<T>(
220
+ id: string,
221
+ readName: string,
222
+ writeName: string,
223
+ remoteOpen: boolean,
224
+ ): ChannelStream<T> {
225
+ return new ChannelStream<T>(
226
+ id,
227
+ { tx: new BroadcastChannel(writeName), rx: new BroadcastChannel(readName) },
228
+ remoteOpen,
229
+ )
230
+ }
package/srpc/index.ts CHANGED
@@ -19,6 +19,12 @@ export {
19
19
  export { MethodProto, createInvokeFn } from './invoker.js'
20
20
  export { Packet, CallStart, CallData } from './rpcproto.pb.js'
21
21
  export { Mux, StaticMux, LookupMethod, createMux } from './mux.js'
22
+ export {
23
+ ChannelStreamMessage,
24
+ ChannelPort,
25
+ ChannelStream,
26
+ newBroadcastChannelStream,
27
+ } from './channel.js'
22
28
  export {
23
29
  BroadcastChannelDuplex,
24
30
  BroadcastChannelConn,