@cocreate/socket 0.0.0-stage → 3.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/.github/FUNDING.yml +3 -0
- package/.github/workflows/automated.yml +69 -0
- package/CHANGELOG.md +1412 -0
- package/CONTRIBUTING.md +97 -0
- package/CoCreate.config.js +23 -0
- package/LICENSE +683 -0
- package/README.md +245 -3
- package/demo/server.js +24 -0
- package/docs/index.html +217 -0
- package/package.json +50 -4
- package/prettier.config.js +16 -0
- package/release.config.js +30 -0
- package/src/client/index.js +785 -0
- package/src/index.js +2 -0
- package/src/server/index.js +1115 -0
- package/src/server/sockets.js +149 -0
- package/test/client/transport.test.js +84 -0
- package/test/server/collections.test.js +111 -0
- package/test/server/send.test.js +257 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Commercial Licensing Information:
|
|
20
|
+
* For commercial use of this software without the copyleft provisions of the AGPLv3,
|
|
21
|
+
* you must obtain a commercial license from CoCreate LLC.
|
|
22
|
+
* For details, visit <https://CoCreateJS.com/licenses/> or contact us at sales@CoCreateJS.com.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { ObjectId, uuid, getClientInfo } from "@cocreate/utils";
|
|
26
|
+
import config from "@cocreate/config";
|
|
27
|
+
|
|
28
|
+
const isBrowser = typeof window !== "undefined";
|
|
29
|
+
|
|
30
|
+
function getBrowserLocation() {
|
|
31
|
+
if (!isBrowser) return;
|
|
32
|
+
if (window.location.host) return window.location;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (window.parent !== window && window.parent.location.host) {
|
|
36
|
+
return window.parent.location;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Cross-origin parents are intentionally inaccessible.
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return window.location;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let WS_Impl;
|
|
46
|
+
|
|
47
|
+
function getIndexeddb() {
|
|
48
|
+
return isBrowser ? window.CoCreate?.indexeddb : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getWebSocketImpl() {
|
|
52
|
+
if (WS_Impl) return WS_Impl;
|
|
53
|
+
if (isBrowser) {
|
|
54
|
+
WS_Impl = window.WebSocket;
|
|
55
|
+
} else {
|
|
56
|
+
const wsModule = await import("ws");
|
|
57
|
+
WS_Impl = wsModule.default || wsModule;
|
|
58
|
+
}
|
|
59
|
+
return WS_Impl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const socketsByUrl = new Map();
|
|
63
|
+
const socketsById = new Map();
|
|
64
|
+
const delay = 1000 + Math.floor(Math.random() * 3000);
|
|
65
|
+
|
|
66
|
+
let organizationPromise = null;
|
|
67
|
+
|
|
68
|
+
async function getOrganization() {
|
|
69
|
+
let organization_id = config.get("organization_id");
|
|
70
|
+
if (!organization_id || organization_id === "canceled") {
|
|
71
|
+
if (isBrowser) {
|
|
72
|
+
const Organization = await import("@cocreate/organizations");
|
|
73
|
+
organization_id = await Organization.default.get();
|
|
74
|
+
} else {
|
|
75
|
+
organization_id = process.env.ORGANIZATION_ID || "";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (organization_id) config.set("organization_id", organization_id);
|
|
79
|
+
|
|
80
|
+
return organization_id;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const CoCreateSocketClient = {
|
|
84
|
+
/**
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
* @environment client
|
|
88
|
+
* @environment server
|
|
89
|
+
*/
|
|
90
|
+
frameId: uuid(8),
|
|
91
|
+
/**
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
* @environment client
|
|
95
|
+
* @environment server
|
|
96
|
+
*/
|
|
97
|
+
connected: false,
|
|
98
|
+
listeners: new Map(),
|
|
99
|
+
/**
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
* @environment client
|
|
103
|
+
* @environment server
|
|
104
|
+
*/
|
|
105
|
+
messageEvents: {},
|
|
106
|
+
/**
|
|
107
|
+
*
|
|
108
|
+
* @public
|
|
109
|
+
* @environment client
|
|
110
|
+
* @environment server
|
|
111
|
+
*/
|
|
112
|
+
messageQueue: new Map(),
|
|
113
|
+
/**
|
|
114
|
+
*
|
|
115
|
+
* @public
|
|
116
|
+
* @environment client
|
|
117
|
+
* @environment server
|
|
118
|
+
*/
|
|
119
|
+
configQueue: new Map(),
|
|
120
|
+
/**
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
* @environment client
|
|
124
|
+
* @environment server
|
|
125
|
+
*/
|
|
126
|
+
maxReconnectDelay: 600000,
|
|
127
|
+
/**
|
|
128
|
+
*
|
|
129
|
+
* @public
|
|
130
|
+
* @environment client
|
|
131
|
+
* @environment server
|
|
132
|
+
*/
|
|
133
|
+
organization: false,
|
|
134
|
+
/**
|
|
135
|
+
*
|
|
136
|
+
* @public
|
|
137
|
+
* @environment client
|
|
138
|
+
* @environment server
|
|
139
|
+
*/
|
|
140
|
+
serverStorage: true,
|
|
141
|
+
/**
|
|
142
|
+
*
|
|
143
|
+
* @public
|
|
144
|
+
* @environment client
|
|
145
|
+
* @environment server
|
|
146
|
+
*/
|
|
147
|
+
serverOrganization: true,
|
|
148
|
+
/**
|
|
149
|
+
*
|
|
150
|
+
* @public
|
|
151
|
+
* @environment client
|
|
152
|
+
* @environment server
|
|
153
|
+
*/
|
|
154
|
+
organizationBalance: true,
|
|
155
|
+
/**
|
|
156
|
+
*
|
|
157
|
+
* @public
|
|
158
|
+
* @environment client
|
|
159
|
+
* @environment server
|
|
160
|
+
*/
|
|
161
|
+
organization_id: async () => {
|
|
162
|
+
return (
|
|
163
|
+
organizationPromise ||
|
|
164
|
+
(organizationPromise = getOrganization())
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async init() {
|
|
169
|
+
this.clientId = config.get("clientId");
|
|
170
|
+
this.clientInfo = await getClientInfo();
|
|
171
|
+
if (!this.clientId || this.clientId === "undefined") {
|
|
172
|
+
this.clientId = ObjectId().toString()
|
|
173
|
+
config.set("clientId", this.clientId);
|
|
174
|
+
}
|
|
175
|
+
if (this.clientId && this.clientId.length !== 24 && ObjectId) {
|
|
176
|
+
this.clientId = ObjectId().toString();
|
|
177
|
+
config.set("clientId", this.clientId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strictly retrieve host using isomorphic config.get
|
|
181
|
+
this.host = config.get("host");
|
|
182
|
+
if (!this.host || this.host === "undefined") {
|
|
183
|
+
if (isBrowser) {
|
|
184
|
+
// Browser: Fall back cleanly to client's current domain
|
|
185
|
+
this.host = getBrowserLocation().host;
|
|
186
|
+
} else {
|
|
187
|
+
// Server: Leave empty so getUrls can safely report configuration parameters are missing
|
|
188
|
+
this.host = "";
|
|
189
|
+
}
|
|
190
|
+
config.set("host", this.host);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.balancer = config.get("balancer");
|
|
194
|
+
if (!this.balancer || this.balancer === "undefined") {
|
|
195
|
+
this.balancer = "";
|
|
196
|
+
config.set("balancer", this.balancer);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
set(socket) {
|
|
201
|
+
socketsByUrl.set(socket.url, socket);
|
|
202
|
+
socketsById.set(socket.id, socket);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
get(key) {
|
|
206
|
+
return socketsByUrl.get(key) || socketsById.get(key);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
has(key) {
|
|
210
|
+
return socketsByUrl.has(key) || socketsById.has(key);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
delete(key) {
|
|
214
|
+
let socket;
|
|
215
|
+
if (typeof key === "string") socket = this.get(key);
|
|
216
|
+
if (!socket || (!socket.id && !socket.url)) return;
|
|
217
|
+
socketsByUrl.delete(socket.url);
|
|
218
|
+
socketsById.delete(socket.id);
|
|
219
|
+
|
|
220
|
+
if (socket.readyState === 1) socket.close();
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async create(data = {}) {
|
|
224
|
+
const self = this;
|
|
225
|
+
|
|
226
|
+
if (this.organization === "canceled") return;
|
|
227
|
+
|
|
228
|
+
const urls = await this.getUrls(data);
|
|
229
|
+
for (let url of urls) {
|
|
230
|
+
let socket = this.get(url);
|
|
231
|
+
if (socket) return;
|
|
232
|
+
|
|
233
|
+
this.set({ url });
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
let token = null;
|
|
237
|
+
if (isBrowser) {
|
|
238
|
+
token = config.get("token");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const socketId = uuid(8);
|
|
242
|
+
|
|
243
|
+
const options = {
|
|
244
|
+
socketId,
|
|
245
|
+
clientId: this.clientId,
|
|
246
|
+
clientInfo: this.clientInfo,
|
|
247
|
+
user_id: data.user_id || this.user_id,
|
|
248
|
+
token: token || ""
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (isBrowser) {
|
|
252
|
+
options.lastSynced = config.get(url);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let opt = JSON.stringify(options);
|
|
256
|
+
opt = encodeURIComponent(opt);
|
|
257
|
+
|
|
258
|
+
const WebSocketClass = await getWebSocketImpl();
|
|
259
|
+
socket = new WebSocketClass(url, opt);
|
|
260
|
+
socket.id = options.socketId;
|
|
261
|
+
socket.connected = false;
|
|
262
|
+
socket.clientId = this.clientId;
|
|
263
|
+
socket.frameId = this.frameId;
|
|
264
|
+
socket.organization_id =
|
|
265
|
+
data.organization_id ||
|
|
266
|
+
(await this.organization_id());
|
|
267
|
+
socket.user_id = data.user_id || this.user_id;
|
|
268
|
+
socket.host = data.host || this.host;
|
|
269
|
+
socket.key = url;
|
|
270
|
+
|
|
271
|
+
this.set(socket);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error("[@cocreate/socket] Connection setup failed:", error);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const notifyDisconnect = (event) => {
|
|
278
|
+
socket.connected = false;
|
|
279
|
+
self.connected = false;
|
|
280
|
+
if (socket.__disconnectNotified) return;
|
|
281
|
+
socket.__disconnectNotified = true;
|
|
282
|
+
self.__fireListeners({
|
|
283
|
+
method: "disconnect",
|
|
284
|
+
socketId: socket.id,
|
|
285
|
+
url: socket.url,
|
|
286
|
+
event
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
socket.onclose = function (event) {
|
|
291
|
+
notifyDisconnect(event);
|
|
292
|
+
switch (event.code) {
|
|
293
|
+
case 1000:
|
|
294
|
+
console.log("websocket: closed");
|
|
295
|
+
break;
|
|
296
|
+
default:
|
|
297
|
+
self.reconnect(data, socket);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
socket.onerror = function (event) {
|
|
303
|
+
notifyDisconnect(event);
|
|
304
|
+
if (!isBrowser) console.error("[@cocreate/socket] Server socket transport error:", event.error || event);
|
|
305
|
+
else if (!window.navigator.onLine)
|
|
306
|
+
console.log("offline");
|
|
307
|
+
|
|
308
|
+
self.reconnect(data, socket);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
socket.onmessage = function (message) {
|
|
312
|
+
try {
|
|
313
|
+
message = JSON.parse(message.data);
|
|
314
|
+
// Transport open precedes server authentication and message setup.
|
|
315
|
+
// Flush requests only after the server confirms it is ready.
|
|
316
|
+
if (message.method === "connect") {
|
|
317
|
+
self.connected = true;
|
|
318
|
+
socket.connected = true;
|
|
319
|
+
socket.__disconnectNotified = false;
|
|
320
|
+
delete data.currentReconnectDelay;
|
|
321
|
+
self.checkMessageQueue(data);
|
|
322
|
+
} else if (message.method === "disconnect") {
|
|
323
|
+
socket.connected = false;
|
|
324
|
+
self.connected = false;
|
|
325
|
+
socket.__disconnectNotified = true;
|
|
326
|
+
}
|
|
327
|
+
message.receiverFrameId = self.frameId;
|
|
328
|
+
if (message.error) {
|
|
329
|
+
console.error(message.method, message.error);
|
|
330
|
+
if (message.serverOrganization === false)
|
|
331
|
+
self.serverOrganization =
|
|
332
|
+
self.serverStorage = false;
|
|
333
|
+
else if (message.serverStorage === false)
|
|
334
|
+
self.serverStorage = false;
|
|
335
|
+
else if (message.organizationBalance === false)
|
|
336
|
+
self.organizationBalance = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
message.method != "connect" &&
|
|
341
|
+
typeof message == "object"
|
|
342
|
+
) {
|
|
343
|
+
if (isBrowser && message._id) {
|
|
344
|
+
let lastSynced = config.get(socket.url);
|
|
345
|
+
|
|
346
|
+
if (!lastSynced) {
|
|
347
|
+
config.set(socket.url, message._id);
|
|
348
|
+
} else if (lastSynced !== message._id) {
|
|
349
|
+
if (
|
|
350
|
+
ObjectId(lastSynced).timestamp <
|
|
351
|
+
ObjectId(message._id).timestamp
|
|
352
|
+
) {
|
|
353
|
+
config.set(socket.url, message._id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let Data = {
|
|
358
|
+
method: "object.delete",
|
|
359
|
+
database: "system",
|
|
360
|
+
array: "messageQueue",
|
|
361
|
+
filter: {
|
|
362
|
+
query: {
|
|
363
|
+
"updated.on": {
|
|
364
|
+
$gt: message.timestamp
|
|
365
|
+
},
|
|
366
|
+
status: "queued"
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
let type = message.method.split(".")[0];
|
|
372
|
+
if (Array.isArray(message[type])) {
|
|
373
|
+
for (let item of message[type]) {
|
|
374
|
+
if (type == "object") {
|
|
375
|
+
Data.filter.query["data._id"] =
|
|
376
|
+
item._id;
|
|
377
|
+
} else if (
|
|
378
|
+
[
|
|
379
|
+
"database",
|
|
380
|
+
"array",
|
|
381
|
+
"index"
|
|
382
|
+
].includes(type)
|
|
383
|
+
) {
|
|
384
|
+
Data.filter.query[
|
|
385
|
+
"data.name"
|
|
386
|
+
] = item.name;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
message.status = "received";
|
|
394
|
+
|
|
395
|
+
if (message.broadcastClient && isBrowser)
|
|
396
|
+
self.broadcastLocalMessage(message).catch((error) => {
|
|
397
|
+
console.error("Failed to broadcast local socket message", error);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (message && message.uuid) {
|
|
401
|
+
if (isBrowser) {
|
|
402
|
+
const event = new window.CustomEvent(
|
|
403
|
+
message.uuid,
|
|
404
|
+
{
|
|
405
|
+
detail: message
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
window.dispatchEvent(event);
|
|
409
|
+
} else {
|
|
410
|
+
process.emit(message.uuid, message);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const isSender =
|
|
415
|
+
message.clientId === self.clientId &&
|
|
416
|
+
message.frameId === self.frameId;
|
|
417
|
+
if (
|
|
418
|
+
message.method === "connect" ||
|
|
419
|
+
!isSender ||
|
|
420
|
+
message.broadcastSender !== false
|
|
421
|
+
)
|
|
422
|
+
self.__fireListeners(message);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.log(e);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
__fireListeners(data) {
|
|
431
|
+
/**
|
|
432
|
+
*
|
|
433
|
+
* @public
|
|
434
|
+
* @environment client
|
|
435
|
+
* @environment server
|
|
436
|
+
*/
|
|
437
|
+
const listeners = this.listeners.get(data.method) || [];
|
|
438
|
+
listeners.forEach((listener) => {
|
|
439
|
+
listener(data);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (data.method.includes(".")) {
|
|
443
|
+
const listeners =
|
|
444
|
+
this.listeners.get(data.method.split(".")[0]) || [];
|
|
445
|
+
listeners.forEach((listener) => {
|
|
446
|
+
listener(data);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
checkMessageQueue(config) {
|
|
452
|
+
if (this.messageQueue.size > 0) {
|
|
453
|
+
for (let [uuid, data] of this.messageQueue) {
|
|
454
|
+
this.send(data);
|
|
455
|
+
this.messageQueue.delete(uuid);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const storage = getIndexeddb();
|
|
460
|
+
if (isBrowser && storage) {
|
|
461
|
+
storage
|
|
462
|
+
.send({
|
|
463
|
+
method: "object.delete",
|
|
464
|
+
database: "system",
|
|
465
|
+
array: "messageQueue",
|
|
466
|
+
filter: {
|
|
467
|
+
query: { status: "queued" }
|
|
468
|
+
},
|
|
469
|
+
organization_id: config.organization_id
|
|
470
|
+
})
|
|
471
|
+
.then((data) => {
|
|
472
|
+
if (data) {
|
|
473
|
+
for (let i = 0; i < data.object.length; i++) {
|
|
474
|
+
this.send(data.object[i].data);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
send(data = {}) {
|
|
482
|
+
return new Promise(async (resolve, reject) => {
|
|
483
|
+
data.clientId = this.clientId;
|
|
484
|
+
data.frameId = this.frameId;
|
|
485
|
+
|
|
486
|
+
if (!data.timeStamp)
|
|
487
|
+
data.timeStamp = new Date().toISOString();
|
|
488
|
+
|
|
489
|
+
if (!data.organization_id)
|
|
490
|
+
data.organization_id = await this.organization_id();
|
|
491
|
+
|
|
492
|
+
if (!data.apikey && this.apikey) data.apikey = this.apikey;
|
|
493
|
+
|
|
494
|
+
if (!data.user_id && this.user_id)
|
|
495
|
+
data.user_id = this.user_id;
|
|
496
|
+
|
|
497
|
+
if (!data.uuid) data.uuid = uuid();
|
|
498
|
+
|
|
499
|
+
if (!data.namespace) delete data.namespace;
|
|
500
|
+
|
|
501
|
+
if (!data.room) delete data.room;
|
|
502
|
+
|
|
503
|
+
const UUID = data.uuid;
|
|
504
|
+
const sockets = await this.getSockets(data);
|
|
505
|
+
|
|
506
|
+
for (let socket of sockets) {
|
|
507
|
+
data.socketId = socket.id;
|
|
508
|
+
if (!this.messageEvents[UUID])
|
|
509
|
+
this.addListener(UUID, resolve);
|
|
510
|
+
|
|
511
|
+
if (
|
|
512
|
+
socket.connected &&
|
|
513
|
+
this.serverOrganization &&
|
|
514
|
+
this.serverStorage &&
|
|
515
|
+
this.organizationBalance
|
|
516
|
+
) {
|
|
517
|
+
delete data.status;
|
|
518
|
+
|
|
519
|
+
socket.send(JSON.stringify(data));
|
|
520
|
+
data.status = "sent";
|
|
521
|
+
} else {
|
|
522
|
+
data.status = "queued";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (isBrowser) {
|
|
526
|
+
if (data.status !== "sent") data.status = "queued";
|
|
527
|
+
|
|
528
|
+
const storage = getIndexeddb();
|
|
529
|
+
if (storage && data.status === "queued") {
|
|
530
|
+
storage.send({
|
|
531
|
+
method: "object.update",
|
|
532
|
+
database: "system",
|
|
533
|
+
array: "messageQueue",
|
|
534
|
+
upsert: true,
|
|
535
|
+
object: {
|
|
536
|
+
_id: UUID,
|
|
537
|
+
data,
|
|
538
|
+
status: "queued"
|
|
539
|
+
},
|
|
540
|
+
organization_id: data.organization_id
|
|
541
|
+
});
|
|
542
|
+
} else if (data.status === "queued") {
|
|
543
|
+
this.messageQueue.set(UUID, data);
|
|
544
|
+
}
|
|
545
|
+
} else if (data.status === "queued") {
|
|
546
|
+
this.messageQueue.set(UUID, data);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
addListener(UUID, resolve) {
|
|
553
|
+
this.messageEvents[UUID] = resolve;
|
|
554
|
+
if (isBrowser) {
|
|
555
|
+
const self = this;
|
|
556
|
+
window.addEventListener(
|
|
557
|
+
UUID,
|
|
558
|
+
function (event) {
|
|
559
|
+
delete self.messageEvents[UUID];
|
|
560
|
+
resolve(event.detail);
|
|
561
|
+
},
|
|
562
|
+
{ once: true }
|
|
563
|
+
);
|
|
564
|
+
} else {
|
|
565
|
+
process.once(UUID, (data) => {
|
|
566
|
+
delete this.messageEvents[UUID];
|
|
567
|
+
resolve(data);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
listen(action, callback) {
|
|
573
|
+
if (!this.listeners.get(action)) {
|
|
574
|
+
this.listeners.set(action, [callback]);
|
|
575
|
+
} else {
|
|
576
|
+
this.listeners.get(action).push(callback);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
reconnect(config, socket) {
|
|
581
|
+
let self = this;
|
|
582
|
+
if (!config.currentReconnectDelay)
|
|
583
|
+
config.currentReconnectDelay = delay;
|
|
584
|
+
|
|
585
|
+
let url = socket.url;
|
|
586
|
+
if (isBrowser && !window.navigator.onLine) {
|
|
587
|
+
const online = () => {
|
|
588
|
+
window.removeEventListener("online", online);
|
|
589
|
+
self.delete(url);
|
|
590
|
+
self.create(config);
|
|
591
|
+
};
|
|
592
|
+
window.addEventListener("online", online);
|
|
593
|
+
} else {
|
|
594
|
+
setTimeout(() => {
|
|
595
|
+
if (
|
|
596
|
+
!self.maxReconnectDelay ||
|
|
597
|
+
config.currentReconnectDelay <
|
|
598
|
+
self.maxReconnectDelay
|
|
599
|
+
) {
|
|
600
|
+
self.delete(url);
|
|
601
|
+
config.currentReconnectDelay *= 2;
|
|
602
|
+
self.create(config);
|
|
603
|
+
}
|
|
604
|
+
}, config.currentReconnectDelay);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
async getUrls(data = {}) {
|
|
609
|
+
let protocol = "wss";
|
|
610
|
+
let host = data.host || config.get("host"); // Strictly resolved via isomorphic configuration
|
|
611
|
+
|
|
612
|
+
if (!host && isBrowser) {
|
|
613
|
+
host = getBrowserLocation().host;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (isBrowser) {
|
|
617
|
+
if (location.protocol.startsWith("about:")) {
|
|
618
|
+
if (
|
|
619
|
+
window.parent &&
|
|
620
|
+
window.parent.location.protocol !== "https:"
|
|
621
|
+
) {
|
|
622
|
+
protocol = "ws";
|
|
623
|
+
}
|
|
624
|
+
} else if (location.protocol !== "https:") protocol = "ws";
|
|
625
|
+
} else {
|
|
626
|
+
if (typeof host === "string") {
|
|
627
|
+
if (host.includes("localhost") || host.includes("127.0.0.1")) {
|
|
628
|
+
protocol = "ws";
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let url,
|
|
634
|
+
urls = [],
|
|
635
|
+
hostUrls = [];
|
|
636
|
+
let balancer = data.balancer || config.get("balancer"); // Strictly resolved via isomorphic configuration
|
|
637
|
+
|
|
638
|
+
if (typeof host === "string" && host) {
|
|
639
|
+
host = host.split(",");
|
|
640
|
+
for (let i = 0; i < host.length; i++) {
|
|
641
|
+
host[i] = host[i].trim();
|
|
642
|
+
if (host[i][host[i].length - 1] === "/")
|
|
643
|
+
host[i] = host[i].slice(0, -1);
|
|
644
|
+
|
|
645
|
+
if (host[i].includes("://")) url = `${host[i]}`;
|
|
646
|
+
else url = `${protocol}://${host[i]}`;
|
|
647
|
+
|
|
648
|
+
url += `/${
|
|
649
|
+
data.organization_id ||
|
|
650
|
+
(await this.organization_id()) ||
|
|
651
|
+
""
|
|
652
|
+
}`;
|
|
653
|
+
if (balancer == "mesh") urls.push(url);
|
|
654
|
+
else {
|
|
655
|
+
let socket = this.get(url);
|
|
656
|
+
if (socket !== false) {
|
|
657
|
+
urls.push(url);
|
|
658
|
+
break;
|
|
659
|
+
} else hostUrls.push(url);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (!urls.length && hostUrls.length) {
|
|
663
|
+
for (let i = 0; i < hostUrls.length; i++) {
|
|
664
|
+
this.delete(hostUrls[i]);
|
|
665
|
+
urls.push(hostUrls[i]);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} else if (isBrowser) {
|
|
669
|
+
let newhost = getBrowserLocation().host;
|
|
670
|
+
url = [`${protocol}://${newhost}`];
|
|
671
|
+
url += `/${
|
|
672
|
+
data.organization_id ||
|
|
673
|
+
(await this.organization_id()) ||
|
|
674
|
+
""
|
|
675
|
+
}`;
|
|
676
|
+
urls.push(url);
|
|
677
|
+
} else {
|
|
678
|
+
console.log("missing host");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return urls;
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
async getSockets(data) {
|
|
685
|
+
let sockets = [];
|
|
686
|
+
let urls = await this.getUrls(data);
|
|
687
|
+
for (let url of urls) {
|
|
688
|
+
let socket = this.get(url);
|
|
689
|
+
if (!socket) {
|
|
690
|
+
await this.create(data);
|
|
691
|
+
socket = this.get(url);
|
|
692
|
+
if (socket) sockets.push(socket);
|
|
693
|
+
} else {
|
|
694
|
+
sockets.push(socket);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return sockets;
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
sendLocalMessage(data) {
|
|
701
|
+
this.__fireListeners(data);
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
async broadcastLocalMessage(message) {
|
|
705
|
+
const storage = getIndexeddb();
|
|
706
|
+
if (!storage) return;
|
|
707
|
+
|
|
708
|
+
const _id = ObjectId().toString();
|
|
709
|
+
const organization_id = message.organization_id ||
|
|
710
|
+
await this.organization_id();
|
|
711
|
+
|
|
712
|
+
await storage.send({
|
|
713
|
+
method: "object.create",
|
|
714
|
+
database: "system",
|
|
715
|
+
array: "localSocketMessages",
|
|
716
|
+
object: {
|
|
717
|
+
_id,
|
|
718
|
+
message
|
|
719
|
+
},
|
|
720
|
+
organization_id
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
config.set(
|
|
724
|
+
"localSocketMessage",
|
|
725
|
+
JSON.stringify({ _id, organization_id })
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
storage.send({
|
|
729
|
+
method: "object.delete",
|
|
730
|
+
database: "system",
|
|
731
|
+
array: "localSocketMessages",
|
|
732
|
+
filter: {
|
|
733
|
+
query: {
|
|
734
|
+
"created.on": {
|
|
735
|
+
$lte: new Date(Date.now() - 5000).toISOString()
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
organization_id
|
|
740
|
+
}).catch((error) => {
|
|
741
|
+
console.error("Failed to clean up local socket messages", error);
|
|
742
|
+
});
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
async receiveLocalMessage(reference) {
|
|
746
|
+
const storage = getIndexeddb();
|
|
747
|
+
if (!storage || !reference?._id || !reference.organization_id) return;
|
|
748
|
+
|
|
749
|
+
const data = await storage.send({
|
|
750
|
+
method: "object.read",
|
|
751
|
+
database: "system",
|
|
752
|
+
array: "localSocketMessages",
|
|
753
|
+
object: { _id: reference._id },
|
|
754
|
+
organization_id: reference.organization_id
|
|
755
|
+
});
|
|
756
|
+
const message = data?.object?.[0]?.message;
|
|
757
|
+
if (message) this.sendLocalMessage(message);
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
CoCreateSocketClient.init();
|
|
762
|
+
|
|
763
|
+
if (isBrowser) {
|
|
764
|
+
window.onstorage = async (e) => {
|
|
765
|
+
if (e.key == "localSocketMessage" && e.newValue) {
|
|
766
|
+
try {
|
|
767
|
+
const reference = JSON.parse(e.newValue);
|
|
768
|
+
await CoCreateSocketClient.receiveLocalMessage(reference);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
console.error("Failed to receive local socket message", error);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export { CoCreateSocketClient as SocketClient };
|
|
777
|
+
export default CoCreateSocketClient;
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* A simple socket component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.
|
|
781
|
+
*
|
|
782
|
+
* @module socket
|
|
783
|
+
* @environment client
|
|
784
|
+
* @environment server
|
|
785
|
+
*/
|