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