@nymphjs/pubsub 1.0.0-beta.13 → 1.0.0-beta.131

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