@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/dist/PubSub.js CHANGED
@@ -1,125 +1,254 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const nymph_1 = require("@nymphjs/nymph");
4
- const websocket_1 = require("websocket");
5
- const lodash_1 = require("lodash");
6
- const conf_1 = require("./conf");
7
- class PubSub {
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 = { ...conf_1.ConfigDefaults, ...config };
10
- nymph.on('beforeSaveEntity', async (nymph, entity) => {
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 etype = entity.constructor.ETYPE;
13
- const off = nymph.on('afterSaveEntity', async (_nymph, result) => {
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
- this.publish(JSON.stringify({
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
- }), configWithDefaults);
55
+ });
56
+ this.transactionPublishes.push({
57
+ nymph: curNymph,
58
+ payload,
59
+ config: configWithDefaults,
60
+ });
61
+ await this.publishTransactionPublishes(curNymph);
25
62
  });
26
- });
27
- nymph.on('beforeDeleteEntity', async (nymph, entity) => {
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 etype = entity.constructor.ETYPE;
30
- const off = nymph.on('afterDeleteEntity', async (_nymph, result) => {
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
- this.publish(JSON.stringify({
81
+ const payload = JSON.stringify({
36
82
  action: 'publish',
37
83
  event: 'delete',
38
84
  guid: guid,
39
85
  etype: etype,
40
- }), configWithDefaults);
86
+ });
87
+ this.transactionPublishes.push({
88
+ nymph: curNymph,
89
+ payload,
90
+ config: configWithDefaults,
91
+ });
92
+ await this.publishTransactionPublishes(curNymph);
41
93
  });
42
- });
43
- nymph.on('beforeDeleteEntityByID', async (nymph, guid, className) => {
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 etype = nymph.getEntityClass(className ?? 'Entity').ETYPE;
46
- const off = nymph.on('afterDeleteEntityByID', async (_nymph, result) => {
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
- this.publish(JSON.stringify({
112
+ const payload = JSON.stringify({
52
113
  action: 'publish',
53
114
  event: 'delete',
54
115
  guid: guid,
55
116
  etype: etype,
56
- }), configWithDefaults);
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 (nymph, name) => {
64
- const off = nymph.on('afterNewUID', async (_nymph, result) => {
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
- this.publish(JSON.stringify({
142
+ const payload = JSON.stringify({
71
143
  action: 'publish',
72
144
  event: 'newUID',
73
145
  name: name,
74
146
  value: value,
75
- }), configWithDefaults);
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
- this.publish(JSON.stringify({
167
+ const payload = JSON.stringify({
85
168
  action: 'publish',
86
169
  event: 'setUID',
87
170
  name: name,
88
171
  value: value,
89
- }), configWithDefaults);
172
+ });
173
+ this.transactionPublishes.push({
174
+ nymph: curNymph,
175
+ payload,
176
+ config: configWithDefaults,
177
+ });
178
+ await this.publishTransactionPublishes(curNymph);
90
179
  });
91
- });
92
- nymph.on('beforeRenameUID', async (nymph, oldName, newName) => {
93
- const off = nymph.on('afterRenameUID', async (_nymph, result) => {
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
- this.publish(JSON.stringify({
192
+ const payload = JSON.stringify({
99
193
  action: 'publish',
100
194
  event: 'renameUID',
101
195
  oldName: oldName,
102
196
  newName: newName,
103
- }), configWithDefaults);
197
+ });
198
+ this.transactionPublishes.push({
199
+ nymph: curNymph,
200
+ payload,
201
+ config: configWithDefaults,
202
+ });
203
+ await this.publishTransactionPublishes(curNymph);
104
204
  });
105
- });
106
- nymph.on('beforeDeleteUID', async (nymph, name) => {
107
- const off = nymph.on('afterDeleteUID', async (_nymph, result) => {
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
- this.publish(JSON.stringify({
217
+ const payload = JSON.stringify({
113
218
  action: 'publish',
114
219
  event: 'deleteUID',
115
220
  name: name,
116
- }), configWithDefaults);
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 websocket_1.client();
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 = { ...conf_1.ConfigDefaults, ...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
- ['authenticate', 'subscribe', 'unsubscribe', 'publish'].indexOf(data.action) === -1) {
369
+ [
370
+ 'authenticate',
371
+ 'subscribe',
372
+ 'unsubscribe',
373
+ 'publish',
374
+ 'multiple',
375
+ ].indexOf(data.action) === -1) {
179
376
  return;
180
377
  }
181
- switch (data.action) {
182
- case 'authenticate':
183
- this.handleAuthentication(from, data);
184
- break;
185
- case 'subscribe':
186
- case 'unsubscribe':
187
- await this.handleSubscription(from, data);
188
- break;
189
- case 'publish':
190
- await this.handlePublish(from, msg, data);
191
- break;
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
- const token = data.token;
264
- if (token != null) {
265
- this.sessions.set(from, token);
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
- if ('query' in data && data.query != null) {
273
- await this.handleSubscriptionQuery(from, data);
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
- else if ('uid' in data &&
276
- data.uid != null &&
277
- typeof data.uid == 'string') {
278
- await this.handleSubscriptionUid(from, data);
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
- try {
285
- args = JSON.parse(data.query);
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: 'guid',
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 token = null;
571
+ let authToken = null;
572
+ let switchToken = null;
318
573
  if (this.sessions.has(from)) {
319
- token = this.sessions.get(from);
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 && token != null) {
323
- const user = await nymph.tilmeld.extractToken(token);
324
- if (user) {
325
- nymph.tilmeld.fillSession(user);
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: await nymph.getEntities(options, ...selectors),
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 && token != 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
- async handlePublish(from, msg, data) {
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
- await this.updateClient(curClient, curData, data);
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
- await this.updateClient(curClient, curData, data);
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
- async updateClient(curClient, curData, data) {
557
- let current;
558
- let token;
559
- const nymph = this.nymph.clone();
560
- try {
561
- const [clientOptions, ...clientSelectors] = JSON.parse(curData.query);
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
- const entityMap = Object.fromEntries(current.map((entity) => [entity.guid, entity]));
586
- const currentGuids = current.map((entity) => entity.guid ?? '');
587
- const removed = (0, lodash_1.difference)(curData.current, currentGuids);
588
- const added = (0, lodash_1.difference)(currentGuids, curData.current);
589
- if (curData.direct) {
590
- for (let guid of removed) {
591
- this.config.logger('log', new Date().toISOString(), `Notifying client of removal! (${curClient.remoteAddress})`);
592
- curClient.sendUTF(JSON.stringify({
593
- query: curData.query,
594
- removed: guid,
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 guid of added) {
598
- const entity = entityMap[guid];
599
- this.config.logger('log', new Date().toISOString(), `Notifying client of new match! (${curClient.remoteAddress})`);
600
- if (typeof entity.updateDataProtection === 'function') {
601
- entity.updateDataProtection();
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
- curClient.sendUTF(JSON.stringify({
604
- query: curData.query,
605
- added: guid,
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
- curClient.sendUTF(JSON.stringify({
616
- query: curData.query,
617
- updated: data.guid,
618
- data: entity,
619
- }));
620
- }
621
- }
622
- curData.current = currentGuids;
623
- if (nymph.tilmeld != null && token != null) {
624
- nymph.tilmeld.clearSession();
625
- }
626
- if ((removed.length || added.length) && curData.qrefParents.length) {
627
- for (const qrefParent of curData.qrefParents) {
628
- const subData = this.querySubs[qrefParent.etype][qrefParent.query].get(curClient);
629
- if (subData) {
630
- this.updateClient(curClient, subData, data);
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 websocket_1.client();
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 i = 0; i < tmpArr.length; i++) {
710
- qrefQueries.push(tmpArr[i][1]);
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 = typeof qrefOptions.class === 'string'
1157
+ const QrefEntityClass = qrefOptions.class
744
1158
  ? this.nymph.getEntityClass(qrefOptions.class)
745
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
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
- let newValue;
750
- const oldValue = newSelector[newKey];
751
- if (!oldValue) {
752
- newValue = [];
753
- }
754
- else if (oldValue.length && !Array.isArray(oldValue[0])) {
755
- newValue = [oldValue];
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
- newValue = oldValue;
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