@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/src/PubSub.ts CHANGED
@@ -7,16 +7,10 @@ import {
7
7
  SerializedEntityData,
8
8
  classNamesToEntityConstructors,
9
9
  } from '@nymphjs/nymph';
10
- import {
11
- request,
12
- client as WebSocketClient,
13
- server as WebSocketServer,
14
- connection,
15
- Message,
16
- } from 'websocket';
17
- import { difference } from 'lodash';
18
-
19
- import { Config, ConfigDefaults as defaults } from './conf';
10
+ import ws from 'websocket';
11
+ import { difference } from 'lodash-es';
12
+
13
+ import { Config, ConfigDefaults as defaults } from './conf/index.js';
20
14
  import type {
21
15
  QuerySubscriptionData,
22
16
  AuthenticateMessageData,
@@ -28,14 +22,14 @@ import type {
28
22
  PublishMessageData,
29
23
  MessageData,
30
24
  MessageOptions,
31
- } from './PubSub.types';
25
+ } from './PubSub.types.js';
32
26
 
33
27
  /**
34
28
  * A publish/subscribe server for Nymph.
35
29
  *
36
30
  * Written by Hunter Perrin for SciActive.
37
31
  *
38
- * @author Hunter Perrin <hperrin@gmail.com>
32
+ * @author Hunter Perrin <hperrin@port87.com>
39
33
  * @copyright SciActive Inc
40
34
  * @see http://nymph.io/
41
35
  */
@@ -53,207 +47,294 @@ export default class PubSub {
53
47
  /**
54
48
  * The WebSocket server.
55
49
  */
56
- public server: WebSocketServer;
50
+ public server: ws.server;
57
51
 
58
- private sessions = new Map<connection, string>();
52
+ private sessions = new Map<
53
+ ws.connection,
54
+ { authToken: string; switchToken?: string }
55
+ >();
59
56
  protected querySubs: {
60
57
  [etype: string]: {
61
- [query: string]: Map<connection, QuerySubscriptionData>;
58
+ [query: string]: Map<ws.connection, QuerySubscriptionData>;
62
59
  };
63
60
  } = {};
64
61
  protected uidSubs: {
65
- [uidName: string]: Map<connection, { count: boolean }>;
62
+ [uidName: string]: Map<ws.connection, { count: boolean }>;
66
63
  } = {};
64
+ protected outgoingClientMessages = new Map<
65
+ ws.connection,
66
+ { subscription: QuerySubscriptionData; publish: PublishEntityMessageData }[]
67
+ >();
68
+ protected outgoingClientMessagesTimeout: NodeJS.Immediate | null = null;
69
+ protected static transactionPublishes: {
70
+ nymph: Nymph;
71
+ payload: string;
72
+ config: Config;
73
+ }[] = [];
67
74
 
68
75
  public static initPublisher(config: Partial<Config>, nymph: Nymph) {
69
76
  const configWithDefaults: Config = { ...defaults, ...config };
77
+ const unsubscribers: (() => boolean)[] = [];
70
78
 
71
- nymph.on(
72
- 'beforeSaveEntity',
73
- async (nymph: Nymph, entity: EntityInterface) => {
79
+ unsubscribers.push(
80
+ nymph.on('beforeSaveEntity', async (enymph, entity) => {
74
81
  const guid = entity.guid;
75
- const etype = (entity.constructor as EntityConstructor).ETYPE;
82
+ const EntityClass = entity.constructor as EntityConstructor;
83
+ const etype = EntityClass.ETYPE;
76
84
 
77
- const off = nymph.on(
78
- 'afterSaveEntity',
79
- async (_nymph: Nymph, result: Promise<boolean>) => {
80
- off();
81
- if (!(await result)) {
82
- return;
83
- }
84
- this.publish(
85
- JSON.stringify({
86
- action: 'publish',
87
- event: guid == null ? 'create' : 'update',
88
- guid: entity.guid,
89
- entity: entity.toJSON(),
90
- etype: etype,
91
- }),
92
- configWithDefaults
93
- );
85
+ if (!EntityClass.pubSubEnabled) {
86
+ return;
87
+ }
88
+
89
+ const off = enymph.on('afterSaveEntity', async (curNymph, result) => {
90
+ off();
91
+ off2();
92
+ if (!(await result)) {
93
+ return;
94
94
  }
95
- );
96
- }
95
+ const payload = JSON.stringify({
96
+ action: 'publish',
97
+ event: guid == null ? 'create' : 'update',
98
+ guid: entity.guid,
99
+ entity: entity.toJSON(),
100
+ etype: etype,
101
+ });
102
+ this.transactionPublishes.push({
103
+ nymph: curNymph,
104
+ payload,
105
+ config: configWithDefaults,
106
+ });
107
+ await this.publishTransactionPublishes(curNymph);
108
+ });
109
+ const off2 = enymph.on('failedSaveEntity', async () => {
110
+ off();
111
+ off2();
112
+ });
113
+ }),
97
114
  );
98
115
 
99
- nymph.on(
100
- 'beforeDeleteEntity',
101
- async (nymph: Nymph, entity: EntityInterface) => {
116
+ unsubscribers.push(
117
+ nymph.on('beforeDeleteEntity', async (enymph, entity) => {
102
118
  const guid = entity.guid;
103
- const etype = (entity.constructor as EntityConstructor).ETYPE;
119
+ const EntityClass = entity.constructor as EntityConstructor;
120
+ const etype = EntityClass.ETYPE;
104
121
 
105
- const off = nymph.on(
106
- 'afterDeleteEntity',
107
- async (_nymph: Nymph, result: Promise<boolean>) => {
108
- off();
109
- if (!(await result)) {
110
- return;
111
- }
112
- this.publish(
113
- JSON.stringify({
114
- action: 'publish',
115
- event: 'delete',
116
- guid: guid,
117
- etype: etype,
118
- }),
119
- configWithDefaults
120
- );
122
+ if (!EntityClass.pubSubEnabled) {
123
+ return;
124
+ }
125
+
126
+ const off = enymph.on('afterDeleteEntity', async (curNymph, result) => {
127
+ off();
128
+ off2();
129
+ if (!(await result)) {
130
+ return;
121
131
  }
122
- );
123
- }
132
+ const payload = JSON.stringify({
133
+ action: 'publish',
134
+ event: 'delete',
135
+ guid: guid,
136
+ etype: etype,
137
+ });
138
+ this.transactionPublishes.push({
139
+ nymph: curNymph,
140
+ payload,
141
+ config: configWithDefaults,
142
+ });
143
+ await this.publishTransactionPublishes(curNymph);
144
+ });
145
+ const off2 = enymph.on('failedDeleteEntity', async () => {
146
+ off();
147
+ off2();
148
+ });
149
+ }),
124
150
  );
125
151
 
126
- nymph.on(
127
- 'beforeDeleteEntityByID',
128
- async (nymph: Nymph, guid: string, className?: string) => {
152
+ unsubscribers.push(
153
+ nymph.on('beforeDeleteEntityByID', async (enymph, guid, className) => {
129
154
  try {
130
- const etype = nymph.getEntityClass(className ?? 'Entity').ETYPE;
155
+ const EntityClass = enymph.getEntityClass(className ?? 'Entity');
156
+ const etype = EntityClass.ETYPE;
131
157
 
132
- const off = nymph.on(
158
+ if (!EntityClass.pubSubEnabled) {
159
+ return;
160
+ }
161
+
162
+ const off = enymph.on(
133
163
  'afterDeleteEntityByID',
134
- async (_nymph: Nymph, result: Promise<boolean>) => {
164
+ async (curNymph, result) => {
135
165
  off();
166
+ off2();
136
167
  if (!(await result)) {
137
168
  return;
138
169
  }
139
- this.publish(
140
- JSON.stringify({
141
- action: 'publish',
142
- event: 'delete',
143
- guid: guid,
144
- etype: etype,
145
- }),
146
- configWithDefaults
147
- );
148
- }
170
+ const payload = JSON.stringify({
171
+ action: 'publish',
172
+ event: 'delete',
173
+ guid: guid,
174
+ etype: etype,
175
+ });
176
+ this.transactionPublishes.push({
177
+ nymph: curNymph,
178
+ payload,
179
+ config: configWithDefaults,
180
+ });
181
+ await this.publishTransactionPublishes(curNymph);
182
+ },
149
183
  );
184
+ const off2 = enymph.on('failedDeleteEntityByID', async () => {
185
+ off();
186
+ off2();
187
+ });
150
188
  } catch (e: any) {
151
189
  return;
152
190
  }
153
- }
191
+ }),
154
192
  );
155
193
 
156
- nymph.on('beforeNewUID', async (nymph: Nymph, name: string) => {
157
- const off = nymph.on(
158
- 'afterNewUID',
159
- async (_nymph: Nymph, result: Promise<number | null>) => {
194
+ unsubscribers.push(
195
+ nymph.on('beforeNewUID', async (enymph, name) => {
196
+ const off = enymph.on('afterNewUID', async (curNymph, result) => {
160
197
  off();
198
+ off2();
161
199
  const value = await result;
162
200
  if (value == null) {
163
201
  return;
164
202
  }
165
- this.publish(
166
- JSON.stringify({
167
- action: 'publish',
168
- event: 'newUID',
169
- name: name,
170
- value: value,
171
- }),
172
- configWithDefaults
173
- );
174
- }
175
- );
176
- });
203
+ const payload = JSON.stringify({
204
+ action: 'publish',
205
+ event: 'newUID',
206
+ name: name,
207
+ value: value,
208
+ });
209
+ this.transactionPublishes.push({
210
+ nymph: curNymph,
211
+ payload,
212
+ config: configWithDefaults,
213
+ });
214
+ await this.publishTransactionPublishes(curNymph);
215
+ });
216
+ const off2 = enymph.on('failedNewUID', async () => {
217
+ off();
218
+ off2();
219
+ });
220
+ }),
221
+ );
177
222
 
178
- nymph.on(
179
- 'beforeSetUID',
180
- async (nymph: Nymph, name: string, value: number) => {
181
- const off = nymph.on(
182
- 'afterSetUID',
183
- async (_nymph: Nymph, result: Promise<boolean>) => {
184
- off();
185
- if (!(await result)) {
186
- return;
187
- }
188
- this.publish(
189
- JSON.stringify({
190
- action: 'publish',
191
- event: 'setUID',
192
- name: name,
193
- value: value,
194
- }),
195
- configWithDefaults
196
- );
223
+ unsubscribers.push(
224
+ nymph.on('beforeSetUID', async (enymph, name, value) => {
225
+ const off = enymph.on('afterSetUID', async (curNymph, result) => {
226
+ off();
227
+ off2();
228
+ if (!(await result)) {
229
+ return;
197
230
  }
198
- );
199
- }
231
+ const payload = JSON.stringify({
232
+ action: 'publish',
233
+ event: 'setUID',
234
+ name: name,
235
+ value: value,
236
+ });
237
+ this.transactionPublishes.push({
238
+ nymph: curNymph,
239
+ payload,
240
+ config: configWithDefaults,
241
+ });
242
+ await this.publishTransactionPublishes(curNymph);
243
+ });
244
+ const off2 = enymph.on('failedSetUID', async () => {
245
+ off();
246
+ off2();
247
+ });
248
+ }),
200
249
  );
201
250
 
202
- nymph.on(
203
- 'beforeRenameUID',
204
- async (nymph: Nymph, oldName: string, newName: string) => {
205
- const off = nymph.on(
206
- 'afterRenameUID',
207
- async (_nymph: Nymph, result: Promise<boolean>) => {
208
- off();
209
- if (!(await result)) {
210
- return;
211
- }
212
- this.publish(
213
- JSON.stringify({
214
- action: 'publish',
215
- event: 'renameUID',
216
- oldName: oldName,
217
- newName: newName,
218
- }),
219
- configWithDefaults
220
- );
251
+ unsubscribers.push(
252
+ nymph.on('beforeRenameUID', async (enymph, oldName, newName) => {
253
+ const off = enymph.on('afterRenameUID', async (curNymph, result) => {
254
+ off();
255
+ off2();
256
+ if (!(await result)) {
257
+ return;
221
258
  }
222
- );
223
- }
259
+ const payload = JSON.stringify({
260
+ action: 'publish',
261
+ event: 'renameUID',
262
+ oldName: oldName,
263
+ newName: newName,
264
+ });
265
+ this.transactionPublishes.push({
266
+ nymph: curNymph,
267
+ payload,
268
+ config: configWithDefaults,
269
+ });
270
+ await this.publishTransactionPublishes(curNymph);
271
+ });
272
+ const off2 = enymph.on('failedRenameUID', async () => {
273
+ off();
274
+ off2();
275
+ });
276
+ }),
224
277
  );
225
278
 
226
- nymph.on('beforeDeleteUID', async (nymph: Nymph, name: string) => {
227
- const off = nymph.on(
228
- 'afterDeleteUID',
229
- async (_nymph: Nymph, result: Promise<boolean>) => {
279
+ unsubscribers.push(
280
+ nymph.on('beforeDeleteUID', async (enymph, name) => {
281
+ const off = enymph.on('afterDeleteUID', async (curNymph, result) => {
230
282
  off();
283
+ off2();
231
284
  if (!(await result)) {
232
285
  return;
233
286
  }
234
- this.publish(
235
- JSON.stringify({
236
- action: 'publish',
237
- event: 'deleteUID',
238
- name: name,
239
- }),
240
- configWithDefaults
241
- );
287
+ const payload = JSON.stringify({
288
+ action: 'publish',
289
+ event: 'deleteUID',
290
+ name: name,
291
+ });
292
+ this.transactionPublishes.push({
293
+ nymph: curNymph,
294
+ payload,
295
+ config: configWithDefaults,
296
+ });
297
+ await this.publishTransactionPublishes(curNymph);
298
+ });
299
+ const off2 = enymph.on('failedDeleteUID', async () => {
300
+ off();
301
+ off2();
302
+ });
303
+ }),
304
+ );
305
+
306
+ unsubscribers.push(
307
+ nymph.on('afterCommitTransaction', async (enymph, _name, result) => {
308
+ if (result) {
309
+ await this.publishTransactionPublishes(enymph);
242
310
  }
243
- );
244
- });
311
+ }),
312
+ );
313
+
314
+ unsubscribers.push(
315
+ nymph.on('afterRollbackTransaction', async (enymph) => {
316
+ this.removeTransactionPublishes(enymph);
317
+ }),
318
+ );
319
+
320
+ return () => {
321
+ for (let unsubscribe of unsubscribers) {
322
+ unsubscribe();
323
+ }
324
+ this.transactionPublishes = [];
325
+ };
245
326
  }
246
327
 
247
328
  private static publish(message: string, config: Config) {
248
329
  for (let host of config.entries ?? []) {
249
- const client = new WebSocketClient();
330
+ const client = new ws.client();
250
331
 
251
332
  client.on('connectFailed', (error) => {
252
333
  if (config.logger) {
253
334
  config.logger(
254
335
  'error',
255
336
  new Date().toISOString(),
256
- `Publish connection failed. (${error.toString()}, ${host})`
337
+ `Publish connection failed. (${error.toString()}, ${host})`,
257
338
  );
258
339
  }
259
340
  });
@@ -264,7 +345,7 @@ export default class PubSub {
264
345
  config.logger(
265
346
  'error',
266
347
  new Date().toISOString(),
267
- `Publish connect error. (${error.toString()}, ${host})`
348
+ `Publish connect error. (${error.toString()}, ${host})`,
268
349
  );
269
350
  }
270
351
  });
@@ -280,16 +361,81 @@ export default class PubSub {
280
361
  }
281
362
  }
282
363
 
364
+ private static isOrIsDescendant(parent: Nymph, child: Nymph) {
365
+ let check: Nymph | null = child;
366
+ while (check) {
367
+ if (check === parent) {
368
+ return true;
369
+ }
370
+ check = check.parent;
371
+ }
372
+ return false;
373
+ }
374
+
375
+ private static async publishTransactionPublishes(nymph: Nymph) {
376
+ if (await nymph.inTransaction()) {
377
+ // This instance is still in a transaction, so nothing gets published yet.
378
+ return;
379
+ }
380
+
381
+ let notReadyTransactionPublishes: typeof this.transactionPublishes = [];
382
+ let readyTransactionPublishes: typeof this.transactionPublishes = [];
383
+
384
+ for (let publish of this.transactionPublishes) {
385
+ if (
386
+ !this.isOrIsDescendant(nymph, publish.nymph) ||
387
+ (await publish.nymph.inTransaction())
388
+ ) {
389
+ notReadyTransactionPublishes.push(publish);
390
+ } else {
391
+ readyTransactionPublishes.push(publish);
392
+ }
393
+ }
394
+
395
+ if (!readyTransactionPublishes.length) {
396
+ return;
397
+ }
398
+
399
+ this.transactionPublishes = notReadyTransactionPublishes;
400
+
401
+ let configMap = new Map<Config, string[]>();
402
+
403
+ for (let publish of readyTransactionPublishes) {
404
+ const arr = configMap.get(publish.config) || [];
405
+ arr.push(publish.payload);
406
+ configMap.set(publish.config, arr);
407
+ }
408
+
409
+ for (let [config, payloads] of configMap.entries()) {
410
+ if (payloads.length === 1) {
411
+ this.publish(payloads[0], config);
412
+ } else {
413
+ this.publish(
414
+ JSON.stringify({
415
+ action: 'multiple',
416
+ messages: payloads.map((payload) => JSON.parse(payload)),
417
+ }),
418
+ config,
419
+ );
420
+ }
421
+ }
422
+ }
423
+
424
+ private static removeTransactionPublishes(nymph: Nymph) {
425
+ this.transactionPublishes = this.transactionPublishes.filter((publish) => {
426
+ if (this.isOrIsDescendant(nymph, publish.nymph)) {
427
+ return false;
428
+ }
429
+ return true;
430
+ });
431
+ }
432
+
283
433
  /**
284
434
  * Initialize Nymph PubSub.
285
435
  *
286
436
  * @param config The PubSub configuration.
287
437
  */
288
- public constructor(
289
- config: Partial<Config>,
290
- nymph: Nymph,
291
- server: WebSocketServer
292
- ) {
438
+ public constructor(config: Partial<Config>, nymph: Nymph, server: ws.server) {
293
439
  this.nymph = nymph;
294
440
  this.config = { ...defaults, ...config };
295
441
  this.server = server;
@@ -301,14 +447,14 @@ export default class PubSub {
301
447
  this.server.shutDown();
302
448
  }
303
449
 
304
- public handleRequest(request: request) {
450
+ public handleRequest(request: ws.request) {
305
451
  if (!this.config.originIsAllowed(request.origin)) {
306
452
  // Make sure we only accept requests from an allowed origin
307
453
  request.reject();
308
454
  this.config.logger(
309
455
  'log',
310
456
  new Date().toISOString(),
311
- 'Client from origin ' + request.origin + ' was kicked by the bouncer.'
457
+ 'Client from origin ' + request.origin + ' was kicked by the bouncer.',
312
458
  );
313
459
  return;
314
460
  }
@@ -317,7 +463,7 @@ export default class PubSub {
317
463
  this.config.logger(
318
464
  'log',
319
465
  new Date().toISOString(),
320
- `Client joined the party! (${connection.remoteAddress}).`
466
+ `Client joined the party! (${connection.remoteAddress}).`,
321
467
  );
322
468
 
323
469
  connection.on('error', (err) => {
@@ -336,41 +482,65 @@ export default class PubSub {
336
482
  /**
337
483
  * Handle a message from a client.
338
484
  */
339
- public async onMessage(from: connection, msg: Message) {
485
+ public async onMessage(from: ws.connection, msg: ws.Message) {
340
486
  if (msg.type !== 'utf8') {
341
487
  throw new Error("This server doesn't accept binary messages.");
342
488
  }
343
489
  const data: MessageData = JSON.parse(msg.utf8Data);
344
490
  if (
345
491
  !data.action ||
346
- ['authenticate', 'subscribe', 'unsubscribe', 'publish'].indexOf(
347
- data.action
348
- ) === -1
492
+ [
493
+ 'authenticate',
494
+ 'subscribe',
495
+ 'unsubscribe',
496
+ 'publish',
497
+ 'multiple',
498
+ ].indexOf(data.action) === -1
349
499
  ) {
350
500
  return;
351
501
  }
352
- switch (data.action) {
353
- case 'authenticate':
354
- this.handleAuthentication(from, data);
355
- break;
356
- case 'subscribe':
357
- case 'unsubscribe':
358
- await this.handleSubscription(from, data);
359
- break;
360
- case 'publish':
361
- await this.handlePublish(from, msg, data);
362
- break;
363
- }
502
+ const handleMessageData = async (data: MessageData, dontRelay = false) => {
503
+ switch (data.action) {
504
+ case 'authenticate':
505
+ this.handleAuthentication(from, data);
506
+ break;
507
+ case 'subscribe':
508
+ case 'unsubscribe':
509
+ await this.handleSubscription(from, data);
510
+ break;
511
+ case 'publish':
512
+ await this.handlePublish(from, data);
513
+ if (!dontRelay) {
514
+ // Relay the publish to other servers.
515
+ this.relay(msg);
516
+ }
517
+ break;
518
+ case 'multiple':
519
+ for (let individual of data.messages) {
520
+ await handleMessageData(
521
+ individual,
522
+ // These will be published with the multiple payload.
523
+ individual.action === 'publish',
524
+ );
525
+ }
526
+ if (data.messages.find((msg) => msg.action === 'publish') != null) {
527
+ // Relay the publish to other servers.
528
+ this.relay(msg);
529
+ }
530
+ break;
531
+ }
532
+ };
533
+ await handleMessageData(data);
364
534
  }
365
535
 
366
536
  /**
367
537
  * Clean up after users who leave.
368
538
  */
369
- public onClose(conn: connection, description: string) {
539
+ public onClose(conn: ws.connection, description: string) {
370
540
  this.config.logger(
371
541
  'log',
372
542
  new Date().toISOString(),
373
- `Client skedaddled. (${description}, ${conn.remoteAddress})`
543
+ `Client skedaddled. (${description}, ${conn.remoteAddress})`,
374
544
  );
375
545
 
376
546
  let mess = 0;
@@ -398,7 +568,7 @@ export default class PubSub {
398
568
  JSON.stringify({
399
569
  query: curData.query,
400
570
  count,
401
- })
571
+ }),
402
572
  );
403
573
  }
404
574
  }
@@ -428,7 +598,7 @@ export default class PubSub {
428
598
  JSON.stringify({
429
599
  uid: curUID,
430
600
  count,
431
- })
601
+ }),
432
602
  );
433
603
  }
434
604
  }
@@ -447,16 +617,16 @@ export default class PubSub {
447
617
  this.config.logger(
448
618
  'log',
449
619
  new Date().toISOString(),
450
- `Cleaned up client's mess. (${mess}, ${conn.remoteAddress})`
620
+ `Cleaned up client's mess. (${mess}, ${conn.remoteAddress})`,
451
621
  );
452
622
  }
453
623
  }
454
624
 
455
- public onError(conn: connection, e: Error) {
625
+ public onError(conn: ws.connection, e: Error) {
456
626
  this.config.logger(
457
627
  'error',
458
628
  new Date().toISOString(),
459
- `An error occured. (${e.message}, ${conn.remoteAddress})`
629
+ `An error occured. (${e.message}, ${conn.remoteAddress})`,
460
630
  );
461
631
  }
462
632
 
@@ -464,13 +634,14 @@ export default class PubSub {
464
634
  * Handle an authentication from a client.
465
635
  */
466
636
  private handleAuthentication(
467
- from: connection,
468
- data: AuthenticateMessageData
637
+ from: ws.connection,
638
+ data: AuthenticateMessageData,
469
639
  ) {
470
640
  // Save the user's auth token in session storage.
471
- const token = data.token;
472
- if (token != null) {
473
- this.sessions.set(from, token);
641
+ const authToken = data.authToken;
642
+ const switchToken = data.switchToken;
643
+ if (authToken != null) {
644
+ this.sessions.set(from, { authToken, switchToken });
474
645
  } else if (this.sessions.has(from)) {
475
646
  this.sessions.delete(from);
476
647
  }
@@ -480,21 +651,31 @@ export default class PubSub {
480
651
  * Handle a subscribe or unsubscribe from a client.
481
652
  */
482
653
  private async handleSubscription(
483
- from: connection,
484
- data: SubscribeMessageData
654
+ from: ws.connection,
655
+ data: SubscribeMessageData,
485
656
  ) {
486
- if ('query' in data && data.query != null) {
487
- // Request is for a query.
488
-
489
- await this.handleSubscriptionQuery(from, data);
490
- } else if (
491
- 'uid' in data &&
492
- data.uid != null &&
493
- typeof data.uid == 'string'
494
- ) {
495
- // Request is for a UID.
496
-
497
- await this.handleSubscriptionUid(from, data);
657
+ try {
658
+ if ('query' in data && data.query != null) {
659
+ // Request is for a query.
660
+
661
+ await this.handleSubscriptionQuery(from, data);
662
+ } else if (
663
+ 'uid' in data &&
664
+ data.uid != null &&
665
+ typeof data.uid == 'string'
666
+ ) {
667
+ // Request is for a UID.
668
+
669
+ await this.handleSubscriptionUid(from, data);
670
+ }
671
+ } catch (e: any) {
672
+ if ('query' in data && data.query != null) {
673
+ from.sendUTF(JSON.stringify({ query: data.query, error: e.message }));
674
+ } else if ('uid' in data && data.uid != null) {
675
+ from.sendUTF(JSON.stringify({ uid: data.uid, error: e.message }));
676
+ } else {
677
+ from.sendUTF(JSON.stringify({ error: e.message }));
678
+ }
498
679
  }
499
680
  }
500
681
 
@@ -502,33 +683,39 @@ export default class PubSub {
502
683
  * Handle a subscribe or unsubscribe for a query from a client.
503
684
  */
504
685
  private async handleSubscriptionQuery(
505
- from: connection,
686
+ from: ws.connection,
506
687
  data: QuerySubscribeMessageData,
507
688
  qrefParent?: {
508
689
  etype: string;
509
690
  query: string;
510
- }
691
+ },
511
692
  ) {
512
- let args: [MessageOptions, ...Selector[]];
513
- let EntityClass: EntityConstructor;
514
- try {
515
- args = JSON.parse(data.query);
516
- EntityClass = this.nymph.getEntityClass(args[0].class);
517
- } catch (e: any) {
518
- return;
693
+ let args: [MessageOptions, ...Selector[]] = JSON.parse(data.query);
694
+ let EntityClass = this.nymph.getEntityClass(args[0].class);
695
+ if (!EntityClass.restEnabled) {
696
+ throw new Error('Not accessible.');
519
697
  }
520
698
  const etype = EntityClass.ETYPE;
521
699
  const serialArgs = JSON.stringify(args);
522
700
  const [clientOptions, ...selectors] = args;
523
- const options: Options & { return: 'guid' } = {
701
+ const options: Options & { return: 'entity' } = {
524
702
  ...clientOptions,
525
703
  class: EntityClass,
526
- return: 'guid',
704
+ return: 'entity',
527
705
  source: 'client',
528
706
  };
529
707
  // Find qref queries.
530
708
  const qrefQueries = this.findQRefQueries(clientOptions, ...selectors);
531
709
 
710
+ // Check that all qref queries are accessible classes.
711
+ for (const qrefQuery of qrefQueries) {
712
+ const args = qrefQuery;
713
+ const EntityClass = this.nymph.getEntityClass(args[0].class);
714
+ if (!EntityClass.restEnabled) {
715
+ throw new Error('Not accessible.');
716
+ }
717
+ }
718
+
532
719
  if (data.action === 'subscribe') {
533
720
  // Client is subscribing to a query.
534
721
 
@@ -543,7 +730,7 @@ export default class PubSub {
543
730
  {
544
731
  etype,
545
732
  query: serialArgs,
546
- }
733
+ },
547
734
  );
548
735
  }
549
736
 
@@ -554,18 +741,33 @@ export default class PubSub {
554
741
  if (!(serialArgs in this.querySubs[etype])) {
555
742
  this.querySubs[etype][serialArgs] = new Map();
556
743
  }
557
- let token = null;
744
+
745
+ let authToken = null;
746
+ let switchToken = null;
558
747
  if (this.sessions.has(from)) {
559
- token = this.sessions.get(from);
748
+ const session = this.sessions.get(from);
749
+ authToken = session?.authToken;
750
+ switchToken = session?.switchToken;
560
751
  }
561
752
  const nymph = this.nymph.clone();
562
- if (nymph.tilmeld != null && token != null) {
563
- const user = await nymph.tilmeld.extractToken(token);
564
- if (user) {
565
- // Log in the user for access controls.
566
- nymph.tilmeld.fillSession(user);
753
+ if (nymph.tilmeld != null && authToken != null) {
754
+ const user = await nymph.tilmeld.extractToken(authToken);
755
+ if (user && user.enabled) {
756
+ if (switchToken != null) {
757
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
758
+ if (switchUser) {
759
+ // Log in the switchUser for access controls.
760
+ await nymph.tilmeld.fillSession(switchUser);
761
+ }
762
+ } else {
763
+ // Log in the user for access controls.
764
+ await nymph.tilmeld.fillSession(user);
765
+ }
567
766
  }
568
767
  }
768
+
769
+ let entities: EntityInterface[] = [];
770
+ let sendEntities = false;
569
771
  const existingSub = this.querySubs[etype][serialArgs].get(from);
570
772
  if (existingSub) {
571
773
  if (qrefParent) {
@@ -577,23 +779,49 @@ export default class PubSub {
577
779
  if (data.count && !existingSub.count) {
578
780
  existingSub.count = true;
579
781
  }
782
+ sendEntities = existingSub.direct;
783
+ if (sendEntities) {
784
+ entities = existingSub.current.length
785
+ ? await nymph.getEntities(
786
+ {
787
+ class: EntityClass,
788
+ source: 'client',
789
+ },
790
+ { type: '|', guid: existingSub.current },
791
+ )
792
+ : [];
793
+ }
580
794
  } else {
795
+ entities = await nymph.getEntities(options, ...selectors);
581
796
  this.querySubs[etype][serialArgs].set(from, {
582
- current: await nymph.getEntities(options, ...selectors),
797
+ current: entities.map((e) => e.guid as string),
583
798
  query: data.query,
584
799
  qrefParents: qrefParent ? [qrefParent] : [],
585
800
  direct: !qrefParent,
586
801
  count: !!data.count,
587
802
  });
803
+ sendEntities = !qrefParent;
588
804
  }
589
- if (nymph.tilmeld != null && token != null) {
805
+
806
+ if (sendEntities) {
807
+ // Notify the client of the current value.
808
+ from.sendUTF(
809
+ JSON.stringify({
810
+ query: data.query,
811
+ set: true,
812
+ data: entities,
813
+ }),
814
+ );
815
+ }
816
+
817
+ if (nymph.tilmeld != null && authToken != null) {
590
818
  // Clear the user that was temporarily logged in.
591
819
  nymph.tilmeld.clearSession();
592
820
  }
593
821
  this.config.logger(
594
822
  'log',
595
823
  new Date().toISOString(),
596
- `Client subscribed to a query! (${serialArgs}, ${from.remoteAddress})`
824
+ `Client subscribed to a query! (${serialArgs}, ${from.remoteAddress})`,
597
825
  );
598
826
 
599
827
  if (this.config.broadcastCounts) {
@@ -606,7 +834,7 @@ export default class PubSub {
606
834
  JSON.stringify({
607
835
  query: curData.query,
608
836
  count,
609
- })
837
+ }),
610
838
  );
611
839
  }
612
840
  }
@@ -627,7 +855,7 @@ export default class PubSub {
627
855
  {
628
856
  etype,
629
857
  query: serialArgs,
630
- }
858
+ },
631
859
  );
632
860
  }
633
861
 
@@ -649,7 +877,7 @@ export default class PubSub {
649
877
  !(
650
878
  qrefParent.etype === parent.etype &&
651
879
  qrefParent.query === parent.query
652
- )
880
+ ),
653
881
  );
654
882
  }
655
883
  if (!qrefParent) {
@@ -662,7 +890,7 @@ export default class PubSub {
662
890
  this.config.logger(
663
891
  'log',
664
892
  new Date().toISOString(),
665
- `Client unsubscribed from a query! (${serialArgs}, ${from.remoteAddress})`
893
+ `Client unsubscribed from a query! (${serialArgs}, ${from.remoteAddress})`,
666
894
  );
667
895
 
668
896
  const count = this.querySubs[etype][serialArgs].size;
@@ -685,7 +913,7 @@ export default class PubSub {
685
913
  JSON.stringify({
686
914
  query: curData.query,
687
915
  count,
688
- })
916
+ }),
689
917
  );
690
918
  }
691
919
  }
@@ -697,8 +925,8 @@ export default class PubSub {
697
925
  * Handle a subscribe or unsubscribe for a UID from a client.
698
926
  */
699
927
  private async handleSubscriptionUid(
700
- from: connection,
701
- data: UidSubscribeMessageData
928
+ from: ws.connection,
929
+ data: UidSubscribeMessageData,
702
930
  ) {
703
931
  if (data.action === 'subscribe') {
704
932
  // Client is subscribing to a UID.
@@ -708,10 +936,49 @@ export default class PubSub {
708
936
  this.uidSubs[data['uid']].set(from, {
709
937
  count: !!data.count,
710
938
  });
939
+
940
+ let authToken = null;
941
+ let switchToken = null;
942
+ if (this.sessions.has(from)) {
943
+ const session = this.sessions.get(from);
944
+ authToken = session?.authToken;
945
+ switchToken = session?.switchToken;
946
+ }
947
+ const nymph = this.nymph.clone();
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
+ } else {
958
+ // Log in the user for access controls.
959
+ await nymph.tilmeld.fillSession(user);
960
+ }
961
+ }
962
+ }
963
+
964
+ // Notify the client of the current value.
965
+ from.sendUTF(
966
+ JSON.stringify({
967
+ uid: data.uid,
968
+ set: true,
969
+ data: await this.nymph.getUID(data.uid),
970
+ }),
971
+ );
972
+
973
+ if (nymph.tilmeld != null && authToken != null) {
974
+ // Clear the user that was temporarily logged in.
975
+ nymph.tilmeld.clearSession();
976
+ }
977
+
711
978
  this.config.logger(
712
979
  'log',
713
980
  new Date().toISOString(),
714
- `Client subscribed to a UID! (${data.uid}, ${from.remoteAddress})`
981
+ `Client subscribed to a UID! (${data.uid}, ${from.remoteAddress})`,
715
982
  );
716
983
 
717
984
  if (this.config.broadcastCounts) {
@@ -724,7 +991,7 @@ export default class PubSub {
724
991
  JSON.stringify({
725
992
  uid: data.uid,
726
993
  count,
727
- })
994
+ }),
728
995
  );
729
996
  }
730
997
  }
@@ -743,7 +1010,7 @@ export default class PubSub {
743
1010
  this.config.logger(
744
1011
  'log',
745
1012
  new Date().toISOString(),
746
- `Client unsubscribed from a UID! (${data.uid}, ${from.remoteAddress})`
1013
+ `Client unsubscribed from a UID! (${data.uid}, ${from.remoteAddress})`,
747
1014
  );
748
1015
 
749
1016
  const count = this.uidSubs[data.uid].size;
@@ -763,7 +1030,7 @@ export default class PubSub {
763
1030
  JSON.stringify({
764
1031
  uid: data.uid,
765
1032
  count,
766
- })
1033
+ }),
767
1034
  );
768
1035
  }
769
1036
  }
@@ -774,11 +1041,7 @@ export default class PubSub {
774
1041
  /**
775
1042
  * Handle a publish from a client.
776
1043
  */
777
- private async handlePublish(
778
- from: connection,
779
- msg: Message,
780
- data: PublishMessageData
781
- ) {
1044
+ private async handlePublish(from: ws.connection, data: PublishMessageData) {
782
1045
  if (
783
1046
  'guid' in data &&
784
1047
  typeof data.guid === 'string' &&
@@ -791,9 +1054,6 @@ export default class PubSub {
791
1054
  // Publish is an entity.
792
1055
 
793
1056
  await this.handlePublishEntity(from, data);
794
-
795
- // Relay the publish to other servers.
796
- this.relay(msg);
797
1057
  }
798
1058
 
799
1059
  if (
@@ -807,9 +1067,6 @@ export default class PubSub {
807
1067
  // Publish is a UID.
808
1068
 
809
1069
  await this.handlePublishUid(from, data);
810
-
811
- // Relay the publish to other servers.
812
- this.relay(msg);
813
1070
  }
814
1071
  }
815
1072
 
@@ -817,13 +1074,13 @@ export default class PubSub {
817
1074
  * Handle an entity publish from a client.
818
1075
  */
819
1076
  private async handlePublishEntity(
820
- from: connection,
821
- data: PublishEntityMessageData
1077
+ from: ws.connection,
1078
+ data: PublishEntityMessageData,
822
1079
  ) {
823
1080
  this.config.logger(
824
1081
  'log',
825
1082
  new Date().toISOString(),
826
- `Received an entity publish! (${data.guid}, ${data.event}, ${from.remoteAddress})`
1083
+ `Received an entity publish! (${data.guid}, ${data.event}, ${from.remoteAddress})`,
827
1084
  );
828
1085
 
829
1086
  const etype = data.etype;
@@ -834,7 +1091,7 @@ export default class PubSub {
834
1091
 
835
1092
  for (let curQuery in this.querySubs[etype]) {
836
1093
  const curClients = this.querySubs[etype][curQuery];
837
- const updatedClients = new Set<connection>();
1094
+ const updatedClients = new Set<ws.connection>();
838
1095
 
839
1096
  if (data.event === 'delete' || data.event === 'update') {
840
1097
  // Check if it is in any client's currents.
@@ -845,7 +1102,7 @@ export default class PubSub {
845
1102
  continue;
846
1103
  }
847
1104
  if (curData.current.indexOf(data.guid) !== -1) {
848
- await this.updateClient(curClient, curData, data);
1105
+ this.addOutgoingClientMessage(curClient, curData, data);
849
1106
  updatedClients.add(curClient);
850
1107
  }
851
1108
  }
@@ -853,7 +1110,7 @@ export default class PubSub {
853
1110
  this.config.logger(
854
1111
  'error',
855
1112
  new Date().toISOString(),
856
- `Error checking for client updates! (${e?.message})`
1113
+ `Error checking for client updates! (${e?.message})`,
857
1114
  );
858
1115
  }
859
1116
  }
@@ -870,7 +1127,7 @@ export default class PubSub {
870
1127
  const entitySData: SerializedEntityData = {};
871
1128
  if (typeof data.entity.class !== 'string') {
872
1129
  throw new Error(
873
- `Received entity data class is not valid: ${data.entity.class}`
1130
+ `Received entity data class is not valid: ${data.entity.class}`,
874
1131
  );
875
1132
  }
876
1133
  const DataEntityClass = this.nymph.getEntityClass(data.entity.class);
@@ -879,11 +1136,12 @@ export default class PubSub {
879
1136
  EntityClass.ETYPE === DataEntityClass.ETYPE &&
880
1137
  (qrefQueries.length ||
881
1138
  this.nymph.driver.checkData(
1139
+ EntityClass.ETYPE,
882
1140
  entityData,
883
1141
  entitySData,
884
1142
  selectors,
885
1143
  data.guid,
886
- data.entity?.tags ?? []
1144
+ data.entity?.tags ?? [],
887
1145
  ))
888
1146
  ) {
889
1147
  // It either matches the query, or there are qref queries.
@@ -903,15 +1161,16 @@ export default class PubSub {
903
1161
  if (qrefQueries.length) {
904
1162
  const translatedSelectors = this.translateQRefSelectors(
905
1163
  curClient,
906
- selectors
1164
+ selectors,
907
1165
  );
908
1166
  if (
909
1167
  !this.nymph.driver.checkData(
1168
+ EntityClass.ETYPE,
910
1169
  entityData,
911
1170
  entitySData,
912
1171
  translatedSelectors,
913
1172
  data.guid,
914
- data.entity?.tags ?? []
1173
+ data.entity?.tags ?? [],
915
1174
  )
916
1175
  ) {
917
1176
  // The query doesn't match when the qref queries are filled.
@@ -919,138 +1178,201 @@ export default class PubSub {
919
1178
  }
920
1179
  }
921
1180
 
922
- await this.updateClient(curClient, curData, data);
1181
+ this.addOutgoingClientMessage(curClient, curData, data);
923
1182
  }
924
1183
  }
925
1184
  } catch (e: any) {
926
1185
  this.config.logger(
927
1186
  'error',
928
1187
  new Date().toISOString(),
929
- `Error checking for client updates! (${e?.message})`
1188
+ `Error checking for client updates! (${e?.message})`,
930
1189
  );
931
1190
  }
932
1191
  }
933
1192
  }
934
1193
  }
935
1194
 
936
- private async updateClient(
937
- curClient: connection,
1195
+ private addOutgoingClientMessage(
1196
+ curClient: ws.connection,
938
1197
  curData: QuerySubscriptionData,
939
- data: PublishEntityMessageData
1198
+ data: PublishEntityMessageData,
940
1199
  ) {
941
- // Update currents list.
942
- let current: EntityInterface[];
943
- let token: string | undefined;
944
- const nymph = this.nymph.clone();
945
- try {
946
- const [clientOptions, ...clientSelectors] = JSON.parse(curData.query);
947
- const options: Options = {
948
- ...clientOptions,
949
- class: nymph.getEntityClass(clientOptions.class),
950
- return: 'entity',
951
- source: 'client',
952
- skipAc: false,
953
- };
954
- const selectors = classNamesToEntityConstructors(nymph, clientSelectors);
955
- if (this.sessions.has(curClient)) {
956
- token = this.sessions.get(curClient);
957
- }
958
- if (nymph.tilmeld != null && token != null) {
959
- const user = await nymph.tilmeld.extractToken(token);
960
- if (user) {
961
- // Log in the user for access controls.
962
- nymph.tilmeld.fillSession(user);
963
- }
964
- }
965
- current = await nymph.getEntities(options, ...selectors);
966
- } catch (e: any) {
967
- this.config.logger(
968
- 'error',
969
- new Date().toISOString(),
970
- `Error updating client! (${e?.message}, ${curClient.remoteAddress})`
971
- );
972
- return;
1200
+ const arr = this.outgoingClientMessages.get(curClient) || [];
1201
+ arr.push({ subscription: curData, publish: data });
1202
+ this.outgoingClientMessages.set(curClient, arr);
1203
+
1204
+ if (this.outgoingClientMessagesTimeout) {
1205
+ clearImmediate(this.outgoingClientMessagesTimeout);
973
1206
  }
974
1207
 
975
- const entityMap = Object.fromEntries(
976
- current.map((entity) => [entity.guid, entity])
977
- );
978
- const currentGuids = current.map((entity) => entity.guid ?? '');
979
- const removed = difference(curData.current, currentGuids);
980
- const added = difference(currentGuids, curData.current);
1208
+ this.outgoingClientMessagesTimeout = setImmediate(async () => {
1209
+ await this.updateClients();
1210
+ });
1211
+ }
981
1212
 
982
- if (curData.direct) {
983
- for (let guid of removed) {
984
- // Notify subscriber.
985
- this.config.logger(
986
- 'log',
987
- new Date().toISOString(),
988
- `Notifying client of removal! (${curClient.remoteAddress})`
989
- );
990
- curClient.sendUTF(
991
- JSON.stringify({
992
- query: curData.query,
993
- removed: guid,
994
- })
995
- );
996
- }
1213
+ private async updateClients() {
1214
+ const outgoingClientMessages = [...this.outgoingClientMessages.entries()];
1215
+ this.outgoingClientMessages.clear();
997
1216
 
998
- for (let guid of added) {
999
- const entity = entityMap[guid];
1000
- // Notify client.
1001
- this.config.logger(
1002
- 'log',
1003
- new Date().toISOString(),
1004
- `Notifying client of new match! (${curClient.remoteAddress})`
1005
- );
1006
- if (typeof entity.updateDataProtection === 'function') {
1007
- entity.updateDataProtection();
1008
- }
1009
- curClient.sendUTF(
1010
- JSON.stringify({
1011
- query: curData.query,
1012
- added: guid,
1013
- data: entity,
1014
- })
1015
- );
1217
+ for (let [curClient, entries] of outgoingClientMessages) {
1218
+ const queryMap: { [k: string]: typeof entries } = {};
1219
+ for (let entry of entries) {
1220
+ const arr = queryMap[entry.subscription.query] || [];
1221
+ arr.push(entry);
1222
+ queryMap[entry.subscription.query] = arr;
1016
1223
  }
1017
1224
 
1018
- if (data.event === 'update' && data.guid in entityMap) {
1019
- const entity = entityMap[data.guid];
1020
- // Notify subscriber.
1021
- this.config.logger(
1022
- 'log',
1023
- new Date().toISOString(),
1024
- `Notifying client of update! (${curClient.remoteAddress})`
1025
- );
1026
- if (typeof entity.updateDataProtection === 'function') {
1027
- entity.updateDataProtection();
1225
+ for (let [query, queryEntries] of Object.entries(queryMap)) {
1226
+ // Update currents list.
1227
+ let current: EntityInterface[];
1228
+ let authToken: string | undefined;
1229
+ let switchToken: string | undefined;
1230
+ const nymph = this.nymph.clone();
1231
+ try {
1232
+ const [clientOptions, ...clientSelectors] = JSON.parse(query);
1233
+ const options: Options = {
1234
+ ...clientOptions,
1235
+ class: nymph.getEntityClass(clientOptions.class),
1236
+ return: 'entity',
1237
+ source: 'client',
1238
+ skipAc: false,
1239
+ };
1240
+ const selectors = classNamesToEntityConstructors(
1241
+ nymph,
1242
+ clientSelectors,
1243
+ true,
1244
+ );
1245
+ if (this.sessions.has(curClient)) {
1246
+ const session = this.sessions.get(curClient);
1247
+ authToken = session?.authToken;
1248
+ switchToken = session?.switchToken;
1249
+ }
1250
+ if (nymph.tilmeld != null && authToken != null) {
1251
+ const user = await nymph.tilmeld.extractToken(authToken);
1252
+ if (user && user.enabled) {
1253
+ if (switchToken != null) {
1254
+ const switchUser =
1255
+ await nymph.tilmeld.extractToken(switchToken);
1256
+ if (switchUser) {
1257
+ // Log in the switchUser for access controls.
1258
+ await nymph.tilmeld.fillSession(switchUser);
1259
+ }
1260
+ } else {
1261
+ // Log in the user for access controls.
1262
+ await nymph.tilmeld.fillSession(user);
1263
+ }
1264
+ }
1265
+ }
1266
+ current = await nymph.getEntities(options, ...selectors);
1267
+ } catch (e: any) {
1268
+ this.config.logger(
1269
+ 'error',
1270
+ new Date().toISOString(),
1271
+ `Error updating client! (${e?.message}, ${curClient.remoteAddress})`,
1272
+ );
1273
+ return;
1028
1274
  }
1029
- curClient.sendUTF(
1030
- JSON.stringify({
1031
- query: curData.query,
1032
- updated: data.guid,
1033
- data: entity,
1034
- })
1275
+
1276
+ const entityMap = Object.fromEntries(
1277
+ current.map((entity) => [entity.guid, entity]),
1035
1278
  );
1036
- }
1037
- }
1279
+ // These are the current GUIDs for this set of subscriptions/publishes.
1280
+ const currentGuids = current.map((entity) => entity.guid ?? '');
1281
+
1282
+ const clientUpdates: {
1283
+ query: string;
1284
+ removed?: string;
1285
+ added?: string;
1286
+ updated?: string;
1287
+ data?: any;
1288
+ }[] = [];
1289
+
1290
+ for (let { subscription, publish } of queryEntries) {
1291
+ const removed = difference(subscription.current, currentGuids);
1292
+ const added = difference(currentGuids, subscription.current);
1293
+
1294
+ if (subscription.direct) {
1295
+ for (let guid of removed) {
1296
+ // Notify subscriber.
1297
+ this.config.logger(
1298
+ 'log',
1299
+ new Date().toISOString(),
1300
+ `Notifying client of removal! (${curClient.remoteAddress})`,
1301
+ );
1302
+ clientUpdates.push({
1303
+ query: subscription.query,
1304
+ removed: guid,
1305
+ });
1306
+ }
1038
1307
 
1039
- // Update curData.
1040
- curData.current = currentGuids;
1308
+ for (let guid of added) {
1309
+ const entity = entityMap[guid];
1310
+ // Notify client.
1311
+ this.config.logger(
1312
+ 'log',
1313
+ new Date().toISOString(),
1314
+ `Notifying client of new match! (${curClient.remoteAddress})`,
1315
+ );
1316
+ if (typeof entity.updateDataProtection === 'function') {
1317
+ entity.updateDataProtection();
1318
+ }
1319
+ clientUpdates.push({
1320
+ query: subscription.query,
1321
+ added: guid,
1322
+ data: entity.toJSON(),
1323
+ });
1324
+ }
1041
1325
 
1042
- if (nymph.tilmeld != null && token != null) {
1043
- // Clear the user that was temporarily logged in.
1044
- nymph.tilmeld.clearSession();
1045
- }
1326
+ if (publish.event === 'update' && publish.guid in entityMap) {
1327
+ const entity = entityMap[publish.guid];
1328
+ // Notify subscriber.
1329
+ this.config.logger(
1330
+ 'log',
1331
+ new Date().toISOString(),
1332
+ `Notifying client of update! (${curClient.remoteAddress})`,
1333
+ );
1334
+ if (typeof entity.updateDataProtection === 'function') {
1335
+ entity.updateDataProtection();
1336
+ }
1337
+ clientUpdates.push({
1338
+ query: subscription.query,
1339
+ updated: publish.guid,
1340
+ data: entity.toJSON(),
1341
+ });
1342
+ }
1343
+ }
1344
+
1345
+ // Update subscription.
1346
+ subscription.current = currentGuids;
1347
+
1348
+ if (nymph.tilmeld != null && authToken != null) {
1349
+ // Clear the user that was temporarily logged in.
1350
+ nymph.tilmeld.clearSession();
1351
+ }
1046
1352
 
1047
- if ((removed.length || added.length) && curData.qrefParents.length) {
1048
- // All qref parents need to be rerun.
1049
- for (const qrefParent of curData.qrefParents) {
1050
- const subData =
1051
- this.querySubs[qrefParent.etype][qrefParent.query].get(curClient);
1052
- if (subData) {
1053
- this.updateClient(curClient, subData, data);
1353
+ if (
1354
+ (removed.length || added.length) &&
1355
+ subscription.qrefParents.length
1356
+ ) {
1357
+ // All qref parents need to be rerun.
1358
+ for (const qrefParent of subscription.qrefParents) {
1359
+ const subData =
1360
+ this.querySubs[qrefParent.etype][qrefParent.query].get(
1361
+ curClient,
1362
+ );
1363
+ if (subData) {
1364
+ this.addOutgoingClientMessage(curClient, subData, publish);
1365
+ }
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ if (clientUpdates.length === 1) {
1371
+ curClient.sendUTF(JSON.stringify(clientUpdates[0]));
1372
+ } else if (clientUpdates.length > 1) {
1373
+ curClient.sendUTF(
1374
+ JSON.stringify({ query, multiple: true, messages: clientUpdates }),
1375
+ );
1054
1376
  }
1055
1377
  }
1056
1378
  }
@@ -1060,19 +1382,19 @@ export default class PubSub {
1060
1382
  * Handle a UID publish from a client.
1061
1383
  */
1062
1384
  private async handlePublishUid(
1063
- from: connection,
1064
- data: PublishUidMessageData
1385
+ from: ws.connection,
1386
+ data: PublishUidMessageData,
1065
1387
  ) {
1066
1388
  this.config.logger(
1067
1389
  'log',
1068
1390
  new Date().toISOString(),
1069
1391
  `Received a UID publish! (${
1070
1392
  'name' in data ? data.name : `${data.oldName} => ${data.newName}`
1071
- }, ${data.event}, ${from.remoteAddress})`
1393
+ }, ${data.event}, ${from.remoteAddress})`,
1072
1394
  );
1073
1395
 
1074
1396
  const names = [data.name, data.oldName].filter(
1075
- (name) => name != null
1397
+ (name) => name != null,
1076
1398
  ) as string[];
1077
1399
  let value = data.value;
1078
1400
  if (data.event === 'renameUID' && data.newName) {
@@ -1087,7 +1409,7 @@ export default class PubSub {
1087
1409
  this.config.logger(
1088
1410
  'log',
1089
1411
  new Date().toISOString(),
1090
- `Notifying client of ${data.event}! (${name}, ${curClient.remoteAddress})`
1412
+ `Notifying client of ${data.event}! (${name}, ${curClient.remoteAddress})`,
1091
1413
  );
1092
1414
  const payload: {
1093
1415
  uid: string;
@@ -1113,14 +1435,14 @@ export default class PubSub {
1113
1435
  this.config.logger(
1114
1436
  'log',
1115
1437
  new Date().toISOString(),
1116
- `Notifying client of new value after rename! (${data.newName}, ${curClient.remoteAddress})`
1438
+ `Notifying client of new value after rename! (${data.newName}, ${curClient.remoteAddress})`,
1117
1439
  );
1118
1440
  curClient.sendUTF(
1119
1441
  JSON.stringify({
1120
1442
  uid: data.newName,
1121
1443
  event: 'setUID',
1122
1444
  value,
1123
- })
1445
+ }),
1124
1446
  );
1125
1447
  }
1126
1448
  }
@@ -1129,24 +1451,24 @@ export default class PubSub {
1129
1451
  /**
1130
1452
  * Relay publish data to other servers.
1131
1453
  */
1132
- private relay(message: Message) {
1454
+ private relay(message: ws.Message) {
1133
1455
  if (message.type !== 'utf8') {
1134
1456
  this.config.logger(
1135
1457
  'error',
1136
1458
  new Date().toISOString(),
1137
- `Can't relay non UTF8 message.`
1459
+ `Can't relay non UTF8 message.`,
1138
1460
  );
1139
1461
  return;
1140
1462
  }
1141
1463
 
1142
1464
  for (let host of this.config.relays) {
1143
- const client = new WebSocketClient();
1465
+ const client = new ws.client();
1144
1466
 
1145
1467
  client.on('connectFailed', (error) => {
1146
1468
  this.config.logger(
1147
1469
  'error',
1148
1470
  new Date().toISOString(),
1149
- `Relay connection failed. (${error.toString()}, ${host})`
1471
+ `Relay connection failed. (${error.toString()}, ${host})`,
1150
1472
  );
1151
1473
  });
1152
1474
 
@@ -1155,7 +1477,7 @@ export default class PubSub {
1155
1477
  this.config.logger(
1156
1478
  'error',
1157
1479
  new Date().toISOString(),
1158
- `Relay connect error. (${error.toString()}, ${host})`
1480
+ `Relay connect error. (${error.toString()}, ${host})`,
1159
1481
  );
1160
1482
  });
1161
1483
 
@@ -1170,8 +1492,8 @@ export default class PubSub {
1170
1492
  }
1171
1493
  }
1172
1494
 
1173
- private findQRefQueries(options: Options, ...selectors: Selector[]) {
1174
- const qrefQueries: [Options, ...Selector[]][] = [];
1495
+ private findQRefQueries(options: MessageOptions, ...selectors: Selector[]) {
1496
+ const qrefQueries: [MessageOptions, ...Selector[]][] = [];
1175
1497
 
1176
1498
  for (const curSelector of selectors) {
1177
1499
  for (const k in curSelector) {
@@ -1191,9 +1513,9 @@ export default class PubSub {
1191
1513
  Array.isArray(((value as Selector['qref']) ?? [])[0])
1192
1514
  ? value
1193
1515
  : [value]
1194
- ) as [string, [Options, ...Selector[]]][];
1195
- for (let i = 0; i < tmpArr.length; i++) {
1196
- qrefQueries.push(tmpArr[i][1]);
1516
+ ) as [string, [MessageOptions, ...Selector[]]][];
1517
+ for (let [_name, qRefQuery] of tmpArr) {
1518
+ qrefQueries.push(qRefQuery);
1197
1519
  }
1198
1520
  } else if (key === 'selector' || key === '!selector') {
1199
1521
  const tmpArr = (Array.isArray(value) ? value : [value]) as Selector[];
@@ -1209,7 +1531,7 @@ export default class PubSub {
1209
1531
  * This translates qref selectors into ref selectors using the "current" GUID
1210
1532
  * list in the existing subscriptions.
1211
1533
  */
1212
- private translateQRefSelectors(client: connection, selectors: Selector[]) {
1534
+ private translateQRefSelectors(client: ws.connection, selectors: Selector[]) {
1213
1535
  const newSelectors: Selector[] = [];
1214
1536
 
1215
1537
  for (const curSelector of selectors) {
@@ -1238,28 +1560,56 @@ export default class PubSub {
1238
1560
  const name = tmpArr[i][0];
1239
1561
  const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
1240
1562
  const query = JSON.stringify(tmpArr[i][1]);
1241
- const QrefEntityClass =
1242
- typeof qrefOptions.class === 'string'
1243
- ? this.nymph.getEntityClass(qrefOptions.class)
1244
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
1563
+ const QrefEntityClass = qrefOptions.class
1564
+ ? this.nymph.getEntityClass(qrefOptions.class)
1565
+ : this.nymph.getEntityClass('Entity');
1245
1566
  const data =
1246
1567
  this.querySubs[QrefEntityClass.ETYPE][query].get(client);
1247
1568
  if (data) {
1248
1569
  const guids = data.current;
1249
- let newValue: [string, string | EntityInterface][];
1250
- const oldValue = newSelector[newKey];
1251
- if (!oldValue) {
1252
- newValue = [];
1253
- } else if (oldValue.length && !Array.isArray(oldValue[0])) {
1254
- newValue = [oldValue] as [string, string | EntityInterface][];
1570
+ if (newKey === '!ref') {
1571
+ // Insert the qref results as a not ref clause.
1572
+ let newValue: [string, string | EntityInterface][];
1573
+ const oldValue = newSelector[newKey];
1574
+ if (
1575
+ oldValue == null ||
1576
+ (Array.isArray(oldValue) && !oldValue.length)
1577
+ ) {
1578
+ newValue = [];
1579
+ } else if (
1580
+ Array.isArray(oldValue) &&
1581
+ !Array.isArray(oldValue[0])
1582
+ ) {
1583
+ newValue = [oldValue] as [string, string | EntityInterface][];
1584
+ } else {
1585
+ newValue = oldValue as [string, string | EntityInterface][];
1586
+ }
1587
+ newValue.push(
1588
+ ...(guids.map((guid) => [name, guid]) as [string, string][]),
1589
+ );
1590
+ newSelector[newKey] = newValue;
1255
1591
  } else {
1256
- newValue = oldValue as [string, string | EntityInterface][];
1592
+ // Insert the qref results as an "or" selector clause with a ref
1593
+ // selector.
1594
+ let newValue: Selector[];
1595
+ const oldValue = newSelector['selector'];
1596
+ delete newSelector[key];
1597
+ if (
1598
+ oldValue == null ||
1599
+ (Array.isArray(oldValue) && !oldValue.length)
1600
+ ) {
1601
+ newValue = [];
1602
+ } else if (!Array.isArray(oldValue)) {
1603
+ newValue = [oldValue] as Selector[];
1604
+ } else {
1605
+ newValue = oldValue as Selector[];
1606
+ }
1607
+ newValue.push({
1608
+ type: '|',
1609
+ ref: guids.map((guid) => [name, guid]) as [string, string][],
1610
+ });
1611
+ newSelector['selector'] = newValue;
1257
1612
  }
1258
- newValue.push(
1259
- ...(guids.map((guid) => [name, guid]) as [string, string][])
1260
- );
1261
- // Insert the qref results as a ref clause.
1262
- newSelector[newKey] = newValue;
1263
1613
  } else {
1264
1614
  // Can't translate, so put the original back in.
1265
1615
  if (!newSelector[key]) {
@@ -1285,7 +1635,7 @@ export default class PubSub {
1285
1635
  newSelector[key] = [];
1286
1636
  }
1287
1637
  (newSelector[key] as [string, string | EntityInterface][]).push(
1288
- ...tmpArr
1638
+ ...tmpArr,
1289
1639
  );
1290
1640
  } else {
1291
1641
  // @ts-ignore: ts doesn't know what value is here.