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