@nymphjs/pubsub 1.0.0-beta.119 → 1.0.0-beta.120

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
@@ -61,6 +61,11 @@ export default class PubSub {
61
61
  protected uidSubs: {
62
62
  [uidName: string]: Map<ws.connection, { count: boolean }>;
63
63
  } = {};
64
+ protected outgoingClientMessages = new Map<
65
+ ws.connection,
66
+ { subscription: QuerySubscriptionData; publish: PublishEntityMessageData }[]
67
+ >();
68
+ protected outgoingClientMessagesTimeout: NodeJS.Immediate | null = null;
64
69
  protected static transactionPublishes: {
65
70
  nymph: Nymph;
66
71
  payload: string;
@@ -373,26 +378,47 @@ export default class PubSub {
373
378
  return;
374
379
  }
375
380
 
376
- this.transactionPublishes = (
377
- await Promise.all(
378
- this.transactionPublishes.map(async (publish) => {
379
- // Check that this instance is a parent and the instance is not in a
380
- // transaction.
381
- if (
382
- !this.isOrIsDescendant(nymph, publish.nymph) ||
383
- (await publish.nymph.inTransaction())
384
- ) {
385
- return publish;
386
- }
387
- this.publish(publish.payload, publish.config);
388
- return null;
389
- }),
390
- )
391
- ).filter((value) => value != null) as {
392
- nymph: Nymph;
393
- payload: string;
394
- config: Config;
395
- }[];
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
+ }
396
422
  }
397
423
 
398
424
  private static removeTransactionPublishes(nymph: Nymph) {
@@ -463,24 +489,48 @@ export default class PubSub {
463
489
  const data: MessageData = JSON.parse(msg.utf8Data);
464
490
  if (
465
491
  !data.action ||
466
- ['authenticate', 'subscribe', 'unsubscribe', 'publish'].indexOf(
467
- data.action,
468
- ) === -1
492
+ [
493
+ 'authenticate',
494
+ 'subscribe',
495
+ 'unsubscribe',
496
+ 'publish',
497
+ 'multiple',
498
+ ].indexOf(data.action) === -1
469
499
  ) {
470
500
  return;
471
501
  }
472
- switch (data.action) {
473
- case 'authenticate':
474
- this.handleAuthentication(from, data);
475
- break;
476
- case 'subscribe':
477
- case 'unsubscribe':
478
- await this.handleSubscription(from, data);
479
- break;
480
- case 'publish':
481
- await this.handlePublish(from, msg, data);
482
- break;
483
- }
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);
484
534
  }
485
535
 
486
536
  /**
@@ -991,11 +1041,7 @@ export default class PubSub {
991
1041
  /**
992
1042
  * Handle a publish from a client.
993
1043
  */
994
- private async handlePublish(
995
- from: ws.connection,
996
- msg: ws.Message,
997
- data: PublishMessageData,
998
- ) {
1044
+ private async handlePublish(from: ws.connection, data: PublishMessageData) {
999
1045
  if (
1000
1046
  'guid' in data &&
1001
1047
  typeof data.guid === 'string' &&
@@ -1008,9 +1054,6 @@ export default class PubSub {
1008
1054
  // Publish is an entity.
1009
1055
 
1010
1056
  await this.handlePublishEntity(from, data);
1011
-
1012
- // Relay the publish to other servers.
1013
- this.relay(msg);
1014
1057
  }
1015
1058
 
1016
1059
  if (
@@ -1024,9 +1067,6 @@ export default class PubSub {
1024
1067
  // Publish is a UID.
1025
1068
 
1026
1069
  await this.handlePublishUid(from, data);
1027
-
1028
- // Relay the publish to other servers.
1029
- this.relay(msg);
1030
1070
  }
1031
1071
  }
1032
1072
 
@@ -1062,7 +1102,7 @@ export default class PubSub {
1062
1102
  continue;
1063
1103
  }
1064
1104
  if (curData.current.indexOf(data.guid) !== -1) {
1065
- await this.updateClient(curClient, curData, data);
1105
+ this.addOutgoingClientMessage(curClient, curData, data);
1066
1106
  updatedClients.add(curClient);
1067
1107
  }
1068
1108
  }
@@ -1138,7 +1178,7 @@ export default class PubSub {
1138
1178
  }
1139
1179
  }
1140
1180
 
1141
- await this.updateClient(curClient, curData, data);
1181
+ this.addOutgoingClientMessage(curClient, curData, data);
1142
1182
  }
1143
1183
  }
1144
1184
  } catch (e: any) {
@@ -1152,139 +1192,187 @@ export default class PubSub {
1152
1192
  }
1153
1193
  }
1154
1194
 
1155
- private async updateClient(
1195
+ private addOutgoingClientMessage(
1156
1196
  curClient: ws.connection,
1157
1197
  curData: QuerySubscriptionData,
1158
1198
  data: PublishEntityMessageData,
1159
1199
  ) {
1160
- // Update currents list.
1161
- let current: EntityInterface[];
1162
- let authToken: string | undefined;
1163
- let switchToken: string | undefined;
1164
- const nymph = this.nymph.clone();
1165
- try {
1166
- const [clientOptions, ...clientSelectors] = JSON.parse(curData.query);
1167
- const options: Options = {
1168
- ...clientOptions,
1169
- class: nymph.getEntityClass(clientOptions.class),
1170
- return: 'entity',
1171
- source: 'client',
1172
- skipAc: false,
1173
- };
1174
- const selectors = classNamesToEntityConstructors(
1175
- nymph,
1176
- clientSelectors,
1177
- true,
1178
- );
1179
- if (this.sessions.has(curClient)) {
1180
- const session = this.sessions.get(curClient);
1181
- authToken = session?.authToken;
1182
- switchToken = session?.switchToken;
1183
- }
1184
- if (nymph.tilmeld != null && authToken != null) {
1185
- const user = await nymph.tilmeld.extractToken(authToken);
1186
- if (user && user.enabled) {
1187
- if (switchToken != null) {
1188
- const switchUser = await nymph.tilmeld.extractToken(switchToken);
1189
- if (switchUser) {
1190
- // Log in the switchUser for access controls.
1191
- await nymph.tilmeld.fillSession(switchUser);
1192
- }
1193
- } else {
1194
- // Log in the user for access controls.
1195
- await nymph.tilmeld.fillSession(user);
1196
- }
1197
- }
1198
- }
1199
- current = await nymph.getEntities(options, ...selectors);
1200
- } catch (e: any) {
1201
- this.config.logger(
1202
- 'error',
1203
- new Date().toISOString(),
1204
- `Error updating client! (${e?.message}, ${curClient.remoteAddress})`,
1205
- );
1206
- 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);
1207
1206
  }
1208
1207
 
1209
- const entityMap = Object.fromEntries(
1210
- current.map((entity) => [entity.guid, entity]),
1211
- );
1212
- const currentGuids = current.map((entity) => entity.guid ?? '');
1213
- const removed = difference(curData.current, currentGuids);
1214
- const added = difference(currentGuids, curData.current);
1208
+ this.outgoingClientMessagesTimeout = setImmediate(async () => {
1209
+ await this.updateClients();
1210
+ });
1211
+ }
1215
1212
 
1216
- if (curData.direct) {
1217
- for (let guid of removed) {
1218
- // Notify subscriber.
1219
- this.config.logger(
1220
- 'log',
1221
- new Date().toISOString(),
1222
- `Notifying client of removal! (${curClient.remoteAddress})`,
1223
- );
1224
- curClient.sendUTF(
1225
- JSON.stringify({
1226
- query: curData.query,
1227
- removed: guid,
1228
- }),
1229
- );
1230
- }
1213
+ private async updateClients() {
1214
+ const outgoingClientMessages = [...this.outgoingClientMessages.entries()];
1215
+ this.outgoingClientMessages.clear();
1231
1216
 
1232
- for (let guid of added) {
1233
- const entity = entityMap[guid];
1234
- // Notify client.
1235
- this.config.logger(
1236
- 'log',
1237
- new Date().toISOString(),
1238
- `Notifying client of new match! (${curClient.remoteAddress})`,
1239
- );
1240
- if (typeof entity.updateDataProtection === 'function') {
1241
- entity.updateDataProtection();
1242
- }
1243
- curClient.sendUTF(
1244
- JSON.stringify({
1245
- query: curData.query,
1246
- added: guid,
1247
- data: entity,
1248
- }),
1249
- );
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;
1250
1223
  }
1251
1224
 
1252
- if (data.event === 'update' && data.guid in entityMap) {
1253
- const entity = entityMap[data.guid];
1254
- // Notify subscriber.
1255
- this.config.logger(
1256
- 'log',
1257
- new Date().toISOString(),
1258
- `Notifying client of update! (${curClient.remoteAddress})`,
1259
- );
1260
- if (typeof entity.updateDataProtection === 'function') {
1261
- 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;
1262
1274
  }
1263
- curClient.sendUTF(
1264
- JSON.stringify({
1265
- query: curData.query,
1266
- updated: data.guid,
1267
- data: entity,
1268
- }),
1275
+
1276
+ const entityMap = Object.fromEntries(
1277
+ current.map((entity) => [entity.guid, entity]),
1269
1278
  );
1270
- }
1271
- }
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
+ }
1272
1307
 
1273
- // Update curData.
1274
- 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
+ }
1275
1325
 
1276
- if (nymph.tilmeld != null && authToken != null) {
1277
- // Clear the user that was temporarily logged in.
1278
- nymph.tilmeld.clearSession();
1279
- }
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
+ }
1352
+
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
+ }
1280
1369
 
1281
- if ((removed.length || added.length) && curData.qrefParents.length) {
1282
- // All qref parents need to be rerun.
1283
- for (const qrefParent of curData.qrefParents) {
1284
- const subData =
1285
- this.querySubs[qrefParent.etype][qrefParent.query].get(curClient);
1286
- if (subData) {
1287
- this.updateClient(curClient, subData, data);
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
+ );
1288
1376
  }
1289
1377
  }
1290
1378
  }
@@ -1426,8 +1514,8 @@ export default class PubSub {
1426
1514
  ? value
1427
1515
  : [value]
1428
1516
  ) as [string, [MessageOptions, ...Selector[]]][];
1429
- for (let i = 0; i < tmpArr.length; i++) {
1430
- qrefQueries.push(tmpArr[i][1]);
1517
+ for (let [_name, qRefQuery] of tmpArr) {
1518
+ qrefQueries.push(qRefQuery);
1431
1519
  }
1432
1520
  } else if (key === 'selector' || key === '!selector') {
1433
1521
  const tmpArr = (Array.isArray(value) ? value : [value]) as Selector[];
@@ -56,8 +56,16 @@ export type PublishUidMessageData = {
56
56
  export type PublishMessageData =
57
57
  PublishEntityMessageData | PublishUidMessageData;
58
58
 
59
+ export type MultipleMessageData = {
60
+ action: 'multiple';
61
+ messages: MessageData[];
62
+ };
63
+
59
64
  export type MessageData =
60
- AuthenticateMessageData | SubscribeMessageData | PublishMessageData;
65
+ | AuthenticateMessageData
66
+ | SubscribeMessageData
67
+ | PublishMessageData
68
+ | MultipleMessageData;
61
69
 
62
70
  export type MessageOptions = Options & {
63
71
  class: string;