@meshagent/meshagent 0.0.11
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 +201 -0
- package/README.md +0 -0
- package/babel.config.cjs +3 -0
- package/client.js +422 -0
- package/dist/entrypoint.js +11262 -0
- package/dist/entrypoint.js.map +7 -0
- package/document-server-client.js +246 -0
- package/entrypoint.js +11568 -0
- package/index.js +2 -0
- package/package.json +33 -0
- package/protocol.js +459 -0
- package/server.js +310 -0
- package/test/assert.js +19 -0
- package/test/document-server-client.test.js +33 -0
- package/test/document.test.js +313 -0
- package/test/protocol.test.js +64 -0
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meshagent/meshagent",
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"description": "Meshagent Client",
|
|
5
|
+
"homepage": "https://www.meshagent.com",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "esbuild entrypoint.js --sourcemap --tree-shaking=true --bundle --outdir=dist --target=esnext '--define:process={\"env\":\"production\",\"NODE_ENV\":\"production\"}' --define:global=window --analyze --format=cjs",
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@babel/core": "^7.26.0",
|
|
14
|
+
"@babel/preset-env": "^7.26.0",
|
|
15
|
+
"@kayahr/text-encoding": "^1.3.0",
|
|
16
|
+
"base-64": "^1.0.0",
|
|
17
|
+
"uuid": "^11.0.3",
|
|
18
|
+
"ws": "^8.18.0",
|
|
19
|
+
"y-indexeddb": "^9.0.12",
|
|
20
|
+
"yjs": "^13.6.7"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^9.14.0",
|
|
24
|
+
"esbuild": "^0.25.0",
|
|
25
|
+
"eslint": "^9.14.0",
|
|
26
|
+
"eslint-plugin-import": "^2.31.0",
|
|
27
|
+
"globals": "^15.12.0",
|
|
28
|
+
"jest": "^29.7.0",
|
|
29
|
+
"mocha": "^11.1.0"
|
|
30
|
+
},
|
|
31
|
+
"author": "",
|
|
32
|
+
"license": "Apache-2.0"
|
|
33
|
+
}
|
package/protocol.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { decode } from "base-64";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { TextEncoder, TextDecoder } from "@kayahr/text-encoding";
|
|
4
|
+
|
|
5
|
+
export class ServerProtocol {
|
|
6
|
+
constructor(onServerUpdate) {
|
|
7
|
+
this.doc = new Y.Doc();
|
|
8
|
+
this.doc.on('update', onServerUpdate);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
update(update) {
|
|
12
|
+
Y.applyUpdate(this.doc, update);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ClientProtocol {
|
|
17
|
+
constructor(onClientUpdate) {
|
|
18
|
+
this.doc = new Y.Doc();
|
|
19
|
+
this.doc.on('update', onClientUpdate);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
update(update) {
|
|
23
|
+
Y.applyUpdate(this.doc, update);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Completer {
|
|
28
|
+
constructor() {
|
|
29
|
+
const self = this;
|
|
30
|
+
this.fut = new Promise((resolve, reject) => {
|
|
31
|
+
self.resolve = () => {
|
|
32
|
+
this.completed = true;
|
|
33
|
+
resolve();
|
|
34
|
+
}
|
|
35
|
+
self.reject = () => {
|
|
36
|
+
this.completed = true;
|
|
37
|
+
reject();
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
this.completed = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class ProtocolMessage {
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Object} message - the message
|
|
49
|
+
* @param {number} id - the message id
|
|
50
|
+
* @param {Uint8List} data - the data of the message
|
|
51
|
+
* @param {string} type - the type of the message
|
|
52
|
+
*/
|
|
53
|
+
constructor({ id, data, type}) {
|
|
54
|
+
this.sent = new Completer()
|
|
55
|
+
this.id = id;
|
|
56
|
+
this.data = data;
|
|
57
|
+
this.type = type;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class ProtocolChannel {
|
|
62
|
+
|
|
63
|
+
/* void Function(Uint8List data) */
|
|
64
|
+
start(onDataReceived) {
|
|
65
|
+
this.onDataReceived = onDataReceived;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dispose() {
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Uint8List} data - the data of the message
|
|
74
|
+
*/
|
|
75
|
+
async sendData(data) {
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
export class StreamProtocolChannel extends ProtocolChannel {
|
|
82
|
+
/**
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} options
|
|
85
|
+
* @param {ProtocolMessageStream} options.input
|
|
86
|
+
* @param {ProtocolMessageStream} options.output
|
|
87
|
+
*/
|
|
88
|
+
constructor({input, output}) {
|
|
89
|
+
super();
|
|
90
|
+
this.input = input;
|
|
91
|
+
this.output = output;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
start(onDataReceived) {
|
|
95
|
+
if (this.started) {
|
|
96
|
+
throw new Error("Already started");
|
|
97
|
+
}
|
|
98
|
+
this.started = true;
|
|
99
|
+
(async () => {
|
|
100
|
+
for await (let message of this.input.stream()) {
|
|
101
|
+
onDataReceived(message);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dispose() {
|
|
107
|
+
this.input.close();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async sendData(data) {
|
|
111
|
+
this.output.add(data);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
export class WebSocketProtocolChannel extends ProtocolChannel {
|
|
117
|
+
/**
|
|
118
|
+
* @param {Object} params - the params
|
|
119
|
+
* @param {string} url - the url to connect to
|
|
120
|
+
*/
|
|
121
|
+
constructor({ url }) {
|
|
122
|
+
super();
|
|
123
|
+
this.url = url;
|
|
124
|
+
this._opened = new Completer();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
*
|
|
129
|
+
* @param {A callback that will be called every time data is received} onDataReceived
|
|
130
|
+
*/
|
|
131
|
+
start(onDataReceived) {
|
|
132
|
+
if(typeof(onDataReceived) != "function") {
|
|
133
|
+
throw new Error("onDataReceived must be a function")
|
|
134
|
+
}
|
|
135
|
+
this.onDataReceived = onDataReceived;
|
|
136
|
+
this.webSocket = new WebSocket(this.url);
|
|
137
|
+
this.webSocket.onopen = () => {
|
|
138
|
+
this._opened.resolve();
|
|
139
|
+
};
|
|
140
|
+
this.webSocket.addEventListener("message", this.onData.bind(this))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
onData(event) {
|
|
145
|
+
this.onDataReceived(new Uint8Array(event.data));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
dispose() {
|
|
150
|
+
this.webSocket.close()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {Uint8Array} data - the data to send
|
|
155
|
+
*/
|
|
156
|
+
async sendData(data) {
|
|
157
|
+
await this._opened.fut;
|
|
158
|
+
this.webSocket.send(data);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
class LivekitProtocolChannel extends ProtocolChannel {
|
|
164
|
+
LivekitProtocolChannel(
|
|
165
|
+
{required this.room, required this.topic, required this.remote});
|
|
166
|
+
|
|
167
|
+
final lk.RemoteParticipant remote;
|
|
168
|
+
final lk.Room room;
|
|
169
|
+
final String topic;
|
|
170
|
+
|
|
171
|
+
lk.EventsListener<lk.RoomEvent>? listener;
|
|
172
|
+
|
|
173
|
+
void Function(Uint8List data)? onDataReceived;
|
|
174
|
+
|
|
175
|
+
@override
|
|
176
|
+
void start(void Function(Uint8List data) onDataReceived) {
|
|
177
|
+
this.onDataReceived = onDataReceived;
|
|
178
|
+
listener = room.createListener();
|
|
179
|
+
listener!.on<lk.DataReceivedEvent>(onDataPacket);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@override
|
|
183
|
+
void dispose() {
|
|
184
|
+
listener?.dispose();
|
|
185
|
+
onDataReceived = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@override
|
|
189
|
+
Future<void> sendData(Uint8List data) async {
|
|
190
|
+
await room.localParticipant!.publishData(data,
|
|
191
|
+
reliable: true, topic: topic, destinationIdentities: [remote.identity]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
void onDataPacket(lk.DataReceivedEvent evt) {
|
|
195
|
+
debugPrint(
|
|
196
|
+
"Message on topic $topic ${evt.participant?.identity} vs ${remote.identity}");
|
|
197
|
+
if (evt.topic == topic && evt.participant == remote) {
|
|
198
|
+
debugPrint("Processing message on topic $topic");
|
|
199
|
+
|
|
200
|
+
onDataReceived!(evt.data as Uint8List);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
export class ProtocolMessageStream {
|
|
207
|
+
constructor() {
|
|
208
|
+
this._messages = [];
|
|
209
|
+
this._messageAdded = new Completer();
|
|
210
|
+
this._closed = false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {ProtocolMessage} message
|
|
215
|
+
*/
|
|
216
|
+
async add(message) {
|
|
217
|
+
this._messages.push(message);
|
|
218
|
+
if(!this._messageAdded.completed) {
|
|
219
|
+
this._messageAdded.resolve();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
close() {
|
|
224
|
+
if(!this._messageAdded.completed) {
|
|
225
|
+
this._closed = true;
|
|
226
|
+
this._messageAdded.resolve();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async *stream() {
|
|
231
|
+
while(!this._closed) {
|
|
232
|
+
await this._messageAdded.fut;
|
|
233
|
+
this._messageAdded = new Completer();
|
|
234
|
+
while(this._messages.length > 0) {
|
|
235
|
+
if(this._closed) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
const msg = this._messages.shift();
|
|
239
|
+
yield msg;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const encoder = new TextEncoder("utf-8");
|
|
246
|
+
const decoder = new TextDecoder("utf-8");
|
|
247
|
+
|
|
248
|
+
export function mergeUint8Arrays(...arrays) {
|
|
249
|
+
const totalSize = arrays.reduce((acc, e) => acc + e.length, 0);
|
|
250
|
+
const merged = new Uint8Array(totalSize);
|
|
251
|
+
|
|
252
|
+
arrays.forEach((array, i, arrays) => {
|
|
253
|
+
const offset = arrays.slice(0, i).reduce((acc, e) => acc + e.length, 0);
|
|
254
|
+
merged.set(array, offset);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return merged;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export class Protocol {
|
|
261
|
+
/**
|
|
262
|
+
* @param {Object} params - the params
|
|
263
|
+
* @param {ProtocolChannel} params.channel - the protocol channel to use
|
|
264
|
+
*/
|
|
265
|
+
constructor({channel}) {
|
|
266
|
+
this.channel = channel;
|
|
267
|
+
this.handlers = {}
|
|
268
|
+
this._id = 0;
|
|
269
|
+
this._send = new ProtocolMessageStream();
|
|
270
|
+
|
|
271
|
+
this._recvPacketId = 0;
|
|
272
|
+
this._recvState = "ready";
|
|
273
|
+
this._recvPacketTotal = 0;
|
|
274
|
+
this._recvMessageId = -1;
|
|
275
|
+
this._recvType = "";
|
|
276
|
+
this._recvPackets = [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {string} type - the type of message to handle
|
|
282
|
+
* @param {Function} handler - the message handler
|
|
283
|
+
*/
|
|
284
|
+
addHandler(type, handler) {
|
|
285
|
+
this.handlers[type] = handler;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {string} type - the type of message to handle
|
|
290
|
+
* @param {Function} handler - the message handler
|
|
291
|
+
*/
|
|
292
|
+
removeHandler(type, handler) {
|
|
293
|
+
delete this.handlers[type];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {number} messageId - the id of the message
|
|
298
|
+
* @param {string} type - the type of the message
|
|
299
|
+
* @param {Uint8Array?} data - the data for the message
|
|
300
|
+
*/
|
|
301
|
+
async handleMessage(messageId, type, data) {
|
|
302
|
+
const handler = this.handlers[type] ?? this.handlers["*"];
|
|
303
|
+
await handler(this, messageId, type, data);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @returns {number} the next message id
|
|
309
|
+
*/
|
|
310
|
+
getNextMessageId() {
|
|
311
|
+
return this._id++;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {string} type - the type of the message
|
|
316
|
+
* @param {Uint8Array} data - the data for the message
|
|
317
|
+
* @param {number?} messageId - the id of the message
|
|
318
|
+
*/
|
|
319
|
+
async send(type, data, id) {
|
|
320
|
+
const msg = new ProtocolMessage({ id: id ?? this.getNextMessageId(), type: type, data: data });
|
|
321
|
+
this._send.add(msg);
|
|
322
|
+
await msg.sent.future;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {Object} object - the type of the message
|
|
327
|
+
*/
|
|
328
|
+
async sendJson(object) {
|
|
329
|
+
return await send("application/json", encoder.encode(JSON.stringify(object)));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
start(onMessage = null) {
|
|
334
|
+
|
|
335
|
+
if(onMessage != null) {
|
|
336
|
+
this.addHandler("*", onMessage);
|
|
337
|
+
}
|
|
338
|
+
this.channel.start(this.onDataReceived.bind(this));
|
|
339
|
+
|
|
340
|
+
(async () => {
|
|
341
|
+
for await (let message of this._send.stream()) {
|
|
342
|
+
console.log(`message recv on protocol ${message.id} ${message.type}`);
|
|
343
|
+
|
|
344
|
+
const packets = Math.ceil((message.data.length / 1024));
|
|
345
|
+
|
|
346
|
+
const header = new Uint8Array(4*4);
|
|
347
|
+
const dataView = new DataView(header.buffer);
|
|
348
|
+
dataView.setUint32(0, (message.id & 0x000fffff00000000) / Math.pow(2, 32), false);
|
|
349
|
+
dataView.setUint32(4, message.id & 0xffffffff, false);
|
|
350
|
+
dataView.setUint32(8, 0, false);
|
|
351
|
+
dataView.setUint32(12, packets, false);
|
|
352
|
+
const headerPacket = mergeUint8Arrays(
|
|
353
|
+
header,
|
|
354
|
+
encoder.encode(message.type)
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
await this.channel.sendData(headerPacket);
|
|
358
|
+
for (var i = 0; i < packets; i++) {
|
|
359
|
+
|
|
360
|
+
const packetHeader = new Uint8Array(3*4);
|
|
361
|
+
const dataView = new DataView(packetHeader.buffer);
|
|
362
|
+
dataView.setUint32(0, (message.id & 0x000fffff00000000) / Math.pow(2, 32), false);
|
|
363
|
+
dataView.setUint32(4, message.id & 0xffffffff, false);
|
|
364
|
+
dataView.setUint32(8, i+1, false);
|
|
365
|
+
const packet = mergeUint8Arrays(
|
|
366
|
+
packetHeader,
|
|
367
|
+
message.data.subarray(i * 1024, Math.min((i + 1) * 1024, message.data.length))
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
await this.channel.sendData(packet);
|
|
371
|
+
}
|
|
372
|
+
message.sent.resolve();
|
|
373
|
+
console.log(`message sent on protocol ${message.id} ${message.type}`);
|
|
374
|
+
}
|
|
375
|
+
console.log("protocol done");
|
|
376
|
+
})();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
dispose() {
|
|
380
|
+
this.channel.dispose();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
*
|
|
385
|
+
* @param {Uint8List} dataPacket
|
|
386
|
+
*/
|
|
387
|
+
onDataReceived(dataPacket) {
|
|
388
|
+
const dataView = new DataView(dataPacket.buffer);
|
|
389
|
+
|
|
390
|
+
const messageId = dataView.getUint32(4, false) + dataView.getUint32(0, false) * Math.pow(2, 32);
|
|
391
|
+
const packet = dataView.getUint32(8, false);
|
|
392
|
+
|
|
393
|
+
if (packet != this._recvPacketId) {
|
|
394
|
+
this._recvState = "error";
|
|
395
|
+
console.log(dataPacket);
|
|
396
|
+
console.log(
|
|
397
|
+
`received out of order packet got ${packet} expected ${this._recvPacketId}, total ${this._recvPacketTotal} message ID: ${messageId}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (packet == 0) {
|
|
401
|
+
if (this._recvState == "ready" || this._recvState == "error") {
|
|
402
|
+
this._recvPacketTotal = dataView.getUint32(12, false);
|
|
403
|
+
this._recvMessageId = messageId;
|
|
404
|
+
this._recvType = decoder.decode(dataPacket.subarray(16));
|
|
405
|
+
console.log(`recieved packet ${this._recvType}`);
|
|
406
|
+
|
|
407
|
+
if (this._recvPacketTotal == 0) {
|
|
408
|
+
try {
|
|
409
|
+
const merged = mergeUint8Arrays(...this._recvPackets);
|
|
410
|
+
this._recvPackets.length = 0;
|
|
411
|
+
this.handleMessage(messageId, this._recvType, merged);
|
|
412
|
+
|
|
413
|
+
} finally {
|
|
414
|
+
console.log("expecting packet reset to 0");
|
|
415
|
+
this._recvState = "ready";
|
|
416
|
+
this._recvPacketId = 0;
|
|
417
|
+
this._recvType = "";
|
|
418
|
+
this._recvMessageId = -1;
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
this._recvPacketId += 1;
|
|
422
|
+
console.log(`expecting packet ${this._recvPacketId}`);
|
|
423
|
+
this._recvState = "processing";
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
this._recvState = "error";
|
|
427
|
+
this._recvPacketId = 0;
|
|
428
|
+
console.log("received packet 0 in invalid state");
|
|
429
|
+
}
|
|
430
|
+
} else if (this._recvState != "processing") {
|
|
431
|
+
this._recvState = "error";
|
|
432
|
+
this._recvPacketId = 0;
|
|
433
|
+
console.log("received datapacket in invalid state");
|
|
434
|
+
} else {
|
|
435
|
+
if (messageId != this._recvMessageId) {
|
|
436
|
+
this._recvState = "error";
|
|
437
|
+
this._recvPacketId = 0;
|
|
438
|
+
console.log("received packet from incorrect message");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this._recvPackets.push(dataPacket.subarray(12));
|
|
442
|
+
|
|
443
|
+
if (this._recvPacketTotal == this._recvPacketId) {
|
|
444
|
+
try {
|
|
445
|
+
const merged = mergeUint8Arrays(...this._recvPackets);
|
|
446
|
+
this._recvPackets.length = 0;
|
|
447
|
+
this.handleMessage(messageId, this._recvType, merged);
|
|
448
|
+
} finally {
|
|
449
|
+
this._recvState = "ready";
|
|
450
|
+
this._recvPacketId = 0;
|
|
451
|
+
this._recvType = "";
|
|
452
|
+
this._recvMessageId = -1;
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
this._recvPacketId += 1;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|