dignity.js 0.7.1 → 0.8.1
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/README.md +48 -3
- package/dist/dignity.cjs.js +899 -8
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +899 -8
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +18 -18
- package/docs/assets/dignity.esm.js +899 -8
- package/docs/assets/playground-demos.js +56 -0
- package/docs/index.html +64 -10
- package/docs/openapi-like.json +61 -8
- package/package.json +2 -2
- package/src/core/dignity-p2p.js +428 -6
- package/src/cqrs/bulk-relay.js +35 -0
- package/src/cqrs/domain-events.js +285 -0
- package/src/cqrs/peer-group-tiers.js +46 -0
- package/src/cqrs/query-replica.js +213 -0
- package/src/gossip/peer-group.js +1 -1
- package/src/index.js +34 -1
package/src/core/dignity-p2p.js
CHANGED
|
@@ -15,8 +15,24 @@ const { deriveKeyPairFromCredentials } = require('../security/derive-key-pair');
|
|
|
15
15
|
const {
|
|
16
16
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
17
17
|
peerGroupScope,
|
|
18
|
+
parsePeerGroupScope,
|
|
18
19
|
selectFanoutPeers
|
|
19
20
|
} = require('../gossip/peer-group');
|
|
21
|
+
const {
|
|
22
|
+
operationToDomainEvent,
|
|
23
|
+
signDomainEvent,
|
|
24
|
+
verifyDomainEvent,
|
|
25
|
+
applyDomainEventToView,
|
|
26
|
+
createEmptyView,
|
|
27
|
+
buildCheckpoint
|
|
28
|
+
} = require('../cqrs/domain-events');
|
|
29
|
+
const {
|
|
30
|
+
DEFAULT_LIVE_CAP,
|
|
31
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
32
|
+
assignPeerGroupTier,
|
|
33
|
+
filterPeersByTier
|
|
34
|
+
} = require('../cqrs/peer-group-tiers');
|
|
35
|
+
const { electBulkRelays } = require('../cqrs/bulk-relay');
|
|
20
36
|
|
|
21
37
|
function computeContentHash(data) {
|
|
22
38
|
const canonical = stableStringify(data || {});
|
|
@@ -111,6 +127,10 @@ class DignityP2P extends EventEmitter {
|
|
|
111
127
|
this.maxAppliedOperations = security && typeof security.maxAppliedOperations === 'number'
|
|
112
128
|
? security.maxAppliedOperations
|
|
113
129
|
: 50000;
|
|
130
|
+
this.domainEventLogs = new Map(); // groupId -> event[]
|
|
131
|
+
this.lastEventHashByGroup = new Map(); // groupId -> hash
|
|
132
|
+
this.bulkRelayByGroup = new Map(); // groupId -> peerId[]
|
|
133
|
+
this.replicaViews = new Map(); // groupId -> view Map
|
|
114
134
|
|
|
115
135
|
this.state = new Map(); // collection -> Map(id -> record)
|
|
116
136
|
this.appliedOperations = new Map(); // opId -> appliedAt
|
|
@@ -281,6 +301,7 @@ class DignityP2P extends EventEmitter {
|
|
|
281
301
|
};
|
|
282
302
|
|
|
283
303
|
this.applyOperation(operation);
|
|
304
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
284
305
|
await this.broadcastMessage('operation', operation, {
|
|
285
306
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
286
307
|
messageType: 'operation',
|
|
@@ -374,6 +395,7 @@ class DignityP2P extends EventEmitter {
|
|
|
374
395
|
}
|
|
375
396
|
|
|
376
397
|
this.applyOperation(operation);
|
|
398
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
377
399
|
await this.broadcastMessage('operation', operation, {
|
|
378
400
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
379
401
|
messageType: 'operation',
|
|
@@ -439,6 +461,7 @@ class DignityP2P extends EventEmitter {
|
|
|
439
461
|
};
|
|
440
462
|
|
|
441
463
|
this.applyOperation(operation);
|
|
464
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
442
465
|
await this.broadcastMessage('operation', operation, {
|
|
443
466
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
444
467
|
messageType: 'operation',
|
|
@@ -476,6 +499,7 @@ class DignityP2P extends EventEmitter {
|
|
|
476
499
|
};
|
|
477
500
|
|
|
478
501
|
this.applyOperation(operation);
|
|
502
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
479
503
|
await this.broadcastMessage('operation', operation, {
|
|
480
504
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
481
505
|
messageType: 'operation',
|
|
@@ -830,9 +854,19 @@ class DignityP2P extends EventEmitter {
|
|
|
830
854
|
return [];
|
|
831
855
|
}
|
|
832
856
|
|
|
833
|
-
selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
|
|
857
|
+
selectPeerGroupFanout(groupId, count, excludePeerIds = [], fanoutOptions = {}) {
|
|
834
858
|
const scope = this.peerGroupScopeFor(groupId);
|
|
835
|
-
const
|
|
859
|
+
const group = this.peerGroups.get(groupId);
|
|
860
|
+
let peers = this.listPeers(scope, { includeSelf: false });
|
|
861
|
+
|
|
862
|
+
if (group && group.tiered && fanoutOptions.tier) {
|
|
863
|
+
peers = filterPeersByTier(peers, fanoutOptions.tier);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (fanoutOptions.bulkRelayOnly) {
|
|
867
|
+
peers = peers.filter((peer) => peer.metadata?.bulkRelay === true);
|
|
868
|
+
}
|
|
869
|
+
|
|
836
870
|
return selectFanoutPeers({
|
|
837
871
|
peers,
|
|
838
872
|
count,
|
|
@@ -869,18 +903,56 @@ class DignityP2P extends EventEmitter {
|
|
|
869
903
|
}
|
|
870
904
|
|
|
871
905
|
const scope = this.peerGroupScopeFor(groupId);
|
|
906
|
+
const role = options.role || options.metadata?.role || 'subscriber';
|
|
907
|
+
const tierMode = options.tierMode || 'auto';
|
|
908
|
+
const tiered = options.tiered === true
|
|
909
|
+
|| options.tierMode !== undefined
|
|
910
|
+
|| options.role !== undefined
|
|
911
|
+
|| typeof options.liveCap === 'number';
|
|
912
|
+
const liveCap = typeof options.liveCap === 'number' ? options.liveCap : DEFAULT_LIVE_CAP;
|
|
913
|
+
const existingMembers = this.listPeerGroupMembers(groupId, { includeSelf: false });
|
|
914
|
+
const existingSubscriberCount = existingMembers.filter((member) => {
|
|
915
|
+
const memberRole = member.metadata?.peerGroupRole || member.metadata?.role;
|
|
916
|
+
return memberRole !== 'publisher';
|
|
917
|
+
}).length;
|
|
918
|
+
let assignedTier = tiered
|
|
919
|
+
? assignPeerGroupTier({
|
|
920
|
+
joinIndex: existingSubscriberCount,
|
|
921
|
+
liveCap,
|
|
922
|
+
requestedTier: tierMode === 'auto' ? null : tierMode,
|
|
923
|
+
role
|
|
924
|
+
})
|
|
925
|
+
: null;
|
|
926
|
+
const publisherId = options.publisherId || (role === 'publisher' ? this.nodeId : null);
|
|
927
|
+
|
|
872
928
|
const config = {
|
|
873
929
|
fanout: typeof options.fanout === 'number' ? options.fanout : this.defaultPeerGroupFanout,
|
|
874
930
|
maxActivePeers: typeof options.maxActivePeers === 'number'
|
|
875
931
|
? options.maxActivePeers
|
|
876
932
|
: this.defaultPeerGroupMaxActivePeers,
|
|
877
933
|
maxHops: typeof options.maxHops === 'number' ? options.maxHops : this.defaultGossipMaxHops,
|
|
878
|
-
relayEnabled: options.relayEnabled !== false
|
|
934
|
+
relayEnabled: options.relayEnabled !== false,
|
|
935
|
+
tiered,
|
|
936
|
+
tierMode,
|
|
937
|
+
liveCap,
|
|
938
|
+
bulkIntervalMs: typeof options.bulkIntervalMs === 'number'
|
|
939
|
+
? options.bulkIntervalMs
|
|
940
|
+
: DEFAULT_BULK_INTERVAL_MS,
|
|
941
|
+
domainEvents: options.domainEvents !== false,
|
|
942
|
+
autoPublishDomainEvents: options.autoPublishDomainEvents !== false,
|
|
943
|
+
role,
|
|
944
|
+
publisherId,
|
|
945
|
+
commandCapable: options.commandCapable !== false,
|
|
946
|
+
peerGroupTier: assignedTier
|
|
879
947
|
};
|
|
880
948
|
|
|
881
949
|
await this.joinDiscovery(scope, {
|
|
882
950
|
metadata: {
|
|
883
951
|
peerGroup: groupId,
|
|
952
|
+
...(assignedTier ? { peerGroupTier: assignedTier } : {}),
|
|
953
|
+
peerGroupRole: role,
|
|
954
|
+
...(publisherId ? { publisherId } : {}),
|
|
955
|
+
bulkRelay: false,
|
|
884
956
|
...(options.metadata || {})
|
|
885
957
|
},
|
|
886
958
|
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
@@ -889,10 +961,74 @@ class DignityP2P extends EventEmitter {
|
|
|
889
961
|
});
|
|
890
962
|
|
|
891
963
|
this.peerGroups.set(groupId, config);
|
|
892
|
-
this.
|
|
964
|
+
if (!this.domainEventLogs.has(groupId)) {
|
|
965
|
+
this.domainEventLogs.set(groupId, []);
|
|
966
|
+
}
|
|
967
|
+
if (!this.replicaViews.has(groupId)) {
|
|
968
|
+
this.replicaViews.set(groupId, createEmptyView());
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
this.refreshBulkRelays(groupId);
|
|
972
|
+
if (tiered && tierMode === 'auto' && role === 'subscriber') {
|
|
973
|
+
assignedTier = this.recalculateOwnPeerGroupTier(groupId) || assignedTier;
|
|
974
|
+
config.peerGroupTier = assignedTier;
|
|
975
|
+
}
|
|
976
|
+
this.emit('peergroupjoined', { groupId, config, tier: assignedTier });
|
|
893
977
|
return config;
|
|
894
978
|
}
|
|
895
979
|
|
|
980
|
+
recalculateOwnPeerGroupTier(groupId) {
|
|
981
|
+
const group = this.peerGroups.get(groupId);
|
|
982
|
+
if (!group || !group.tiered || group.role !== 'subscriber') {
|
|
983
|
+
return group ? group.peerGroupTier : null;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (group.tierMode !== 'auto') {
|
|
987
|
+
return group.peerGroupTier;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const scope = this.peerGroupScopeFor(groupId);
|
|
991
|
+
const members = this.listPeerGroupMembers(groupId, { includeSelf: true });
|
|
992
|
+
const subscribers = members
|
|
993
|
+
.filter((member) => {
|
|
994
|
+
const memberRole = member.metadata?.peerGroupRole || member.metadata?.role;
|
|
995
|
+
return memberRole !== 'publisher';
|
|
996
|
+
})
|
|
997
|
+
.map((member) => member.peerId)
|
|
998
|
+
.sort();
|
|
999
|
+
|
|
1000
|
+
const joinIndex = subscribers.indexOf(this.nodeId);
|
|
1001
|
+
if (joinIndex < 0) {
|
|
1002
|
+
return group.peerGroupTier;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const newTier = assignPeerGroupTier({
|
|
1006
|
+
joinIndex,
|
|
1007
|
+
liveCap: group.liveCap,
|
|
1008
|
+
requestedTier: null,
|
|
1009
|
+
role: 'subscriber'
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
if (newTier === group.peerGroupTier) {
|
|
1013
|
+
return newTier;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
group.peerGroupTier = newTier;
|
|
1017
|
+
const room = this.discoveryRooms.get(scope);
|
|
1018
|
+
if (room) {
|
|
1019
|
+
room.metadata = {
|
|
1020
|
+
...(room.metadata || {}),
|
|
1021
|
+
peerGroupTier: newTier
|
|
1022
|
+
};
|
|
1023
|
+
this.upsertPresence(scope, this.nodeId, room.metadata, room.ttlMs, this.now());
|
|
1024
|
+
this.announcePresence(scope).catch((error) => {
|
|
1025
|
+
this.emit('warning', { type: 'tier-announce-failed', groupId, error });
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return newTier;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
896
1032
|
async leavePeerGroup(groupId) {
|
|
897
1033
|
if (!groupId) {
|
|
898
1034
|
return;
|
|
@@ -901,6 +1037,7 @@ class DignityP2P extends EventEmitter {
|
|
|
901
1037
|
const scope = this.peerGroupScopeFor(groupId);
|
|
902
1038
|
await this.leaveDiscovery(scope);
|
|
903
1039
|
this.peerGroups.delete(groupId);
|
|
1040
|
+
this.bulkRelayByGroup.delete(groupId);
|
|
904
1041
|
this.emit('peergroupleft', { groupId });
|
|
905
1042
|
}
|
|
906
1043
|
|
|
@@ -932,7 +1069,12 @@ class DignityP2P extends EventEmitter {
|
|
|
932
1069
|
? options.maxHops
|
|
933
1070
|
: (group ? group.maxHops : this.defaultGossipMaxHops);
|
|
934
1071
|
|
|
935
|
-
const
|
|
1072
|
+
const fanoutOptions = {};
|
|
1073
|
+
if (group && group.tiered && options.tier !== 'bulk') {
|
|
1074
|
+
fanoutOptions.tier = options.tier || 'live';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId], fanoutOptions);
|
|
936
1078
|
if (fanoutPeerIds.length > 0) {
|
|
937
1079
|
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
938
1080
|
await this.enforceConnectionBudget();
|
|
@@ -958,6 +1100,248 @@ class DignityP2P extends EventEmitter {
|
|
|
958
1100
|
return { gossipId, fanoutPeerIds };
|
|
959
1101
|
}
|
|
960
1102
|
|
|
1103
|
+
async publishPeerGroupBulk(groupId, innerMessageType, innerPayload, options = {}) {
|
|
1104
|
+
const group = this.peerGroups.get(groupId);
|
|
1105
|
+
if (!group && options.allowUnjoined !== true) {
|
|
1106
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (group && group.role !== 'publisher') {
|
|
1110
|
+
throw new Error(`Only publisher can bulk-publish to PeerGroup ${groupId}`);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const fanout = typeof options.fanout === 'number'
|
|
1114
|
+
? options.fanout
|
|
1115
|
+
: (group ? group.fanout : this.defaultPeerGroupFanout);
|
|
1116
|
+
const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
|
|
1117
|
+
const maxHop = typeof options.maxHops === 'number'
|
|
1118
|
+
? options.maxHops
|
|
1119
|
+
: (group ? group.maxHops : this.defaultGossipMaxHops);
|
|
1120
|
+
|
|
1121
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(
|
|
1122
|
+
groupId,
|
|
1123
|
+
fanout,
|
|
1124
|
+
[this.nodeId],
|
|
1125
|
+
{ tier: 'bulk', bulkRelayOnly: group?.tiered === true }
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
if (fanoutPeerIds.length > 0) {
|
|
1129
|
+
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
1130
|
+
await this.enforceConnectionBudget();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const gossipId = options.gossipId || this.idGenerator();
|
|
1134
|
+
this.markSeenGossip(gossipId);
|
|
1135
|
+
|
|
1136
|
+
await this.broadcastMessage('peer-group:gossip', {
|
|
1137
|
+
groupId,
|
|
1138
|
+
gossipId,
|
|
1139
|
+
publisherId: this.nodeId,
|
|
1140
|
+
hop: 0,
|
|
1141
|
+
maxHop,
|
|
1142
|
+
deliveryTier: 'bulk',
|
|
1143
|
+
innerMessageType,
|
|
1144
|
+
innerPayload
|
|
1145
|
+
}, {
|
|
1146
|
+
broadcastScope: this.peerGroupScopeFor(groupId),
|
|
1147
|
+
fanoutPeerIds
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
return { gossipId, fanoutPeerIds };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async publishPeerGroupCheckpoint(groupId, options = {}) {
|
|
1154
|
+
const group = this.peerGroups.get(groupId);
|
|
1155
|
+
if (!group) {
|
|
1156
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const events = this.domainEventLogs.get(groupId) || [];
|
|
1160
|
+
const checkpoint = buildCheckpoint(groupId, events, {
|
|
1161
|
+
publisherId: options.publisherId || group.publisherId || this.nodeId
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
await this.publishPeerGroupBulk(groupId, 'domain:checkpoint', checkpoint, options);
|
|
1165
|
+
this.emit('checkpointpublished', { groupId, checkpoint });
|
|
1166
|
+
return checkpoint;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
resolvePublisherGroupIds(options = {}) {
|
|
1170
|
+
if (options.peerGroupId) {
|
|
1171
|
+
return [options.peerGroupId];
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const groups = [];
|
|
1175
|
+
for (const [groupId, config] of this.peerGroups.entries()) {
|
|
1176
|
+
if (config.domainEvents && config.autoPublishDomainEvents && config.role === 'publisher') {
|
|
1177
|
+
groups.push(groupId);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return groups;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async maybePublishDomainEvent(operation, options = {}) {
|
|
1184
|
+
const groupIds = this.resolvePublisherGroupIds(options);
|
|
1185
|
+
if (groupIds.length === 0) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
for (const groupId of groupIds) {
|
|
1190
|
+
await this.publishDomainEventForOperation(groupId, operation);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
async publishDomainEventForOperation(groupId, operation) {
|
|
1195
|
+
const group = this.peerGroups.get(groupId);
|
|
1196
|
+
if (!group || !group.domainEvents) {
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (group.role !== 'publisher') {
|
|
1201
|
+
throw new Error(`Only publisher can emit domain events for PeerGroup ${groupId}`);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const prevHash = this.lastEventHashByGroup.get(groupId) || null;
|
|
1205
|
+
let event = operationToDomainEvent(operation, {
|
|
1206
|
+
publisherId: this.nodeId,
|
|
1207
|
+
groupId,
|
|
1208
|
+
prevHash,
|
|
1209
|
+
eventIdGenerator: () => this.idGenerator()
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
if (this.securityService.options.signingEnabled && this.securityService.signingSecretKey) {
|
|
1213
|
+
event = signDomainEvent(event, this.securityService.signingSecretKey);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const log = this.domainEventLogs.get(groupId) || [];
|
|
1217
|
+
log.push(event);
|
|
1218
|
+
this.domainEventLogs.set(groupId, log);
|
|
1219
|
+
this.lastEventHashByGroup.set(groupId, event.eventHash);
|
|
1220
|
+
|
|
1221
|
+
this.emit('domainevent', event);
|
|
1222
|
+
|
|
1223
|
+
if (group.autoPublishDomainEvents) {
|
|
1224
|
+
await this.publishToPeerGroup(groupId, 'domain:event', event, { tier: 'live' });
|
|
1225
|
+
if (group.tiered) {
|
|
1226
|
+
await this.publishPeerGroupBulk(groupId, 'domain:event', event);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return event;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
refreshBulkRelays(groupId) {
|
|
1234
|
+
const group = this.peerGroups.get(groupId);
|
|
1235
|
+
if (!group || !group.tiered) {
|
|
1236
|
+
return [];
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const peers = this.listPeerGroupMembers(groupId, { includeSelf: false });
|
|
1240
|
+
const relays = electBulkRelays(peers);
|
|
1241
|
+
const previous = this.bulkRelayByGroup.get(groupId) || [];
|
|
1242
|
+
this.bulkRelayByGroup.set(groupId, relays);
|
|
1243
|
+
|
|
1244
|
+
const changed = previous.length !== relays.length
|
|
1245
|
+
|| previous.some((id, index) => id !== relays[index]);
|
|
1246
|
+
|
|
1247
|
+
if (changed) {
|
|
1248
|
+
this.emit('bulkrelaychanged', { groupId, relays, previous });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return relays;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
ingestRemoteDomainEvent(event, context = {}) {
|
|
1255
|
+
const groupId = event.groupId || context.groupId;
|
|
1256
|
+
if (!groupId) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const group = this.peerGroups.get(groupId);
|
|
1261
|
+
if (!group) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const publisherId = event.publisherId || context.publisherId;
|
|
1266
|
+
if (group.publisherId && publisherId !== group.publisherId) {
|
|
1267
|
+
this.emit('warning', {
|
|
1268
|
+
type: 'domain-event-rejected',
|
|
1269
|
+
groupId,
|
|
1270
|
+
reason: 'publisher-mismatch',
|
|
1271
|
+
eventId: event.eventId,
|
|
1272
|
+
expectedPublisher: group.publisherId,
|
|
1273
|
+
actualPublisher: publisherId
|
|
1274
|
+
});
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
let signingPublicKey = null;
|
|
1279
|
+
if (this.securityService.options.signingEnabled && publisherId) {
|
|
1280
|
+
const peerKey = this.securityService.resolvePeerPublicKey(publisherId, null);
|
|
1281
|
+
signingPublicKey = peerKey ? peerKey.signingPublicKey : null;
|
|
1282
|
+
if (!signingPublicKey) {
|
|
1283
|
+
this.emit('warning', {
|
|
1284
|
+
type: 'domain-event-rejected',
|
|
1285
|
+
groupId,
|
|
1286
|
+
reason: 'missing-publisher-key',
|
|
1287
|
+
eventId: event.eventId,
|
|
1288
|
+
publisherId
|
|
1289
|
+
});
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const verified = verifyDomainEvent(event, { signingPublicKey });
|
|
1295
|
+
if (!verified.ok) {
|
|
1296
|
+
this.emit('warning', {
|
|
1297
|
+
type: 'domain-event-rejected',
|
|
1298
|
+
groupId,
|
|
1299
|
+
reason: verified.reason,
|
|
1300
|
+
eventId: event.eventId
|
|
1301
|
+
});
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (this.securityService.options.signingEnabled && verified.unsigned) {
|
|
1306
|
+
this.emit('warning', {
|
|
1307
|
+
type: 'domain-event-rejected',
|
|
1308
|
+
groupId,
|
|
1309
|
+
reason: 'unsigned-event',
|
|
1310
|
+
eventId: event.eventId
|
|
1311
|
+
});
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const log = this.domainEventLogs.get(groupId) || [];
|
|
1316
|
+
if (log.some((entry) => entry.eventId === event.eventId)) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const expectedPrev = log.length > 0 ? log[log.length - 1].eventHash : null;
|
|
1321
|
+
if (event.prevHash !== expectedPrev) {
|
|
1322
|
+
this.emit('chainbroken', {
|
|
1323
|
+
groupId,
|
|
1324
|
+
expectedPrev,
|
|
1325
|
+
actualPrev: event.prevHash,
|
|
1326
|
+
eventId: event.eventId
|
|
1327
|
+
});
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
log.push(event);
|
|
1332
|
+
this.domainEventLogs.set(groupId, log);
|
|
1333
|
+
this.lastEventHashByGroup.set(groupId, event.eventHash);
|
|
1334
|
+
|
|
1335
|
+
if (!group.commandCapable) {
|
|
1336
|
+
const view = this.replicaViews.get(groupId) || createEmptyView();
|
|
1337
|
+
applyDomainEventToView(view, event);
|
|
1338
|
+
this.replicaViews.set(groupId, view);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
this.emit('domainevent', event);
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
961
1345
|
async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
|
|
962
1346
|
const collection = this.getCollection(collectionName);
|
|
963
1347
|
const raw = collection.get(id);
|
|
@@ -1004,6 +1388,12 @@ class DignityP2P extends EventEmitter {
|
|
|
1004
1388
|
});
|
|
1005
1389
|
|
|
1006
1390
|
const group = this.peerGroups.get(groupId);
|
|
1391
|
+
const deliveryTier = payload.deliveryTier || 'live';
|
|
1392
|
+
|
|
1393
|
+
if (group && group.tiered && group.peerGroupTier === 'bulk' && deliveryTier !== 'bulk') {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1007
1397
|
const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
|
|
1008
1398
|
const maxHop = typeof payloadMaxHop === 'number'
|
|
1009
1399
|
? Math.min(payloadMaxHop, configuredMaxHop)
|
|
@@ -1013,10 +1403,18 @@ class DignityP2P extends EventEmitter {
|
|
|
1013
1403
|
return;
|
|
1014
1404
|
}
|
|
1015
1405
|
|
|
1406
|
+
const relayOptions = {};
|
|
1407
|
+
if (group.tiered) {
|
|
1408
|
+
relayOptions.tier = deliveryTier === 'bulk' ? 'bulk' : 'live';
|
|
1409
|
+
if (deliveryTier === 'bulk') {
|
|
1410
|
+
relayOptions.bulkRelayOnly = true;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1016
1414
|
const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
|
|
1017
1415
|
decrypted.senderId,
|
|
1018
1416
|
this.nodeId
|
|
1019
|
-
]);
|
|
1417
|
+
], relayOptions);
|
|
1020
1418
|
|
|
1021
1419
|
if (relayPeers.length === 0) {
|
|
1022
1420
|
return;
|
|
@@ -1031,6 +1429,7 @@ class DignityP2P extends EventEmitter {
|
|
|
1031
1429
|
publisherId,
|
|
1032
1430
|
hop: hop + 1,
|
|
1033
1431
|
maxHop,
|
|
1432
|
+
deliveryTier,
|
|
1034
1433
|
innerMessageType,
|
|
1035
1434
|
innerPayload
|
|
1036
1435
|
}, {
|
|
@@ -1080,6 +1479,21 @@ class DignityP2P extends EventEmitter {
|
|
|
1080
1479
|
return;
|
|
1081
1480
|
}
|
|
1082
1481
|
|
|
1482
|
+
if (innerMessageType === 'domain:event') {
|
|
1483
|
+
this.ingestRemoteDomainEvent(innerPayload, context);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (innerMessageType === 'domain:checkpoint') {
|
|
1488
|
+
this.emit('peergroupmessage', {
|
|
1489
|
+
groupId: context.groupId,
|
|
1490
|
+
senderId: context.senderId,
|
|
1491
|
+
type: 'domain:checkpoint',
|
|
1492
|
+
payload: innerPayload
|
|
1493
|
+
});
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1083
1497
|
if (innerMessageType === 'record:snapshot') {
|
|
1084
1498
|
const { collectionName, record } = innerPayload || {};
|
|
1085
1499
|
if (collectionName && record) {
|
|
@@ -1131,6 +1545,14 @@ class DignityP2P extends EventEmitter {
|
|
|
1131
1545
|
|
|
1132
1546
|
this.trustPeerFromMetadata(peerId, next.metadata);
|
|
1133
1547
|
|
|
1548
|
+
const groupId = parsePeerGroupScope(scope);
|
|
1549
|
+
if (groupId && this.peerGroups.has(groupId)) {
|
|
1550
|
+
this.refreshBulkRelays(groupId);
|
|
1551
|
+
if (peerId !== this.nodeId) {
|
|
1552
|
+
this.recalculateOwnPeerGroupTier(groupId);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1134
1556
|
if (!existing) {
|
|
1135
1557
|
this.emit('peerdiscovered', { scope, peerId, metadata: next.metadata });
|
|
1136
1558
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { getPeerTier } = require('./peer-group-tiers');
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BULK_RELAY_COUNT = 3;
|
|
4
|
+
|
|
5
|
+
function electBulkRelays(peers, { count = DEFAULT_BULK_RELAY_COUNT } = {}) {
|
|
6
|
+
const bulkPeers = peers
|
|
7
|
+
.filter((peer) => getPeerTier(peer) === 'bulk')
|
|
8
|
+
.map((peer) => peer.peerId || peer)
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.sort();
|
|
11
|
+
|
|
12
|
+
return bulkPeers.slice(0, Math.max(0, count));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isBulkRelay(metadata) {
|
|
16
|
+
return metadata?.bulkRelay === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function applyBulkRelayFlags(peers, relayPeerIds) {
|
|
20
|
+
const relaySet = new Set(relayPeerIds);
|
|
21
|
+
return peers.map((peer) => ({
|
|
22
|
+
...peer,
|
|
23
|
+
metadata: {
|
|
24
|
+
...(peer.metadata || {}),
|
|
25
|
+
bulkRelay: relaySet.has(peer.peerId)
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
DEFAULT_BULK_RELAY_COUNT,
|
|
32
|
+
electBulkRelays,
|
|
33
|
+
isBulkRelay,
|
|
34
|
+
applyBulkRelayFlags
|
|
35
|
+
};
|