insite-ws-transfers 1.0.465

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Eugene Nesvetaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # inSite file transfers over WebSockets
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ $ npm install insite-ws-transfers
7
+ ```
8
+
9
+ Or
10
+
11
+ ```sh
12
+ $ yarn add insite-ws-transfers
13
+ ```
@@ -0,0 +1,272 @@
1
+ import { callArgsSymbol, headers, progressInterval } from "./common";
2
+
3
+
4
+ export class IncomingTransfer {
5
+ constructor(ws, kind, id, { type, collect, encoding, size, metadata }) {
6
+ this.ws = ws;
7
+ this[callArgsSymbol] = ws.wss ? [ ws.wss, ws ] : [ ws ];
8
+ this.incomingTransport = (ws.wss ?? ws).incomingTransport;
9
+
10
+ this.kind = kind;
11
+ this.id = id;
12
+ this.type = type;
13
+ this.collect = !!collect;
14
+ this.encoding = encoding;
15
+ this.size = size;
16
+ this.metadata = metadata;
17
+
18
+ this.listeners = this.incomingTransport.listeners.get(this.kind);
19
+
20
+ this.incomingTransport.transfers.set(this.id, this);
21
+
22
+ return (async () => {
23
+ await this.#setup();
24
+
25
+ this.confirm();
26
+
27
+ return this;
28
+ })();
29
+ }
30
+
31
+ #methods;
32
+ isAborted = false;
33
+ isAbortedBySender = false;
34
+ isAbortedByReceiver = false;
35
+ isTransfered = false;
36
+ confirmResponse;
37
+ beginAt = null;
38
+ #prevChunkAt = null;
39
+ #chunksQueue = [];
40
+ #isProcessing = false;
41
+ duration = null;
42
+ transferedSize = 0;
43
+ processedSize = 0;
44
+ bytesPerMs = null;
45
+ progress = 0;
46
+ endAt = null;
47
+ error = null;
48
+
49
+
50
+ #progressInterval;
51
+ #lastProgress = 0;
52
+
53
+ async #setup() {
54
+
55
+ this.#methods = this.constructor.methods[this.type];
56
+
57
+ if (this.#methods)
58
+ await this.#methods.setup.call(this);
59
+ else
60
+ throw new Error(`Unknown type of transfer "${this.type}"`);
61
+
62
+ }
63
+
64
+ async confirm() {
65
+
66
+ for (const { begin } of this.listeners)
67
+ if (await begin?.call(...this[callArgsSymbol], this) === false)
68
+ return this.throw("Transfer was rejected by receiver");
69
+
70
+ this.beginAt =
71
+ this.#prevChunkAt =
72
+ Date.now();
73
+
74
+ this.ws.sendMessage(headers.confirm, this.id, this.confirmResponse);
75
+
76
+ this.#progressInterval = setInterval(() => {
77
+
78
+ if (this.#lastProgress !== this.progress) {
79
+ this.#lastProgress = this.progress;
80
+ this.ws.sendMessage(headers.progress, this.id, this.progress);
81
+ }
82
+
83
+ }, progressInterval);
84
+
85
+ }
86
+
87
+ handleChunk(chunk, length = chunk.length) {
88
+
89
+ const ts = Date.now();
90
+ this.bytesPerMs = length / (ts - this.#prevChunkAt);
91
+ this.#prevChunkAt = ts;
92
+ this.transferedSize += length;
93
+
94
+ this.#chunksQueue.push([ chunk, length ]);
95
+
96
+ if (!this.#isProcessing) {
97
+ this.#isProcessing = true;
98
+ this.#process();
99
+ }
100
+
101
+ }
102
+
103
+ async #process() {
104
+
105
+ let [ chunk, length ] = this.#chunksQueue.shift();
106
+
107
+ if (this.#methods.transformChunk)
108
+ chunk = await this.#methods.transformChunk.call(this, chunk);
109
+
110
+ if (this.collect)
111
+ await this.#methods.collect.call(this, chunk);
112
+
113
+ for (const { chunk: chunkListener, progress: progressListener } of this.listeners)
114
+ await (chunkListener ?? progressListener)?.call(...this[callArgsSymbol], this, chunk);
115
+
116
+ this.processedSize += length;
117
+ if (this.size)
118
+ this.progress = this.processedSize / this.size;
119
+
120
+ if (this.#chunksQueue.length)
121
+ this.#process();
122
+ else {
123
+ this.#isProcessing = false;
124
+
125
+ if (this.isTransfered)
126
+ this.#complete();
127
+ }
128
+
129
+ }
130
+
131
+ handleSent() {
132
+
133
+ this.isTransfered = true;
134
+
135
+ if (!this.size)
136
+ this.size = this.transferedSize;
137
+
138
+ this.endAt = Date.now();
139
+ this.duration = this.endAt - this.beginAt;
140
+ this.bytesPerMs = this.size / this.duration;
141
+
142
+ if (!this.#isProcessing)
143
+ this.#complete();
144
+
145
+ }
146
+
147
+ async #complete() {
148
+
149
+ this.progress = 1;
150
+
151
+ await this.#methods.done?.call(this);
152
+
153
+ for (const { end } of this.listeners)
154
+ await end?.call(...this[callArgsSymbol], this);
155
+
156
+ clearInterval(this.#progressInterval);
157
+
158
+ this.incomingTransport.transfers.delete(this.id);
159
+
160
+ this.ws.sendMessage(headers.completed, this.id);
161
+
162
+ }
163
+
164
+ abort(bySender = false) {
165
+
166
+ this.isAborted = true;
167
+ this.isAbortedBySender = bySender;
168
+ this.isAbortedByReceiver = !bySender;
169
+
170
+ return this.throw(`Transfer is aborted by ${bySender ? "sender" : "receiver"}`, !bySender);
171
+ }
172
+
173
+ throw(errorMessage, sendToSender = true) {
174
+
175
+ this.incomingTransport.transfers.delete(this.id);
176
+
177
+ clearInterval(this.#progressInterval);
178
+
179
+ this.error = new Error(errorMessage);
180
+
181
+ for (const { error } of this.listeners)
182
+ error?.call(...this[callArgsSymbol], this, this.error);
183
+
184
+ if (sendToSender)
185
+ this.ws.sendMessage(headers.error, this.id, errorMessage);
186
+
187
+ }
188
+
189
+ serialize() {
190
+ return {
191
+ id: this.id,
192
+ kind: this.kind,
193
+ type: this.type,
194
+ collect: this.collect,
195
+ encoding: this.encoding,
196
+ size: this.size,
197
+ metadata: this.metadata,
198
+ isAborted: this.isAborted,
199
+ isAbortedBySender: this.isAbortedBySender,
200
+ isAbortedByReceiver: this.isAbortedByReceiver,
201
+ isTransfered: this.isTransfered,
202
+ confirmResponse: this.confirmResponse,
203
+ beginAt: this.beginAt,
204
+ duration: this.duration,
205
+ transferedSize: this.transferedSize,
206
+ processedSize: this.processedSize,
207
+ bytesPerMs: this.bytesPerMs,
208
+ progress: this.progress,
209
+ endAt: this.endAt,
210
+ error: this.error
211
+ };
212
+ }
213
+
214
+
215
+ static methods = {
216
+
217
+ object: {
218
+ setup() {
219
+
220
+ this.data = "";
221
+
222
+ },
223
+
224
+ collect(chunk) {
225
+ this.data += chunk;
226
+
227
+ },
228
+
229
+ done() {
230
+ this.data = JSON.parse(this.data);
231
+
232
+ }
233
+ },
234
+
235
+ datauri: {
236
+ setup() {
237
+
238
+ if (this.collect)
239
+ this.data = "";
240
+
241
+ },
242
+
243
+ collect(chunk) {
244
+ this.data += chunk;
245
+
246
+ },
247
+
248
+ done() {
249
+
250
+ if (this.collect)
251
+ this.data = `data:${this.metadata.type};base64,${this.data}`;
252
+
253
+ }
254
+ },
255
+
256
+ string: {
257
+ setup() {
258
+
259
+ if (this.collect)
260
+ this.data = "";
261
+
262
+ },
263
+
264
+ collect(chunk) {
265
+ this.data += chunk;
266
+
267
+ }
268
+ }
269
+
270
+ };
271
+
272
+ }
@@ -0,0 +1,92 @@
1
+ import { headers, sizeLimit as defaultSizeLimit } from "./common";
2
+ import { IncomingTransfer } from "./IncomingTransfer";
3
+
4
+
5
+ export class IncomingTransport {
6
+ constructor(ws, options = {}) {
7
+ ws.incomingTransport = this;
8
+
9
+ const {
10
+ sizeLimit = defaultSizeLimit
11
+ } = options;
12
+
13
+ this.sizeLimit = sizeLimit;
14
+
15
+ ws.listenerHandlers.add(this.listenerHandler);
16
+
17
+ if (ws.isWebSocketServer) {
18
+ this.listenerHandlerRegExp = /^client-transfer:(.*)$/;
19
+
20
+ ws.on(`client-message:${headers.request}`, (...args) => IncomingTransport.handleRequest.call(...args));
21
+ ws.on(`client-message:${headers.chunk}`, (...args) => IncomingTransport.handleChunk.call(...args));
22
+ ws.on(`client-message:${headers.sent}`, (...args) => IncomingTransport.handleSent.call(...args));
23
+ ws.on(`client-message:${headers.abort}`, (...args) => IncomingTransport.handleAbort.call(...args));
24
+ } else {
25
+ this.listenerHandlerRegExp = /^transfer:(.*)$/;
26
+
27
+ ws.on(`message:${headers.request}`, IncomingTransport.handleRequest);
28
+ ws.on(`message:${headers.chunk}`, IncomingTransport.handleChunk);
29
+ ws.on(`message:${headers.sent}`, IncomingTransport.handleSent);
30
+ ws.on(`message:${headers.abort}`, IncomingTransport.handleAbort);
31
+ }
32
+
33
+ }
34
+
35
+ listeners = new Map();
36
+
37
+ listenerHandler = (add, eventName, listener) => {
38
+ const kind = eventName.match(this.listenerHandlerRegExp)?.[1];
39
+ if (kind) {
40
+ if (add)
41
+ this.listeners.get(kind)?.add(listener) ??
42
+ this.listeners.set(kind, new Set([ listener ]));
43
+ else
44
+ if (listener)
45
+ this.listeners.get(kind)?.delete(listener);
46
+ else
47
+ this.listeners.delete(kind);
48
+
49
+ return true;
50
+ }
51
+
52
+ };
53
+
54
+ transfers = new Map();
55
+
56
+
57
+ static IncomingTransfer = IncomingTransfer;
58
+
59
+ static handleRequest(kind, id, { type, size, metadata, ...restProps }) {
60
+
61
+ const { incomingTransport } = this.wss ?? this;
62
+ const { IncomingTransportTransfer } = incomingTransport.constructor;
63
+
64
+ if (!incomingTransport.listeners.has(kind))
65
+ this.sendMessage(headers.error, id, `Unknown kind of file "${kind}"`);
66
+ else if (incomingTransport.transfers.has(id))
67
+ this.sendMessage(headers.error, id, "Transfer already exists");
68
+ else if (!IncomingTransportTransfer.methods[type])
69
+ this.sendMessage(headers.error, id, "Unknown type of transfer");
70
+ else if (size > this.sizeLimit)
71
+ this.sendMessage(headers.error, id, `Transfer size (${size} bytes) exeeds limit of ${this.sizeLimit} bytes`);
72
+ else
73
+ new IncomingTransportTransfer(this, kind, id, { type, size, metadata, ...restProps });
74
+
75
+ }
76
+
77
+ static handleChunk(id, ...restArgs) {
78
+ (this.wss ?? this).incomingTransport.transfers.get(id)?.handleChunk(...restArgs);
79
+
80
+ }
81
+
82
+ static handleSent(id) {
83
+ (this.wss ?? this).incomingTransport.transfers.get(id)?.handleSent();
84
+
85
+ }
86
+
87
+ static handleAbort(id) {
88
+ (this.wss ?? this).incomingTransport.transfers.get(id)?.abort(true);
89
+
90
+ }
91
+
92
+ }
@@ -0,0 +1,296 @@
1
+ import { uid } from "@nesvet/n";
2
+ import { callArgsSymbol, chunkSize, headers } from "./common";
3
+ import { StringStreamer } from "./StringStreamer";
4
+
5
+
6
+ // FIXME
7
+
8
+
9
+ export class OutgoingTransfer {
10
+ constructor(ws, kind, {
11
+ data,
12
+ type,
13
+ incomingType,
14
+ collect,
15
+ metadata,
16
+ size,
17
+ chunkSize,
18
+ encoding,
19
+ incomingEncoding,
20
+ onBegin,
21
+ onSenderProgress,
22
+ onProgress,
23
+ onEnd,
24
+ onError
25
+ }) {
26
+
27
+ this.ws = ws;
28
+ this[callArgsSymbol] = ws.wss ? [ ws.wss, ws ] : [ ws ];
29
+ this.outgoingTransport = (ws.wss ?? ws).outgoingTransport;
30
+
31
+ this.kind = kind;
32
+
33
+ this.data = data;
34
+ this.collect = collect ?? false;
35
+ this.metadata = metadata;
36
+ this.size = size ?? null;
37
+ this.encoding = encoding;
38
+ this.chunkSize = chunkSize ?? this.constructor.chunkSize;
39
+
40
+ if (onBegin)
41
+ this.onBegin = onBegin;
42
+ if (onSenderProgress)
43
+ this.onSenderProgress = onSenderProgress;
44
+ if (onProgress)
45
+ this.onProgress = onProgress;
46
+ if (onEnd)
47
+ this.onEnd = onEnd;
48
+ if (onError)
49
+ this.onError = onError;
50
+
51
+ this.outgoingTransport.transfers.set(this.id, this);
52
+
53
+ return (async () => {
54
+
55
+ await this.#setup(type);
56
+
57
+ this.ws.sendMessage(headers.request, this.kind, this.id, {
58
+ type: incomingType ?? this.type,
59
+ collect: this.collect,
60
+ encoding: incomingEncoding ?? this.encoding,
61
+ size: this.size,
62
+ metadata: this.metadata
63
+ });
64
+
65
+ return this;
66
+ })();
67
+ }
68
+
69
+ id = uid();
70
+ #methods = null;
71
+ isAborted = false;
72
+ isTransfered = false;
73
+ confirmResponse;
74
+ beginAt = null;
75
+ #prevChunkAt = null;
76
+ #chunksQueue = [];
77
+ #isProcessing = false;
78
+ duration = null;
79
+ transferedSize = 0;
80
+ bytesPerMs = null;
81
+ senderProgress = 0;
82
+ progress = 0;
83
+ endAt = null;
84
+ error = null;
85
+
86
+ async #setup(type) {
87
+
88
+ for (const [ availableType, test, methods ] of this.constructor.methods)
89
+ if (type === availableType || test(this.data)) {
90
+ this.type = availableType;
91
+ this.#methods = methods;
92
+ break;
93
+ }
94
+
95
+ if (this.type) {
96
+ await this.#methods.setup.call(this);
97
+ delete this.data;
98
+ } else
99
+ throw new Error("Unknown type of transfer");
100
+
101
+ }
102
+
103
+ async handleConfirm(confirmResponse) {
104
+ this.confirmResponse = confirmResponse;
105
+
106
+ this.beginAt =
107
+ this.#prevChunkAt =
108
+ Date.now();
109
+
110
+ await this.onBegin?.call(...this[callArgsSymbol], this);
111
+
112
+ this.#methods.confirm.call(this);
113
+
114
+ }
115
+
116
+ handleChunk = chunk => {
117
+ this.#chunksQueue.push(chunk);
118
+
119
+ if (!this.#isProcessing) {
120
+ this.#isProcessing = true;
121
+ this.#process();
122
+ }
123
+
124
+ };
125
+
126
+ async #process() {
127
+
128
+ let chunk = this.#chunksQueue.shift();
129
+
130
+ const chunkLength = chunk.length;
131
+
132
+ if (this.#methods.transformChunk)
133
+ chunk = await this.#methods.transformChunk.call(this, chunk);
134
+
135
+ this.ws.sendMessage(headers.chunk, this.id, chunk, chunkLength);
136
+
137
+ const ts = Date.now();
138
+ this.bytesPerMs = chunkLength / (ts - this.#prevChunkAt);
139
+ this.#prevChunkAt = ts;
140
+ this.transferedSize += chunkLength;
141
+
142
+ if (this.size)
143
+ this.senderProgress = this.transferedSize / this.size;
144
+
145
+ await this.onSenderProgress?.call(...this[callArgsSymbol], this);
146
+
147
+ if (this.#chunksQueue.length)
148
+ this.#process();
149
+ else {
150
+ this.#isProcessing = false;
151
+
152
+ if (this.senderProgress === 1)
153
+ this.sent();
154
+ }
155
+
156
+ }
157
+
158
+ sent = () => this.ws.sendMessage(headers.sent, this.id);
159
+
160
+ handleProgress(progress) {
161
+ this.progress = progress;
162
+
163
+ this.onProgress?.call(...this[callArgsSymbol], this);
164
+
165
+ }
166
+
167
+ async handleCompleted() {
168
+
169
+ this.isTransfered = true;
170
+
171
+ if (!this.size) {
172
+ this.size = this.transferedSize;
173
+ this.senderProgress = 1;
174
+ }
175
+
176
+ this.endAt = Date.now();
177
+ this.duration = this.endAt - this.beginAt;
178
+ this.bytesPerMs = this.size / this.duration;
179
+
180
+ this.outgoingTransport.transfers.delete(this.id);
181
+
182
+ if (this.progress !== 1) {
183
+ this.progress = 1;
184
+ await this.onProgress?.call(...this[callArgsSymbol], this);
185
+ }
186
+
187
+ this.onEnd?.call(...this[callArgsSymbol], this);
188
+
189
+ }
190
+
191
+ throw(errorMessage) {
192
+
193
+ this.outgoingTransport.transfers.delete(this.id);
194
+
195
+ this.error = new Error(errorMessage);
196
+
197
+ this.onError?.call(...this[callArgsSymbol], this, this.error);
198
+
199
+ }
200
+
201
+ abort() {
202
+
203
+ this.isAborted = true;
204
+
205
+ this.ws.sendMessage(headers.abort, this.id);
206
+
207
+ return this.throw("Transfer is aborted by sender");
208
+ }
209
+
210
+ serialize() {
211
+ return {
212
+ id: this.id,
213
+ kind: this.kind,
214
+ type: this.type,
215
+ collect: this.collect,
216
+ encoding: this.encoding,
217
+ size: this.size,
218
+ metadata: this.metadata,
219
+ isAborted: this.isAborted,
220
+ isTransfered: this.isTransfered,
221
+ confirmResponse: this.confirmResponse,
222
+ beginAt: this.beginAt,
223
+ duration: this.duration,
224
+ transferedSize: this.transferedSize,
225
+ bytesPerMs: this.bytesPerMs,
226
+ senderProgress: this.senderProgress,
227
+ progress: this.progress,
228
+ endAt: this.endAt,
229
+ error: this.error
230
+ };
231
+ }
232
+
233
+
234
+ static methods = [
235
+
236
+ [ "object", data => typeof data == "object", {
237
+
238
+ setup() {
239
+
240
+ this.stringStreamer = new StringStreamer(JSON.stringify(this.data), { chunkSize: this.chunkSize });
241
+ this.collect = true;
242
+ this.size = this.stringStreamer.size;
243
+ this.encoding = "utf8";
244
+
245
+ },
246
+
247
+ confirm() {
248
+
249
+ this.stringStreamer.start(this.handleChunk);
250
+
251
+ }
252
+ } ],
253
+
254
+ [ "datauri", data => typeof data == "string" && /^data:[\w-.]+\/[\w-.+,]+(?:;base64)?,/.test(data), {
255
+
256
+ setup() {
257
+
258
+ const [ type, data ] = this.data.split(/^data:|;base64,|,/).slice(1);
259
+
260
+ this.stringStreamer = new StringStreamer(data, { chunkSize: this.chunkSize });
261
+ this.size = this.stringStreamer.size;
262
+ this.encoding = "base64";
263
+ this.metadata = { ...this.metadata, type };
264
+
265
+ },
266
+
267
+ confirm() {
268
+
269
+ this.stringStreamer.start(this.handleChunk);
270
+
271
+ }
272
+ } ],
273
+
274
+ [ "string", data => typeof data == "string", {
275
+
276
+ setup() {
277
+
278
+ this.stringStreamer = new StringStreamer(this.data, { chunkSize: this.chunkSize });
279
+ this.size = this.stringStreamer.size;
280
+ if (!this.encoding)
281
+ this.encoding = "utf8";
282
+
283
+ },
284
+
285
+ confirm() {
286
+
287
+ this.stringStreamer.start(this.handleChunk);
288
+
289
+ }
290
+ } ]
291
+
292
+ ];
293
+
294
+ static chunkSize = chunkSize;
295
+
296
+ }
@@ -0,0 +1,59 @@
1
+ import { headers } from "./common";
2
+ import { OutgoingTransfer } from "./OutgoingTransfer";
3
+
4
+
5
+ export class OutgoingTransport {
6
+ constructor(ws) {
7
+ ws.outgoingTransport = this;
8
+
9
+ if (ws.isWebSocketServer) {
10
+ ws.options.WebSocket.prototype.transfer = OutgoingTransport.transfer;
11
+
12
+ ws.on(`client-message:${headers.confirm}`, (...args) => OutgoingTransport.handleConfirm.call(...args));
13
+ ws.on(`client-message:${headers.progress}`, (...args) => OutgoingTransport.handleProgress.call(...args));
14
+ ws.on(`client-message:${headers.completed}`, (...args) => OutgoingTransport.handleCompleted.call(...args));
15
+ ws.on(`client-message:${headers.error}`, (...args) => OutgoingTransport.handleError.call(...args));
16
+ } else {
17
+ ws.transfer = OutgoingTransport.transfer;
18
+
19
+ ws.on(`message:${headers.confirm}`, OutgoingTransport.handleConfirm);
20
+ ws.on(`message:${headers.progress}`, OutgoingTransport.handleProgress);
21
+ ws.on(`message:${headers.completed}`, OutgoingTransport.handleCompleted);
22
+ ws.on(`message:${headers.error}`, OutgoingTransport.handleError);
23
+ }
24
+
25
+ }
26
+
27
+ transfers = new Map();
28
+
29
+
30
+ static OutgoingTransfer = OutgoingTransfer;
31
+
32
+ static async transfer(...args) {
33
+
34
+ const { OutgoingTransportTransfer } = (this.wss ?? this).outgoingTransport.constructor;
35
+
36
+ return new OutgoingTransportTransfer(this, ...args);
37
+ }
38
+
39
+ static handleConfirm(id, ...restArgs) {
40
+ (this.wss ?? this).outgoingTransport.transfers.get(id)?.handleConfirm(...restArgs);
41
+
42
+ }
43
+
44
+ static handleProgress(id, ...restArgs) {
45
+ (this.wss ?? this).outgoingTransport.transfers.get(id)?.handleProgress(...restArgs);
46
+
47
+ }
48
+
49
+ static handleCompleted(id, ...restArgs) {
50
+ (this.wss ?? this).outgoingTransport.transfers.get(id)?.handleCompleted(...restArgs);
51
+
52
+ }
53
+
54
+ static handleError(id, errorMessage) {
55
+ (this.wss ?? this).outgoingTransport.transfers.get(id)?.throw(errorMessage);
56
+
57
+ }
58
+
59
+ }
@@ -0,0 +1,52 @@
1
+ export class StringStreamer {
2
+ constructor(string, options = {}) {
3
+ this.string = string;
4
+ this.size = this.string.length;
5
+
6
+ const {
7
+ chunkSize = 1024 * 256
8
+ } = options;
9
+
10
+ this.chunkSize = chunkSize;
11
+
12
+ }
13
+
14
+ #start = 0;
15
+ #end = 0;
16
+
17
+ isAborted = false;
18
+
19
+ start(listener) {
20
+ this.listener = listener;
21
+
22
+ this.#next();
23
+
24
+ }
25
+
26
+ async #next() {
27
+
28
+ if (!this.isAborted) {
29
+ this.#end = Math.min(this.#start + this.chunkSize, this.size);
30
+
31
+ const chunk = await new Promise(resolve => resolve(this.string.slice(this.#start, this.#end)));
32
+
33
+ if (!this.isAborted) {
34
+ await this.listener(chunk);
35
+
36
+ if (this.#end < this.size) {
37
+ this.#start = this.#end;
38
+
39
+ this.#next();
40
+ }
41
+ }
42
+ }
43
+
44
+ }
45
+
46
+ abort() {
47
+
48
+ this.isAborted = true;
49
+
50
+ }
51
+
52
+ }
@@ -0,0 +1,63 @@
1
+ export class FileStreamer {
2
+ constructor(file, options = {}) {
3
+ this.file = file;
4
+ this.size = this.file.size;
5
+
6
+ const {
7
+ chunkSize = 1024 * 256
8
+ } = options;
9
+
10
+ this.chunkSize = chunkSize;
11
+
12
+ this.fileReader = new FileReader();
13
+
14
+ this.fileReader.onload = this.#handleFileReaderLoad;
15
+
16
+ }
17
+
18
+ #start = 0;
19
+ #end = 0;
20
+
21
+ isAborted = false;
22
+
23
+ start(listener) {
24
+ this.listener = listener;
25
+
26
+ this.#next();
27
+
28
+ }
29
+
30
+ #next() {
31
+
32
+ if (!this.isAborted) {
33
+ this.#end = Math.min(this.#start + this.chunkSize, this.size);
34
+
35
+ this.fileReader.readAsArrayBuffer(this.file.slice(this.#start, this.#end));
36
+ }
37
+
38
+ }
39
+
40
+ #handleFileReaderLoad = async () => {
41
+
42
+ if (!this.isAborted) {
43
+ const arrayBuffer = this.fileReader.result;
44
+ arrayBuffer.length = arrayBuffer.byteLength;
45
+
46
+ await this.listener(arrayBuffer);
47
+
48
+ if (this.#end < this.size) {
49
+ this.#start = this.#end;
50
+
51
+ this.#next();
52
+ }
53
+ }
54
+
55
+ };
56
+
57
+ abort() {
58
+
59
+ this.isAborted = true;
60
+
61
+ }
62
+
63
+ }
@@ -0,0 +1,11 @@
1
+ export async function arrayBufferToBase64(arrayBuffer) {
2
+ return new Promise(resolve => {
3
+
4
+ const fileReader = new FileReader();
5
+
6
+ fileReader.onload = ({ target: { result } }) => resolve(result.substring(result.indexOf(",") + 1));
7
+
8
+ fileReader.readAsDataURL(new Blob([ arrayBuffer ]));
9
+
10
+ });
11
+ }
@@ -0,0 +1,41 @@
1
+ import { IncomingTransfer } from "../IncomingTransfer";
2
+ import { IncomingTransport } from "../IncomingTransport";
3
+
4
+
5
+ class BrowserIncomingTransfer extends IncomingTransfer {
6
+
7
+ static methods = {
8
+
9
+ file: {
10
+ setup() {
11
+
12
+ if (this.collect)
13
+ this.data = "";
14
+
15
+ },
16
+
17
+ collect(chunk) {
18
+ this.data += chunk;
19
+
20
+ },
21
+
22
+ end() {
23
+
24
+ if (this.collect && this.encoding === "utf8")
25
+ this.data = atob(this.data);
26
+
27
+ }
28
+ }
29
+
30
+ };
31
+
32
+ }
33
+
34
+ class BrowserIncomingTransport extends IncomingTransport {
35
+
36
+ static IncomingTransfer = BrowserIncomingTransfer;
37
+
38
+ }
39
+
40
+
41
+ export { BrowserIncomingTransfer as IncomingTransfer, BrowserIncomingTransport as IncomingTransport };
@@ -0,0 +1,2 @@
1
+ export * from "./incoming";
2
+ export * from "./outgoing";
@@ -0,0 +1,61 @@
1
+ import { OutgoingTransfer } from "../OutgoingTransfer";
2
+ import { OutgoingTransport } from "../OutgoingTransport";
3
+ import { arrayBufferToBase64 } from "./arrayBufferToBase64";
4
+ import { FileStreamer } from "./FileStreamer";
5
+
6
+
7
+ class BrowserOutgoingTransfer extends OutgoingTransfer {
8
+
9
+ static methods = [
10
+
11
+ [ "file", data => typeof data == "object" && data instanceof File, {
12
+
13
+ setup() {
14
+
15
+ const file = this.data;
16
+
17
+ this.fileStreamer = new FileStreamer(file, { chunkSize: this.chunkSize });
18
+
19
+ const metadata = {
20
+ name: file.name,
21
+ type: file.type,
22
+ size: file.size,
23
+ modifiedAt: file.lastModified
24
+ };
25
+
26
+ if (this.metadata)
27
+ Object.assign(this.metadata, metadata);
28
+ else
29
+ this.metadata = metadata;
30
+
31
+ this.size = this.fileStreamer.size;
32
+ this.encoding = "buffer";
33
+
34
+ },
35
+
36
+ confirm() {
37
+
38
+ this.fileStreamer.start(this.handleChunk);
39
+
40
+ },
41
+
42
+ transformChunk(chunk) {
43
+ return arrayBufferToBase64(chunk);
44
+ }
45
+ } ],
46
+
47
+ ...OutgoingTransfer.methods
48
+
49
+ ];
50
+
51
+ }
52
+
53
+ class BrowserOutgoingTransport extends OutgoingTransport {
54
+
55
+ static OutgoingTransfer = BrowserOutgoingTransfer;
56
+
57
+ }
58
+
59
+
60
+ export { BrowserOutgoingTransfer as OutgoingTransfer,
61
+ BrowserOutgoingTransport as OutgoingTransport };
package/lib/common.js ADDED
@@ -0,0 +1,18 @@
1
+ export const headers = {
2
+ request: "~t-request",
3
+ confirm: "~t-confirm",
4
+ chunk: "~t-c",
5
+ progress: "~t-p",
6
+ sent: "~t-sent",
7
+ completed: "~t-completed",
8
+ abort: "~t-abort",
9
+ error: "~t-error"
10
+ };
11
+
12
+ export const sizeLimit = 10 * 1024 * 1024 * 1024;
13
+
14
+ export const chunkSize = 1024 * 256;
15
+
16
+ export const callArgsSymbol = Symbol("callArgs");
17
+
18
+ export const progressInterval = 250;
@@ -0,0 +1,108 @@
1
+ import { Readable } from "node:stream";
2
+ import { IncomingTransfer } from "../IncomingTransfer";
3
+ import { IncomingTransport } from "../IncomingTransport";
4
+
5
+
6
+ class TransferStream extends Readable {
7
+ constructor(writableStream) {
8
+ super();
9
+
10
+ this.writable = writableStream;
11
+
12
+ this.promise = new Promise((resolve, reject) => {
13
+ writableStream.on("close", resolve);
14
+ writableStream.on("error", reject);
15
+
16
+ });
17
+
18
+ this.pipe(writableStream);
19
+
20
+ }
21
+
22
+ _read() {}
23
+
24
+ }
25
+
26
+ function pipeTo(writableStream) {
27
+ const stream = new TransferStream(writableStream);
28
+
29
+ this.streams?.push(stream) ??
30
+ (this.streams = [ stream ]);
31
+
32
+ return writableStream;
33
+ }
34
+
35
+ const streamMethod = {
36
+ setup() {
37
+
38
+ if (!this.encoding)
39
+ this.encoding = "buffer";
40
+
41
+ if (this.encoding === "buffer")
42
+ this.isBuffer = true;
43
+
44
+ if (this.collect)
45
+ this.data = this.isBuffer ? Buffer.from("") : "";
46
+
47
+ this.pipeTo = pipeTo;
48
+
49
+ },
50
+
51
+ transformChunk(chunk) {
52
+ chunk = this.isBuffer ? Buffer.from(chunk, "base64") : Buffer.from(chunk, "base64").toString(this.encoding);
53
+
54
+ if (this.streams)
55
+ for (const stream of this.streams)
56
+ stream.push(chunk);
57
+
58
+ return chunk;
59
+ },
60
+
61
+ collect(chunk) {
62
+ if (this.isBuffer)
63
+ this.data = Buffer.concat([ this.data, chunk ]);
64
+ else
65
+ this.data += chunk;
66
+
67
+ },
68
+
69
+ async done() {
70
+
71
+ if (this.streams) {
72
+ const promises = [];
73
+
74
+ for (const stream of this.streams) {
75
+ stream.push(null);
76
+ promises.push(stream.promise);
77
+ }
78
+
79
+ await Promise.all(promises);
80
+ }
81
+
82
+ }
83
+ };
84
+
85
+
86
+ class NodeIncomingTransfer extends IncomingTransfer {
87
+
88
+ static methods = {
89
+
90
+ stream: streamMethod,
91
+
92
+ file: streamMethod,
93
+
94
+ ...IncomingTransfer.methods
95
+
96
+ };
97
+
98
+ }
99
+
100
+ class NodeIncomingTransport extends IncomingTransport {
101
+
102
+ static IncomingTransfer = NodeIncomingTransfer;
103
+
104
+ }
105
+
106
+
107
+ export { NodeIncomingTransfer as IncomingTransfer,
108
+ NodeIncomingTransport as IncomingTransport };
@@ -0,0 +1,2 @@
1
+ export * from "./incoming";
2
+ export * from "./outgoing";
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import mime from "mime";
4
+ import { OutgoingTransfer } from "../OutgoingTransfer";
5
+ import { OutgoingTransport } from "../OutgoingTransport";
6
+
7
+
8
+ class NodeOutgoingTransfer extends OutgoingTransfer {
9
+
10
+ static methods = [
11
+
12
+ [ "stream", data => typeof data == "object" && typeof data.on == "function" && typeof data.read == "function", {
13
+
14
+ setup() {
15
+
16
+ this.stream = this.data;
17
+
18
+ if (!this.encoding)
19
+ this.encoding = "buffer";
20
+
21
+ if (this.encoding === "buffer")
22
+ this.isBuffer = true;
23
+
24
+ },
25
+
26
+ confirm() {
27
+
28
+ this.stream.on("data", this.handleChunk);
29
+ if (!this.size)
30
+ this.stream.on("end", this.sent);
31
+
32
+ },
33
+
34
+ transformChunk(chunk) {
35
+ return (this.isBuffer ? chunk : Buffer.from(chunk, this.encoding)).toString("base64");
36
+ }
37
+ } ],
38
+
39
+ [ "file", data => typeof data == "string" && /^(\/|(\/[^/]+)+)$/.test(data), {
40
+
41
+ async setup() {
42
+
43
+ const fileName = this.data;
44
+ const name = path.basename(fileName);
45
+ const { size, mtimeMs: modifiedAt } = await fs.promises.stat(fileName);
46
+
47
+ const metadata = {
48
+ name,
49
+ type: mime.getType(path.extname(name).slice(1)),
50
+ size,
51
+ modifiedAt
52
+ };
53
+
54
+ this.stream = fs.createReadStream(fileName);
55
+
56
+ this.size = size;
57
+ this.encoding = "buffer";
58
+
59
+ if (this.metadata)
60
+ Object.assign(this.metadata, metadata);
61
+ else
62
+ this.metadata = metadata;
63
+
64
+ },
65
+
66
+ confirm() {
67
+
68
+ this.stream.on("data", this.handleChunk);
69
+
70
+ },
71
+
72
+ transformChunk(chunk) {
73
+ return chunk.toString("base64");
74
+ }
75
+ } ],
76
+
77
+ ...OutgoingTransfer.methods
78
+
79
+ ];
80
+
81
+ }
82
+
83
+ class NodeOutgoingTransport extends OutgoingTransport {
84
+
85
+ static OutgoingTransfer = NodeOutgoingTransfer;
86
+
87
+ }
88
+
89
+
90
+ export { NodeOutgoingTransfer as OutgoingTransfer,
91
+ NodeOutgoingTransport as OutgoingTransport };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "insite-ws-transfers",
3
+ "version": "1.0.465",
4
+ "description": "inSite file transfers over WebSockets",
5
+ "type": "module",
6
+ "main": "./lib/node/index.js",
7
+ "module": "./lib/node/index.js",
8
+ "exports": {
9
+ ".": "./lib/node/index.js",
10
+ "./node": "./lib/node/index.js",
11
+ "./node/incoming": "./lib/node/incoming.js",
12
+ "./node/outgoing": "./lib/node/outgoing.js",
13
+ "./browser": "./lib/browser/index.js",
14
+ "./browser/incoming": "./lib/browser/incoming.js",
15
+ "./browser/outgoing": "./lib/browser/outgoing.js"
16
+ },
17
+ "packageManager": "yarn@1.22.22",
18
+ "scripts": {
19
+ "test": "echo \"Error: no test specified\" && exit 1"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/nesvet/insite-ws-transfers.git"
24
+ },
25
+ "keywords": [
26
+ "insite",
27
+ "ws",
28
+ "websockets",
29
+ "files",
30
+ "transfers"
31
+ ],
32
+ "author": "Eugene Nesvetaev <dev@nesvet.dev> (https://nesvet.dev/)",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@nesvet/n": "^1",
36
+ "mime": "^2"
37
+ },
38
+ "devDependencies": {
39
+ "@nesvet/eslint-config": "^1"
40
+ }
41
+ }