@nymphjs/pubsub 1.0.0-beta.11 → 1.0.0-beta.111

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,252 @@
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@gmail.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
+ static transactionPublishes = [];
8
31
  static initPublisher(config, nymph) {
9
- const configWithDefaults = { ...conf_1.ConfigDefaults, ...config };
10
- nymph.on('beforeSaveEntity', async (nymph, entity) => {
32
+ const configWithDefaults = { ...defaults, ...config };
33
+ const unsubscribers = [];
34
+ unsubscribers.push(nymph.on('beforeSaveEntity', async (enymph, entity) => {
11
35
  const guid = entity.guid;
12
- const etype = entity.constructor.ETYPE;
13
- const off = nymph.on('afterSaveEntity', async (_nymph, result) => {
36
+ const EntityClass = entity.constructor;
37
+ const etype = EntityClass.ETYPE;
38
+ if (!EntityClass.pubSubEnabled) {
39
+ return;
40
+ }
41
+ const off = enymph.on('afterSaveEntity', async (curNymph, result) => {
14
42
  off();
43
+ off2();
15
44
  if (!(await result)) {
16
45
  return;
17
46
  }
18
- this.publish(JSON.stringify({
47
+ const payload = JSON.stringify({
19
48
  action: 'publish',
20
49
  event: guid == null ? 'create' : 'update',
21
50
  guid: entity.guid,
22
51
  entity: entity.toJSON(),
23
52
  etype: etype,
24
- }), configWithDefaults);
53
+ });
54
+ this.transactionPublishes.push({
55
+ nymph: curNymph,
56
+ payload,
57
+ config: configWithDefaults,
58
+ });
59
+ await this.publishTransactionPublishes(curNymph);
25
60
  });
26
- });
27
- nymph.on('beforeDeleteEntity', async (nymph, entity) => {
61
+ const off2 = enymph.on('failedSaveEntity', async () => {
62
+ off();
63
+ off2();
64
+ });
65
+ }));
66
+ unsubscribers.push(nymph.on('beforeDeleteEntity', async (enymph, entity) => {
28
67
  const guid = entity.guid;
29
- const etype = entity.constructor.ETYPE;
30
- const off = nymph.on('afterDeleteEntity', async (_nymph, result) => {
68
+ const EntityClass = entity.constructor;
69
+ const etype = EntityClass.ETYPE;
70
+ if (!EntityClass.pubSubEnabled) {
71
+ return;
72
+ }
73
+ const off = enymph.on('afterDeleteEntity', async (curNymph, result) => {
31
74
  off();
75
+ off2();
32
76
  if (!(await result)) {
33
77
  return;
34
78
  }
35
- this.publish(JSON.stringify({
79
+ const payload = JSON.stringify({
36
80
  action: 'publish',
37
81
  event: 'delete',
38
82
  guid: guid,
39
83
  etype: etype,
40
- }), configWithDefaults);
84
+ });
85
+ this.transactionPublishes.push({
86
+ nymph: curNymph,
87
+ payload,
88
+ config: configWithDefaults,
89
+ });
90
+ await this.publishTransactionPublishes(curNymph);
41
91
  });
42
- });
43
- nymph.on('beforeDeleteEntityByID', async (nymph, guid, className) => {
92
+ const off2 = enymph.on('failedDeleteEntity', async () => {
93
+ off();
94
+ off2();
95
+ });
96
+ }));
97
+ unsubscribers.push(nymph.on('beforeDeleteEntityByID', async (enymph, guid, className) => {
44
98
  try {
45
- const etype = nymph.getEntityClass(className ?? 'Entity').ETYPE;
46
- const off = nymph.on('afterDeleteEntityByID', async (_nymph, result) => {
99
+ const EntityClass = enymph.getEntityClass(className ?? 'Entity');
100
+ const etype = EntityClass.ETYPE;
101
+ if (!EntityClass.pubSubEnabled) {
102
+ return;
103
+ }
104
+ const off = enymph.on('afterDeleteEntityByID', async (curNymph, result) => {
47
105
  off();
106
+ off2();
48
107
  if (!(await result)) {
49
108
  return;
50
109
  }
51
- this.publish(JSON.stringify({
110
+ const payload = JSON.stringify({
52
111
  action: 'publish',
53
112
  event: 'delete',
54
113
  guid: guid,
55
114
  etype: etype,
56
- }), configWithDefaults);
115
+ });
116
+ this.transactionPublishes.push({
117
+ nymph: curNymph,
118
+ payload,
119
+ config: configWithDefaults,
120
+ });
121
+ await this.publishTransactionPublishes(curNymph);
122
+ });
123
+ const off2 = enymph.on('failedDeleteEntityByID', async () => {
124
+ off();
125
+ off2();
57
126
  });
58
127
  }
59
128
  catch (e) {
60
129
  return;
61
130
  }
62
- });
63
- nymph.on('beforeNewUID', async (nymph, name) => {
64
- const off = nymph.on('afterNewUID', async (_nymph, result) => {
131
+ }));
132
+ unsubscribers.push(nymph.on('beforeNewUID', async (enymph, name) => {
133
+ const off = enymph.on('afterNewUID', async (curNymph, result) => {
65
134
  off();
135
+ off2();
66
136
  const value = await result;
67
137
  if (value == null) {
68
138
  return;
69
139
  }
70
- this.publish(JSON.stringify({
140
+ const payload = JSON.stringify({
71
141
  action: 'publish',
72
142
  event: 'newUID',
73
143
  name: name,
74
144
  value: value,
75
- }), configWithDefaults);
145
+ });
146
+ this.transactionPublishes.push({
147
+ nymph: curNymph,
148
+ payload,
149
+ config: configWithDefaults,
150
+ });
151
+ await this.publishTransactionPublishes(curNymph);
76
152
  });
77
- });
78
- nymph.on('beforeSetUID', async (nymph, name, value) => {
79
- const off = nymph.on('afterSetUID', async (_nymph, result) => {
153
+ const off2 = enymph.on('failedNewUID', async () => {
154
+ off();
155
+ off2();
156
+ });
157
+ }));
158
+ unsubscribers.push(nymph.on('beforeSetUID', async (enymph, name, value) => {
159
+ const off = enymph.on('afterSetUID', async (curNymph, result) => {
80
160
  off();
161
+ off2();
81
162
  if (!(await result)) {
82
163
  return;
83
164
  }
84
- this.publish(JSON.stringify({
165
+ const payload = JSON.stringify({
85
166
  action: 'publish',
86
167
  event: 'setUID',
87
168
  name: name,
88
169
  value: value,
89
- }), configWithDefaults);
170
+ });
171
+ this.transactionPublishes.push({
172
+ nymph: curNymph,
173
+ payload,
174
+ config: configWithDefaults,
175
+ });
176
+ await this.publishTransactionPublishes(curNymph);
90
177
  });
91
- });
92
- nymph.on('beforeRenameUID', async (nymph, oldName, newName) => {
93
- const off = nymph.on('afterRenameUID', async (_nymph, result) => {
178
+ const off2 = enymph.on('failedSetUID', async () => {
94
179
  off();
180
+ off2();
181
+ });
182
+ }));
183
+ unsubscribers.push(nymph.on('beforeRenameUID', async (enymph, oldName, newName) => {
184
+ const off = enymph.on('afterRenameUID', async (curNymph, result) => {
185
+ off();
186
+ off2();
95
187
  if (!(await result)) {
96
188
  return;
97
189
  }
98
- this.publish(JSON.stringify({
190
+ const payload = JSON.stringify({
99
191
  action: 'publish',
100
192
  event: 'renameUID',
101
193
  oldName: oldName,
102
194
  newName: newName,
103
- }), configWithDefaults);
195
+ });
196
+ this.transactionPublishes.push({
197
+ nymph: curNymph,
198
+ payload,
199
+ config: configWithDefaults,
200
+ });
201
+ await this.publishTransactionPublishes(curNymph);
104
202
  });
105
- });
106
- nymph.on('beforeDeleteUID', async (nymph, name) => {
107
- const off = nymph.on('afterDeleteUID', async (_nymph, result) => {
203
+ const off2 = enymph.on('failedRenameUID', async () => {
204
+ off();
205
+ off2();
206
+ });
207
+ }));
208
+ unsubscribers.push(nymph.on('beforeDeleteUID', async (enymph, name) => {
209
+ const off = enymph.on('afterDeleteUID', async (curNymph, result) => {
108
210
  off();
211
+ off2();
109
212
  if (!(await result)) {
110
213
  return;
111
214
  }
112
- this.publish(JSON.stringify({
215
+ const payload = JSON.stringify({
113
216
  action: 'publish',
114
217
  event: 'deleteUID',
115
218
  name: name,
116
- }), configWithDefaults);
219
+ });
220
+ this.transactionPublishes.push({
221
+ nymph: curNymph,
222
+ payload,
223
+ config: configWithDefaults,
224
+ });
225
+ await this.publishTransactionPublishes(curNymph);
117
226
  });
118
- });
227
+ const off2 = enymph.on('failedDeleteUID', async () => {
228
+ off();
229
+ off2();
230
+ });
231
+ }));
232
+ unsubscribers.push(nymph.on('afterCommitTransaction', async (enymph, _name, result) => {
233
+ if (result) {
234
+ await this.publishTransactionPublishes(enymph);
235
+ }
236
+ }));
237
+ unsubscribers.push(nymph.on('afterRollbackTransaction', async (enymph) => {
238
+ this.removeTransactionPublishes(enymph);
239
+ }));
240
+ return () => {
241
+ for (let unsubscribe of unsubscribers) {
242
+ unsubscribe();
243
+ }
244
+ this.transactionPublishes = [];
245
+ };
119
246
  }
120
247
  static publish(message, config) {
121
248
  for (let host of config.entries ?? []) {
122
- const client = new websocket_1.client();
249
+ const client = new ws.client();
123
250
  client.on('connectFailed', (error) => {
124
251
  if (config.logger) {
125
252
  config.logger('error', new Date().toISOString(), `Publish connection failed. (${error.toString()}, ${host})`);
@@ -139,12 +266,48 @@ class PubSub {
139
266
  client.connect(host, 'nymph');
140
267
  }
141
268
  }
269
+ static isOrIsDescendant(parent, child) {
270
+ let check = child;
271
+ while (check) {
272
+ if (check === parent) {
273
+ return true;
274
+ }
275
+ check = check.parent;
276
+ }
277
+ return false;
278
+ }
279
+ static async publishTransactionPublishes(nymph) {
280
+ if (await nymph.inTransaction()) {
281
+ // This instance is still in a transaction, so nothing gets published yet.
282
+ return;
283
+ }
284
+ this.transactionPublishes = (await Promise.all(this.transactionPublishes.map(async (publish) => {
285
+ // Check that this instance is a parent and the instance is not in a
286
+ // transaction.
287
+ if (!this.isOrIsDescendant(nymph, publish.nymph) ||
288
+ (await publish.nymph.inTransaction())) {
289
+ return publish;
290
+ }
291
+ this.publish(publish.payload, publish.config);
292
+ return null;
293
+ }))).filter((value) => value != null);
294
+ }
295
+ static removeTransactionPublishes(nymph) {
296
+ this.transactionPublishes = this.transactionPublishes.filter((publish) => {
297
+ if (this.isOrIsDescendant(nymph, publish.nymph)) {
298
+ return false;
299
+ }
300
+ return true;
301
+ });
302
+ }
303
+ /**
304
+ * Initialize Nymph PubSub.
305
+ *
306
+ * @param config The PubSub configuration.
307
+ */
142
308
  constructor(config, nymph, server) {
143
- this.sessions = new Map();
144
- this.querySubs = {};
145
- this.uidSubs = {};
146
309
  this.nymph = nymph;
147
- this.config = { ...conf_1.ConfigDefaults, ...config };
310
+ this.config = { ...defaults, ...config };
148
311
  this.server = server;
149
312
  this.server.on('request', this.handleRequest.bind(this));
150
313
  }
@@ -153,6 +316,7 @@ class PubSub {
153
316
  }
154
317
  handleRequest(request) {
155
318
  if (!this.config.originIsAllowed(request.origin)) {
319
+ // Make sure we only accept requests from an allowed origin
156
320
  request.reject();
157
321
  this.config.logger('log', new Date().toISOString(), 'Client from origin ' + request.origin + ' was kicked by the bouncer.');
158
322
  return;
@@ -169,6 +333,9 @@ class PubSub {
169
333
  this.onClose(connection, description);
170
334
  });
171
335
  }
336
+ /**
337
+ * Handle a message from a client.
338
+ */
172
339
  async onMessage(from, msg) {
173
340
  if (msg.type !== 'utf8') {
174
341
  throw new Error("This server doesn't accept binary messages.");
@@ -191,6 +358,9 @@ class PubSub {
191
358
  break;
192
359
  }
193
360
  }
361
+ /**
362
+ * Clean up after users who leave.
363
+ */
194
364
  onClose(conn, description) {
195
365
  this.config.logger('log', new Date().toISOString(), `Client skedaddled. (${description}, ${conn.remoteAddress})`);
196
366
  let mess = 0;
@@ -209,6 +379,7 @@ class PubSub {
209
379
  }
210
380
  else {
211
381
  if (this.config.broadcastCounts) {
382
+ // Notify clients of the subscription count.
212
383
  for (let key of curClients.keys()) {
213
384
  const curData = curClients.get(key);
214
385
  if (curData && curData.count) {
@@ -234,6 +405,7 @@ class PubSub {
234
405
  }
235
406
  else {
236
407
  if (this.config.broadcastCounts) {
408
+ // Notify clients of the subscription count.
237
409
  for (const key of curClients.keys()) {
238
410
  const curData = curClients.get(key);
239
411
  if (curData && curData.count) {
@@ -259,34 +431,56 @@ class PubSub {
259
431
  onError(conn, e) {
260
432
  this.config.logger('error', new Date().toISOString(), `An error occured. (${e.message}, ${conn.remoteAddress})`);
261
433
  }
434
+ /**
435
+ * Handle an authentication from a client.
436
+ */
262
437
  handleAuthentication(from, data) {
263
- const token = data.token;
264
- if (token != null) {
265
- this.sessions.set(from, token);
438
+ // Save the user's auth token in session storage.
439
+ const authToken = data.authToken;
440
+ const switchToken = data.switchToken;
441
+ if (authToken != null) {
442
+ this.sessions.set(from, { authToken, switchToken });
266
443
  }
267
444
  else if (this.sessions.has(from)) {
268
445
  this.sessions.delete(from);
269
446
  }
270
447
  }
448
+ /**
449
+ * Handle a subscribe or unsubscribe from a client.
450
+ */
271
451
  async handleSubscription(from, data) {
272
- if ('query' in data && data.query != null) {
273
- await this.handleSubscriptionQuery(from, data);
452
+ try {
453
+ if ('query' in data && data.query != null) {
454
+ // Request is for a query.
455
+ await this.handleSubscriptionQuery(from, data);
456
+ }
457
+ else if ('uid' in data &&
458
+ data.uid != null &&
459
+ typeof data.uid == 'string') {
460
+ // Request is for a UID.
461
+ await this.handleSubscriptionUid(from, data);
462
+ }
274
463
  }
275
- else if ('uid' in data &&
276
- data.uid != null &&
277
- typeof data.uid == 'string') {
278
- await this.handleSubscriptionUid(from, data);
464
+ catch (e) {
465
+ if ('query' in data && data.query != null) {
466
+ from.sendUTF(JSON.stringify({ query: data.query, error: e.message }));
467
+ }
468
+ else if ('uid' in data && data.uid != null) {
469
+ from.sendUTF(JSON.stringify({ uid: data.uid, error: e.message }));
470
+ }
471
+ else {
472
+ from.sendUTF(JSON.stringify({ error: e.message }));
473
+ }
279
474
  }
280
475
  }
476
+ /**
477
+ * Handle a subscribe or unsubscribe for a query from a client.
478
+ */
281
479
  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;
480
+ let args = JSON.parse(data.query);
481
+ let EntityClass = this.nymph.getEntityClass(args[0].class);
482
+ if (!EntityClass.restEnabled) {
483
+ throw new Error('Not accessible.');
290
484
  }
291
485
  const etype = EntityClass.ETYPE;
292
486
  const serialArgs = JSON.stringify(args);
@@ -294,11 +488,22 @@ class PubSub {
294
488
  const options = {
295
489
  ...clientOptions,
296
490
  class: EntityClass,
297
- return: 'guid',
491
+ return: 'entity',
298
492
  source: 'client',
299
493
  };
494
+ // Find qref queries.
300
495
  const qrefQueries = this.findQRefQueries(clientOptions, ...selectors);
496
+ // Check that all qref queries are accessible classes.
497
+ for (const qrefQuery of qrefQueries) {
498
+ const args = qrefQuery;
499
+ const EntityClass = this.nymph.getEntityClass(args[0].class);
500
+ if (!EntityClass.restEnabled) {
501
+ throw new Error('Not accessible.');
502
+ }
503
+ }
301
504
  if (data.action === 'subscribe') {
505
+ // Client is subscribing to a query.
506
+ // First subscribe to qrefQueries, giving this one as a reference.
302
507
  for (const qrefQuery of qrefQueries) {
303
508
  await this.handleSubscriptionQuery(from, {
304
509
  action: 'subscribe',
@@ -308,23 +513,39 @@ class PubSub {
308
513
  query: serialArgs,
309
514
  });
310
515
  }
516
+ // Now subscribe to this query.
311
517
  if (!(etype in this.querySubs)) {
312
518
  this.querySubs[etype] = {};
313
519
  }
314
520
  if (!(serialArgs in this.querySubs[etype])) {
315
521
  this.querySubs[etype][serialArgs] = new Map();
316
522
  }
317
- let token = null;
523
+ let authToken = null;
524
+ let switchToken = null;
318
525
  if (this.sessions.has(from)) {
319
- token = this.sessions.get(from);
526
+ const session = this.sessions.get(from);
527
+ authToken = session?.authToken;
528
+ switchToken = session?.switchToken;
320
529
  }
321
530
  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);
531
+ if (nymph.tilmeld != null && authToken != null) {
532
+ const user = await nymph.tilmeld.extractToken(authToken);
533
+ if (user && user.enabled) {
534
+ if (switchToken != null) {
535
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
536
+ if (switchUser) {
537
+ // Log in the switchUser for access controls.
538
+ await nymph.tilmeld.fillSession(switchUser);
539
+ }
540
+ }
541
+ else {
542
+ // Log in the user for access controls.
543
+ await nymph.tilmeld.fillSession(user);
544
+ }
326
545
  }
327
546
  }
547
+ let entities = [];
548
+ let sendEntities = false;
328
549
  const existingSub = this.querySubs[etype][serialArgs].get(from);
329
550
  if (existingSub) {
330
551
  if (qrefParent) {
@@ -336,21 +557,42 @@ class PubSub {
336
557
  if (data.count && !existingSub.count) {
337
558
  existingSub.count = true;
338
559
  }
560
+ sendEntities = existingSub.direct;
561
+ if (sendEntities) {
562
+ entities = existingSub.current.length
563
+ ? await nymph.getEntities({
564
+ class: EntityClass,
565
+ source: 'client',
566
+ }, { type: '|', guid: existingSub.current })
567
+ : [];
568
+ }
339
569
  }
340
570
  else {
571
+ entities = await nymph.getEntities(options, ...selectors);
341
572
  this.querySubs[etype][serialArgs].set(from, {
342
- current: await nymph.getEntities(options, ...selectors),
573
+ current: entities.map((e) => e.guid),
343
574
  query: data.query,
344
575
  qrefParents: qrefParent ? [qrefParent] : [],
345
576
  direct: !qrefParent,
346
577
  count: !!data.count,
347
578
  });
579
+ sendEntities = !qrefParent;
580
+ }
581
+ if (sendEntities) {
582
+ // Notify the client of the current value.
583
+ from.sendUTF(JSON.stringify({
584
+ query: data.query,
585
+ set: true,
586
+ data: entities,
587
+ }));
348
588
  }
349
- if (nymph.tilmeld != null && token != null) {
589
+ if (nymph.tilmeld != null && authToken != null) {
590
+ // Clear the user that was temporarily logged in.
350
591
  nymph.tilmeld.clearSession();
351
592
  }
352
593
  this.config.logger('log', new Date().toISOString(), `Client subscribed to a query! (${serialArgs}, ${from.remoteAddress})`);
353
594
  if (this.config.broadcastCounts) {
595
+ // Notify clients of the subscription count.
354
596
  const count = this.querySubs[etype][serialArgs].size;
355
597
  for (let key of this.querySubs[etype][serialArgs].keys()) {
356
598
  const curData = this.querySubs[etype][serialArgs].get(key);
@@ -364,6 +606,8 @@ class PubSub {
364
606
  }
365
607
  }
366
608
  if (data.action === 'unsubscribe') {
609
+ // Client is unsubscribing from a query.
610
+ // First unsubscribe from qrefQueries.
367
611
  for (const qrefQuery of qrefQueries) {
368
612
  await this.handleSubscriptionQuery(from, {
369
613
  action: 'unsubscribe',
@@ -373,6 +617,7 @@ class PubSub {
373
617
  query: serialArgs,
374
618
  });
375
619
  }
620
+ // Now unsubscribe from this query.
376
621
  if (!(etype in this.querySubs)) {
377
622
  return;
378
623
  }
@@ -398,6 +643,7 @@ class PubSub {
398
643
  this.config.logger('log', new Date().toISOString(), `Client unsubscribed from a query! (${serialArgs}, ${from.remoteAddress})`);
399
644
  const count = this.querySubs[etype][serialArgs].size;
400
645
  if (count === 0) {
646
+ // No more subscribed clients.
401
647
  delete this.querySubs[etype][serialArgs];
402
648
  if (Object.keys(this.querySubs[etype]).length === 0) {
403
649
  delete this.querySubs[etype];
@@ -405,6 +651,7 @@ class PubSub {
405
651
  return;
406
652
  }
407
653
  if (this.config.broadcastCounts) {
654
+ // Notify clients of the subscription count.
408
655
  for (let key of this.querySubs[etype][serialArgs].keys()) {
409
656
  const curData = this.querySubs[etype][serialArgs].get(key);
410
657
  if (curData && curData.count) {
@@ -417,16 +664,55 @@ class PubSub {
417
664
  }
418
665
  }
419
666
  }
667
+ /**
668
+ * Handle a subscribe or unsubscribe for a UID from a client.
669
+ */
420
670
  async handleSubscriptionUid(from, data) {
421
671
  if (data.action === 'subscribe') {
672
+ // Client is subscribing to a UID.
422
673
  if (!(data.uid in this.uidSubs)) {
423
674
  this.uidSubs[data.uid] = new Map();
424
675
  }
425
676
  this.uidSubs[data['uid']].set(from, {
426
677
  count: !!data.count,
427
678
  });
679
+ let authToken = null;
680
+ let switchToken = null;
681
+ if (this.sessions.has(from)) {
682
+ const session = this.sessions.get(from);
683
+ authToken = session?.authToken;
684
+ switchToken = session?.switchToken;
685
+ }
686
+ const nymph = this.nymph.clone();
687
+ if (nymph.tilmeld != null && authToken != null) {
688
+ const user = await nymph.tilmeld.extractToken(authToken);
689
+ if (user && user.enabled) {
690
+ if (switchToken != null) {
691
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
692
+ if (switchUser) {
693
+ // Log in the switchUser for access controls.
694
+ await nymph.tilmeld.fillSession(switchUser);
695
+ }
696
+ }
697
+ else {
698
+ // Log in the user for access controls.
699
+ await nymph.tilmeld.fillSession(user);
700
+ }
701
+ }
702
+ }
703
+ // Notify the client of the current value.
704
+ from.sendUTF(JSON.stringify({
705
+ uid: data.uid,
706
+ set: true,
707
+ data: await this.nymph.getUID(data.uid),
708
+ }));
709
+ if (nymph.tilmeld != null && authToken != null) {
710
+ // Clear the user that was temporarily logged in.
711
+ nymph.tilmeld.clearSession();
712
+ }
428
713
  this.config.logger('log', new Date().toISOString(), `Client subscribed to a UID! (${data.uid}, ${from.remoteAddress})`);
429
714
  if (this.config.broadcastCounts) {
715
+ // Notify clients of the subscription count.
430
716
  const count = this.uidSubs[data.uid].size;
431
717
  for (let key of this.uidSubs[data.uid].keys()) {
432
718
  const curData = this.uidSubs[data.uid].get(key);
@@ -440,6 +726,7 @@ class PubSub {
440
726
  }
441
727
  }
442
728
  if (data.action === 'unsubscribe') {
729
+ // Client is unsubscribing from a UID.
443
730
  if (!(data.uid in this.uidSubs)) {
444
731
  return;
445
732
  }
@@ -450,10 +737,12 @@ class PubSub {
450
737
  this.config.logger('log', new Date().toISOString(), `Client unsubscribed from a UID! (${data.uid}, ${from.remoteAddress})`);
451
738
  const count = this.uidSubs[data.uid].size;
452
739
  if (count === 0) {
740
+ // No more subscribed clients.
453
741
  delete this.uidSubs[data.uid];
454
742
  return;
455
743
  }
456
744
  if (this.config.broadcastCounts) {
745
+ // Notify clients of the subscription count.
457
746
  for (let key of this.uidSubs[data.uid].keys()) {
458
747
  const curData = this.uidSubs[data.uid].get(key);
459
748
  if (curData && curData.count) {
@@ -466,6 +755,9 @@ class PubSub {
466
755
  }
467
756
  }
468
757
  }
758
+ /**
759
+ * Handle a publish from a client.
760
+ */
469
761
  async handlePublish(from, msg, data) {
470
762
  if ('guid' in data &&
471
763
  typeof data.guid === 'string' &&
@@ -474,7 +766,9 @@ class PubSub {
474
766
  ((data.event === 'create' || data.event === 'update') &&
475
767
  'entity' in data &&
476
768
  data.entity != null))) {
769
+ // Publish is an entity.
477
770
  await this.handlePublishEntity(from, data);
771
+ // Relay the publish to other servers.
478
772
  this.relay(msg);
479
773
  }
480
774
  if ((('name' in data && typeof data.name === 'string') ||
@@ -483,10 +777,15 @@ class PubSub {
483
777
  'newName' in data &&
484
778
  typeof data.newName === 'string')) &&
485
779
  ['newUID', 'setUID', 'renameUID', 'deleteUID'].indexOf(data.event) !== -1) {
780
+ // Publish is a UID.
486
781
  await this.handlePublishUid(from, data);
782
+ // Relay the publish to other servers.
487
783
  this.relay(msg);
488
784
  }
489
785
  }
786
+ /**
787
+ * Handle an entity publish from a client.
788
+ */
490
789
  async handlePublishEntity(from, data) {
491
790
  this.config.logger('log', new Date().toISOString(), `Received an entity publish! (${data.guid}, ${data.event}, ${from.remoteAddress})`);
492
791
  const etype = data.etype;
@@ -497,6 +796,7 @@ class PubSub {
497
796
  const curClients = this.querySubs[etype][curQuery];
498
797
  const updatedClients = new Set();
499
798
  if (data.event === 'delete' || data.event === 'update') {
799
+ // Check if it is in any client's currents.
500
800
  try {
501
801
  for (const curClient of curClients.keys()) {
502
802
  const curData = curClients.get(curClient);
@@ -514,6 +814,7 @@ class PubSub {
514
814
  }
515
815
  }
516
816
  if ((data.event === 'create' || data.event === 'update') && data.entity) {
817
+ // Check if it matches the query.
517
818
  try {
518
819
  const [clientOptions, ...selectors] = JSON.parse(curQuery);
519
820
  const qrefQueries = this.findQRefQueries(clientOptions, ...selectors);
@@ -528,18 +829,23 @@ class PubSub {
528
829
  const DataEntityClass = this.nymph.getEntityClass(data.entity.class);
529
830
  if (EntityClass.ETYPE === DataEntityClass.ETYPE &&
530
831
  (qrefQueries.length ||
531
- this.nymph.driver.checkData(entityData, entitySData, selectors, data.guid, data.entity?.tags ?? []))) {
832
+ this.nymph.driver.checkData(EntityClass.ETYPE, entityData, entitySData, selectors, data.guid, data.entity?.tags ?? []))) {
833
+ // It either matches the query, or there are qref queries.
532
834
  for (let curClient of curClients.keys()) {
533
835
  if (updatedClients.has(curClient)) {
836
+ // The user was already notified. (Of an update.)
534
837
  continue;
535
838
  }
536
839
  const curData = curClients.get(curClient);
537
840
  if (!curData) {
538
841
  continue;
539
842
  }
843
+ // If there are qref queries, we need to dive into the user's
844
+ // current data and translate them before running checkData.
540
845
  if (qrefQueries.length) {
541
846
  const translatedSelectors = this.translateQRefSelectors(curClient, selectors);
542
- if (!this.nymph.driver.checkData(entityData, entitySData, translatedSelectors, data.guid, data.entity?.tags ?? [])) {
847
+ if (!this.nymph.driver.checkData(EntityClass.ETYPE, entityData, entitySData, translatedSelectors, data.guid, data.entity?.tags ?? [])) {
848
+ // The query doesn't match when the qref queries are filled.
543
849
  continue;
544
850
  }
545
851
  }
@@ -554,8 +860,10 @@ class PubSub {
554
860
  }
555
861
  }
556
862
  async updateClient(curClient, curData, data) {
863
+ // Update currents list.
557
864
  let current;
558
- let token;
865
+ let authToken;
866
+ let switchToken;
559
867
  const nymph = this.nymph.clone();
560
868
  try {
561
869
  const [clientOptions, ...clientSelectors] = JSON.parse(curData.query);
@@ -566,14 +874,26 @@ class PubSub {
566
874
  source: 'client',
567
875
  skipAc: false,
568
876
  };
569
- const selectors = (0, nymph_1.classNamesToEntityConstructors)(nymph, clientSelectors);
877
+ const selectors = classNamesToEntityConstructors(nymph, clientSelectors, true);
570
878
  if (this.sessions.has(curClient)) {
571
- token = this.sessions.get(curClient);
879
+ const session = this.sessions.get(curClient);
880
+ authToken = session?.authToken;
881
+ switchToken = session?.switchToken;
572
882
  }
573
- if (nymph.tilmeld != null && token != null) {
574
- const user = await nymph.tilmeld.extractToken(token);
575
- if (user) {
576
- nymph.tilmeld.fillSession(user);
883
+ if (nymph.tilmeld != null && authToken != null) {
884
+ const user = await nymph.tilmeld.extractToken(authToken);
885
+ if (user && user.enabled) {
886
+ if (switchToken != null) {
887
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
888
+ if (switchUser) {
889
+ // Log in the switchUser for access controls.
890
+ await nymph.tilmeld.fillSession(switchUser);
891
+ }
892
+ }
893
+ else {
894
+ // Log in the user for access controls.
895
+ await nymph.tilmeld.fillSession(user);
896
+ }
577
897
  }
578
898
  }
579
899
  current = await nymph.getEntities(options, ...selectors);
@@ -584,10 +904,11 @@ class PubSub {
584
904
  }
585
905
  const entityMap = Object.fromEntries(current.map((entity) => [entity.guid, entity]));
586
906
  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);
907
+ const removed = difference(curData.current, currentGuids);
908
+ const added = difference(currentGuids, curData.current);
589
909
  if (curData.direct) {
590
910
  for (let guid of removed) {
911
+ // Notify subscriber.
591
912
  this.config.logger('log', new Date().toISOString(), `Notifying client of removal! (${curClient.remoteAddress})`);
592
913
  curClient.sendUTF(JSON.stringify({
593
914
  query: curData.query,
@@ -596,6 +917,7 @@ class PubSub {
596
917
  }
597
918
  for (let guid of added) {
598
919
  const entity = entityMap[guid];
920
+ // Notify client.
599
921
  this.config.logger('log', new Date().toISOString(), `Notifying client of new match! (${curClient.remoteAddress})`);
600
922
  if (typeof entity.updateDataProtection === 'function') {
601
923
  entity.updateDataProtection();
@@ -608,6 +930,7 @@ class PubSub {
608
930
  }
609
931
  if (data.event === 'update' && data.guid in entityMap) {
610
932
  const entity = entityMap[data.guid];
933
+ // Notify subscriber.
611
934
  this.config.logger('log', new Date().toISOString(), `Notifying client of update! (${curClient.remoteAddress})`);
612
935
  if (typeof entity.updateDataProtection === 'function') {
613
936
  entity.updateDataProtection();
@@ -619,11 +942,14 @@ class PubSub {
619
942
  }));
620
943
  }
621
944
  }
945
+ // Update curData.
622
946
  curData.current = currentGuids;
623
- if (nymph.tilmeld != null && token != null) {
947
+ if (nymph.tilmeld != null && authToken != null) {
948
+ // Clear the user that was temporarily logged in.
624
949
  nymph.tilmeld.clearSession();
625
950
  }
626
951
  if ((removed.length || added.length) && curData.qrefParents.length) {
952
+ // All qref parents need to be rerun.
627
953
  for (const qrefParent of curData.qrefParents) {
628
954
  const subData = this.querySubs[qrefParent.etype][qrefParent.query].get(curClient);
629
955
  if (subData) {
@@ -632,6 +958,9 @@ class PubSub {
632
958
  }
633
959
  }
634
960
  }
961
+ /**
962
+ * Handle a UID publish from a client.
963
+ */
635
964
  async handlePublishUid(from, data) {
636
965
  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
966
  const names = [data.name, data.oldName].filter((name) => name != null);
@@ -668,13 +997,16 @@ class PubSub {
668
997
  }
669
998
  }
670
999
  }
1000
+ /**
1001
+ * Relay publish data to other servers.
1002
+ */
671
1003
  relay(message) {
672
1004
  if (message.type !== 'utf8') {
673
1005
  this.config.logger('error', new Date().toISOString(), `Can't relay non UTF8 message.`);
674
1006
  return;
675
1007
  }
676
1008
  for (let host of this.config.relays) {
677
- const client = new websocket_1.client();
1009
+ const client = new ws.client();
678
1010
  client.on('connectFailed', (error) => {
679
1011
  this.config.logger('error', new Date().toISOString(), `Relay connection failed. (${error.toString()}, ${host})`);
680
1012
  });
@@ -718,6 +1050,10 @@ class PubSub {
718
1050
  }
719
1051
  return qrefQueries;
720
1052
  }
1053
+ /**
1054
+ * This translates qref selectors into ref selectors using the "current" GUID
1055
+ * list in the existing subscriptions.
1056
+ */
721
1057
  translateQRefSelectors(client, selectors) {
722
1058
  const newSelectors = [];
723
1059
  for (const curSelector of selectors) {
@@ -740,27 +1076,55 @@ class PubSub {
740
1076
  const name = tmpArr[i][0];
741
1077
  const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
742
1078
  const query = JSON.stringify(tmpArr[i][1]);
743
- const QrefEntityClass = typeof qrefOptions.class === 'string'
1079
+ const QrefEntityClass = qrefOptions.class
744
1080
  ? this.nymph.getEntityClass(qrefOptions.class)
745
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
1081
+ : this.nymph.getEntityClass('Entity');
746
1082
  const data = this.querySubs[QrefEntityClass.ETYPE][query].get(client);
747
1083
  if (data) {
748
1084
  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];
1085
+ if (newKey === '!ref') {
1086
+ // Insert the qref results as a not ref clause.
1087
+ let newValue;
1088
+ const oldValue = newSelector[newKey];
1089
+ if (oldValue == null ||
1090
+ (Array.isArray(oldValue) && !oldValue.length)) {
1091
+ newValue = [];
1092
+ }
1093
+ else if (Array.isArray(oldValue) &&
1094
+ !Array.isArray(oldValue[0])) {
1095
+ newValue = [oldValue];
1096
+ }
1097
+ else {
1098
+ newValue = oldValue;
1099
+ }
1100
+ newValue.push(...guids.map((guid) => [name, guid]));
1101
+ newSelector[newKey] = newValue;
756
1102
  }
757
1103
  else {
758
- newValue = oldValue;
1104
+ // Insert the qref results as an "or" selector clause with a ref
1105
+ // selector.
1106
+ let newValue;
1107
+ const oldValue = newSelector['selector'];
1108
+ delete newSelector[key];
1109
+ if (oldValue == null ||
1110
+ (Array.isArray(oldValue) && !oldValue.length)) {
1111
+ newValue = [];
1112
+ }
1113
+ else if (!Array.isArray(oldValue)) {
1114
+ newValue = [oldValue];
1115
+ }
1116
+ else {
1117
+ newValue = oldValue;
1118
+ }
1119
+ newValue.push({
1120
+ type: '|',
1121
+ ref: guids.map((guid) => [name, guid]),
1122
+ });
1123
+ newSelector['selector'] = newValue;
759
1124
  }
760
- newValue.push(...guids.map((guid) => [name, guid]));
761
- newSelector[newKey] = newValue;
762
1125
  }
763
1126
  else {
1127
+ // Can't translate, so put the original back in.
764
1128
  if (!newSelector[key]) {
765
1129
  newSelector[key] = [];
766
1130
  }
@@ -785,6 +1149,7 @@ class PubSub {
785
1149
  newSelector[key].push(...tmpArr);
786
1150
  }
787
1151
  else {
1152
+ // @ts-ignore: ts doesn't know what value is here.
788
1153
  newSelector[key] = value;
789
1154
  }
790
1155
  }
@@ -793,5 +1158,4 @@ class PubSub {
793
1158
  return newSelectors;
794
1159
  }
795
1160
  }
796
- exports.default = PubSub;
797
1161
  //# sourceMappingURL=PubSub.js.map