dignity.js 0.7.1 → 0.8.0
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 +34 -0
- 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 +17 -4
- 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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
const nacl = require('tweetnacl');
|
|
2
|
+
const naclUtil = require('tweetnacl-util');
|
|
3
|
+
const { stableStringify } = require('../security/message-security-service');
|
|
4
|
+
|
|
5
|
+
const DOMAIN_EVENT_SCHEMA_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
const OPERATION_KIND_TO_EVENT_KIND = {
|
|
8
|
+
create: 'record:created',
|
|
9
|
+
update: 'record:updated',
|
|
10
|
+
delete: 'record:removed',
|
|
11
|
+
'transfer-ownership': 'ownership:transferred'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function computeContentHash(data) {
|
|
15
|
+
const canonical = stableStringify(data || {});
|
|
16
|
+
const bytes = naclUtil.decodeUTF8(canonical);
|
|
17
|
+
const hash = nacl.hash(bytes);
|
|
18
|
+
const hex = Array.from(hash, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
+
return `sha512:${hex}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canonicalEventBody(event) {
|
|
23
|
+
return stableStringify({
|
|
24
|
+
schemaVersion: event.schemaVersion,
|
|
25
|
+
eventId: event.eventId,
|
|
26
|
+
groupId: event.groupId,
|
|
27
|
+
publisherId: event.publisherId,
|
|
28
|
+
kind: event.kind,
|
|
29
|
+
collectionName: event.collectionName,
|
|
30
|
+
id: event.id,
|
|
31
|
+
payload: event.payload,
|
|
32
|
+
timestamp: event.timestamp,
|
|
33
|
+
baseVersion: event.baseVersion,
|
|
34
|
+
prevHash: event.prevHash || null,
|
|
35
|
+
newOwnerId: event.newOwnerId || null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function computeEventHash(event) {
|
|
40
|
+
const canonical = canonicalEventBody(event);
|
|
41
|
+
const bytes = naclUtil.decodeUTF8(canonical);
|
|
42
|
+
const hash = nacl.hash(bytes);
|
|
43
|
+
const hex = Array.from(hash, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
44
|
+
return `sha512:${hex}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function operationToDomainEvent(operation, { publisherId, groupId, prevHash, eventIdGenerator }) {
|
|
48
|
+
if (!operation || !publisherId || !groupId) {
|
|
49
|
+
throw new Error('operationToDomainEvent requires operation, publisherId, and groupId');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const kind = OPERATION_KIND_TO_EVENT_KIND[operation.kind];
|
|
53
|
+
if (!kind) {
|
|
54
|
+
throw new Error(`Unsupported operation kind for domain event: ${operation.kind}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const event = {
|
|
58
|
+
schemaVersion: DOMAIN_EVENT_SCHEMA_VERSION,
|
|
59
|
+
eventId: eventIdGenerator ? eventIdGenerator() : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
60
|
+
groupId,
|
|
61
|
+
publisherId,
|
|
62
|
+
kind,
|
|
63
|
+
collectionName: operation.collectionName,
|
|
64
|
+
id: operation.id,
|
|
65
|
+
payload: operation.payload || {},
|
|
66
|
+
timestamp: operation.timestamp,
|
|
67
|
+
baseVersion: operation.baseVersion || null,
|
|
68
|
+
prevHash: prevHash || null,
|
|
69
|
+
newOwnerId: operation.newOwnerId || null,
|
|
70
|
+
eventHash: null,
|
|
71
|
+
signature: null
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
event.eventHash = computeEventHash(event);
|
|
75
|
+
return event;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function signDomainEvent(event, signingSecretKey) {
|
|
79
|
+
if (!signingSecretKey) {
|
|
80
|
+
return { ...event };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const unsigned = { ...event, signature: null };
|
|
84
|
+
const eventHash = computeEventHash(unsigned);
|
|
85
|
+
const signature = nacl.sign.detached(
|
|
86
|
+
naclUtil.decodeUTF8(eventHash),
|
|
87
|
+
signingSecretKey
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...unsigned,
|
|
92
|
+
eventHash,
|
|
93
|
+
signature: naclUtil.encodeBase64(signature)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function verifyDomainEventSignature(event, signingPublicKey) {
|
|
98
|
+
if (!event || !event.eventHash) {
|
|
99
|
+
return { ok: false, reason: 'missing-event-hash' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const recomputed = computeEventHash({ ...event, signature: null });
|
|
103
|
+
if (recomputed !== event.eventHash) {
|
|
104
|
+
return { ok: false, reason: 'event-hash-mismatch' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!event.signature) {
|
|
108
|
+
return { ok: true, unsigned: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!signingPublicKey) {
|
|
112
|
+
return { ok: false, reason: 'missing-public-key' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const keyBytes = typeof signingPublicKey === 'string'
|
|
116
|
+
? naclUtil.decodeBase64(signingPublicKey)
|
|
117
|
+
: signingPublicKey;
|
|
118
|
+
|
|
119
|
+
const valid = nacl.sign.detached.verify(
|
|
120
|
+
naclUtil.decodeUTF8(event.eventHash),
|
|
121
|
+
naclUtil.decodeBase64(event.signature),
|
|
122
|
+
keyBytes
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return valid ? { ok: true } : { ok: false, reason: 'invalid-signature' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function verifyDomainEvent(event, { signingPublicKey, supportedVersions } = {}) {
|
|
129
|
+
if (!event || typeof event !== 'object') {
|
|
130
|
+
return { ok: false, reason: 'invalid-event' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const versions = supportedVersions || [DOMAIN_EVENT_SCHEMA_VERSION];
|
|
134
|
+
if (!versions.includes(event.schemaVersion)) {
|
|
135
|
+
return { ok: false, reason: 'unsupported-schema-version', schemaVersion: event.schemaVersion };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!event.eventId || !event.groupId || !event.publisherId || !event.kind) {
|
|
139
|
+
return { ok: false, reason: 'missing-required-fields' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return verifyDomainEventSignature(event, signingPublicKey);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createEmptyView(collections = []) {
|
|
146
|
+
const view = new Map();
|
|
147
|
+
for (const name of collections) {
|
|
148
|
+
view.set(name, new Map());
|
|
149
|
+
}
|
|
150
|
+
return view;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureCollectionView(view, collectionName) {
|
|
154
|
+
if (!view.has(collectionName)) {
|
|
155
|
+
view.set(collectionName, new Map());
|
|
156
|
+
}
|
|
157
|
+
return view.get(collectionName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function applyDomainEventToView(view, event, { collectionsFilter } = {}) {
|
|
161
|
+
if (!event || !event.collectionName) {
|
|
162
|
+
return { applied: false, reason: 'invalid-event' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Array.isArray(collectionsFilter) && collectionsFilter.length > 0
|
|
166
|
+
&& !collectionsFilter.includes(event.collectionName)) {
|
|
167
|
+
return { applied: false, reason: 'collection-filtered' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const collection = ensureCollectionView(view, event.collectionName);
|
|
171
|
+
|
|
172
|
+
if (event.kind === 'record:created') {
|
|
173
|
+
if (collection.has(event.id)) {
|
|
174
|
+
return { applied: false, reason: 'already-exists' };
|
|
175
|
+
}
|
|
176
|
+
collection.set(event.id, {
|
|
177
|
+
id: event.id,
|
|
178
|
+
ownerId: event.publisherId,
|
|
179
|
+
data: { ...(event.payload || {}) },
|
|
180
|
+
hash: computeContentHash(event.payload || {}),
|
|
181
|
+
createdAt: event.timestamp,
|
|
182
|
+
updatedAt: event.timestamp,
|
|
183
|
+
deletedAt: null,
|
|
184
|
+
version: 1
|
|
185
|
+
});
|
|
186
|
+
return { applied: true, kind: event.kind };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const current = collection.get(event.id);
|
|
190
|
+
if (!current || current.deletedAt) {
|
|
191
|
+
if (event.kind === 'record:removed') {
|
|
192
|
+
return { applied: false, reason: 'not-found' };
|
|
193
|
+
}
|
|
194
|
+
return { applied: false, reason: 'missing-record' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (event.kind === 'record:updated') {
|
|
198
|
+
if (typeof event.baseVersion === 'number' && current.version !== event.baseVersion) {
|
|
199
|
+
return { applied: false, reason: 'version-conflict', currentVersion: current.version };
|
|
200
|
+
}
|
|
201
|
+
current.data = { ...current.data, ...(event.payload || {}) };
|
|
202
|
+
current.hash = computeContentHash(current.data);
|
|
203
|
+
current.updatedAt = event.timestamp;
|
|
204
|
+
current.version += 1;
|
|
205
|
+
return { applied: true, kind: event.kind };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (event.kind === 'record:removed') {
|
|
209
|
+
if (typeof event.baseVersion === 'number' && current.version !== event.baseVersion) {
|
|
210
|
+
return { applied: false, reason: 'version-conflict', currentVersion: current.version };
|
|
211
|
+
}
|
|
212
|
+
current.deletedAt = event.timestamp;
|
|
213
|
+
current.version += 1;
|
|
214
|
+
return { applied: true, kind: event.kind };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (event.kind === 'ownership:transferred') {
|
|
218
|
+
if (typeof event.baseVersion === 'number' && current.version !== event.baseVersion) {
|
|
219
|
+
return { applied: false, reason: 'version-conflict', currentVersion: current.version };
|
|
220
|
+
}
|
|
221
|
+
current.ownerId = event.newOwnerId;
|
|
222
|
+
current.updatedAt = event.timestamp;
|
|
223
|
+
current.version += 1;
|
|
224
|
+
return { applied: true, kind: event.kind };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { applied: false, reason: 'unknown-kind' };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function verifyEventChain(events, { genesisHash = null } = {}) {
|
|
231
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
232
|
+
return { ok: true, length: 0 };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let expectedPrev = genesisHash;
|
|
236
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
237
|
+
const event = events[index];
|
|
238
|
+
const prevHash = event.prevHash || null;
|
|
239
|
+
|
|
240
|
+
if (prevHash !== expectedPrev) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: 'chain-break',
|
|
244
|
+
index,
|
|
245
|
+
expectedPrev,
|
|
246
|
+
actualPrev: prevHash
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const hashCheck = verifyDomainEventSignature(event, null);
|
|
251
|
+
if (!hashCheck.ok) {
|
|
252
|
+
return { ok: false, reason: hashCheck.reason, index };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
expectedPrev = event.eventHash;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { ok: true, length: events.length, lastHash: expectedPrev };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildCheckpoint(groupId, events, { publisherId } = {}) {
|
|
262
|
+
const chain = verifyEventChain(events);
|
|
263
|
+
return {
|
|
264
|
+
schemaVersion: DOMAIN_EVENT_SCHEMA_VERSION,
|
|
265
|
+
groupId,
|
|
266
|
+
publisherId: publisherId || null,
|
|
267
|
+
lastEventHash: chain.lastHash || null,
|
|
268
|
+
recordCount: events.length,
|
|
269
|
+
timestamp: Date.now()
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
275
|
+
OPERATION_KIND_TO_EVENT_KIND,
|
|
276
|
+
computeEventHash,
|
|
277
|
+
operationToDomainEvent,
|
|
278
|
+
signDomainEvent,
|
|
279
|
+
verifyDomainEvent,
|
|
280
|
+
verifyDomainEventSignature,
|
|
281
|
+
createEmptyView,
|
|
282
|
+
applyDomainEventToView,
|
|
283
|
+
verifyEventChain,
|
|
284
|
+
buildCheckpoint
|
|
285
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const DEFAULT_LIVE_CAP = 5000;
|
|
2
|
+
const DEFAULT_BULK_INTERVAL_MS = 30000;
|
|
3
|
+
|
|
4
|
+
function assignPeerGroupTier({ joinIndex, liveCap = DEFAULT_LIVE_CAP, requestedTier, role }) {
|
|
5
|
+
if (role === 'publisher') {
|
|
6
|
+
return 'live';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (requestedTier === 'live' || requestedTier === 'bulk') {
|
|
10
|
+
if (requestedTier === 'live' && joinIndex >= liveCap) {
|
|
11
|
+
return 'bulk';
|
|
12
|
+
}
|
|
13
|
+
return requestedTier;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return joinIndex < liveCap ? 'live' : 'bulk';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getPeerTier(peer) {
|
|
20
|
+
return peer?.metadata?.peerGroupTier || peer?.peerGroupTier || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function filterPeersByTier(peers, tier) {
|
|
24
|
+
if (!tier) {
|
|
25
|
+
return peers;
|
|
26
|
+
}
|
|
27
|
+
return peers.filter((peer) => getPeerTier(peer) === tier);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function countLivePeers(peers) {
|
|
31
|
+
return peers.filter((peer) => getPeerTier(peer) === 'live').length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function countBulkPeers(peers) {
|
|
35
|
+
return peers.filter((peer) => getPeerTier(peer) === 'bulk').length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
DEFAULT_LIVE_CAP,
|
|
40
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
41
|
+
assignPeerGroupTier,
|
|
42
|
+
getPeerTier,
|
|
43
|
+
filterPeersByTier,
|
|
44
|
+
countLivePeers,
|
|
45
|
+
countBulkPeers
|
|
46
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const EventEmitter = require('../utils/event-emitter');
|
|
2
|
+
const {
|
|
3
|
+
createEmptyView,
|
|
4
|
+
applyDomainEventToView,
|
|
5
|
+
verifyEventChain,
|
|
6
|
+
verifyDomainEvent,
|
|
7
|
+
DOMAIN_EVENT_SCHEMA_VERSION
|
|
8
|
+
} = require('./domain-events');
|
|
9
|
+
|
|
10
|
+
class DignityQueryReplica extends EventEmitter {
|
|
11
|
+
constructor(dignityP2P, { groupId, collections = [], tierMode = 'auto', publisherId = null } = {}) {
|
|
12
|
+
super();
|
|
13
|
+
|
|
14
|
+
if (!dignityP2P) {
|
|
15
|
+
throw new Error('DignityQueryReplica requires dignityP2P');
|
|
16
|
+
}
|
|
17
|
+
if (!groupId) {
|
|
18
|
+
throw new Error('DignityQueryReplica requires groupId');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.dignity = dignityP2P;
|
|
22
|
+
this.groupId = groupId;
|
|
23
|
+
this.collections = [...collections];
|
|
24
|
+
this.tierMode = tierMode;
|
|
25
|
+
this.publisherId = publisherId;
|
|
26
|
+
this.view = createEmptyView(this.collections);
|
|
27
|
+
this.eventLog = [];
|
|
28
|
+
this.started = false;
|
|
29
|
+
this.boundDomainHandler = this.handleDomainEvent.bind(this);
|
|
30
|
+
this.boundPeerGroupHandler = this.handlePeerGroupMessage.bind(this);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async start(options = {}) {
|
|
34
|
+
if (this.started) {
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await this.dignity.joinPeerGroup(this.groupId, {
|
|
39
|
+
tierMode: this.tierMode,
|
|
40
|
+
role: 'subscriber',
|
|
41
|
+
commandCapable: false,
|
|
42
|
+
domainEvents: true,
|
|
43
|
+
publisherId: this.publisherId,
|
|
44
|
+
liveCap: options.liveCap,
|
|
45
|
+
bulkIntervalMs: options.bulkIntervalMs,
|
|
46
|
+
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
47
|
+
metadata: { role: 'subscriber', replica: true }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.dignity.on('domainevent', this.boundDomainHandler);
|
|
51
|
+
this.dignity.on('peergroupmessage', this.boundPeerGroupHandler);
|
|
52
|
+
this.started = true;
|
|
53
|
+
this.emit('started', { groupId: this.groupId });
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async stop() {
|
|
58
|
+
if (!this.started) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.dignity.off('domainevent', this.boundDomainHandler);
|
|
63
|
+
this.dignity.off('peergroupmessage', this.boundPeerGroupHandler);
|
|
64
|
+
await this.dignity.leavePeerGroup(this.groupId);
|
|
65
|
+
this.started = false;
|
|
66
|
+
this.emit('stopped', { groupId: this.groupId });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
handleDomainEvent(event) {
|
|
70
|
+
if (!event || event.groupId !== this.groupId) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.publisherId && event.publisherId !== this.publisherId) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.ingestEvent(event);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
handlePeerGroupMessage(message) {
|
|
82
|
+
if (!message || message.groupId !== this.groupId) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (message.type === 'domain:checkpoint') {
|
|
87
|
+
this.emit('checkpoint', message.payload);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ingestEvent(event, { skipChainCheck = false } = {}) {
|
|
92
|
+
const verified = verifyDomainEvent(event, {
|
|
93
|
+
supportedVersions: [DOMAIN_EVENT_SCHEMA_VERSION]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!verified.ok) {
|
|
97
|
+
this.emit('warning', { type: 'domain-event-rejected', reason: verified.reason, event });
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!skipChainCheck && this.eventLog.length > 0) {
|
|
102
|
+
const lastHash = this.eventLog[this.eventLog.length - 1].eventHash;
|
|
103
|
+
if (event.prevHash !== lastHash) {
|
|
104
|
+
this.emit('chainbroken', {
|
|
105
|
+
groupId: this.groupId,
|
|
106
|
+
expectedPrev: lastHash,
|
|
107
|
+
actualPrev: event.prevHash,
|
|
108
|
+
eventId: event.eventId
|
|
109
|
+
});
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
} else if (!skipChainCheck && this.eventLog.length === 0 && event.prevHash) {
|
|
113
|
+
this.emit('chainbroken', {
|
|
114
|
+
groupId: this.groupId,
|
|
115
|
+
expectedPrev: null,
|
|
116
|
+
actualPrev: event.prevHash,
|
|
117
|
+
eventId: event.eventId
|
|
118
|
+
});
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const duplicate = this.eventLog.some((entry) => entry.eventId === event.eventId);
|
|
123
|
+
if (duplicate) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = applyDomainEventToView(this.view, event, {
|
|
128
|
+
collectionsFilter: this.collections.length > 0 ? this.collections : null
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (result.applied || result.reason === 'collection-filtered') {
|
|
132
|
+
this.eventLog.push({ ...event });
|
|
133
|
+
this.emit('change', { event, result });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.emit('warning', { type: 'domain-event-not-applied', reason: result.reason, event });
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
read(collectionName, id) {
|
|
142
|
+
const collection = this.view.get(collectionName);
|
|
143
|
+
if (!collection) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const record = collection.get(id);
|
|
147
|
+
if (!record || record.deletedAt) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return { ...record, data: { ...record.data } };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
list(collectionName, options = {}) {
|
|
154
|
+
const collection = this.view.get(collectionName);
|
|
155
|
+
if (!collection) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const includeDeleted = options.includeDeleted || false;
|
|
160
|
+
const records = [];
|
|
161
|
+
|
|
162
|
+
for (const record of collection.values()) {
|
|
163
|
+
if (record.deletedAt && !includeDeleted) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (record.deletedAt && includeDeleted) {
|
|
167
|
+
records.push({
|
|
168
|
+
id: record.id,
|
|
169
|
+
ownerId: record.ownerId,
|
|
170
|
+
deletedAt: record.deletedAt,
|
|
171
|
+
version: record.version
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
records.push({ ...record, data: { ...record.data } });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return records;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
verifyChain() {
|
|
182
|
+
const result = verifyEventChain(this.eventLog);
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
this.emit('chainbroken', { groupId: this.groupId, ...result });
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getViewStats() {
|
|
190
|
+
const stats = {
|
|
191
|
+
groupId: this.groupId,
|
|
192
|
+
eventCount: this.eventLog.length,
|
|
193
|
+
collections: {}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
for (const [name, collection] of this.view.entries()) {
|
|
197
|
+
let active = 0;
|
|
198
|
+
let deleted = 0;
|
|
199
|
+
for (const record of collection.values()) {
|
|
200
|
+
if (record.deletedAt) {
|
|
201
|
+
deleted += 1;
|
|
202
|
+
} else {
|
|
203
|
+
active += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
stats.collections[name] = { active, deleted };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return stats;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = DignityQueryReplica;
|
package/src/gossip/peer-group.js
CHANGED
package/src/index.js
CHANGED
|
@@ -49,6 +49,24 @@ const {
|
|
|
49
49
|
parsePeerGroupScope,
|
|
50
50
|
selectFanoutPeers
|
|
51
51
|
} = require('./gossip/peer-group');
|
|
52
|
+
const {
|
|
53
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
54
|
+
operationToDomainEvent,
|
|
55
|
+
signDomainEvent,
|
|
56
|
+
verifyDomainEvent,
|
|
57
|
+
verifyEventChain,
|
|
58
|
+
buildCheckpoint,
|
|
59
|
+
createEmptyView,
|
|
60
|
+
applyDomainEventToView
|
|
61
|
+
} = require('./cqrs/domain-events');
|
|
62
|
+
const {
|
|
63
|
+
DEFAULT_LIVE_CAP,
|
|
64
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
65
|
+
assignPeerGroupTier,
|
|
66
|
+
filterPeersByTier
|
|
67
|
+
} = require('./cqrs/peer-group-tiers');
|
|
68
|
+
const { electBulkRelays, DEFAULT_BULK_RELAY_COUNT } = require('./cqrs/bulk-relay');
|
|
69
|
+
const DignityQueryReplica = require('./cqrs/query-replica');
|
|
52
70
|
|
|
53
71
|
module.exports = {
|
|
54
72
|
DignityP2P,
|
|
@@ -83,5 +101,20 @@ module.exports = {
|
|
|
83
101
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
84
102
|
peerGroupScope,
|
|
85
103
|
parsePeerGroupScope,
|
|
86
|
-
selectFanoutPeers
|
|
104
|
+
selectFanoutPeers,
|
|
105
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
106
|
+
operationToDomainEvent,
|
|
107
|
+
signDomainEvent,
|
|
108
|
+
verifyDomainEvent,
|
|
109
|
+
verifyEventChain,
|
|
110
|
+
buildCheckpoint,
|
|
111
|
+
createEmptyView,
|
|
112
|
+
applyDomainEventToView,
|
|
113
|
+
DEFAULT_LIVE_CAP,
|
|
114
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
115
|
+
assignPeerGroupTier,
|
|
116
|
+
filterPeersByTier,
|
|
117
|
+
electBulkRelays,
|
|
118
|
+
DEFAULT_BULK_RELAY_COUNT,
|
|
119
|
+
DignityQueryReplica
|
|
87
120
|
};
|