@nymphjs/pubsub 1.0.0-beta.13 → 1.0.0-beta.131
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/CHANGELOG.md +538 -0
- package/README.md +12 -10
- package/dist/PubSub.d.ts +77 -14
- package/dist/PubSub.js +548 -213
- package/dist/PubSub.js.map +1 -1
- package/dist/PubSub.test.js +265 -81
- package/dist/PubSub.test.js.map +1 -1
- package/dist/PubSub.types.d.ts +11 -2
- package/dist/PubSub.types.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +4 -4
- package/dist/conf/defaults.js.map +1 -1
- package/dist/conf/index.d.ts +31 -2
- package/dist/conf/index.js +2 -8
- package/dist/conf/index.js.map +1 -1
- package/dist/createServer.d.ts +2 -2
- package/dist/createServer.js +14 -15
- package/dist/createServer.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -27
- package/dist/index.js.map +1 -1
- package/jest.config.js +31 -5
- package/package.json +37 -28
- package/src/PubSub.test.ts +343 -137
- package/src/PubSub.ts +796 -481
- package/src/PubSub.types.ts +11 -6
- package/src/conf/defaults.ts +2 -2
- package/src/conf/index.ts +31 -2
- package/src/createServer.ts +8 -8
- package/src/index.ts +4 -4
- package/tsconfig.json +6 -3
- package/typedoc.json +4 -0
- package/dist/conf/d.d.ts +0 -7
- package/dist/conf/d.js +0 -3
- package/dist/conf/d.js.map +0 -1
- package/src/conf/d.ts +0 -30
package/dist/PubSub.js
CHANGED
|
@@ -1,16 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { Entity, classNamesToEntityConstructors, referencesToEntities, entityConstructorsToClassNames, entitiesToReferences, } from '@nymphjs/nymph';
|
|
2
|
+
import ws from 'websocket';
|
|
3
|
+
import { difference } from 'lodash-es';
|
|
4
|
+
import { ConfigDefaults as defaults } from './conf/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* A publish/subscribe server for Nymph.
|
|
7
|
+
*
|
|
8
|
+
* Written by Hunter Perrin for SciActive.
|
|
9
|
+
*
|
|
10
|
+
* @author Hunter Perrin <hperrin@port87.com>
|
|
11
|
+
* @copyright SciActive Inc
|
|
12
|
+
* @see http://nymph.io/
|
|
13
|
+
*/
|
|
14
|
+
export default class PubSub {
|
|
15
|
+
/**
|
|
16
|
+
* The Nymph instance.
|
|
17
|
+
*/
|
|
18
|
+
nymph;
|
|
19
|
+
/**
|
|
20
|
+
* The PubSub config.
|
|
21
|
+
*/
|
|
22
|
+
config;
|
|
23
|
+
/**
|
|
24
|
+
* The WebSocket server.
|
|
25
|
+
*/
|
|
26
|
+
server;
|
|
27
|
+
sessions = new Map();
|
|
28
|
+
querySubs = {};
|
|
29
|
+
uidSubs = {};
|
|
30
|
+
outgoingClientMessages = new Map();
|
|
31
|
+
outgoingClientMessagesTimeout = null;
|
|
32
|
+
static transactionPublishes = [];
|
|
8
33
|
static initPublisher(config, nymph) {
|
|
9
|
-
const configWithDefaults = { ...
|
|
10
|
-
|
|
34
|
+
const configWithDefaults = { ...defaults, ...config };
|
|
35
|
+
const unsubscribers = [];
|
|
36
|
+
unsubscribers.push(nymph.on('beforeSaveEntity', async (enymph, entity) => {
|
|
11
37
|
const guid = entity.guid;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
38
|
+
const EntityClass = entity.constructor;
|
|
39
|
+
const etype = EntityClass.ETYPE;
|
|
40
|
+
if (!EntityClass.pubSubEnabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const off = enymph.on('afterSaveEntity', async (curNymph, result) => {
|
|
14
44
|
off();
|
|
15
45
|
off2();
|
|
16
46
|
if (!(await result)) {
|
|
@@ -30,15 +60,19 @@ class PubSub {
|
|
|
30
60
|
});
|
|
31
61
|
await this.publishTransactionPublishes(curNymph);
|
|
32
62
|
});
|
|
33
|
-
const off2 =
|
|
63
|
+
const off2 = enymph.on('failedSaveEntity', async () => {
|
|
34
64
|
off();
|
|
35
65
|
off2();
|
|
36
66
|
});
|
|
37
|
-
});
|
|
38
|
-
nymph.on('beforeDeleteEntity', async (
|
|
67
|
+
}));
|
|
68
|
+
unsubscribers.push(nymph.on('beforeDeleteEntity', async (enymph, entity) => {
|
|
39
69
|
const guid = entity.guid;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
70
|
+
const EntityClass = entity.constructor;
|
|
71
|
+
const etype = EntityClass.ETYPE;
|
|
72
|
+
if (!EntityClass.pubSubEnabled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const off = enymph.on('afterDeleteEntity', async (curNymph, result) => {
|
|
42
76
|
off();
|
|
43
77
|
off2();
|
|
44
78
|
if (!(await result)) {
|
|
@@ -57,15 +91,19 @@ class PubSub {
|
|
|
57
91
|
});
|
|
58
92
|
await this.publishTransactionPublishes(curNymph);
|
|
59
93
|
});
|
|
60
|
-
const off2 =
|
|
94
|
+
const off2 = enymph.on('failedDeleteEntity', async () => {
|
|
61
95
|
off();
|
|
62
96
|
off2();
|
|
63
97
|
});
|
|
64
|
-
});
|
|
65
|
-
nymph.on('beforeDeleteEntityByID', async (
|
|
98
|
+
}));
|
|
99
|
+
unsubscribers.push(nymph.on('beforeDeleteEntityByID', async (enymph, guid, className) => {
|
|
66
100
|
try {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
101
|
+
const EntityClass = enymph.getEntityClass(className ?? 'Entity');
|
|
102
|
+
const etype = EntityClass.ETYPE;
|
|
103
|
+
if (!EntityClass.pubSubEnabled) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const off = enymph.on('afterDeleteEntityByID', async (curNymph, result) => {
|
|
69
107
|
off();
|
|
70
108
|
off2();
|
|
71
109
|
if (!(await result)) {
|
|
@@ -84,7 +122,7 @@ class PubSub {
|
|
|
84
122
|
});
|
|
85
123
|
await this.publishTransactionPublishes(curNymph);
|
|
86
124
|
});
|
|
87
|
-
const off2 =
|
|
125
|
+
const off2 = enymph.on('failedDeleteEntityByID', async () => {
|
|
88
126
|
off();
|
|
89
127
|
off2();
|
|
90
128
|
});
|
|
@@ -92,9 +130,9 @@ class PubSub {
|
|
|
92
130
|
catch (e) {
|
|
93
131
|
return;
|
|
94
132
|
}
|
|
95
|
-
});
|
|
96
|
-
nymph.on('beforeNewUID', async (
|
|
97
|
-
const off =
|
|
133
|
+
}));
|
|
134
|
+
unsubscribers.push(nymph.on('beforeNewUID', async (enymph, name) => {
|
|
135
|
+
const off = enymph.on('afterNewUID', async (curNymph, result) => {
|
|
98
136
|
off();
|
|
99
137
|
off2();
|
|
100
138
|
const value = await result;
|
|
@@ -114,13 +152,13 @@ class PubSub {
|
|
|
114
152
|
});
|
|
115
153
|
await this.publishTransactionPublishes(curNymph);
|
|
116
154
|
});
|
|
117
|
-
const off2 =
|
|
155
|
+
const off2 = enymph.on('failedNewUID', async () => {
|
|
118
156
|
off();
|
|
119
157
|
off2();
|
|
120
158
|
});
|
|
121
|
-
});
|
|
122
|
-
nymph.on('beforeSetUID', async (
|
|
123
|
-
const off =
|
|
159
|
+
}));
|
|
160
|
+
unsubscribers.push(nymph.on('beforeSetUID', async (enymph, name, value) => {
|
|
161
|
+
const off = enymph.on('afterSetUID', async (curNymph, result) => {
|
|
124
162
|
off();
|
|
125
163
|
off2();
|
|
126
164
|
if (!(await result)) {
|
|
@@ -139,13 +177,13 @@ class PubSub {
|
|
|
139
177
|
});
|
|
140
178
|
await this.publishTransactionPublishes(curNymph);
|
|
141
179
|
});
|
|
142
|
-
const off2 =
|
|
180
|
+
const off2 = enymph.on('failedSetUID', async () => {
|
|
143
181
|
off();
|
|
144
182
|
off2();
|
|
145
183
|
});
|
|
146
|
-
});
|
|
147
|
-
nymph.on('beforeRenameUID', async (
|
|
148
|
-
const off =
|
|
184
|
+
}));
|
|
185
|
+
unsubscribers.push(nymph.on('beforeRenameUID', async (enymph, oldName, newName) => {
|
|
186
|
+
const off = enymph.on('afterRenameUID', async (curNymph, result) => {
|
|
149
187
|
off();
|
|
150
188
|
off2();
|
|
151
189
|
if (!(await result)) {
|
|
@@ -164,13 +202,13 @@ class PubSub {
|
|
|
164
202
|
});
|
|
165
203
|
await this.publishTransactionPublishes(curNymph);
|
|
166
204
|
});
|
|
167
|
-
const off2 =
|
|
205
|
+
const off2 = enymph.on('failedRenameUID', async () => {
|
|
168
206
|
off();
|
|
169
207
|
off2();
|
|
170
208
|
});
|
|
171
|
-
});
|
|
172
|
-
nymph.on('beforeDeleteUID', async (
|
|
173
|
-
const off =
|
|
209
|
+
}));
|
|
210
|
+
unsubscribers.push(nymph.on('beforeDeleteUID', async (enymph, name) => {
|
|
211
|
+
const off = enymph.on('afterDeleteUID', async (curNymph, result) => {
|
|
174
212
|
off();
|
|
175
213
|
off2();
|
|
176
214
|
if (!(await result)) {
|
|
@@ -188,23 +226,29 @@ class PubSub {
|
|
|
188
226
|
});
|
|
189
227
|
await this.publishTransactionPublishes(curNymph);
|
|
190
228
|
});
|
|
191
|
-
const off2 =
|
|
229
|
+
const off2 = enymph.on('failedDeleteUID', async () => {
|
|
192
230
|
off();
|
|
193
231
|
off2();
|
|
194
232
|
});
|
|
195
|
-
});
|
|
196
|
-
nymph.on('afterCommitTransaction', async (
|
|
233
|
+
}));
|
|
234
|
+
unsubscribers.push(nymph.on('afterCommitTransaction', async (enymph, _name, result) => {
|
|
197
235
|
if (result) {
|
|
198
|
-
await this.publishTransactionPublishes(
|
|
236
|
+
await this.publishTransactionPublishes(enymph);
|
|
199
237
|
}
|
|
200
|
-
});
|
|
201
|
-
nymph.on('afterRollbackTransaction', async (
|
|
202
|
-
this.removeTransactionPublishes(
|
|
203
|
-
});
|
|
238
|
+
}));
|
|
239
|
+
unsubscribers.push(nymph.on('afterRollbackTransaction', async (enymph) => {
|
|
240
|
+
this.removeTransactionPublishes(enymph);
|
|
241
|
+
}));
|
|
242
|
+
return () => {
|
|
243
|
+
for (let unsubscribe of unsubscribers) {
|
|
244
|
+
unsubscribe();
|
|
245
|
+
}
|
|
246
|
+
this.transactionPublishes = [];
|
|
247
|
+
};
|
|
204
248
|
}
|
|
205
249
|
static publish(message, config) {
|
|
206
250
|
for (let host of config.entries ?? []) {
|
|
207
|
-
const client = new
|
|
251
|
+
const client = new ws.client();
|
|
208
252
|
client.on('connectFailed', (error) => {
|
|
209
253
|
if (config.logger) {
|
|
210
254
|
config.logger('error', new Date().toISOString(), `Publish connection failed. (${error.toString()}, ${host})`);
|
|
@@ -224,7 +268,7 @@ class PubSub {
|
|
|
224
268
|
client.connect(host, 'nymph');
|
|
225
269
|
}
|
|
226
270
|
}
|
|
227
|
-
static
|
|
271
|
+
static isOrIsDescendant(parent, child) {
|
|
228
272
|
let check = child;
|
|
229
273
|
while (check) {
|
|
230
274
|
if (check === parent) {
|
|
@@ -236,31 +280,58 @@ class PubSub {
|
|
|
236
280
|
}
|
|
237
281
|
static async publishTransactionPublishes(nymph) {
|
|
238
282
|
if (await nymph.inTransaction()) {
|
|
283
|
+
// This instance is still in a transaction, so nothing gets published yet.
|
|
239
284
|
return;
|
|
240
285
|
}
|
|
241
|
-
|
|
242
|
-
|
|
286
|
+
let notReadyTransactionPublishes = [];
|
|
287
|
+
let readyTransactionPublishes = [];
|
|
288
|
+
for (let publish of this.transactionPublishes) {
|
|
289
|
+
if (!this.isOrIsDescendant(nymph, publish.nymph) ||
|
|
243
290
|
(await publish.nymph.inTransaction())) {
|
|
244
|
-
|
|
291
|
+
notReadyTransactionPublishes.push(publish);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
readyTransactionPublishes.push(publish);
|
|
245
295
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
296
|
+
}
|
|
297
|
+
if (!readyTransactionPublishes.length) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.transactionPublishes = notReadyTransactionPublishes;
|
|
301
|
+
let configMap = new Map();
|
|
302
|
+
for (let publish of readyTransactionPublishes) {
|
|
303
|
+
const arr = configMap.get(publish.config) || [];
|
|
304
|
+
arr.push(publish.payload);
|
|
305
|
+
configMap.set(publish.config, arr);
|
|
306
|
+
}
|
|
307
|
+
for (let [config, payloads] of configMap.entries()) {
|
|
308
|
+
if (payloads.length === 1) {
|
|
309
|
+
this.publish(payloads[0], config);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
this.publish(JSON.stringify({
|
|
313
|
+
action: 'multiple',
|
|
314
|
+
messages: payloads.map((payload) => JSON.parse(payload)),
|
|
315
|
+
}), config);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
249
318
|
}
|
|
250
319
|
static removeTransactionPublishes(nymph) {
|
|
251
320
|
this.transactionPublishes = this.transactionPublishes.filter((publish) => {
|
|
252
|
-
if (this.
|
|
321
|
+
if (this.isOrIsDescendant(nymph, publish.nymph)) {
|
|
253
322
|
return false;
|
|
254
323
|
}
|
|
255
324
|
return true;
|
|
256
325
|
});
|
|
257
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Initialize Nymph PubSub.
|
|
329
|
+
*
|
|
330
|
+
* @param config The PubSub configuration.
|
|
331
|
+
*/
|
|
258
332
|
constructor(config, nymph, server) {
|
|
259
|
-
this.sessions = new Map();
|
|
260
|
-
this.querySubs = {};
|
|
261
|
-
this.uidSubs = {};
|
|
262
333
|
this.nymph = nymph;
|
|
263
|
-
this.config = { ...
|
|
334
|
+
this.config = { ...defaults, ...config };
|
|
264
335
|
this.server = server;
|
|
265
336
|
this.server.on('request', this.handleRequest.bind(this));
|
|
266
337
|
}
|
|
@@ -269,6 +340,7 @@ class PubSub {
|
|
|
269
340
|
}
|
|
270
341
|
handleRequest(request) {
|
|
271
342
|
if (!this.config.originIsAllowed(request.origin)) {
|
|
343
|
+
// Make sure we only accept requests from an allowed origin
|
|
272
344
|
request.reject();
|
|
273
345
|
this.config.logger('log', new Date().toISOString(), 'Client from origin ' + request.origin + ' was kicked by the bouncer.');
|
|
274
346
|
return;
|
|
@@ -285,28 +357,58 @@ class PubSub {
|
|
|
285
357
|
this.onClose(connection, description);
|
|
286
358
|
});
|
|
287
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Handle a message from a client.
|
|
362
|
+
*/
|
|
288
363
|
async onMessage(from, msg) {
|
|
289
364
|
if (msg.type !== 'utf8') {
|
|
290
365
|
throw new Error("This server doesn't accept binary messages.");
|
|
291
366
|
}
|
|
292
367
|
const data = JSON.parse(msg.utf8Data);
|
|
293
368
|
if (!data.action ||
|
|
294
|
-
[
|
|
369
|
+
[
|
|
370
|
+
'authenticate',
|
|
371
|
+
'subscribe',
|
|
372
|
+
'unsubscribe',
|
|
373
|
+
'publish',
|
|
374
|
+
'multiple',
|
|
375
|
+
].indexOf(data.action) === -1) {
|
|
295
376
|
return;
|
|
296
377
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
378
|
+
const handleMessageData = async (data, dontRelay = false) => {
|
|
379
|
+
switch (data.action) {
|
|
380
|
+
case 'authenticate':
|
|
381
|
+
this.handleAuthentication(from, data);
|
|
382
|
+
break;
|
|
383
|
+
case 'subscribe':
|
|
384
|
+
case 'unsubscribe':
|
|
385
|
+
await this.handleSubscription(from, data);
|
|
386
|
+
break;
|
|
387
|
+
case 'publish':
|
|
388
|
+
await this.handlePublish(from, data);
|
|
389
|
+
if (!dontRelay) {
|
|
390
|
+
// Relay the publish to other servers.
|
|
391
|
+
this.relay(msg);
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
case 'multiple':
|
|
395
|
+
for (let individual of data.messages) {
|
|
396
|
+
await handleMessageData(individual,
|
|
397
|
+
// These will be published with the multiple payload.
|
|
398
|
+
individual.action === 'publish');
|
|
399
|
+
}
|
|
400
|
+
if (data.messages.find((msg) => msg.action === 'publish') != null) {
|
|
401
|
+
// Relay the publish to other servers.
|
|
402
|
+
this.relay(msg);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
await handleMessageData(data);
|
|
309
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Clean up after users who leave.
|
|
411
|
+
*/
|
|
310
412
|
onClose(conn, description) {
|
|
311
413
|
this.config.logger('log', new Date().toISOString(), `Client skedaddled. (${description}, ${conn.remoteAddress})`);
|
|
312
414
|
let mess = 0;
|
|
@@ -325,6 +427,7 @@ class PubSub {
|
|
|
325
427
|
}
|
|
326
428
|
else {
|
|
327
429
|
if (this.config.broadcastCounts) {
|
|
430
|
+
// Notify clients of the subscription count.
|
|
328
431
|
for (let key of curClients.keys()) {
|
|
329
432
|
const curData = curClients.get(key);
|
|
330
433
|
if (curData && curData.count) {
|
|
@@ -350,6 +453,7 @@ class PubSub {
|
|
|
350
453
|
}
|
|
351
454
|
else {
|
|
352
455
|
if (this.config.broadcastCounts) {
|
|
456
|
+
// Notify clients of the subscription count.
|
|
353
457
|
for (const key of curClients.keys()) {
|
|
354
458
|
const curData = curClients.get(key);
|
|
355
459
|
if (curData && curData.count) {
|
|
@@ -375,72 +479,115 @@ class PubSub {
|
|
|
375
479
|
onError(conn, e) {
|
|
376
480
|
this.config.logger('error', new Date().toISOString(), `An error occured. (${e.message}, ${conn.remoteAddress})`);
|
|
377
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Handle an authentication from a client.
|
|
484
|
+
*/
|
|
378
485
|
handleAuthentication(from, data) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
486
|
+
// Save the user's auth token in session storage.
|
|
487
|
+
const authToken = data.authToken;
|
|
488
|
+
const switchToken = data.switchToken;
|
|
489
|
+
if (authToken != null) {
|
|
490
|
+
this.sessions.set(from, { authToken, switchToken });
|
|
382
491
|
}
|
|
383
492
|
else if (this.sessions.has(from)) {
|
|
384
493
|
this.sessions.delete(from);
|
|
385
494
|
}
|
|
386
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* Handle a subscribe or unsubscribe from a client.
|
|
498
|
+
*/
|
|
387
499
|
async handleSubscription(from, data) {
|
|
388
|
-
if ('query' in data && data.query != null) {
|
|
389
|
-
await this.handleSubscriptionQuery(from, data);
|
|
390
|
-
}
|
|
391
|
-
else if ('uid' in data &&
|
|
392
|
-
data.uid != null &&
|
|
393
|
-
typeof data.uid == 'string') {
|
|
394
|
-
await this.handleSubscriptionUid(from, data);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
async handleSubscriptionQuery(from, data, qrefParent) {
|
|
398
|
-
let args;
|
|
399
|
-
let EntityClass;
|
|
400
500
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
501
|
+
if ('query' in data && data.query != null) {
|
|
502
|
+
// Request is for a query.
|
|
503
|
+
await this.handleSubscriptionQuery(from, data);
|
|
504
|
+
}
|
|
505
|
+
else if ('uid' in data &&
|
|
506
|
+
data.uid != null &&
|
|
507
|
+
typeof data.uid == 'string') {
|
|
508
|
+
// Request is for a UID.
|
|
509
|
+
await this.handleSubscriptionUid(from, data);
|
|
510
|
+
}
|
|
403
511
|
}
|
|
404
512
|
catch (e) {
|
|
405
|
-
|
|
513
|
+
if ('query' in data && data.query != null) {
|
|
514
|
+
from.sendUTF(JSON.stringify({ query: data.query, error: e.message }));
|
|
515
|
+
}
|
|
516
|
+
else if ('uid' in data && data.uid != null) {
|
|
517
|
+
from.sendUTF(JSON.stringify({ uid: data.uid, error: e.message }));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
from.sendUTF(JSON.stringify({ error: e.message }));
|
|
521
|
+
}
|
|
406
522
|
}
|
|
407
|
-
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Handle a subscribe or unsubscribe for a query from a client.
|
|
526
|
+
*/
|
|
527
|
+
async handleSubscriptionQuery(from, data, qrefParent) {
|
|
528
|
+
let args = JSON.parse(data.query);
|
|
408
529
|
const serialArgs = JSON.stringify(args);
|
|
409
|
-
|
|
530
|
+
let [clientOptions, ...selectors] = args;
|
|
531
|
+
clientOptions = classNamesToEntityConstructors(this.nymph, clientOptions, true);
|
|
532
|
+
selectors = classNamesToEntityConstructors(this.nymph, referencesToEntities(selectors, this.nymph, false, true), true);
|
|
533
|
+
let EntityClass = clientOptions.class;
|
|
534
|
+
const etype = EntityClass.ETYPE;
|
|
410
535
|
const options = {
|
|
411
536
|
...clientOptions,
|
|
412
537
|
class: EntityClass,
|
|
413
|
-
return: '
|
|
538
|
+
return: 'entity',
|
|
414
539
|
source: 'client',
|
|
415
540
|
};
|
|
416
|
-
|
|
541
|
+
// Find qref queries.
|
|
542
|
+
const qrefQueries = this.findQRefQueries(...selectors);
|
|
417
543
|
if (data.action === 'subscribe') {
|
|
544
|
+
// Client is subscribing to a query.
|
|
545
|
+
// First subscribe to qrefQueries, giving this one as a reference.
|
|
418
546
|
for (const qrefQuery of qrefQueries) {
|
|
547
|
+
let [options, ...selectors] = qrefQuery;
|
|
548
|
+
options = entityConstructorsToClassNames(options);
|
|
549
|
+
selectors = entityConstructorsToClassNames(entitiesToReferences(selectors));
|
|
419
550
|
await this.handleSubscriptionQuery(from, {
|
|
420
551
|
action: 'subscribe',
|
|
421
|
-
query: JSON.stringify(
|
|
552
|
+
query: JSON.stringify([options, ...selectors]),
|
|
422
553
|
}, {
|
|
423
554
|
etype,
|
|
424
555
|
query: serialArgs,
|
|
425
556
|
});
|
|
426
557
|
}
|
|
558
|
+
// Now subscribe to this query.
|
|
427
559
|
if (!(etype in this.querySubs)) {
|
|
428
560
|
this.querySubs[etype] = {};
|
|
429
561
|
}
|
|
430
562
|
if (!(serialArgs in this.querySubs[etype])) {
|
|
431
563
|
this.querySubs[etype][serialArgs] = new Map();
|
|
432
564
|
}
|
|
433
|
-
let
|
|
565
|
+
let authToken = null;
|
|
566
|
+
let switchToken = null;
|
|
434
567
|
if (this.sessions.has(from)) {
|
|
435
|
-
|
|
568
|
+
const session = this.sessions.get(from);
|
|
569
|
+
authToken = session?.authToken;
|
|
570
|
+
switchToken = session?.switchToken;
|
|
436
571
|
}
|
|
437
572
|
const nymph = this.nymph.clone();
|
|
438
|
-
if (nymph.tilmeld != null &&
|
|
439
|
-
const user = await nymph.tilmeld.extractToken(
|
|
440
|
-
if (user) {
|
|
441
|
-
|
|
573
|
+
if (nymph.tilmeld != null && authToken != null) {
|
|
574
|
+
const user = await nymph.tilmeld.extractToken(authToken);
|
|
575
|
+
if (user && user.enabled) {
|
|
576
|
+
if (switchToken != null) {
|
|
577
|
+
const switchUser = await nymph.tilmeld.extractToken(switchToken);
|
|
578
|
+
if (switchUser) {
|
|
579
|
+
// Log in the switchUser for access controls.
|
|
580
|
+
await nymph.tilmeld.fillSession(switchUser);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
// Log in the user for access controls.
|
|
585
|
+
await nymph.tilmeld.fillSession(user);
|
|
586
|
+
}
|
|
442
587
|
}
|
|
443
588
|
}
|
|
589
|
+
let entities = [];
|
|
590
|
+
let sendEntities = false;
|
|
444
591
|
const existingSub = this.querySubs[etype][serialArgs].get(from);
|
|
445
592
|
if (existingSub) {
|
|
446
593
|
if (qrefParent) {
|
|
@@ -452,21 +599,42 @@ class PubSub {
|
|
|
452
599
|
if (data.count && !existingSub.count) {
|
|
453
600
|
existingSub.count = true;
|
|
454
601
|
}
|
|
602
|
+
sendEntities = existingSub.direct;
|
|
603
|
+
if (sendEntities) {
|
|
604
|
+
entities = existingSub.current.length
|
|
605
|
+
? await nymph.getEntities({
|
|
606
|
+
class: EntityClass,
|
|
607
|
+
source: 'client',
|
|
608
|
+
}, { type: '|', guid: existingSub.current })
|
|
609
|
+
: [];
|
|
610
|
+
}
|
|
455
611
|
}
|
|
456
612
|
else {
|
|
613
|
+
entities = await nymph.getEntities(options, ...selectors);
|
|
457
614
|
this.querySubs[etype][serialArgs].set(from, {
|
|
458
|
-
current:
|
|
615
|
+
current: entities.map((e) => e.guid),
|
|
459
616
|
query: data.query,
|
|
460
617
|
qrefParents: qrefParent ? [qrefParent] : [],
|
|
461
618
|
direct: !qrefParent,
|
|
462
619
|
count: !!data.count,
|
|
463
620
|
});
|
|
621
|
+
sendEntities = !qrefParent;
|
|
464
622
|
}
|
|
465
|
-
if (
|
|
623
|
+
if (sendEntities) {
|
|
624
|
+
// Notify the client of the current value.
|
|
625
|
+
from.sendUTF(JSON.stringify({
|
|
626
|
+
query: data.query,
|
|
627
|
+
set: true,
|
|
628
|
+
data: entities,
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
if (nymph.tilmeld != null && authToken != null) {
|
|
632
|
+
// Clear the user that was temporarily logged in.
|
|
466
633
|
nymph.tilmeld.clearSession();
|
|
467
634
|
}
|
|
468
635
|
this.config.logger('log', new Date().toISOString(), `Client subscribed to a query! (${serialArgs}, ${from.remoteAddress})`);
|
|
469
636
|
if (this.config.broadcastCounts) {
|
|
637
|
+
// Notify clients of the subscription count.
|
|
470
638
|
const count = this.querySubs[etype][serialArgs].size;
|
|
471
639
|
for (let key of this.querySubs[etype][serialArgs].keys()) {
|
|
472
640
|
const curData = this.querySubs[etype][serialArgs].get(key);
|
|
@@ -480,15 +648,21 @@ class PubSub {
|
|
|
480
648
|
}
|
|
481
649
|
}
|
|
482
650
|
if (data.action === 'unsubscribe') {
|
|
651
|
+
// Client is unsubscribing from a query.
|
|
652
|
+
// First unsubscribe from qrefQueries.
|
|
483
653
|
for (const qrefQuery of qrefQueries) {
|
|
654
|
+
let [options, ...selectors] = qrefQuery;
|
|
655
|
+
options = entityConstructorsToClassNames(options);
|
|
656
|
+
selectors = entityConstructorsToClassNames(entitiesToReferences(selectors));
|
|
484
657
|
await this.handleSubscriptionQuery(from, {
|
|
485
658
|
action: 'unsubscribe',
|
|
486
|
-
query: JSON.stringify(
|
|
659
|
+
query: JSON.stringify([options, ...selectors]),
|
|
487
660
|
}, {
|
|
488
661
|
etype,
|
|
489
662
|
query: serialArgs,
|
|
490
663
|
});
|
|
491
664
|
}
|
|
665
|
+
// Now unsubscribe from this query.
|
|
492
666
|
if (!(etype in this.querySubs)) {
|
|
493
667
|
return;
|
|
494
668
|
}
|
|
@@ -514,6 +688,7 @@ class PubSub {
|
|
|
514
688
|
this.config.logger('log', new Date().toISOString(), `Client unsubscribed from a query! (${serialArgs}, ${from.remoteAddress})`);
|
|
515
689
|
const count = this.querySubs[etype][serialArgs].size;
|
|
516
690
|
if (count === 0) {
|
|
691
|
+
// No more subscribed clients.
|
|
517
692
|
delete this.querySubs[etype][serialArgs];
|
|
518
693
|
if (Object.keys(this.querySubs[etype]).length === 0) {
|
|
519
694
|
delete this.querySubs[etype];
|
|
@@ -521,6 +696,7 @@ class PubSub {
|
|
|
521
696
|
return;
|
|
522
697
|
}
|
|
523
698
|
if (this.config.broadcastCounts) {
|
|
699
|
+
// Notify clients of the subscription count.
|
|
524
700
|
for (let key of this.querySubs[etype][serialArgs].keys()) {
|
|
525
701
|
const curData = this.querySubs[etype][serialArgs].get(key);
|
|
526
702
|
if (curData && curData.count) {
|
|
@@ -533,16 +709,55 @@ class PubSub {
|
|
|
533
709
|
}
|
|
534
710
|
}
|
|
535
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Handle a subscribe or unsubscribe for a UID from a client.
|
|
714
|
+
*/
|
|
536
715
|
async handleSubscriptionUid(from, data) {
|
|
537
716
|
if (data.action === 'subscribe') {
|
|
717
|
+
// Client is subscribing to a UID.
|
|
538
718
|
if (!(data.uid in this.uidSubs)) {
|
|
539
719
|
this.uidSubs[data.uid] = new Map();
|
|
540
720
|
}
|
|
541
721
|
this.uidSubs[data['uid']].set(from, {
|
|
542
722
|
count: !!data.count,
|
|
543
723
|
});
|
|
724
|
+
let authToken = null;
|
|
725
|
+
let switchToken = null;
|
|
726
|
+
if (this.sessions.has(from)) {
|
|
727
|
+
const session = this.sessions.get(from);
|
|
728
|
+
authToken = session?.authToken;
|
|
729
|
+
switchToken = session?.switchToken;
|
|
730
|
+
}
|
|
731
|
+
const nymph = this.nymph.clone();
|
|
732
|
+
if (nymph.tilmeld != null && authToken != null) {
|
|
733
|
+
const user = await nymph.tilmeld.extractToken(authToken);
|
|
734
|
+
if (user && user.enabled) {
|
|
735
|
+
if (switchToken != null) {
|
|
736
|
+
const switchUser = await nymph.tilmeld.extractToken(switchToken);
|
|
737
|
+
if (switchUser) {
|
|
738
|
+
// Log in the switchUser for access controls.
|
|
739
|
+
await nymph.tilmeld.fillSession(switchUser);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
// Log in the user for access controls.
|
|
744
|
+
await nymph.tilmeld.fillSession(user);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// Notify the client of the current value.
|
|
749
|
+
from.sendUTF(JSON.stringify({
|
|
750
|
+
uid: data.uid,
|
|
751
|
+
set: true,
|
|
752
|
+
data: await this.nymph.getUID(data.uid),
|
|
753
|
+
}));
|
|
754
|
+
if (nymph.tilmeld != null && authToken != null) {
|
|
755
|
+
// Clear the user that was temporarily logged in.
|
|
756
|
+
nymph.tilmeld.clearSession();
|
|
757
|
+
}
|
|
544
758
|
this.config.logger('log', new Date().toISOString(), `Client subscribed to a UID! (${data.uid}, ${from.remoteAddress})`);
|
|
545
759
|
if (this.config.broadcastCounts) {
|
|
760
|
+
// Notify clients of the subscription count.
|
|
546
761
|
const count = this.uidSubs[data.uid].size;
|
|
547
762
|
for (let key of this.uidSubs[data.uid].keys()) {
|
|
548
763
|
const curData = this.uidSubs[data.uid].get(key);
|
|
@@ -556,6 +771,7 @@ class PubSub {
|
|
|
556
771
|
}
|
|
557
772
|
}
|
|
558
773
|
if (data.action === 'unsubscribe') {
|
|
774
|
+
// Client is unsubscribing from a UID.
|
|
559
775
|
if (!(data.uid in this.uidSubs)) {
|
|
560
776
|
return;
|
|
561
777
|
}
|
|
@@ -566,10 +782,12 @@ class PubSub {
|
|
|
566
782
|
this.config.logger('log', new Date().toISOString(), `Client unsubscribed from a UID! (${data.uid}, ${from.remoteAddress})`);
|
|
567
783
|
const count = this.uidSubs[data.uid].size;
|
|
568
784
|
if (count === 0) {
|
|
785
|
+
// No more subscribed clients.
|
|
569
786
|
delete this.uidSubs[data.uid];
|
|
570
787
|
return;
|
|
571
788
|
}
|
|
572
789
|
if (this.config.broadcastCounts) {
|
|
790
|
+
// Notify clients of the subscription count.
|
|
573
791
|
for (let key of this.uidSubs[data.uid].keys()) {
|
|
574
792
|
const curData = this.uidSubs[data.uid].get(key);
|
|
575
793
|
if (curData && curData.count) {
|
|
@@ -582,7 +800,10 @@ class PubSub {
|
|
|
582
800
|
}
|
|
583
801
|
}
|
|
584
802
|
}
|
|
585
|
-
|
|
803
|
+
/**
|
|
804
|
+
* Handle a publish from a client.
|
|
805
|
+
*/
|
|
806
|
+
async handlePublish(from, data) {
|
|
586
807
|
if ('guid' in data &&
|
|
587
808
|
typeof data.guid === 'string' &&
|
|
588
809
|
data.guid.match(/^[0-9a-f]{24}$/) &&
|
|
@@ -590,8 +811,8 @@ class PubSub {
|
|
|
590
811
|
((data.event === 'create' || data.event === 'update') &&
|
|
591
812
|
'entity' in data &&
|
|
592
813
|
data.entity != null))) {
|
|
814
|
+
// Publish is an entity.
|
|
593
815
|
await this.handlePublishEntity(from, data);
|
|
594
|
-
this.relay(msg);
|
|
595
816
|
}
|
|
596
817
|
if ((('name' in data && typeof data.name === 'string') ||
|
|
597
818
|
('oldName' in data &&
|
|
@@ -599,10 +820,13 @@ class PubSub {
|
|
|
599
820
|
'newName' in data &&
|
|
600
821
|
typeof data.newName === 'string')) &&
|
|
601
822
|
['newUID', 'setUID', 'renameUID', 'deleteUID'].indexOf(data.event) !== -1) {
|
|
823
|
+
// Publish is a UID.
|
|
602
824
|
await this.handlePublishUid(from, data);
|
|
603
|
-
this.relay(msg);
|
|
604
825
|
}
|
|
605
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Handle an entity publish from a client.
|
|
829
|
+
*/
|
|
606
830
|
async handlePublishEntity(from, data) {
|
|
607
831
|
this.config.logger('log', new Date().toISOString(), `Received an entity publish! (${data.guid}, ${data.event}, ${from.remoteAddress})`);
|
|
608
832
|
const etype = data.etype;
|
|
@@ -613,6 +837,7 @@ class PubSub {
|
|
|
613
837
|
const curClients = this.querySubs[etype][curQuery];
|
|
614
838
|
const updatedClients = new Set();
|
|
615
839
|
if (data.event === 'delete' || data.event === 'update') {
|
|
840
|
+
// Check if it is in any client's currents.
|
|
616
841
|
try {
|
|
617
842
|
for (const curClient of curClients.keys()) {
|
|
618
843
|
const curData = curClients.get(curClient);
|
|
@@ -620,7 +845,7 @@ class PubSub {
|
|
|
620
845
|
continue;
|
|
621
846
|
}
|
|
622
847
|
if (curData.current.indexOf(data.guid) !== -1) {
|
|
623
|
-
|
|
848
|
+
this.addOutgoingClientMessage(curClient, curData, data);
|
|
624
849
|
updatedClients.add(curClient);
|
|
625
850
|
}
|
|
626
851
|
}
|
|
@@ -630,36 +855,46 @@ class PubSub {
|
|
|
630
855
|
}
|
|
631
856
|
}
|
|
632
857
|
if ((data.event === 'create' || data.event === 'update') && data.entity) {
|
|
858
|
+
// Check if it matches the query.
|
|
633
859
|
try {
|
|
634
|
-
|
|
860
|
+
let [clientOptions, ...selectors] = JSON.parse(curQuery);
|
|
861
|
+
clientOptions = classNamesToEntityConstructors(this.nymph, clientOptions, true);
|
|
862
|
+
selectors = classNamesToEntityConstructors(this.nymph, referencesToEntities(selectors, this.nymph, false, true), true);
|
|
635
863
|
const qrefQueries = this.findQRefQueries(clientOptions, ...selectors);
|
|
636
|
-
const EntityClass =
|
|
637
|
-
const
|
|
638
|
-
entityData
|
|
639
|
-
entityData.
|
|
864
|
+
const EntityClass = clientOptions.class;
|
|
865
|
+
const entityJson = classNamesToEntityConstructors(this.nymph, referencesToEntities(data.entity, this.nymph, false, true), true);
|
|
866
|
+
const entityData = entityJson.data;
|
|
867
|
+
entityData.cdate = entityJson.cdate;
|
|
868
|
+
entityData.mdate = entityJson.mdate;
|
|
640
869
|
const entitySData = {};
|
|
641
|
-
if (
|
|
870
|
+
if (!(entityJson.class.prototype instanceof Entity) ||
|
|
871
|
+
!entityJson.class.restEnabled) {
|
|
642
872
|
throw new Error(`Received entity data class is not valid: ${data.entity.class}`);
|
|
643
873
|
}
|
|
644
|
-
const DataEntityClass =
|
|
874
|
+
const DataEntityClass = entityJson.class;
|
|
645
875
|
if (EntityClass.ETYPE === DataEntityClass.ETYPE &&
|
|
646
876
|
(qrefQueries.length ||
|
|
647
|
-
this.nymph.driver.checkData(entityData, entitySData, selectors, data.guid,
|
|
877
|
+
this.nymph.driver.checkData(EntityClass.ETYPE, entityData, entitySData, selectors, data.guid, entityJson.tags ?? []))) {
|
|
878
|
+
// It either matches the query, or there are qref queries.
|
|
648
879
|
for (let curClient of curClients.keys()) {
|
|
649
880
|
if (updatedClients.has(curClient)) {
|
|
881
|
+
// The user was already notified. (Of an update.)
|
|
650
882
|
continue;
|
|
651
883
|
}
|
|
652
884
|
const curData = curClients.get(curClient);
|
|
653
885
|
if (!curData) {
|
|
654
886
|
continue;
|
|
655
887
|
}
|
|
888
|
+
// If there are qref queries, we need to dive into the user's
|
|
889
|
+
// current data and translate them before running checkData.
|
|
656
890
|
if (qrefQueries.length) {
|
|
657
891
|
const translatedSelectors = this.translateQRefSelectors(curClient, selectors);
|
|
658
|
-
if (!this.nymph.driver.checkData(entityData, entitySData, translatedSelectors, data.guid,
|
|
892
|
+
if (!this.nymph.driver.checkData(EntityClass.ETYPE, entityData, entitySData, translatedSelectors, data.guid, entityJson.tags ?? [])) {
|
|
893
|
+
// The query doesn't match when the qref queries are filled.
|
|
659
894
|
continue;
|
|
660
895
|
}
|
|
661
896
|
}
|
|
662
|
-
|
|
897
|
+
this.addOutgoingClientMessage(curClient, curData, data);
|
|
663
898
|
}
|
|
664
899
|
}
|
|
665
900
|
}
|
|
@@ -669,85 +904,142 @@ class PubSub {
|
|
|
669
904
|
}
|
|
670
905
|
}
|
|
671
906
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const options = {
|
|
679
|
-
...clientOptions,
|
|
680
|
-
class: nymph.getEntityClass(clientOptions.class),
|
|
681
|
-
return: 'entity',
|
|
682
|
-
source: 'client',
|
|
683
|
-
skipAc: false,
|
|
684
|
-
};
|
|
685
|
-
const selectors = (0, nymph_1.classNamesToEntityConstructors)(nymph, clientSelectors);
|
|
686
|
-
if (this.sessions.has(curClient)) {
|
|
687
|
-
token = this.sessions.get(curClient);
|
|
688
|
-
}
|
|
689
|
-
if (nymph.tilmeld != null && token != null) {
|
|
690
|
-
const user = await nymph.tilmeld.extractToken(token);
|
|
691
|
-
if (user) {
|
|
692
|
-
nymph.tilmeld.fillSession(user);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
current = await nymph.getEntities(options, ...selectors);
|
|
907
|
+
addOutgoingClientMessage(curClient, curData, data) {
|
|
908
|
+
const arr = this.outgoingClientMessages.get(curClient) || [];
|
|
909
|
+
arr.push({ subscription: curData, publish: data });
|
|
910
|
+
this.outgoingClientMessages.set(curClient, arr);
|
|
911
|
+
if (this.outgoingClientMessagesTimeout) {
|
|
912
|
+
clearImmediate(this.outgoingClientMessagesTimeout);
|
|
696
913
|
}
|
|
697
|
-
|
|
698
|
-
this.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
for (let
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
removed: guid,
|
|
711
|
-
}));
|
|
914
|
+
this.outgoingClientMessagesTimeout = setImmediate(async () => {
|
|
915
|
+
await this.updateClients();
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
async updateClients() {
|
|
919
|
+
const outgoingClientMessages = [...this.outgoingClientMessages.entries()];
|
|
920
|
+
this.outgoingClientMessages.clear();
|
|
921
|
+
for (let [curClient, entries] of outgoingClientMessages) {
|
|
922
|
+
const queryMap = {};
|
|
923
|
+
for (let entry of entries) {
|
|
924
|
+
const arr = queryMap[entry.subscription.query] || [];
|
|
925
|
+
arr.push(entry);
|
|
926
|
+
queryMap[entry.subscription.query] = arr;
|
|
712
927
|
}
|
|
713
|
-
for (let
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
928
|
+
for (let [query, queryEntries] of Object.entries(queryMap)) {
|
|
929
|
+
// Update currents list.
|
|
930
|
+
let current;
|
|
931
|
+
let authToken;
|
|
932
|
+
let switchToken;
|
|
933
|
+
const nymph = this.nymph.clone();
|
|
934
|
+
try {
|
|
935
|
+
const [clientOptions, ...clientSelectors] = JSON.parse(query);
|
|
936
|
+
const options = {
|
|
937
|
+
...clientOptions,
|
|
938
|
+
class: classNamesToEntityConstructors(nymph, clientOptions.class, true),
|
|
939
|
+
return: 'entity',
|
|
940
|
+
source: 'client',
|
|
941
|
+
skipAc: false,
|
|
942
|
+
};
|
|
943
|
+
const selectors = classNamesToEntityConstructors(nymph, referencesToEntities(clientSelectors, nymph, false, true), true);
|
|
944
|
+
if (this.sessions.has(curClient)) {
|
|
945
|
+
const session = this.sessions.get(curClient);
|
|
946
|
+
authToken = session?.authToken;
|
|
947
|
+
switchToken = session?.switchToken;
|
|
948
|
+
}
|
|
949
|
+
if (nymph.tilmeld != null && authToken != null) {
|
|
950
|
+
const user = await nymph.tilmeld.extractToken(authToken);
|
|
951
|
+
if (user && user.enabled) {
|
|
952
|
+
if (switchToken != null) {
|
|
953
|
+
const switchUser = await nymph.tilmeld.extractToken(switchToken);
|
|
954
|
+
if (switchUser) {
|
|
955
|
+
// Log in the switchUser for access controls.
|
|
956
|
+
await nymph.tilmeld.fillSession(switchUser);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
// Log in the user for access controls.
|
|
961
|
+
await nymph.tilmeld.fillSession(user);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
current = await nymph.getEntities(options, ...selectors);
|
|
718
966
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
data: entity,
|
|
723
|
-
}));
|
|
724
|
-
}
|
|
725
|
-
if (data.event === 'update' && data.guid in entityMap) {
|
|
726
|
-
const entity = entityMap[data.guid];
|
|
727
|
-
this.config.logger('log', new Date().toISOString(), `Notifying client of update! (${curClient.remoteAddress})`);
|
|
728
|
-
if (typeof entity.updateDataProtection === 'function') {
|
|
729
|
-
entity.updateDataProtection();
|
|
967
|
+
catch (e) {
|
|
968
|
+
this.config.logger('error', new Date().toISOString(), `Error updating client! (${e?.message}, ${curClient.remoteAddress})`);
|
|
969
|
+
return;
|
|
730
970
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
971
|
+
const entityMap = Object.fromEntries(current.map((entity) => [entity.guid, entity]));
|
|
972
|
+
// These are the current GUIDs for this set of subscriptions/publishes.
|
|
973
|
+
const currentGuids = current.map((entity) => entity.guid ?? '');
|
|
974
|
+
const clientUpdates = [];
|
|
975
|
+
for (let { subscription, publish } of queryEntries) {
|
|
976
|
+
const removed = difference(subscription.current, currentGuids);
|
|
977
|
+
const added = difference(currentGuids, subscription.current);
|
|
978
|
+
if (subscription.direct) {
|
|
979
|
+
for (let guid of removed) {
|
|
980
|
+
// Notify subscriber.
|
|
981
|
+
this.config.logger('log', new Date().toISOString(), `Notifying client of removal! (${curClient.remoteAddress})`);
|
|
982
|
+
clientUpdates.push({
|
|
983
|
+
query: subscription.query,
|
|
984
|
+
removed: guid,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
for (let guid of added) {
|
|
988
|
+
const entity = entityMap[guid];
|
|
989
|
+
// Notify client.
|
|
990
|
+
this.config.logger('log', new Date().toISOString(), `Notifying client of new match! (${curClient.remoteAddress})`);
|
|
991
|
+
if (typeof entity.updateDataProtection === 'function') {
|
|
992
|
+
entity.updateDataProtection();
|
|
993
|
+
}
|
|
994
|
+
clientUpdates.push({
|
|
995
|
+
query: subscription.query,
|
|
996
|
+
added: guid,
|
|
997
|
+
data: entity.toJSON(),
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (publish.event === 'update' && publish.guid in entityMap) {
|
|
1001
|
+
const entity = entityMap[publish.guid];
|
|
1002
|
+
// Notify subscriber.
|
|
1003
|
+
this.config.logger('log', new Date().toISOString(), `Notifying client of update! (${curClient.remoteAddress})`);
|
|
1004
|
+
if (typeof entity.updateDataProtection === 'function') {
|
|
1005
|
+
entity.updateDataProtection();
|
|
1006
|
+
}
|
|
1007
|
+
clientUpdates.push({
|
|
1008
|
+
query: subscription.query,
|
|
1009
|
+
updated: publish.guid,
|
|
1010
|
+
data: entity.toJSON(),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Update subscription.
|
|
1015
|
+
subscription.current = currentGuids;
|
|
1016
|
+
if (nymph.tilmeld != null && authToken != null) {
|
|
1017
|
+
// Clear the user that was temporarily logged in.
|
|
1018
|
+
nymph.tilmeld.clearSession();
|
|
1019
|
+
}
|
|
1020
|
+
if ((removed.length || added.length) &&
|
|
1021
|
+
subscription.qrefParents.length) {
|
|
1022
|
+
// All qref parents need to be rerun.
|
|
1023
|
+
for (const qrefParent of subscription.qrefParents) {
|
|
1024
|
+
const subData = this.querySubs[qrefParent.etype][qrefParent.query].get(curClient);
|
|
1025
|
+
if (subData) {
|
|
1026
|
+
this.addOutgoingClientMessage(curClient, subData, publish);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (clientUpdates.length === 1) {
|
|
1032
|
+
curClient.sendUTF(JSON.stringify(clientUpdates[0]));
|
|
1033
|
+
}
|
|
1034
|
+
else if (clientUpdates.length > 1) {
|
|
1035
|
+
curClient.sendUTF(JSON.stringify({ query, multiple: true, messages: clientUpdates }));
|
|
747
1036
|
}
|
|
748
1037
|
}
|
|
749
1038
|
}
|
|
750
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Handle a UID publish from a client.
|
|
1042
|
+
*/
|
|
751
1043
|
async handlePublishUid(from, data) {
|
|
752
1044
|
this.config.logger('log', new Date().toISOString(), `Received a UID publish! (${'name' in data ? data.name : `${data.oldName} => ${data.newName}`}, ${data.event}, ${from.remoteAddress})`);
|
|
753
1045
|
const names = [data.name, data.oldName].filter((name) => name != null);
|
|
@@ -784,13 +1076,16 @@ class PubSub {
|
|
|
784
1076
|
}
|
|
785
1077
|
}
|
|
786
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Relay publish data to other servers.
|
|
1081
|
+
*/
|
|
787
1082
|
relay(message) {
|
|
788
1083
|
if (message.type !== 'utf8') {
|
|
789
1084
|
this.config.logger('error', new Date().toISOString(), `Can't relay non UTF8 message.`);
|
|
790
1085
|
return;
|
|
791
1086
|
}
|
|
792
1087
|
for (let host of this.config.relays) {
|
|
793
|
-
const client = new
|
|
1088
|
+
const client = new ws.client();
|
|
794
1089
|
client.on('connectFailed', (error) => {
|
|
795
1090
|
this.config.logger('error', new Date().toISOString(), `Relay connection failed. (${error.toString()}, ${host})`);
|
|
796
1091
|
});
|
|
@@ -806,7 +1101,7 @@ class PubSub {
|
|
|
806
1101
|
client.connect(host, 'nymph');
|
|
807
1102
|
}
|
|
808
1103
|
}
|
|
809
|
-
findQRefQueries(
|
|
1104
|
+
findQRefQueries(...selectors) {
|
|
810
1105
|
const qrefQueries = [];
|
|
811
1106
|
for (const curSelector of selectors) {
|
|
812
1107
|
for (const k in curSelector) {
|
|
@@ -815,25 +1110,29 @@ class PubSub {
|
|
|
815
1110
|
if (key === 'type') {
|
|
816
1111
|
continue;
|
|
817
1112
|
}
|
|
818
|
-
if (value
|
|
1113
|
+
if (value == null) {
|
|
819
1114
|
continue;
|
|
820
1115
|
}
|
|
821
1116
|
if (key === 'qref' || key === '!qref') {
|
|
822
1117
|
const tmpArr = (Array.isArray((value ?? [])[0])
|
|
823
1118
|
? value
|
|
824
1119
|
: [value]);
|
|
825
|
-
for (let
|
|
826
|
-
qrefQueries.push(
|
|
1120
|
+
for (let [_name, qRefQuery] of tmpArr) {
|
|
1121
|
+
qrefQueries.push(qRefQuery);
|
|
827
1122
|
}
|
|
828
1123
|
}
|
|
829
1124
|
else if (key === 'selector' || key === '!selector') {
|
|
830
1125
|
const tmpArr = (Array.isArray(value) ? value : [value]);
|
|
831
|
-
qrefQueries.push(...this.findQRefQueries(
|
|
1126
|
+
qrefQueries.push(...this.findQRefQueries(...tmpArr));
|
|
832
1127
|
}
|
|
833
1128
|
}
|
|
834
1129
|
}
|
|
835
1130
|
return qrefQueries;
|
|
836
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* This translates qref selectors into ref selectors using the "current" GUID
|
|
1134
|
+
* list in the existing subscriptions.
|
|
1135
|
+
*/
|
|
837
1136
|
translateQRefSelectors(client, selectors) {
|
|
838
1137
|
const newSelectors = [];
|
|
839
1138
|
for (const curSelector of selectors) {
|
|
@@ -855,28 +1154,56 @@ class PubSub {
|
|
|
855
1154
|
for (let i = 0; i < tmpArr.length; i++) {
|
|
856
1155
|
const name = tmpArr[i][0];
|
|
857
1156
|
const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
|
|
858
|
-
const query = JSON.stringify(
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1157
|
+
const query = JSON.stringify([
|
|
1158
|
+
entityConstructorsToClassNames(qrefOptions),
|
|
1159
|
+
...entityConstructorsToClassNames(entitiesToReferences(qrefSelectors)),
|
|
1160
|
+
]);
|
|
1161
|
+
const QrefEntityClass = qrefOptions.class ?? Entity;
|
|
862
1162
|
const data = this.querySubs[QrefEntityClass.ETYPE][query].get(client);
|
|
863
1163
|
if (data) {
|
|
864
1164
|
const guids = data.current;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1165
|
+
if (newKey === '!ref') {
|
|
1166
|
+
// Insert the qref results as a not ref clause.
|
|
1167
|
+
let newValue;
|
|
1168
|
+
const oldValue = newSelector[newKey];
|
|
1169
|
+
if (oldValue == null ||
|
|
1170
|
+
(Array.isArray(oldValue) && !oldValue.length)) {
|
|
1171
|
+
newValue = [];
|
|
1172
|
+
}
|
|
1173
|
+
else if (Array.isArray(oldValue) &&
|
|
1174
|
+
!Array.isArray(oldValue[0])) {
|
|
1175
|
+
newValue = [oldValue];
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
newValue = oldValue;
|
|
1179
|
+
}
|
|
1180
|
+
newValue.push(...guids.map((guid) => [name, guid]));
|
|
1181
|
+
newSelector[newKey] = newValue;
|
|
872
1182
|
}
|
|
873
1183
|
else {
|
|
874
|
-
|
|
1184
|
+
// Insert the qref results as an "or" selector clause with a ref
|
|
1185
|
+
// selector.
|
|
1186
|
+
let newValue;
|
|
1187
|
+
const oldValue = newSelector['selector'];
|
|
1188
|
+
if (oldValue == null ||
|
|
1189
|
+
(Array.isArray(oldValue) && !oldValue.length)) {
|
|
1190
|
+
newValue = [];
|
|
1191
|
+
}
|
|
1192
|
+
else if (!Array.isArray(oldValue)) {
|
|
1193
|
+
newValue = [oldValue];
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
newValue = oldValue;
|
|
1197
|
+
}
|
|
1198
|
+
newValue.push({
|
|
1199
|
+
type: '|',
|
|
1200
|
+
ref: guids.map((guid) => [name, guid]),
|
|
1201
|
+
});
|
|
1202
|
+
newSelector['selector'] = newValue;
|
|
875
1203
|
}
|
|
876
|
-
newValue.push(...guids.map((guid) => [name, guid]));
|
|
877
|
-
newSelector[newKey] = newValue;
|
|
878
1204
|
}
|
|
879
1205
|
else {
|
|
1206
|
+
// Can't translate, so put the original back in.
|
|
880
1207
|
if (!newSelector[key]) {
|
|
881
1208
|
newSelector[key] = [];
|
|
882
1209
|
}
|
|
@@ -888,10 +1215,18 @@ class PubSub {
|
|
|
888
1215
|
}
|
|
889
1216
|
}
|
|
890
1217
|
else if (key === 'selector' || key === '!selector') {
|
|
891
|
-
|
|
892
|
-
|
|
1218
|
+
// This one gets inserted for qref queries, so it might be in there
|
|
1219
|
+
// already.
|
|
1220
|
+
const tmpArr = (Array.isArray(value ?? []) ? value : [value]);
|
|
1221
|
+
if (!newSelector[key]) {
|
|
1222
|
+
newSelector[key] = [];
|
|
1223
|
+
}
|
|
1224
|
+
// It also needs to be translated.
|
|
1225
|
+
newSelector[key].push(...this.translateQRefSelectors(client, tmpArr));
|
|
893
1226
|
}
|
|
894
|
-
else if (key === '
|
|
1227
|
+
else if (key === '!ref') {
|
|
1228
|
+
// This one gets inserted for !qref queries, so it might be in there
|
|
1229
|
+
// already.
|
|
895
1230
|
const tmpArr = (Array.isArray((value ?? [])[0])
|
|
896
1231
|
? value
|
|
897
1232
|
: [value]);
|
|
@@ -901,6 +1236,8 @@ class PubSub {
|
|
|
901
1236
|
newSelector[key].push(...tmpArr);
|
|
902
1237
|
}
|
|
903
1238
|
else {
|
|
1239
|
+
// This is a clause we don't need to translate.
|
|
1240
|
+
// @ts-ignore: ts doesn't know what value is here.
|
|
904
1241
|
newSelector[key] = value;
|
|
905
1242
|
}
|
|
906
1243
|
}
|
|
@@ -909,6 +1246,4 @@ class PubSub {
|
|
|
909
1246
|
return newSelectors;
|
|
910
1247
|
}
|
|
911
1248
|
}
|
|
912
|
-
exports.default = PubSub;
|
|
913
|
-
PubSub.transactionPublishes = [];
|
|
914
1249
|
//# sourceMappingURL=PubSub.js.map
|