@standardserver/peer 0.0.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/LICENCE +21 -0
- package/dist/index.d.mts +280 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.mjs +602 -0
- package/package.json +30 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Standard Server
|
|
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/dist/index.d.mts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { StandardRequest, StandardResponse } from '@standardserver/core';
|
|
2
|
+
import { AsyncIdQueueCloseOptions, AsyncCleanupFn, AsyncIteratorClass } from '@standardserver/shared';
|
|
3
|
+
import { EventStreamMessage } from '@standardserver/core/event-stream';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base interface for all peer messages.
|
|
7
|
+
*
|
|
8
|
+
* SHOULD only contain data that friendly with structure clone algorithm.
|
|
9
|
+
*/
|
|
10
|
+
interface PeerMessage {
|
|
11
|
+
/**
|
|
12
|
+
* Correlation ID for a single request/response lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* The same ID is shared by the initial request, its response,
|
|
15
|
+
* and any related stream or event messages.
|
|
16
|
+
*/
|
|
17
|
+
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* Discriminator that defines the message semantics and payload shape.
|
|
20
|
+
*/
|
|
21
|
+
kind: string;
|
|
22
|
+
/**
|
|
23
|
+
* Structured payload.
|
|
24
|
+
*
|
|
25
|
+
* Its shape is determined by `kind`.
|
|
26
|
+
*/
|
|
27
|
+
json?: unknown;
|
|
28
|
+
/**
|
|
29
|
+
* Binary payload.
|
|
30
|
+
*
|
|
31
|
+
* Only present for message kinds that support binary transfer.
|
|
32
|
+
*/
|
|
33
|
+
binary?: Uint8Array<ArrayBuffer> | Blob | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Starts a new request from client to server.
|
|
37
|
+
*
|
|
38
|
+
* This is always the first message in a request/response cycle.
|
|
39
|
+
*/
|
|
40
|
+
interface PeerRequestMessage extends PeerMessage {
|
|
41
|
+
/**
|
|
42
|
+
* The kind of the message.
|
|
43
|
+
*/
|
|
44
|
+
kind: 'request';
|
|
45
|
+
/**
|
|
46
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
47
|
+
*/
|
|
48
|
+
json: Omit<StandardRequest, 'signal'>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sends the final response from server to client.
|
|
52
|
+
*
|
|
53
|
+
* Typically sent once per `PeerRequestMessage`, unless followed by streaming messages.
|
|
54
|
+
*/
|
|
55
|
+
interface PeerResponseMessage extends PeerMessage {
|
|
56
|
+
/**
|
|
57
|
+
* The kind of the message.
|
|
58
|
+
*/
|
|
59
|
+
kind: 'response';
|
|
60
|
+
/**
|
|
61
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
62
|
+
*/
|
|
63
|
+
json: StandardResponse;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Indicates that a request/response/stream should be terminated.
|
|
67
|
+
*
|
|
68
|
+
* - **Client → Server**: Cancel an in-flight request or stop consuming a stream.
|
|
69
|
+
* - **Server → Client**: Signal an error or premature termination.
|
|
70
|
+
*/
|
|
71
|
+
interface PeerAbortMessage extends PeerMessage {
|
|
72
|
+
/**
|
|
73
|
+
* The kind of the message.
|
|
74
|
+
*/
|
|
75
|
+
kind: 'abort';
|
|
76
|
+
/**
|
|
77
|
+
* This message does not have a JSON payload.
|
|
78
|
+
*/
|
|
79
|
+
json?: undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Abort messages carry no binary payload.
|
|
82
|
+
*/
|
|
83
|
+
binary?: undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Transfers a single event from an event stream/iterator.
|
|
87
|
+
*
|
|
88
|
+
* Can flow in either direction (Client ↔ Server),
|
|
89
|
+
* depending on which side owns the event iterator.
|
|
90
|
+
*
|
|
91
|
+
* **Constraint**:
|
|
92
|
+
* Must be sent after:
|
|
93
|
+
* - `PeerRequestMessage` (client-to-server streaming), or
|
|
94
|
+
* - `PeerResponseMessage` (server-to-client streaming).
|
|
95
|
+
*/
|
|
96
|
+
interface PeerEventStreamMessage extends PeerMessage {
|
|
97
|
+
/**
|
|
98
|
+
* The kind of the message.
|
|
99
|
+
*/
|
|
100
|
+
kind: 'event-stream';
|
|
101
|
+
/**
|
|
102
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
103
|
+
*/
|
|
104
|
+
json: Omit<EventStreamMessage, 'data'> & {
|
|
105
|
+
/**
|
|
106
|
+
* The event data.
|
|
107
|
+
*/
|
|
108
|
+
data?: unknown;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Event-stream messages never carry binary payloads.
|
|
112
|
+
*/
|
|
113
|
+
binary?: undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Transfers a binary chunk for an octet-stream.
|
|
117
|
+
*
|
|
118
|
+
* Can flow in either direction (Client ↔ Server).
|
|
119
|
+
*
|
|
120
|
+
* **Constraint**:
|
|
121
|
+
* Must be sent after:
|
|
122
|
+
* - `PeerRequestMessage` (client-to-server streaming), or
|
|
123
|
+
* - `PeerResponseMessage` (server-to-client streaming).
|
|
124
|
+
*/
|
|
125
|
+
interface PeerOctetStreamMessage extends PeerMessage {
|
|
126
|
+
/**
|
|
127
|
+
* The kind of the message.
|
|
128
|
+
*/
|
|
129
|
+
kind: 'octet-stream';
|
|
130
|
+
/**
|
|
131
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
132
|
+
*/
|
|
133
|
+
json: {
|
|
134
|
+
/**
|
|
135
|
+
* Marks the final chunk of the stream.
|
|
136
|
+
*
|
|
137
|
+
* @default false
|
|
138
|
+
*/
|
|
139
|
+
close: boolean;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Binary payload.
|
|
143
|
+
*
|
|
144
|
+
* SHOULD always be present even if `json.close` is `true`.
|
|
145
|
+
*/
|
|
146
|
+
binary?: Uint8Array<ArrayBuffer> | Blob | undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Indicates that an octet-stream/event-stream should be cancelled.
|
|
150
|
+
*
|
|
151
|
+
* - **Server → Client**: Server no longer needs more octet-stream/event-stream messages.
|
|
152
|
+
*/
|
|
153
|
+
interface PeerStreamCancelMessage extends PeerMessage {
|
|
154
|
+
/**
|
|
155
|
+
* The kind of the message.
|
|
156
|
+
*/
|
|
157
|
+
kind: 'stream/cancel';
|
|
158
|
+
/**
|
|
159
|
+
* This message does not have a JSON payload.
|
|
160
|
+
*/
|
|
161
|
+
json?: undefined;
|
|
162
|
+
/**
|
|
163
|
+
* This message does not have a binary payload.
|
|
164
|
+
*/
|
|
165
|
+
binary?: undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface ClientPeerCloseOptions extends AsyncIdQueueCloseOptions {
|
|
169
|
+
}
|
|
170
|
+
declare class ClientPeer {
|
|
171
|
+
private readonly send;
|
|
172
|
+
private readonly idGenerator;
|
|
173
|
+
/**
|
|
174
|
+
* Messages waiting to be processed
|
|
175
|
+
*/
|
|
176
|
+
private readonly responseMessageQueue;
|
|
177
|
+
private readonly eventStreamMessageQueue;
|
|
178
|
+
private readonly octetStreamMessageQueue;
|
|
179
|
+
/**
|
|
180
|
+
* Transmitters for event streams and octet streams
|
|
181
|
+
* Should be cancelled when needed
|
|
182
|
+
*/
|
|
183
|
+
private readonly requestEventStreamTransmitters;
|
|
184
|
+
private readonly requestOctetStreamTransmitters;
|
|
185
|
+
/**
|
|
186
|
+
* Cleanup functions invoked when the request/response is completed
|
|
187
|
+
*/
|
|
188
|
+
private readonly cleanupFns;
|
|
189
|
+
constructor(send: (message: PeerAbortMessage | PeerRequestMessage | PeerEventStreamMessage | PeerOctetStreamMessage) => Promise<void>);
|
|
190
|
+
/**
|
|
191
|
+
* Use to measure resources usage
|
|
192
|
+
*/
|
|
193
|
+
get size(): number;
|
|
194
|
+
/**
|
|
195
|
+
* Send a request to the server peer
|
|
196
|
+
*/
|
|
197
|
+
request(request: StandardRequest): Promise<StandardResponse>;
|
|
198
|
+
/**
|
|
199
|
+
* Handle a message from server
|
|
200
|
+
*/
|
|
201
|
+
message(message: PeerResponseMessage | PeerAbortMessage | PeerEventStreamMessage | PeerOctetStreamMessage | PeerStreamCancelMessage): Promise<void>;
|
|
202
|
+
close(options?: AsyncIdQueueCloseOptions): Promise<void>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Encodes a PeerMessage into a wire-safe representation.
|
|
207
|
+
*
|
|
208
|
+
* - If no binary data is present, the message is encoded as a JSON string.
|
|
209
|
+
* - If binary data exists, the output is:
|
|
210
|
+
* [ UTF-8 JSON bytes | separator byte | raw binary bytes ]
|
|
211
|
+
*/
|
|
212
|
+
declare function encodePeerMessage(message: PeerMessage): Promise<string | Uint8Array<ArrayBuffer>>;
|
|
213
|
+
/**
|
|
214
|
+
* Decodes a wire-encoded PeerMessage.
|
|
215
|
+
*
|
|
216
|
+
* - String input is treated as pure JSON.
|
|
217
|
+
* - Binary input may contain only JSON bytes, or JSON followed by binary data
|
|
218
|
+
* separated by the separator byte.
|
|
219
|
+
*/
|
|
220
|
+
declare function decodePeerMessage(data: string | Uint8Array<ArrayBuffer>): PeerMessage;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Creates an AsyncIterator from a queue of peer event-stream messages.
|
|
224
|
+
* The iterator yields normal events, throws error events, and completes on done.
|
|
225
|
+
*/
|
|
226
|
+
declare function toEventIterator(pull: () => Promise<PeerEventStreamMessage>, cleanup: AsyncCleanupFn): AsyncIteratorClass<unknown>;
|
|
227
|
+
/**
|
|
228
|
+
* Transmits events to a peer event-stream.
|
|
229
|
+
*/
|
|
230
|
+
declare class EventStreamTransmitter {
|
|
231
|
+
private readonly iterator;
|
|
232
|
+
private readonly messageId;
|
|
233
|
+
private readonly send;
|
|
234
|
+
private isCompleted;
|
|
235
|
+
constructor(iterator: AsyncIterator<unknown>, messageId: string, send: (message: PeerEventStreamMessage) => Promise<void>);
|
|
236
|
+
cancel(): Promise<void>;
|
|
237
|
+
transmit(): Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface HibernationEventIteratorCallback {
|
|
241
|
+
(id: string): void;
|
|
242
|
+
}
|
|
243
|
+
declare class HibernationEventIterator<T, TReturn = unknown, TNext = unknown> extends AsyncIteratorClass<T, TReturn, TNext> {
|
|
244
|
+
/**
|
|
245
|
+
* In the client library, server results are typically represented by an `AsyncIteratorClass`.
|
|
246
|
+
* Since `AsyncIteratorClass` does not include a `hibernationCallback` property, this property should be optional.
|
|
247
|
+
*/
|
|
248
|
+
readonly hibernationCallback?: HibernationEventIteratorCallback;
|
|
249
|
+
constructor(hibernationCallback: HibernationEventIteratorCallback);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions {
|
|
253
|
+
}
|
|
254
|
+
declare class ServerPeer {
|
|
255
|
+
private readonly send;
|
|
256
|
+
/**
|
|
257
|
+
* Messages waiting to be processed
|
|
258
|
+
*/
|
|
259
|
+
private readonly eventStreamMessageQueue;
|
|
260
|
+
private readonly octetStreamMessageQueue;
|
|
261
|
+
private readonly eventStreamTransmitters;
|
|
262
|
+
private readonly octetStreamTransmitters;
|
|
263
|
+
/**
|
|
264
|
+
* Map of abort controllers for each request
|
|
265
|
+
*/
|
|
266
|
+
private readonly controller;
|
|
267
|
+
constructor(send: (message: PeerResponseMessage | PeerAbortMessage | PeerOctetStreamMessage | PeerEventStreamMessage | PeerStreamCancelMessage) => Promise<void>);
|
|
268
|
+
/**
|
|
269
|
+
* Use for measure resources usage
|
|
270
|
+
*/
|
|
271
|
+
get size(): number;
|
|
272
|
+
/**
|
|
273
|
+
* Handle a message from client
|
|
274
|
+
*/
|
|
275
|
+
message(message: PeerRequestMessage | PeerEventStreamMessage | PeerOctetStreamMessage | PeerAbortMessage, handleRequest: (request: StandardRequest) => Promise<StandardResponse>): Promise<void>;
|
|
276
|
+
close(options?: ServerPeerCloseOptions): Promise<void>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { ClientPeer, EventStreamTransmitter, HibernationEventIterator, ServerPeer, decodePeerMessage, encodePeerMessage, toEventIterator };
|
|
280
|
+
export type { ClientPeerCloseOptions, HibernationEventIteratorCallback, PeerAbortMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerCloseOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { StandardRequest, StandardResponse } from '@standardserver/core';
|
|
2
|
+
import { AsyncIdQueueCloseOptions, AsyncCleanupFn, AsyncIteratorClass } from '@standardserver/shared';
|
|
3
|
+
import { EventStreamMessage } from '@standardserver/core/event-stream';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base interface for all peer messages.
|
|
7
|
+
*
|
|
8
|
+
* SHOULD only contain data that friendly with structure clone algorithm.
|
|
9
|
+
*/
|
|
10
|
+
interface PeerMessage {
|
|
11
|
+
/**
|
|
12
|
+
* Correlation ID for a single request/response lifecycle.
|
|
13
|
+
*
|
|
14
|
+
* The same ID is shared by the initial request, its response,
|
|
15
|
+
* and any related stream or event messages.
|
|
16
|
+
*/
|
|
17
|
+
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* Discriminator that defines the message semantics and payload shape.
|
|
20
|
+
*/
|
|
21
|
+
kind: string;
|
|
22
|
+
/**
|
|
23
|
+
* Structured payload.
|
|
24
|
+
*
|
|
25
|
+
* Its shape is determined by `kind`.
|
|
26
|
+
*/
|
|
27
|
+
json?: unknown;
|
|
28
|
+
/**
|
|
29
|
+
* Binary payload.
|
|
30
|
+
*
|
|
31
|
+
* Only present for message kinds that support binary transfer.
|
|
32
|
+
*/
|
|
33
|
+
binary?: Uint8Array<ArrayBuffer> | Blob | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Starts a new request from client to server.
|
|
37
|
+
*
|
|
38
|
+
* This is always the first message in a request/response cycle.
|
|
39
|
+
*/
|
|
40
|
+
interface PeerRequestMessage extends PeerMessage {
|
|
41
|
+
/**
|
|
42
|
+
* The kind of the message.
|
|
43
|
+
*/
|
|
44
|
+
kind: 'request';
|
|
45
|
+
/**
|
|
46
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
47
|
+
*/
|
|
48
|
+
json: Omit<StandardRequest, 'signal'>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sends the final response from server to client.
|
|
52
|
+
*
|
|
53
|
+
* Typically sent once per `PeerRequestMessage`, unless followed by streaming messages.
|
|
54
|
+
*/
|
|
55
|
+
interface PeerResponseMessage extends PeerMessage {
|
|
56
|
+
/**
|
|
57
|
+
* The kind of the message.
|
|
58
|
+
*/
|
|
59
|
+
kind: 'response';
|
|
60
|
+
/**
|
|
61
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
62
|
+
*/
|
|
63
|
+
json: StandardResponse;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Indicates that a request/response/stream should be terminated.
|
|
67
|
+
*
|
|
68
|
+
* - **Client → Server**: Cancel an in-flight request or stop consuming a stream.
|
|
69
|
+
* - **Server → Client**: Signal an error or premature termination.
|
|
70
|
+
*/
|
|
71
|
+
interface PeerAbortMessage extends PeerMessage {
|
|
72
|
+
/**
|
|
73
|
+
* The kind of the message.
|
|
74
|
+
*/
|
|
75
|
+
kind: 'abort';
|
|
76
|
+
/**
|
|
77
|
+
* This message does not have a JSON payload.
|
|
78
|
+
*/
|
|
79
|
+
json?: undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Abort messages carry no binary payload.
|
|
82
|
+
*/
|
|
83
|
+
binary?: undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Transfers a single event from an event stream/iterator.
|
|
87
|
+
*
|
|
88
|
+
* Can flow in either direction (Client ↔ Server),
|
|
89
|
+
* depending on which side owns the event iterator.
|
|
90
|
+
*
|
|
91
|
+
* **Constraint**:
|
|
92
|
+
* Must be sent after:
|
|
93
|
+
* - `PeerRequestMessage` (client-to-server streaming), or
|
|
94
|
+
* - `PeerResponseMessage` (server-to-client streaming).
|
|
95
|
+
*/
|
|
96
|
+
interface PeerEventStreamMessage extends PeerMessage {
|
|
97
|
+
/**
|
|
98
|
+
* The kind of the message.
|
|
99
|
+
*/
|
|
100
|
+
kind: 'event-stream';
|
|
101
|
+
/**
|
|
102
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
103
|
+
*/
|
|
104
|
+
json: Omit<EventStreamMessage, 'data'> & {
|
|
105
|
+
/**
|
|
106
|
+
* The event data.
|
|
107
|
+
*/
|
|
108
|
+
data?: unknown;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Event-stream messages never carry binary payloads.
|
|
112
|
+
*/
|
|
113
|
+
binary?: undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Transfers a binary chunk for an octet-stream.
|
|
117
|
+
*
|
|
118
|
+
* Can flow in either direction (Client ↔ Server).
|
|
119
|
+
*
|
|
120
|
+
* **Constraint**:
|
|
121
|
+
* Must be sent after:
|
|
122
|
+
* - `PeerRequestMessage` (client-to-server streaming), or
|
|
123
|
+
* - `PeerResponseMessage` (server-to-client streaming).
|
|
124
|
+
*/
|
|
125
|
+
interface PeerOctetStreamMessage extends PeerMessage {
|
|
126
|
+
/**
|
|
127
|
+
* The kind of the message.
|
|
128
|
+
*/
|
|
129
|
+
kind: 'octet-stream';
|
|
130
|
+
/**
|
|
131
|
+
* The actual content of the message. The structure depends on the `kind`.
|
|
132
|
+
*/
|
|
133
|
+
json: {
|
|
134
|
+
/**
|
|
135
|
+
* Marks the final chunk of the stream.
|
|
136
|
+
*
|
|
137
|
+
* @default false
|
|
138
|
+
*/
|
|
139
|
+
close: boolean;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Binary payload.
|
|
143
|
+
*
|
|
144
|
+
* SHOULD always be present even if `json.close` is `true`.
|
|
145
|
+
*/
|
|
146
|
+
binary?: Uint8Array<ArrayBuffer> | Blob | undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Indicates that an octet-stream/event-stream should be cancelled.
|
|
150
|
+
*
|
|
151
|
+
* - **Server → Client**: Server no longer needs more octet-stream/event-stream messages.
|
|
152
|
+
*/
|
|
153
|
+
interface PeerStreamCancelMessage extends PeerMessage {
|
|
154
|
+
/**
|
|
155
|
+
* The kind of the message.
|
|
156
|
+
*/
|
|
157
|
+
kind: 'stream/cancel';
|
|
158
|
+
/**
|
|
159
|
+
* This message does not have a JSON payload.
|
|
160
|
+
*/
|
|
161
|
+
json?: undefined;
|
|
162
|
+
/**
|
|
163
|
+
* This message does not have a binary payload.
|
|
164
|
+
*/
|
|
165
|
+
binary?: undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface ClientPeerCloseOptions extends AsyncIdQueueCloseOptions {
|
|
169
|
+
}
|
|
170
|
+
declare class ClientPeer {
|
|
171
|
+
private readonly send;
|
|
172
|
+
private readonly idGenerator;
|
|
173
|
+
/**
|
|
174
|
+
* Messages waiting to be processed
|
|
175
|
+
*/
|
|
176
|
+
private readonly responseMessageQueue;
|
|
177
|
+
private readonly eventStreamMessageQueue;
|
|
178
|
+
private readonly octetStreamMessageQueue;
|
|
179
|
+
/**
|
|
180
|
+
* Transmitters for event streams and octet streams
|
|
181
|
+
* Should be cancelled when needed
|
|
182
|
+
*/
|
|
183
|
+
private readonly requestEventStreamTransmitters;
|
|
184
|
+
private readonly requestOctetStreamTransmitters;
|
|
185
|
+
/**
|
|
186
|
+
* Cleanup functions invoked when the request/response is completed
|
|
187
|
+
*/
|
|
188
|
+
private readonly cleanupFns;
|
|
189
|
+
constructor(send: (message: PeerAbortMessage | PeerRequestMessage | PeerEventStreamMessage | PeerOctetStreamMessage) => Promise<void>);
|
|
190
|
+
/**
|
|
191
|
+
* Use to measure resources usage
|
|
192
|
+
*/
|
|
193
|
+
get size(): number;
|
|
194
|
+
/**
|
|
195
|
+
* Send a request to the server peer
|
|
196
|
+
*/
|
|
197
|
+
request(request: StandardRequest): Promise<StandardResponse>;
|
|
198
|
+
/**
|
|
199
|
+
* Handle a message from server
|
|
200
|
+
*/
|
|
201
|
+
message(message: PeerResponseMessage | PeerAbortMessage | PeerEventStreamMessage | PeerOctetStreamMessage | PeerStreamCancelMessage): Promise<void>;
|
|
202
|
+
close(options?: AsyncIdQueueCloseOptions): Promise<void>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Encodes a PeerMessage into a wire-safe representation.
|
|
207
|
+
*
|
|
208
|
+
* - If no binary data is present, the message is encoded as a JSON string.
|
|
209
|
+
* - If binary data exists, the output is:
|
|
210
|
+
* [ UTF-8 JSON bytes | separator byte | raw binary bytes ]
|
|
211
|
+
*/
|
|
212
|
+
declare function encodePeerMessage(message: PeerMessage): Promise<string | Uint8Array<ArrayBuffer>>;
|
|
213
|
+
/**
|
|
214
|
+
* Decodes a wire-encoded PeerMessage.
|
|
215
|
+
*
|
|
216
|
+
* - String input is treated as pure JSON.
|
|
217
|
+
* - Binary input may contain only JSON bytes, or JSON followed by binary data
|
|
218
|
+
* separated by the separator byte.
|
|
219
|
+
*/
|
|
220
|
+
declare function decodePeerMessage(data: string | Uint8Array<ArrayBuffer>): PeerMessage;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Creates an AsyncIterator from a queue of peer event-stream messages.
|
|
224
|
+
* The iterator yields normal events, throws error events, and completes on done.
|
|
225
|
+
*/
|
|
226
|
+
declare function toEventIterator(pull: () => Promise<PeerEventStreamMessage>, cleanup: AsyncCleanupFn): AsyncIteratorClass<unknown>;
|
|
227
|
+
/**
|
|
228
|
+
* Transmits events to a peer event-stream.
|
|
229
|
+
*/
|
|
230
|
+
declare class EventStreamTransmitter {
|
|
231
|
+
private readonly iterator;
|
|
232
|
+
private readonly messageId;
|
|
233
|
+
private readonly send;
|
|
234
|
+
private isCompleted;
|
|
235
|
+
constructor(iterator: AsyncIterator<unknown>, messageId: string, send: (message: PeerEventStreamMessage) => Promise<void>);
|
|
236
|
+
cancel(): Promise<void>;
|
|
237
|
+
transmit(): Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface HibernationEventIteratorCallback {
|
|
241
|
+
(id: string): void;
|
|
242
|
+
}
|
|
243
|
+
declare class HibernationEventIterator<T, TReturn = unknown, TNext = unknown> extends AsyncIteratorClass<T, TReturn, TNext> {
|
|
244
|
+
/**
|
|
245
|
+
* In the client library, server results are typically represented by an `AsyncIteratorClass`.
|
|
246
|
+
* Since `AsyncIteratorClass` does not include a `hibernationCallback` property, this property should be optional.
|
|
247
|
+
*/
|
|
248
|
+
readonly hibernationCallback?: HibernationEventIteratorCallback;
|
|
249
|
+
constructor(hibernationCallback: HibernationEventIteratorCallback);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface ServerPeerCloseOptions extends AsyncIdQueueCloseOptions {
|
|
253
|
+
}
|
|
254
|
+
declare class ServerPeer {
|
|
255
|
+
private readonly send;
|
|
256
|
+
/**
|
|
257
|
+
* Messages waiting to be processed
|
|
258
|
+
*/
|
|
259
|
+
private readonly eventStreamMessageQueue;
|
|
260
|
+
private readonly octetStreamMessageQueue;
|
|
261
|
+
private readonly eventStreamTransmitters;
|
|
262
|
+
private readonly octetStreamTransmitters;
|
|
263
|
+
/**
|
|
264
|
+
* Map of abort controllers for each request
|
|
265
|
+
*/
|
|
266
|
+
private readonly controller;
|
|
267
|
+
constructor(send: (message: PeerResponseMessage | PeerAbortMessage | PeerOctetStreamMessage | PeerEventStreamMessage | PeerStreamCancelMessage) => Promise<void>);
|
|
268
|
+
/**
|
|
269
|
+
* Use for measure resources usage
|
|
270
|
+
*/
|
|
271
|
+
get size(): number;
|
|
272
|
+
/**
|
|
273
|
+
* Handle a message from client
|
|
274
|
+
*/
|
|
275
|
+
message(message: PeerRequestMessage | PeerEventStreamMessage | PeerOctetStreamMessage | PeerAbortMessage, handleRequest: (request: StandardRequest) => Promise<StandardResponse>): Promise<void>;
|
|
276
|
+
close(options?: ServerPeerCloseOptions): Promise<void>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export { ClientPeer, EventStreamTransmitter, HibernationEventIterator, ServerPeer, decodePeerMessage, encodePeerMessage, toEventIterator };
|
|
280
|
+
export type { ClientPeerCloseOptions, HibernationEventIteratorCallback, PeerAbortMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerCloseOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { flattenStandardHeader, getFilenameFromContentDisposition, generateContentDisposition } from '@standardserver/core';
|
|
2
|
+
import { AsyncIteratorClass, isTypescriptObject, SequentialIdGenerator, AsyncIdQueue, isAsyncIteratorObject, AbortError, stringifyJSON } from '@standardserver/shared';
|
|
3
|
+
import { withEventIteratorEventMeta, EventIteratorErrorEvent, resolveEventIteratorEvent } from '@standardserver/core/event-stream';
|
|
4
|
+
|
|
5
|
+
function toEventIterator(pull, cleanup) {
|
|
6
|
+
return new AsyncIteratorClass(async () => {
|
|
7
|
+
while (true) {
|
|
8
|
+
const { json } = await pull();
|
|
9
|
+
switch (json.event) {
|
|
10
|
+
case "message": {
|
|
11
|
+
let data = json.data;
|
|
12
|
+
if (isTypescriptObject(data)) {
|
|
13
|
+
data = withEventIteratorEventMeta(data, json);
|
|
14
|
+
}
|
|
15
|
+
return { value: data, done: false };
|
|
16
|
+
}
|
|
17
|
+
case "error": {
|
|
18
|
+
throw withEventIteratorEventMeta(
|
|
19
|
+
new EventIteratorErrorEvent(json.data),
|
|
20
|
+
json
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
case "close": {
|
|
24
|
+
let data = json.data;
|
|
25
|
+
if (isTypescriptObject(data)) {
|
|
26
|
+
data = withEventIteratorEventMeta(data, json);
|
|
27
|
+
}
|
|
28
|
+
return { value: data, done: true };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, cleanup);
|
|
33
|
+
}
|
|
34
|
+
class EventStreamTransmitter {
|
|
35
|
+
constructor(iterator, messageId, send) {
|
|
36
|
+
this.iterator = iterator;
|
|
37
|
+
this.messageId = messageId;
|
|
38
|
+
this.send = send;
|
|
39
|
+
}
|
|
40
|
+
isCompleted = false;
|
|
41
|
+
async cancel() {
|
|
42
|
+
if (!this.isCompleted) {
|
|
43
|
+
this.isCompleted = true;
|
|
44
|
+
await this.iterator.return?.();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async transmit() {
|
|
48
|
+
while (true) {
|
|
49
|
+
let json;
|
|
50
|
+
try {
|
|
51
|
+
const item = await this.iterator.next();
|
|
52
|
+
if (this.isCompleted) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (item.done) {
|
|
56
|
+
this.isCompleted = true;
|
|
57
|
+
}
|
|
58
|
+
const [data, meta] = resolveEventIteratorEvent(item.value);
|
|
59
|
+
json = { ...meta, event: item.done ? "close" : "message", data };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof EventIteratorErrorEvent) {
|
|
62
|
+
if (this.isCompleted) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.isCompleted = true;
|
|
66
|
+
const [resolvedError, meta] = resolveEventIteratorEvent(err);
|
|
67
|
+
json = { ...meta, event: "error", data: resolvedError.data };
|
|
68
|
+
} else {
|
|
69
|
+
this.isCompleted = true;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await this.send({
|
|
74
|
+
json,
|
|
75
|
+
kind: "event-stream",
|
|
76
|
+
id: this.messageId
|
|
77
|
+
});
|
|
78
|
+
if (this.isCompleted) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toOctetStream(pull, cleanup) {
|
|
86
|
+
return new ReadableStream({
|
|
87
|
+
async pull(controller) {
|
|
88
|
+
try {
|
|
89
|
+
const { json, binary } = await pull();
|
|
90
|
+
if (binary) {
|
|
91
|
+
controller.enqueue(binary instanceof Uint8Array ? binary : new Uint8Array(await binary.arrayBuffer()));
|
|
92
|
+
}
|
|
93
|
+
if (json.close) {
|
|
94
|
+
await cleanup(true);
|
|
95
|
+
controller.close();
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
await cleanup(true);
|
|
99
|
+
controller.error(err);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
async cancel() {
|
|
103
|
+
await cleanup(false);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
class OctetStreamTransmitter {
|
|
108
|
+
constructor(stream, messageId, send) {
|
|
109
|
+
this.messageId = messageId;
|
|
110
|
+
this.send = send;
|
|
111
|
+
this.reader = stream.getReader();
|
|
112
|
+
}
|
|
113
|
+
isCompleted = false;
|
|
114
|
+
reader;
|
|
115
|
+
async cancel() {
|
|
116
|
+
if (!this.isCompleted) {
|
|
117
|
+
this.isCompleted = true;
|
|
118
|
+
await this.reader.cancel();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async transmit() {
|
|
122
|
+
while (true) {
|
|
123
|
+
const { done, value } = await this.reader.read();
|
|
124
|
+
if (!this.isCompleted) {
|
|
125
|
+
try {
|
|
126
|
+
await this.send({
|
|
127
|
+
json: { close: done },
|
|
128
|
+
binary: value,
|
|
129
|
+
kind: "octet-stream",
|
|
130
|
+
id: this.messageId
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
await this.cancel();
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (done) {
|
|
138
|
+
this.isCompleted = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function toStandardBody(message, eventStreamMessageQueue, octetStreamMessageQueue, cleanup) {
|
|
146
|
+
const bodyHint = flattenStandardHeader(message.json.headers["standard-server"]);
|
|
147
|
+
if (bodyHint === "event-stream") {
|
|
148
|
+
return toEventIterator(
|
|
149
|
+
() => eventStreamMessageQueue.pull(message.id),
|
|
150
|
+
cleanup
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (bodyHint === "octet-stream") {
|
|
154
|
+
return toOctetStream(
|
|
155
|
+
() => octetStreamMessageQueue.pull(message.id),
|
|
156
|
+
cleanup
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
if (bodyHint === "file") {
|
|
161
|
+
const contentDisposition = flattenStandardHeader(message.json.headers["content-disposition"]);
|
|
162
|
+
const filename = contentDisposition !== void 0 ? getFilenameFromContentDisposition(contentDisposition) : "undefined";
|
|
163
|
+
return new File(message.binary ? [message.binary] : [], filename ?? "blob", {
|
|
164
|
+
type: flattenStandardHeader(message.json.headers["content-type"]) ?? "application/octet-stream"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (bodyHint === "form-data") {
|
|
168
|
+
const res = new Response(message.binary, {
|
|
169
|
+
headers: {
|
|
170
|
+
"content-type": flattenStandardHeader(message.json.headers["content-type"]) ?? "multipart/form-data"
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const fromData = await res.formData();
|
|
174
|
+
return fromData;
|
|
175
|
+
}
|
|
176
|
+
if (bodyHint === "url-search-params" && typeof message.json.body === "string") {
|
|
177
|
+
return new URLSearchParams(message.json.body);
|
|
178
|
+
}
|
|
179
|
+
return message.json.body;
|
|
180
|
+
} finally {
|
|
181
|
+
await cleanup(true);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class ClientPeer {
|
|
186
|
+
constructor(send) {
|
|
187
|
+
this.send = send;
|
|
188
|
+
}
|
|
189
|
+
idGenerator = new SequentialIdGenerator();
|
|
190
|
+
/**
|
|
191
|
+
* Messages waiting to be processed
|
|
192
|
+
*/
|
|
193
|
+
responseMessageQueue = new AsyncIdQueue();
|
|
194
|
+
eventStreamMessageQueue = new AsyncIdQueue();
|
|
195
|
+
octetStreamMessageQueue = new AsyncIdQueue();
|
|
196
|
+
/**
|
|
197
|
+
* Transmitters for event streams and octet streams
|
|
198
|
+
* Should be cancelled when needed
|
|
199
|
+
*/
|
|
200
|
+
requestEventStreamTransmitters = /* @__PURE__ */ new Map();
|
|
201
|
+
requestOctetStreamTransmitters = /* @__PURE__ */ new Map();
|
|
202
|
+
/**
|
|
203
|
+
* Cleanup functions invoked when the request/response is completed
|
|
204
|
+
*/
|
|
205
|
+
cleanupFns = /* @__PURE__ */ new Map();
|
|
206
|
+
/**
|
|
207
|
+
* Use to measure resources usage
|
|
208
|
+
*/
|
|
209
|
+
get size() {
|
|
210
|
+
return this.responseMessageQueue.length + this.eventStreamMessageQueue.length + this.octetStreamMessageQueue.length + this.requestEventStreamTransmitters.size + this.requestOctetStreamTransmitters.size + this.cleanupFns.size;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Send a request to the server peer
|
|
214
|
+
*/
|
|
215
|
+
async request(request) {
|
|
216
|
+
const signal = request.signal;
|
|
217
|
+
signal?.throwIfAborted();
|
|
218
|
+
const id = this.idGenerator.generate();
|
|
219
|
+
this.eventStreamMessageQueue.open(id);
|
|
220
|
+
this.octetStreamMessageQueue.open(id);
|
|
221
|
+
this.responseMessageQueue.open(id);
|
|
222
|
+
let abortListener;
|
|
223
|
+
signal?.addEventListener("abort", abortListener = async () => {
|
|
224
|
+
await Promise.all([
|
|
225
|
+
/**
|
|
226
|
+
* Let server know request was aborted
|
|
227
|
+
*
|
|
228
|
+
* We don't need to check if is there any abort message already sent
|
|
229
|
+
* since this listener is removed when the request is closed.
|
|
230
|
+
*/
|
|
231
|
+
this.send({ id, kind: "abort" }),
|
|
232
|
+
this.close({ id, reason: signal.reason })
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
const cleanupFns = [
|
|
236
|
+
/**
|
|
237
|
+
* Make sure to remove the abort listener when the request/response is closed.
|
|
238
|
+
* Since a signal can be reused for multiple requests, if each request
|
|
239
|
+
* adds listeners without removing them, it can lead to excessive memory usage
|
|
240
|
+
* until the signal is garbage collected.
|
|
241
|
+
*/
|
|
242
|
+
() => {
|
|
243
|
+
signal?.removeEventListener("abort", abortListener);
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
this.cleanupFns.set(id, cleanupFns);
|
|
247
|
+
try {
|
|
248
|
+
const requestMessage = {
|
|
249
|
+
id,
|
|
250
|
+
kind: "request",
|
|
251
|
+
json: {
|
|
252
|
+
...{ ...request, signal: void 0 },
|
|
253
|
+
// clone and remove signal from request
|
|
254
|
+
headers: { ...request.headers }
|
|
255
|
+
// clone headers
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
if (request.body instanceof ReadableStream) {
|
|
259
|
+
requestMessage.json.body = void 0;
|
|
260
|
+
requestMessage.json.headers["standard-server"] = "octet-stream";
|
|
261
|
+
} else if (isAsyncIteratorObject(request.body)) {
|
|
262
|
+
requestMessage.json.body = void 0;
|
|
263
|
+
requestMessage.json.headers["standard-server"] = "event-stream";
|
|
264
|
+
} else if (request.body instanceof FormData) {
|
|
265
|
+
const res = new Response(request.body);
|
|
266
|
+
requestMessage.binary = await res.blob();
|
|
267
|
+
requestMessage.json.body = void 0;
|
|
268
|
+
requestMessage.json.headers["standard-server"] = "form-data";
|
|
269
|
+
requestMessage.json.headers["content-type"] = res.headers.get("content-type") ?? void 0;
|
|
270
|
+
} else if (request.body instanceof Blob) {
|
|
271
|
+
requestMessage.binary = request.body;
|
|
272
|
+
requestMessage.json.body = void 0;
|
|
273
|
+
requestMessage.json.headers["standard-server"] = "file";
|
|
274
|
+
requestMessage.json.headers["content-disposition"] = generateContentDisposition(request.body instanceof File ? request.body.name : "blob");
|
|
275
|
+
requestMessage.json.headers["content-type"] = request.body.type;
|
|
276
|
+
} else if (request.body instanceof URLSearchParams) {
|
|
277
|
+
requestMessage.json.body = request.body.toString();
|
|
278
|
+
requestMessage.json.headers["standard-server"] = "url-search-params";
|
|
279
|
+
}
|
|
280
|
+
signal?.throwIfAborted();
|
|
281
|
+
await this.send(requestMessage);
|
|
282
|
+
signal?.throwIfAborted();
|
|
283
|
+
if (isAsyncIteratorObject(request.body)) {
|
|
284
|
+
const transmitter = new EventStreamTransmitter(request.body, id, this.send);
|
|
285
|
+
this.requestEventStreamTransmitters.set(id, transmitter);
|
|
286
|
+
void transmitter.transmit().catch(async (reason) => {
|
|
287
|
+
await Promise.all([
|
|
288
|
+
/**
|
|
289
|
+
* We don't need to send abort message if transmitter was cancelled
|
|
290
|
+
* or request was aborted
|
|
291
|
+
*/
|
|
292
|
+
this.requestEventStreamTransmitters.has(id) ? this.send({ id, kind: "abort" }) : void 0,
|
|
293
|
+
this.close({ id, reason })
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
} else if (request.body instanceof ReadableStream) {
|
|
297
|
+
const transmitter = new OctetStreamTransmitter(request.body, id, this.send);
|
|
298
|
+
this.requestOctetStreamTransmitters.set(id, transmitter);
|
|
299
|
+
void transmitter.transmit().catch(async (reason) => {
|
|
300
|
+
await Promise.all([
|
|
301
|
+
/**
|
|
302
|
+
* We don't need to send abort message if transmitter was cancelled
|
|
303
|
+
* or request was aborted
|
|
304
|
+
*/
|
|
305
|
+
this.requestOctetStreamTransmitters.has(id) ? this.send({ id, kind: "abort" }) : void 0,
|
|
306
|
+
this.close({ id, reason })
|
|
307
|
+
]);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const peerResponseMessage = await this.responseMessageQueue.pull(id);
|
|
311
|
+
return {
|
|
312
|
+
...peerResponseMessage.json,
|
|
313
|
+
body: await toStandardBody(
|
|
314
|
+
peerResponseMessage,
|
|
315
|
+
this.eventStreamMessageQueue,
|
|
316
|
+
this.octetStreamMessageQueue,
|
|
317
|
+
async (isCompleted) => {
|
|
318
|
+
await Promise.all([
|
|
319
|
+
/**
|
|
320
|
+
* We don't need to send abort message if completed
|
|
321
|
+
* or request was aborted
|
|
322
|
+
*/
|
|
323
|
+
!isCompleted && (this.eventStreamMessageQueue.isOpen(id) || this.octetStreamMessageQueue.isOpen(id)) ? this.send({ id, kind: "abort" }) : void 0,
|
|
324
|
+
this.close({ id })
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
};
|
|
329
|
+
} catch (reason) {
|
|
330
|
+
await this.close({ id, reason });
|
|
331
|
+
throw reason;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Handle a message from server
|
|
336
|
+
*/
|
|
337
|
+
async message(message) {
|
|
338
|
+
if (message.kind === "stream/cancel") {
|
|
339
|
+
const promise = Promise.all([
|
|
340
|
+
this.requestEventStreamTransmitters.get(message.id)?.cancel(),
|
|
341
|
+
this.requestOctetStreamTransmitters.get(message.id)?.cancel()
|
|
342
|
+
]);
|
|
343
|
+
this.requestEventStreamTransmitters.delete(message.id);
|
|
344
|
+
this.requestOctetStreamTransmitters.delete(message.id);
|
|
345
|
+
await promise;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (message.kind === "abort") {
|
|
349
|
+
await this.close({ id: message.id, reason: new AbortError("Server peer aborted the request") });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (message.kind === "event-stream") {
|
|
353
|
+
if (this.eventStreamMessageQueue.isOpen(message.id)) {
|
|
354
|
+
this.eventStreamMessageQueue.push(message.id, message);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (message.kind === "octet-stream") {
|
|
359
|
+
if (this.octetStreamMessageQueue.isOpen(message.id)) {
|
|
360
|
+
this.octetStreamMessageQueue.push(message.id, message);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (this.responseMessageQueue.isOpen(message.id)) {
|
|
365
|
+
this.responseMessageQueue.push(message.id, message);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async close(options = {}) {
|
|
369
|
+
const promises = [];
|
|
370
|
+
this.responseMessageQueue.close(options);
|
|
371
|
+
this.eventStreamMessageQueue.close(options);
|
|
372
|
+
this.octetStreamMessageQueue.close(options);
|
|
373
|
+
if (options.id !== void 0) {
|
|
374
|
+
promises.push(
|
|
375
|
+
this.requestEventStreamTransmitters.get(options.id)?.cancel(),
|
|
376
|
+
this.requestOctetStreamTransmitters.get(options.id)?.cancel()
|
|
377
|
+
);
|
|
378
|
+
this.requestEventStreamTransmitters.delete(options.id);
|
|
379
|
+
this.requestOctetStreamTransmitters.delete(options.id);
|
|
380
|
+
this.cleanupFns.get(options.id)?.forEach((fn) => fn());
|
|
381
|
+
this.cleanupFns.delete(options.id);
|
|
382
|
+
} else {
|
|
383
|
+
this.requestEventStreamTransmitters.forEach((t) => promises.push(t.cancel()));
|
|
384
|
+
this.requestOctetStreamTransmitters.forEach((t) => promises.push(t.cancel()));
|
|
385
|
+
this.requestEventStreamTransmitters.clear();
|
|
386
|
+
this.requestOctetStreamTransmitters.clear();
|
|
387
|
+
this.cleanupFns.forEach((fns) => fns.forEach((fn) => fn()));
|
|
388
|
+
this.cleanupFns.clear();
|
|
389
|
+
}
|
|
390
|
+
await Promise.all(promises);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const JSON_BINARY_SEPARATOR_BYTE = 255;
|
|
395
|
+
const textEncoder = new TextEncoder();
|
|
396
|
+
const textDecoder = new TextDecoder();
|
|
397
|
+
async function encodePeerMessage(message) {
|
|
398
|
+
if (message.binary === void 0) {
|
|
399
|
+
return stringifyJSON(message);
|
|
400
|
+
}
|
|
401
|
+
const jsonBytes = textEncoder.encode(stringifyJSON(message));
|
|
402
|
+
const binaryBytes = message.binary instanceof Blob ? new Uint8Array(await message.binary.arrayBuffer()) : message.binary;
|
|
403
|
+
const output = new Uint8Array(
|
|
404
|
+
jsonBytes.length + 1 + binaryBytes.length
|
|
405
|
+
);
|
|
406
|
+
output.set(jsonBytes, 0);
|
|
407
|
+
output[jsonBytes.length] = JSON_BINARY_SEPARATOR_BYTE;
|
|
408
|
+
output.set(binaryBytes, jsonBytes.length + 1);
|
|
409
|
+
return output;
|
|
410
|
+
}
|
|
411
|
+
function decodePeerMessage(data) {
|
|
412
|
+
if (typeof data === "string") {
|
|
413
|
+
return JSON.parse(data);
|
|
414
|
+
}
|
|
415
|
+
const separatorIndex = data.indexOf(JSON_BINARY_SEPARATOR_BYTE);
|
|
416
|
+
if (separatorIndex === -1) {
|
|
417
|
+
return JSON.parse(textDecoder.decode(data));
|
|
418
|
+
}
|
|
419
|
+
const jsonBytes = data.subarray(0, separatorIndex);
|
|
420
|
+
const binaryBytes = data.subarray(separatorIndex + 1);
|
|
421
|
+
return {
|
|
422
|
+
...JSON.parse(textDecoder.decode(jsonBytes)),
|
|
423
|
+
binary: binaryBytes
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
class HibernationEventIterator extends AsyncIteratorClass {
|
|
428
|
+
/**
|
|
429
|
+
* In the client library, server results are typically represented by an `AsyncIteratorClass`.
|
|
430
|
+
* Since `AsyncIteratorClass` does not include a `hibernationCallback` property, this property should be optional.
|
|
431
|
+
*/
|
|
432
|
+
hibernationCallback;
|
|
433
|
+
constructor(hibernationCallback) {
|
|
434
|
+
super(async () => {
|
|
435
|
+
throw new Error("Cannot use hibernating iterator directly");
|
|
436
|
+
}, async (isCompleted) => {
|
|
437
|
+
if (!isCompleted) {
|
|
438
|
+
throw new Error("Cannot use hibernating iterator directly");
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
this.hibernationCallback = hibernationCallback;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
class ServerPeer {
|
|
446
|
+
constructor(send) {
|
|
447
|
+
this.send = send;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Messages waiting to be processed
|
|
451
|
+
*/
|
|
452
|
+
eventStreamMessageQueue = new AsyncIdQueue();
|
|
453
|
+
octetStreamMessageQueue = new AsyncIdQueue();
|
|
454
|
+
eventStreamTransmitters = /* @__PURE__ */ new Map();
|
|
455
|
+
octetStreamTransmitters = /* @__PURE__ */ new Map();
|
|
456
|
+
/**
|
|
457
|
+
* Map of abort controllers for each request
|
|
458
|
+
*/
|
|
459
|
+
controller = /* @__PURE__ */ new Map();
|
|
460
|
+
/**
|
|
461
|
+
* Use for measure resources usage
|
|
462
|
+
*/
|
|
463
|
+
get size() {
|
|
464
|
+
return this.eventStreamMessageQueue.length + this.octetStreamMessageQueue.length + this.controller.size + this.eventStreamTransmitters.size + this.octetStreamTransmitters.size;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Handle a message from client
|
|
468
|
+
*/
|
|
469
|
+
async message(message, handleRequest) {
|
|
470
|
+
if (message.kind === "abort") {
|
|
471
|
+
await this.close({ id: message.id, reason: new AbortError("Client peer aborted the request") });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (message.kind === "event-stream") {
|
|
475
|
+
if (this.eventStreamMessageQueue.isOpen(message.id)) {
|
|
476
|
+
this.eventStreamMessageQueue.push(message.id, message);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (message.kind === "octet-stream") {
|
|
481
|
+
if (this.octetStreamMessageQueue.isOpen(message.id)) {
|
|
482
|
+
this.octetStreamMessageQueue.push(message.id, message);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.eventStreamMessageQueue.open(message.id);
|
|
487
|
+
this.octetStreamMessageQueue.open(message.id);
|
|
488
|
+
const controller = new AbortController();
|
|
489
|
+
this.controller.set(message.id, controller);
|
|
490
|
+
const signal = controller.signal;
|
|
491
|
+
try {
|
|
492
|
+
const request = {
|
|
493
|
+
...message.json,
|
|
494
|
+
signal,
|
|
495
|
+
body: await toStandardBody(
|
|
496
|
+
message,
|
|
497
|
+
this.eventStreamMessageQueue,
|
|
498
|
+
this.octetStreamMessageQueue,
|
|
499
|
+
async (isCompleted) => {
|
|
500
|
+
this.eventStreamMessageQueue.close({ id: message.id });
|
|
501
|
+
this.octetStreamMessageQueue.close({ id: message.id });
|
|
502
|
+
if (!isCompleted && this.controller.has(message.id)) {
|
|
503
|
+
await this.send({ id: message.id, kind: "stream/cancel" });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
};
|
|
508
|
+
const response = await handleRequest(request);
|
|
509
|
+
if (signal.aborted) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const responseMessage = {
|
|
513
|
+
id: message.id,
|
|
514
|
+
kind: "response",
|
|
515
|
+
json: {
|
|
516
|
+
...{ ...response, signal: void 0 },
|
|
517
|
+
// clone and remove signal from request
|
|
518
|
+
headers: { ...response.headers }
|
|
519
|
+
// clone headers
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
if (response.body instanceof ReadableStream) {
|
|
523
|
+
responseMessage.json.body = void 0;
|
|
524
|
+
responseMessage.json.headers["standard-server"] = "octet-stream";
|
|
525
|
+
} else if (isAsyncIteratorObject(response.body)) {
|
|
526
|
+
responseMessage.json.body = void 0;
|
|
527
|
+
responseMessage.json.headers["standard-server"] = "event-stream";
|
|
528
|
+
} else if (response.body instanceof FormData) {
|
|
529
|
+
const res = new Response(response.body);
|
|
530
|
+
responseMessage.binary = await res.blob();
|
|
531
|
+
responseMessage.json.body = void 0;
|
|
532
|
+
responseMessage.json.headers["standard-server"] = "form-data";
|
|
533
|
+
responseMessage.json.headers["content-type"] = res.headers.get("content-type") ?? void 0;
|
|
534
|
+
} else if (response.body instanceof Blob) {
|
|
535
|
+
responseMessage.binary = response.body;
|
|
536
|
+
responseMessage.json.body = void 0;
|
|
537
|
+
responseMessage.json.headers["standard-server"] = "file";
|
|
538
|
+
responseMessage.json.headers["content-disposition"] = generateContentDisposition(response.body instanceof File ? response.body.name : "blob");
|
|
539
|
+
responseMessage.json.headers["content-type"] = response.body.type;
|
|
540
|
+
} else if (response.body instanceof URLSearchParams) {
|
|
541
|
+
responseMessage.json.body = response.body.toString();
|
|
542
|
+
responseMessage.json.headers["standard-server"] = "url-search-params";
|
|
543
|
+
}
|
|
544
|
+
if (signal.aborted) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
await this.send(responseMessage);
|
|
548
|
+
if (signal.aborted) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (isAsyncIteratorObject(response.body)) {
|
|
552
|
+
if (response.body instanceof HibernationEventIterator) {
|
|
553
|
+
response.body.hibernationCallback?.(message.id);
|
|
554
|
+
} else {
|
|
555
|
+
const transmitter = new EventStreamTransmitter(response.body, message.id, this.send);
|
|
556
|
+
this.eventStreamTransmitters.set(message.id, transmitter);
|
|
557
|
+
await transmitter.transmit();
|
|
558
|
+
}
|
|
559
|
+
} else if (response.body instanceof ReadableStream) {
|
|
560
|
+
const transmitter = new OctetStreamTransmitter(response.body, message.id, this.send);
|
|
561
|
+
this.octetStreamTransmitters.set(message.id, transmitter);
|
|
562
|
+
await transmitter.transmit();
|
|
563
|
+
}
|
|
564
|
+
this.controller.delete(message.id);
|
|
565
|
+
await this.close({ id: message.id });
|
|
566
|
+
} catch (reason) {
|
|
567
|
+
await Promise.all([
|
|
568
|
+
/**
|
|
569
|
+
* Do not need to send abort message if request was closed or aborted
|
|
570
|
+
*/
|
|
571
|
+
this.controller.has(message.id) ? this.send({ id: message.id, kind: "abort" }) : void 0,
|
|
572
|
+
this.close({ id: message.id, reason })
|
|
573
|
+
]);
|
|
574
|
+
throw reason;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async close(options = {}) {
|
|
578
|
+
const promises = [];
|
|
579
|
+
this.eventStreamMessageQueue.close(options);
|
|
580
|
+
this.octetStreamMessageQueue.close(options);
|
|
581
|
+
if (options.id === void 0) {
|
|
582
|
+
this.eventStreamTransmitters.forEach((t) => promises.push(t.cancel()));
|
|
583
|
+
this.octetStreamTransmitters.forEach((t) => promises.push(t.cancel()));
|
|
584
|
+
this.eventStreamTransmitters.clear();
|
|
585
|
+
this.octetStreamTransmitters.clear();
|
|
586
|
+
this.controller.forEach((c) => c.abort(options.reason));
|
|
587
|
+
this.controller.clear();
|
|
588
|
+
} else {
|
|
589
|
+
promises.push(
|
|
590
|
+
this.eventStreamTransmitters.get(options.id)?.cancel(),
|
|
591
|
+
this.octetStreamTransmitters.get(options.id)?.cancel()
|
|
592
|
+
);
|
|
593
|
+
this.eventStreamTransmitters.delete(options.id);
|
|
594
|
+
this.octetStreamTransmitters.delete(options.id);
|
|
595
|
+
this.controller.get(options.id)?.abort(options.reason);
|
|
596
|
+
this.controller.delete(options.id);
|
|
597
|
+
}
|
|
598
|
+
await Promise.all(promises);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export { ClientPeer, EventStreamTransmitter, HibernationEventIterator, ServerPeer, decodePeerMessage, encodePeerMessage, toEventIterator };
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@standardserver/peer",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://standardserver.dev",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/standardserver/standardserver.git",
|
|
10
|
+
"directory": "packages/peer"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@standardserver/core": "0.0.0",
|
|
24
|
+
"@standardserver/shared": "0.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "unbuild",
|
|
28
|
+
"type:check": "tsc -b"
|
|
29
|
+
}
|
|
30
|
+
}
|