dignity.js 0.7.0 → 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 +63 -3
- package/docs/assets/playground.css +70 -4
- package/docs/assets/playground.js +49 -2
- package/docs/assets/styles.css +2 -2
- package/docs/index.html +65 -9
- 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
|
@@ -3449,7 +3449,7 @@ var require_peer_group = __commonJS({
|
|
|
3449
3449
|
var DEFAULT_PEER_GROUP_OPTIONS = {
|
|
3450
3450
|
fanout: 3,
|
|
3451
3451
|
maxActivePeers: 8,
|
|
3452
|
-
maxHops:
|
|
3452
|
+
maxHops: 64,
|
|
3453
3453
|
relayEnabled: true
|
|
3454
3454
|
};
|
|
3455
3455
|
function peerGroupScope(groupId) {
|
|
@@ -3501,6 +3501,327 @@ var require_peer_group = __commonJS({
|
|
|
3501
3501
|
}
|
|
3502
3502
|
});
|
|
3503
3503
|
|
|
3504
|
+
// src/cqrs/domain-events.js
|
|
3505
|
+
var require_domain_events = __commonJS({
|
|
3506
|
+
"src/cqrs/domain-events.js"(exports, module) {
|
|
3507
|
+
var nacl = require_nacl_fast();
|
|
3508
|
+
var naclUtil = require_nacl_util();
|
|
3509
|
+
var { stableStringify } = require_message_security_service();
|
|
3510
|
+
var DOMAIN_EVENT_SCHEMA_VERSION = 1;
|
|
3511
|
+
var OPERATION_KIND_TO_EVENT_KIND = {
|
|
3512
|
+
create: "record:created",
|
|
3513
|
+
update: "record:updated",
|
|
3514
|
+
delete: "record:removed",
|
|
3515
|
+
"transfer-ownership": "ownership:transferred"
|
|
3516
|
+
};
|
|
3517
|
+
function computeContentHash(data) {
|
|
3518
|
+
const canonical = stableStringify(data || {});
|
|
3519
|
+
const bytes = naclUtil.decodeUTF8(canonical);
|
|
3520
|
+
const hash = nacl.hash(bytes);
|
|
3521
|
+
const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3522
|
+
return `sha512:${hex}`;
|
|
3523
|
+
}
|
|
3524
|
+
function canonicalEventBody(event) {
|
|
3525
|
+
return stableStringify({
|
|
3526
|
+
schemaVersion: event.schemaVersion,
|
|
3527
|
+
eventId: event.eventId,
|
|
3528
|
+
groupId: event.groupId,
|
|
3529
|
+
publisherId: event.publisherId,
|
|
3530
|
+
kind: event.kind,
|
|
3531
|
+
collectionName: event.collectionName,
|
|
3532
|
+
id: event.id,
|
|
3533
|
+
payload: event.payload,
|
|
3534
|
+
timestamp: event.timestamp,
|
|
3535
|
+
baseVersion: event.baseVersion,
|
|
3536
|
+
prevHash: event.prevHash || null,
|
|
3537
|
+
newOwnerId: event.newOwnerId || null
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
function computeEventHash(event) {
|
|
3541
|
+
const canonical = canonicalEventBody(event);
|
|
3542
|
+
const bytes = naclUtil.decodeUTF8(canonical);
|
|
3543
|
+
const hash = nacl.hash(bytes);
|
|
3544
|
+
const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3545
|
+
return `sha512:${hex}`;
|
|
3546
|
+
}
|
|
3547
|
+
function operationToDomainEvent(operation, { publisherId, groupId, prevHash, eventIdGenerator }) {
|
|
3548
|
+
if (!operation || !publisherId || !groupId) {
|
|
3549
|
+
throw new Error("operationToDomainEvent requires operation, publisherId, and groupId");
|
|
3550
|
+
}
|
|
3551
|
+
const kind = OPERATION_KIND_TO_EVENT_KIND[operation.kind];
|
|
3552
|
+
if (!kind) {
|
|
3553
|
+
throw new Error(`Unsupported operation kind for domain event: ${operation.kind}`);
|
|
3554
|
+
}
|
|
3555
|
+
const event = {
|
|
3556
|
+
schemaVersion: DOMAIN_EVENT_SCHEMA_VERSION,
|
|
3557
|
+
eventId: eventIdGenerator ? eventIdGenerator() : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
3558
|
+
groupId,
|
|
3559
|
+
publisherId,
|
|
3560
|
+
kind,
|
|
3561
|
+
collectionName: operation.collectionName,
|
|
3562
|
+
id: operation.id,
|
|
3563
|
+
payload: operation.payload || {},
|
|
3564
|
+
timestamp: operation.timestamp,
|
|
3565
|
+
baseVersion: operation.baseVersion || null,
|
|
3566
|
+
prevHash: prevHash || null,
|
|
3567
|
+
newOwnerId: operation.newOwnerId || null,
|
|
3568
|
+
eventHash: null,
|
|
3569
|
+
signature: null
|
|
3570
|
+
};
|
|
3571
|
+
event.eventHash = computeEventHash(event);
|
|
3572
|
+
return event;
|
|
3573
|
+
}
|
|
3574
|
+
function signDomainEvent(event, signingSecretKey) {
|
|
3575
|
+
if (!signingSecretKey) {
|
|
3576
|
+
return { ...event };
|
|
3577
|
+
}
|
|
3578
|
+
const unsigned = { ...event, signature: null };
|
|
3579
|
+
const eventHash = computeEventHash(unsigned);
|
|
3580
|
+
const signature = nacl.sign.detached(
|
|
3581
|
+
naclUtil.decodeUTF8(eventHash),
|
|
3582
|
+
signingSecretKey
|
|
3583
|
+
);
|
|
3584
|
+
return {
|
|
3585
|
+
...unsigned,
|
|
3586
|
+
eventHash,
|
|
3587
|
+
signature: naclUtil.encodeBase64(signature)
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
function verifyDomainEventSignature(event, signingPublicKey) {
|
|
3591
|
+
if (!event || !event.eventHash) {
|
|
3592
|
+
return { ok: false, reason: "missing-event-hash" };
|
|
3593
|
+
}
|
|
3594
|
+
const recomputed = computeEventHash({ ...event, signature: null });
|
|
3595
|
+
if (recomputed !== event.eventHash) {
|
|
3596
|
+
return { ok: false, reason: "event-hash-mismatch" };
|
|
3597
|
+
}
|
|
3598
|
+
if (!event.signature) {
|
|
3599
|
+
return { ok: true, unsigned: true };
|
|
3600
|
+
}
|
|
3601
|
+
if (!signingPublicKey) {
|
|
3602
|
+
return { ok: false, reason: "missing-public-key" };
|
|
3603
|
+
}
|
|
3604
|
+
const keyBytes = typeof signingPublicKey === "string" ? naclUtil.decodeBase64(signingPublicKey) : signingPublicKey;
|
|
3605
|
+
const valid = nacl.sign.detached.verify(
|
|
3606
|
+
naclUtil.decodeUTF8(event.eventHash),
|
|
3607
|
+
naclUtil.decodeBase64(event.signature),
|
|
3608
|
+
keyBytes
|
|
3609
|
+
);
|
|
3610
|
+
return valid ? { ok: true } : { ok: false, reason: "invalid-signature" };
|
|
3611
|
+
}
|
|
3612
|
+
function verifyDomainEvent(event, { signingPublicKey, supportedVersions } = {}) {
|
|
3613
|
+
if (!event || typeof event !== "object") {
|
|
3614
|
+
return { ok: false, reason: "invalid-event" };
|
|
3615
|
+
}
|
|
3616
|
+
const versions = supportedVersions || [DOMAIN_EVENT_SCHEMA_VERSION];
|
|
3617
|
+
if (!versions.includes(event.schemaVersion)) {
|
|
3618
|
+
return { ok: false, reason: "unsupported-schema-version", schemaVersion: event.schemaVersion };
|
|
3619
|
+
}
|
|
3620
|
+
if (!event.eventId || !event.groupId || !event.publisherId || !event.kind) {
|
|
3621
|
+
return { ok: false, reason: "missing-required-fields" };
|
|
3622
|
+
}
|
|
3623
|
+
return verifyDomainEventSignature(event, signingPublicKey);
|
|
3624
|
+
}
|
|
3625
|
+
function createEmptyView(collections = []) {
|
|
3626
|
+
const view = /* @__PURE__ */ new Map();
|
|
3627
|
+
for (const name of collections) {
|
|
3628
|
+
view.set(name, /* @__PURE__ */ new Map());
|
|
3629
|
+
}
|
|
3630
|
+
return view;
|
|
3631
|
+
}
|
|
3632
|
+
function ensureCollectionView(view, collectionName) {
|
|
3633
|
+
if (!view.has(collectionName)) {
|
|
3634
|
+
view.set(collectionName, /* @__PURE__ */ new Map());
|
|
3635
|
+
}
|
|
3636
|
+
return view.get(collectionName);
|
|
3637
|
+
}
|
|
3638
|
+
function applyDomainEventToView(view, event, { collectionsFilter } = {}) {
|
|
3639
|
+
if (!event || !event.collectionName) {
|
|
3640
|
+
return { applied: false, reason: "invalid-event" };
|
|
3641
|
+
}
|
|
3642
|
+
if (Array.isArray(collectionsFilter) && collectionsFilter.length > 0 && !collectionsFilter.includes(event.collectionName)) {
|
|
3643
|
+
return { applied: false, reason: "collection-filtered" };
|
|
3644
|
+
}
|
|
3645
|
+
const collection = ensureCollectionView(view, event.collectionName);
|
|
3646
|
+
if (event.kind === "record:created") {
|
|
3647
|
+
if (collection.has(event.id)) {
|
|
3648
|
+
return { applied: false, reason: "already-exists" };
|
|
3649
|
+
}
|
|
3650
|
+
collection.set(event.id, {
|
|
3651
|
+
id: event.id,
|
|
3652
|
+
ownerId: event.publisherId,
|
|
3653
|
+
data: { ...event.payload || {} },
|
|
3654
|
+
hash: computeContentHash(event.payload || {}),
|
|
3655
|
+
createdAt: event.timestamp,
|
|
3656
|
+
updatedAt: event.timestamp,
|
|
3657
|
+
deletedAt: null,
|
|
3658
|
+
version: 1
|
|
3659
|
+
});
|
|
3660
|
+
return { applied: true, kind: event.kind };
|
|
3661
|
+
}
|
|
3662
|
+
const current = collection.get(event.id);
|
|
3663
|
+
if (!current || current.deletedAt) {
|
|
3664
|
+
if (event.kind === "record:removed") {
|
|
3665
|
+
return { applied: false, reason: "not-found" };
|
|
3666
|
+
}
|
|
3667
|
+
return { applied: false, reason: "missing-record" };
|
|
3668
|
+
}
|
|
3669
|
+
if (event.kind === "record:updated") {
|
|
3670
|
+
if (typeof event.baseVersion === "number" && current.version !== event.baseVersion) {
|
|
3671
|
+
return { applied: false, reason: "version-conflict", currentVersion: current.version };
|
|
3672
|
+
}
|
|
3673
|
+
current.data = { ...current.data, ...event.payload || {} };
|
|
3674
|
+
current.hash = computeContentHash(current.data);
|
|
3675
|
+
current.updatedAt = event.timestamp;
|
|
3676
|
+
current.version += 1;
|
|
3677
|
+
return { applied: true, kind: event.kind };
|
|
3678
|
+
}
|
|
3679
|
+
if (event.kind === "record:removed") {
|
|
3680
|
+
if (typeof event.baseVersion === "number" && current.version !== event.baseVersion) {
|
|
3681
|
+
return { applied: false, reason: "version-conflict", currentVersion: current.version };
|
|
3682
|
+
}
|
|
3683
|
+
current.deletedAt = event.timestamp;
|
|
3684
|
+
current.version += 1;
|
|
3685
|
+
return { applied: true, kind: event.kind };
|
|
3686
|
+
}
|
|
3687
|
+
if (event.kind === "ownership:transferred") {
|
|
3688
|
+
if (typeof event.baseVersion === "number" && current.version !== event.baseVersion) {
|
|
3689
|
+
return { applied: false, reason: "version-conflict", currentVersion: current.version };
|
|
3690
|
+
}
|
|
3691
|
+
current.ownerId = event.newOwnerId;
|
|
3692
|
+
current.updatedAt = event.timestamp;
|
|
3693
|
+
current.version += 1;
|
|
3694
|
+
return { applied: true, kind: event.kind };
|
|
3695
|
+
}
|
|
3696
|
+
return { applied: false, reason: "unknown-kind" };
|
|
3697
|
+
}
|
|
3698
|
+
function verifyEventChain(events, { genesisHash = null } = {}) {
|
|
3699
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
3700
|
+
return { ok: true, length: 0 };
|
|
3701
|
+
}
|
|
3702
|
+
let expectedPrev = genesisHash;
|
|
3703
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
3704
|
+
const event = events[index];
|
|
3705
|
+
const prevHash = event.prevHash || null;
|
|
3706
|
+
if (prevHash !== expectedPrev) {
|
|
3707
|
+
return {
|
|
3708
|
+
ok: false,
|
|
3709
|
+
reason: "chain-break",
|
|
3710
|
+
index,
|
|
3711
|
+
expectedPrev,
|
|
3712
|
+
actualPrev: prevHash
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3715
|
+
const hashCheck = verifyDomainEventSignature(event, null);
|
|
3716
|
+
if (!hashCheck.ok) {
|
|
3717
|
+
return { ok: false, reason: hashCheck.reason, index };
|
|
3718
|
+
}
|
|
3719
|
+
expectedPrev = event.eventHash;
|
|
3720
|
+
}
|
|
3721
|
+
return { ok: true, length: events.length, lastHash: expectedPrev };
|
|
3722
|
+
}
|
|
3723
|
+
function buildCheckpoint(groupId, events, { publisherId } = {}) {
|
|
3724
|
+
const chain = verifyEventChain(events);
|
|
3725
|
+
return {
|
|
3726
|
+
schemaVersion: DOMAIN_EVENT_SCHEMA_VERSION,
|
|
3727
|
+
groupId,
|
|
3728
|
+
publisherId: publisherId || null,
|
|
3729
|
+
lastEventHash: chain.lastHash || null,
|
|
3730
|
+
recordCount: events.length,
|
|
3731
|
+
timestamp: Date.now()
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
module.exports = {
|
|
3735
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
3736
|
+
OPERATION_KIND_TO_EVENT_KIND,
|
|
3737
|
+
computeEventHash,
|
|
3738
|
+
operationToDomainEvent,
|
|
3739
|
+
signDomainEvent,
|
|
3740
|
+
verifyDomainEvent,
|
|
3741
|
+
verifyDomainEventSignature,
|
|
3742
|
+
createEmptyView,
|
|
3743
|
+
applyDomainEventToView,
|
|
3744
|
+
verifyEventChain,
|
|
3745
|
+
buildCheckpoint
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
});
|
|
3749
|
+
|
|
3750
|
+
// src/cqrs/peer-group-tiers.js
|
|
3751
|
+
var require_peer_group_tiers = __commonJS({
|
|
3752
|
+
"src/cqrs/peer-group-tiers.js"(exports, module) {
|
|
3753
|
+
var DEFAULT_LIVE_CAP = 5e3;
|
|
3754
|
+
var DEFAULT_BULK_INTERVAL_MS = 3e4;
|
|
3755
|
+
function assignPeerGroupTier({ joinIndex, liveCap = DEFAULT_LIVE_CAP, requestedTier, role }) {
|
|
3756
|
+
if (role === "publisher") {
|
|
3757
|
+
return "live";
|
|
3758
|
+
}
|
|
3759
|
+
if (requestedTier === "live" || requestedTier === "bulk") {
|
|
3760
|
+
if (requestedTier === "live" && joinIndex >= liveCap) {
|
|
3761
|
+
return "bulk";
|
|
3762
|
+
}
|
|
3763
|
+
return requestedTier;
|
|
3764
|
+
}
|
|
3765
|
+
return joinIndex < liveCap ? "live" : "bulk";
|
|
3766
|
+
}
|
|
3767
|
+
function getPeerTier(peer) {
|
|
3768
|
+
return peer?.metadata?.peerGroupTier || peer?.peerGroupTier || null;
|
|
3769
|
+
}
|
|
3770
|
+
function filterPeersByTier(peers, tier) {
|
|
3771
|
+
if (!tier) {
|
|
3772
|
+
return peers;
|
|
3773
|
+
}
|
|
3774
|
+
return peers.filter((peer) => getPeerTier(peer) === tier);
|
|
3775
|
+
}
|
|
3776
|
+
function countLivePeers(peers) {
|
|
3777
|
+
return peers.filter((peer) => getPeerTier(peer) === "live").length;
|
|
3778
|
+
}
|
|
3779
|
+
function countBulkPeers(peers) {
|
|
3780
|
+
return peers.filter((peer) => getPeerTier(peer) === "bulk").length;
|
|
3781
|
+
}
|
|
3782
|
+
module.exports = {
|
|
3783
|
+
DEFAULT_LIVE_CAP,
|
|
3784
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
3785
|
+
assignPeerGroupTier,
|
|
3786
|
+
getPeerTier,
|
|
3787
|
+
filterPeersByTier,
|
|
3788
|
+
countLivePeers,
|
|
3789
|
+
countBulkPeers
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
});
|
|
3793
|
+
|
|
3794
|
+
// src/cqrs/bulk-relay.js
|
|
3795
|
+
var require_bulk_relay = __commonJS({
|
|
3796
|
+
"src/cqrs/bulk-relay.js"(exports, module) {
|
|
3797
|
+
var { getPeerTier } = require_peer_group_tiers();
|
|
3798
|
+
var DEFAULT_BULK_RELAY_COUNT = 3;
|
|
3799
|
+
function electBulkRelays(peers, { count = DEFAULT_BULK_RELAY_COUNT } = {}) {
|
|
3800
|
+
const bulkPeers = peers.filter((peer) => getPeerTier(peer) === "bulk").map((peer) => peer.peerId || peer).filter(Boolean).sort();
|
|
3801
|
+
return bulkPeers.slice(0, Math.max(0, count));
|
|
3802
|
+
}
|
|
3803
|
+
function isBulkRelay(metadata) {
|
|
3804
|
+
return metadata?.bulkRelay === true;
|
|
3805
|
+
}
|
|
3806
|
+
function applyBulkRelayFlags(peers, relayPeerIds) {
|
|
3807
|
+
const relaySet = new Set(relayPeerIds);
|
|
3808
|
+
return peers.map((peer) => ({
|
|
3809
|
+
...peer,
|
|
3810
|
+
metadata: {
|
|
3811
|
+
...peer.metadata || {},
|
|
3812
|
+
bulkRelay: relaySet.has(peer.peerId)
|
|
3813
|
+
}
|
|
3814
|
+
}));
|
|
3815
|
+
}
|
|
3816
|
+
module.exports = {
|
|
3817
|
+
DEFAULT_BULK_RELAY_COUNT,
|
|
3818
|
+
electBulkRelays,
|
|
3819
|
+
isBulkRelay,
|
|
3820
|
+
applyBulkRelayFlags
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3824
|
+
|
|
3504
3825
|
// src/core/dignity-p2p.js
|
|
3505
3826
|
var require_dignity_p2p = __commonJS({
|
|
3506
3827
|
"src/core/dignity-p2p.js"(exports, module) {
|
|
@@ -3521,8 +3842,24 @@ var require_dignity_p2p = __commonJS({
|
|
|
3521
3842
|
var {
|
|
3522
3843
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
3523
3844
|
peerGroupScope,
|
|
3845
|
+
parsePeerGroupScope,
|
|
3524
3846
|
selectFanoutPeers
|
|
3525
3847
|
} = require_peer_group();
|
|
3848
|
+
var {
|
|
3849
|
+
operationToDomainEvent,
|
|
3850
|
+
signDomainEvent,
|
|
3851
|
+
verifyDomainEvent,
|
|
3852
|
+
applyDomainEventToView,
|
|
3853
|
+
createEmptyView,
|
|
3854
|
+
buildCheckpoint
|
|
3855
|
+
} = require_domain_events();
|
|
3856
|
+
var {
|
|
3857
|
+
DEFAULT_LIVE_CAP,
|
|
3858
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
3859
|
+
assignPeerGroupTier,
|
|
3860
|
+
filterPeersByTier
|
|
3861
|
+
} = require_peer_group_tiers();
|
|
3862
|
+
var { electBulkRelays } = require_bulk_relay();
|
|
3526
3863
|
function computeContentHash(data) {
|
|
3527
3864
|
const canonical = stableStringify(data || {});
|
|
3528
3865
|
const bytes = naclUtil.decodeUTF8(canonical);
|
|
@@ -3566,6 +3903,10 @@ var require_dignity_p2p = __commonJS({
|
|
|
3566
3903
|
this.gossipPublishMinIntervalMs = security && typeof security.gossipPublishMinIntervalMs === "number" ? security.gossipPublishMinIntervalMs : 0;
|
|
3567
3904
|
this.lastGossipPublishAt = /* @__PURE__ */ new Map();
|
|
3568
3905
|
this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
|
|
3906
|
+
this.domainEventLogs = /* @__PURE__ */ new Map();
|
|
3907
|
+
this.lastEventHashByGroup = /* @__PURE__ */ new Map();
|
|
3908
|
+
this.bulkRelayByGroup = /* @__PURE__ */ new Map();
|
|
3909
|
+
this.replicaViews = /* @__PURE__ */ new Map();
|
|
3569
3910
|
this.state = /* @__PURE__ */ new Map();
|
|
3570
3911
|
this.appliedOperations = /* @__PURE__ */ new Map();
|
|
3571
3912
|
this.boundMessageHandler = this.handleIncomingMessage.bind(this);
|
|
@@ -3704,6 +4045,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3704
4045
|
payload: { ...data || {} }
|
|
3705
4046
|
};
|
|
3706
4047
|
this.applyOperation(operation);
|
|
4048
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3707
4049
|
await this.broadcastMessage("operation", operation, {
|
|
3708
4050
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3709
4051
|
messageType: "operation",
|
|
@@ -3781,6 +4123,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3781
4123
|
operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
|
|
3782
4124
|
}
|
|
3783
4125
|
this.applyOperation(operation);
|
|
4126
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3784
4127
|
await this.broadcastMessage("operation", operation, {
|
|
3785
4128
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3786
4129
|
messageType: "operation",
|
|
@@ -3835,6 +4178,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3835
4178
|
keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
|
|
3836
4179
|
};
|
|
3837
4180
|
this.applyOperation(operation);
|
|
4181
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3838
4182
|
await this.broadcastMessage("operation", operation, {
|
|
3839
4183
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3840
4184
|
messageType: "operation",
|
|
@@ -3866,6 +4210,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3866
4210
|
baseVersion: existing.version
|
|
3867
4211
|
};
|
|
3868
4212
|
this.applyOperation(operation);
|
|
4213
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3869
4214
|
await this.broadcastMessage("operation", operation, {
|
|
3870
4215
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3871
4216
|
messageType: "operation",
|
|
@@ -4156,9 +4501,16 @@ var require_dignity_p2p = __commonJS({
|
|
|
4156
4501
|
}
|
|
4157
4502
|
return [];
|
|
4158
4503
|
}
|
|
4159
|
-
selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
|
|
4504
|
+
selectPeerGroupFanout(groupId, count, excludePeerIds = [], fanoutOptions = {}) {
|
|
4160
4505
|
const scope = this.peerGroupScopeFor(groupId);
|
|
4161
|
-
const
|
|
4506
|
+
const group = this.peerGroups.get(groupId);
|
|
4507
|
+
let peers = this.listPeers(scope, { includeSelf: false });
|
|
4508
|
+
if (group && group.tiered && fanoutOptions.tier) {
|
|
4509
|
+
peers = filterPeersByTier(peers, fanoutOptions.tier);
|
|
4510
|
+
}
|
|
4511
|
+
if (fanoutOptions.bulkRelayOnly) {
|
|
4512
|
+
peers = peers.filter((peer) => peer.metadata?.bulkRelay === true);
|
|
4513
|
+
}
|
|
4162
4514
|
return selectFanoutPeers({
|
|
4163
4515
|
peers,
|
|
4164
4516
|
count,
|
|
@@ -4190,15 +4542,45 @@ var require_dignity_p2p = __commonJS({
|
|
|
4190
4542
|
throw new Error("joinPeerGroup requires groupId");
|
|
4191
4543
|
}
|
|
4192
4544
|
const scope = this.peerGroupScopeFor(groupId);
|
|
4545
|
+
const role = options.role || options.metadata?.role || "subscriber";
|
|
4546
|
+
const tierMode = options.tierMode || "auto";
|
|
4547
|
+
const tiered = options.tiered === true || options.tierMode !== void 0 || options.role !== void 0 || typeof options.liveCap === "number";
|
|
4548
|
+
const liveCap = typeof options.liveCap === "number" ? options.liveCap : DEFAULT_LIVE_CAP;
|
|
4549
|
+
const existingMembers = this.listPeerGroupMembers(groupId, { includeSelf: false });
|
|
4550
|
+
const existingSubscriberCount = existingMembers.filter((member) => {
|
|
4551
|
+
const memberRole = member.metadata?.peerGroupRole || member.metadata?.role;
|
|
4552
|
+
return memberRole !== "publisher";
|
|
4553
|
+
}).length;
|
|
4554
|
+
let assignedTier = tiered ? assignPeerGroupTier({
|
|
4555
|
+
joinIndex: existingSubscriberCount,
|
|
4556
|
+
liveCap,
|
|
4557
|
+
requestedTier: tierMode === "auto" ? null : tierMode,
|
|
4558
|
+
role
|
|
4559
|
+
}) : null;
|
|
4560
|
+
const publisherId = options.publisherId || (role === "publisher" ? this.nodeId : null);
|
|
4193
4561
|
const config = {
|
|
4194
4562
|
fanout: typeof options.fanout === "number" ? options.fanout : this.defaultPeerGroupFanout,
|
|
4195
4563
|
maxActivePeers: typeof options.maxActivePeers === "number" ? options.maxActivePeers : this.defaultPeerGroupMaxActivePeers,
|
|
4196
4564
|
maxHops: typeof options.maxHops === "number" ? options.maxHops : this.defaultGossipMaxHops,
|
|
4197
|
-
relayEnabled: options.relayEnabled !== false
|
|
4565
|
+
relayEnabled: options.relayEnabled !== false,
|
|
4566
|
+
tiered,
|
|
4567
|
+
tierMode,
|
|
4568
|
+
liveCap,
|
|
4569
|
+
bulkIntervalMs: typeof options.bulkIntervalMs === "number" ? options.bulkIntervalMs : DEFAULT_BULK_INTERVAL_MS,
|
|
4570
|
+
domainEvents: options.domainEvents !== false,
|
|
4571
|
+
autoPublishDomainEvents: options.autoPublishDomainEvents !== false,
|
|
4572
|
+
role,
|
|
4573
|
+
publisherId,
|
|
4574
|
+
commandCapable: options.commandCapable !== false,
|
|
4575
|
+
peerGroupTier: assignedTier
|
|
4198
4576
|
};
|
|
4199
4577
|
await this.joinDiscovery(scope, {
|
|
4200
4578
|
metadata: {
|
|
4201
4579
|
peerGroup: groupId,
|
|
4580
|
+
...assignedTier ? { peerGroupTier: assignedTier } : {},
|
|
4581
|
+
peerGroupRole: role,
|
|
4582
|
+
...publisherId ? { publisherId } : {},
|
|
4583
|
+
bulkRelay: false,
|
|
4202
4584
|
...options.metadata || {}
|
|
4203
4585
|
},
|
|
4204
4586
|
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
@@ -4206,9 +4588,61 @@ var require_dignity_p2p = __commonJS({
|
|
|
4206
4588
|
ttlMs: options.ttlMs
|
|
4207
4589
|
});
|
|
4208
4590
|
this.peerGroups.set(groupId, config);
|
|
4209
|
-
this.
|
|
4591
|
+
if (!this.domainEventLogs.has(groupId)) {
|
|
4592
|
+
this.domainEventLogs.set(groupId, []);
|
|
4593
|
+
}
|
|
4594
|
+
if (!this.replicaViews.has(groupId)) {
|
|
4595
|
+
this.replicaViews.set(groupId, createEmptyView());
|
|
4596
|
+
}
|
|
4597
|
+
this.refreshBulkRelays(groupId);
|
|
4598
|
+
if (tiered && tierMode === "auto" && role === "subscriber") {
|
|
4599
|
+
assignedTier = this.recalculateOwnPeerGroupTier(groupId) || assignedTier;
|
|
4600
|
+
config.peerGroupTier = assignedTier;
|
|
4601
|
+
}
|
|
4602
|
+
this.emit("peergroupjoined", { groupId, config, tier: assignedTier });
|
|
4210
4603
|
return config;
|
|
4211
4604
|
}
|
|
4605
|
+
recalculateOwnPeerGroupTier(groupId) {
|
|
4606
|
+
const group = this.peerGroups.get(groupId);
|
|
4607
|
+
if (!group || !group.tiered || group.role !== "subscriber") {
|
|
4608
|
+
return group ? group.peerGroupTier : null;
|
|
4609
|
+
}
|
|
4610
|
+
if (group.tierMode !== "auto") {
|
|
4611
|
+
return group.peerGroupTier;
|
|
4612
|
+
}
|
|
4613
|
+
const scope = this.peerGroupScopeFor(groupId);
|
|
4614
|
+
const members = this.listPeerGroupMembers(groupId, { includeSelf: true });
|
|
4615
|
+
const subscribers = members.filter((member) => {
|
|
4616
|
+
const memberRole = member.metadata?.peerGroupRole || member.metadata?.role;
|
|
4617
|
+
return memberRole !== "publisher";
|
|
4618
|
+
}).map((member) => member.peerId).sort();
|
|
4619
|
+
const joinIndex = subscribers.indexOf(this.nodeId);
|
|
4620
|
+
if (joinIndex < 0) {
|
|
4621
|
+
return group.peerGroupTier;
|
|
4622
|
+
}
|
|
4623
|
+
const newTier = assignPeerGroupTier({
|
|
4624
|
+
joinIndex,
|
|
4625
|
+
liveCap: group.liveCap,
|
|
4626
|
+
requestedTier: null,
|
|
4627
|
+
role: "subscriber"
|
|
4628
|
+
});
|
|
4629
|
+
if (newTier === group.peerGroupTier) {
|
|
4630
|
+
return newTier;
|
|
4631
|
+
}
|
|
4632
|
+
group.peerGroupTier = newTier;
|
|
4633
|
+
const room = this.discoveryRooms.get(scope);
|
|
4634
|
+
if (room) {
|
|
4635
|
+
room.metadata = {
|
|
4636
|
+
...room.metadata || {},
|
|
4637
|
+
peerGroupTier: newTier
|
|
4638
|
+
};
|
|
4639
|
+
this.upsertPresence(scope, this.nodeId, room.metadata, room.ttlMs, this.now());
|
|
4640
|
+
this.announcePresence(scope).catch((error) => {
|
|
4641
|
+
this.emit("warning", { type: "tier-announce-failed", groupId, error });
|
|
4642
|
+
});
|
|
4643
|
+
}
|
|
4644
|
+
return newTier;
|
|
4645
|
+
}
|
|
4212
4646
|
async leavePeerGroup(groupId) {
|
|
4213
4647
|
if (!groupId) {
|
|
4214
4648
|
return;
|
|
@@ -4216,6 +4650,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
4216
4650
|
const scope = this.peerGroupScopeFor(groupId);
|
|
4217
4651
|
await this.leaveDiscovery(scope);
|
|
4218
4652
|
this.peerGroups.delete(groupId);
|
|
4653
|
+
this.bulkRelayByGroup.delete(groupId);
|
|
4219
4654
|
this.emit("peergroupleft", { groupId });
|
|
4220
4655
|
}
|
|
4221
4656
|
async publishToPeerGroup(groupId, innerMessageType, innerPayload, options = {}) {
|
|
@@ -4238,7 +4673,11 @@ var require_dignity_p2p = __commonJS({
|
|
|
4238
4673
|
const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
|
|
4239
4674
|
const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
|
|
4240
4675
|
const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
|
|
4241
|
-
const
|
|
4676
|
+
const fanoutOptions = {};
|
|
4677
|
+
if (group && group.tiered && options.tier !== "bulk") {
|
|
4678
|
+
fanoutOptions.tier = options.tier || "live";
|
|
4679
|
+
}
|
|
4680
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId], fanoutOptions);
|
|
4242
4681
|
if (fanoutPeerIds.length > 0) {
|
|
4243
4682
|
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
4244
4683
|
await this.enforceConnectionBudget();
|
|
@@ -4260,6 +4699,204 @@ var require_dignity_p2p = __commonJS({
|
|
|
4260
4699
|
});
|
|
4261
4700
|
return { gossipId, fanoutPeerIds };
|
|
4262
4701
|
}
|
|
4702
|
+
async publishPeerGroupBulk(groupId, innerMessageType, innerPayload, options = {}) {
|
|
4703
|
+
const group = this.peerGroups.get(groupId);
|
|
4704
|
+
if (!group && options.allowUnjoined !== true) {
|
|
4705
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
4706
|
+
}
|
|
4707
|
+
if (group && group.role !== "publisher") {
|
|
4708
|
+
throw new Error(`Only publisher can bulk-publish to PeerGroup ${groupId}`);
|
|
4709
|
+
}
|
|
4710
|
+
const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
|
|
4711
|
+
const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
|
|
4712
|
+
const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
|
|
4713
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(
|
|
4714
|
+
groupId,
|
|
4715
|
+
fanout,
|
|
4716
|
+
[this.nodeId],
|
|
4717
|
+
{ tier: "bulk", bulkRelayOnly: group?.tiered === true }
|
|
4718
|
+
);
|
|
4719
|
+
if (fanoutPeerIds.length > 0) {
|
|
4720
|
+
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
4721
|
+
await this.enforceConnectionBudget();
|
|
4722
|
+
}
|
|
4723
|
+
const gossipId = options.gossipId || this.idGenerator();
|
|
4724
|
+
this.markSeenGossip(gossipId);
|
|
4725
|
+
await this.broadcastMessage("peer-group:gossip", {
|
|
4726
|
+
groupId,
|
|
4727
|
+
gossipId,
|
|
4728
|
+
publisherId: this.nodeId,
|
|
4729
|
+
hop: 0,
|
|
4730
|
+
maxHop,
|
|
4731
|
+
deliveryTier: "bulk",
|
|
4732
|
+
innerMessageType,
|
|
4733
|
+
innerPayload
|
|
4734
|
+
}, {
|
|
4735
|
+
broadcastScope: this.peerGroupScopeFor(groupId),
|
|
4736
|
+
fanoutPeerIds
|
|
4737
|
+
});
|
|
4738
|
+
return { gossipId, fanoutPeerIds };
|
|
4739
|
+
}
|
|
4740
|
+
async publishPeerGroupCheckpoint(groupId, options = {}) {
|
|
4741
|
+
const group = this.peerGroups.get(groupId);
|
|
4742
|
+
if (!group) {
|
|
4743
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
4744
|
+
}
|
|
4745
|
+
const events = this.domainEventLogs.get(groupId) || [];
|
|
4746
|
+
const checkpoint = buildCheckpoint(groupId, events, {
|
|
4747
|
+
publisherId: options.publisherId || group.publisherId || this.nodeId
|
|
4748
|
+
});
|
|
4749
|
+
await this.publishPeerGroupBulk(groupId, "domain:checkpoint", checkpoint, options);
|
|
4750
|
+
this.emit("checkpointpublished", { groupId, checkpoint });
|
|
4751
|
+
return checkpoint;
|
|
4752
|
+
}
|
|
4753
|
+
resolvePublisherGroupIds(options = {}) {
|
|
4754
|
+
if (options.peerGroupId) {
|
|
4755
|
+
return [options.peerGroupId];
|
|
4756
|
+
}
|
|
4757
|
+
const groups = [];
|
|
4758
|
+
for (const [groupId, config] of this.peerGroups.entries()) {
|
|
4759
|
+
if (config.domainEvents && config.autoPublishDomainEvents && config.role === "publisher") {
|
|
4760
|
+
groups.push(groupId);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
return groups;
|
|
4764
|
+
}
|
|
4765
|
+
async maybePublishDomainEvent(operation, options = {}) {
|
|
4766
|
+
const groupIds = this.resolvePublisherGroupIds(options);
|
|
4767
|
+
if (groupIds.length === 0) {
|
|
4768
|
+
return;
|
|
4769
|
+
}
|
|
4770
|
+
for (const groupId of groupIds) {
|
|
4771
|
+
await this.publishDomainEventForOperation(groupId, operation);
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
async publishDomainEventForOperation(groupId, operation) {
|
|
4775
|
+
const group = this.peerGroups.get(groupId);
|
|
4776
|
+
if (!group || !group.domainEvents) {
|
|
4777
|
+
return null;
|
|
4778
|
+
}
|
|
4779
|
+
if (group.role !== "publisher") {
|
|
4780
|
+
throw new Error(`Only publisher can emit domain events for PeerGroup ${groupId}`);
|
|
4781
|
+
}
|
|
4782
|
+
const prevHash = this.lastEventHashByGroup.get(groupId) || null;
|
|
4783
|
+
let event = operationToDomainEvent(operation, {
|
|
4784
|
+
publisherId: this.nodeId,
|
|
4785
|
+
groupId,
|
|
4786
|
+
prevHash,
|
|
4787
|
+
eventIdGenerator: () => this.idGenerator()
|
|
4788
|
+
});
|
|
4789
|
+
if (this.securityService.options.signingEnabled && this.securityService.signingSecretKey) {
|
|
4790
|
+
event = signDomainEvent(event, this.securityService.signingSecretKey);
|
|
4791
|
+
}
|
|
4792
|
+
const log = this.domainEventLogs.get(groupId) || [];
|
|
4793
|
+
log.push(event);
|
|
4794
|
+
this.domainEventLogs.set(groupId, log);
|
|
4795
|
+
this.lastEventHashByGroup.set(groupId, event.eventHash);
|
|
4796
|
+
this.emit("domainevent", event);
|
|
4797
|
+
if (group.autoPublishDomainEvents) {
|
|
4798
|
+
await this.publishToPeerGroup(groupId, "domain:event", event, { tier: "live" });
|
|
4799
|
+
if (group.tiered) {
|
|
4800
|
+
await this.publishPeerGroupBulk(groupId, "domain:event", event);
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
return event;
|
|
4804
|
+
}
|
|
4805
|
+
refreshBulkRelays(groupId) {
|
|
4806
|
+
const group = this.peerGroups.get(groupId);
|
|
4807
|
+
if (!group || !group.tiered) {
|
|
4808
|
+
return [];
|
|
4809
|
+
}
|
|
4810
|
+
const peers = this.listPeerGroupMembers(groupId, { includeSelf: false });
|
|
4811
|
+
const relays = electBulkRelays(peers);
|
|
4812
|
+
const previous = this.bulkRelayByGroup.get(groupId) || [];
|
|
4813
|
+
this.bulkRelayByGroup.set(groupId, relays);
|
|
4814
|
+
const changed = previous.length !== relays.length || previous.some((id, index) => id !== relays[index]);
|
|
4815
|
+
if (changed) {
|
|
4816
|
+
this.emit("bulkrelaychanged", { groupId, relays, previous });
|
|
4817
|
+
}
|
|
4818
|
+
return relays;
|
|
4819
|
+
}
|
|
4820
|
+
ingestRemoteDomainEvent(event, context = {}) {
|
|
4821
|
+
const groupId = event.groupId || context.groupId;
|
|
4822
|
+
if (!groupId) {
|
|
4823
|
+
return false;
|
|
4824
|
+
}
|
|
4825
|
+
const group = this.peerGroups.get(groupId);
|
|
4826
|
+
if (!group) {
|
|
4827
|
+
return false;
|
|
4828
|
+
}
|
|
4829
|
+
const publisherId = event.publisherId || context.publisherId;
|
|
4830
|
+
if (group.publisherId && publisherId !== group.publisherId) {
|
|
4831
|
+
this.emit("warning", {
|
|
4832
|
+
type: "domain-event-rejected",
|
|
4833
|
+
groupId,
|
|
4834
|
+
reason: "publisher-mismatch",
|
|
4835
|
+
eventId: event.eventId,
|
|
4836
|
+
expectedPublisher: group.publisherId,
|
|
4837
|
+
actualPublisher: publisherId
|
|
4838
|
+
});
|
|
4839
|
+
return false;
|
|
4840
|
+
}
|
|
4841
|
+
let signingPublicKey = null;
|
|
4842
|
+
if (this.securityService.options.signingEnabled && publisherId) {
|
|
4843
|
+
const peerKey = this.securityService.resolvePeerPublicKey(publisherId, null);
|
|
4844
|
+
signingPublicKey = peerKey ? peerKey.signingPublicKey : null;
|
|
4845
|
+
if (!signingPublicKey) {
|
|
4846
|
+
this.emit("warning", {
|
|
4847
|
+
type: "domain-event-rejected",
|
|
4848
|
+
groupId,
|
|
4849
|
+
reason: "missing-publisher-key",
|
|
4850
|
+
eventId: event.eventId,
|
|
4851
|
+
publisherId
|
|
4852
|
+
});
|
|
4853
|
+
return false;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
const verified = verifyDomainEvent(event, { signingPublicKey });
|
|
4857
|
+
if (!verified.ok) {
|
|
4858
|
+
this.emit("warning", {
|
|
4859
|
+
type: "domain-event-rejected",
|
|
4860
|
+
groupId,
|
|
4861
|
+
reason: verified.reason,
|
|
4862
|
+
eventId: event.eventId
|
|
4863
|
+
});
|
|
4864
|
+
return false;
|
|
4865
|
+
}
|
|
4866
|
+
if (this.securityService.options.signingEnabled && verified.unsigned) {
|
|
4867
|
+
this.emit("warning", {
|
|
4868
|
+
type: "domain-event-rejected",
|
|
4869
|
+
groupId,
|
|
4870
|
+
reason: "unsigned-event",
|
|
4871
|
+
eventId: event.eventId
|
|
4872
|
+
});
|
|
4873
|
+
return false;
|
|
4874
|
+
}
|
|
4875
|
+
const log = this.domainEventLogs.get(groupId) || [];
|
|
4876
|
+
if (log.some((entry) => entry.eventId === event.eventId)) {
|
|
4877
|
+
return false;
|
|
4878
|
+
}
|
|
4879
|
+
const expectedPrev = log.length > 0 ? log[log.length - 1].eventHash : null;
|
|
4880
|
+
if (event.prevHash !== expectedPrev) {
|
|
4881
|
+
this.emit("chainbroken", {
|
|
4882
|
+
groupId,
|
|
4883
|
+
expectedPrev,
|
|
4884
|
+
actualPrev: event.prevHash,
|
|
4885
|
+
eventId: event.eventId
|
|
4886
|
+
});
|
|
4887
|
+
return false;
|
|
4888
|
+
}
|
|
4889
|
+
log.push(event);
|
|
4890
|
+
this.domainEventLogs.set(groupId, log);
|
|
4891
|
+
this.lastEventHashByGroup.set(groupId, event.eventHash);
|
|
4892
|
+
if (!group.commandCapable) {
|
|
4893
|
+
const view = this.replicaViews.get(groupId) || createEmptyView();
|
|
4894
|
+
applyDomainEventToView(view, event);
|
|
4895
|
+
this.replicaViews.set(groupId, view);
|
|
4896
|
+
}
|
|
4897
|
+
this.emit("domainevent", event);
|
|
4898
|
+
return true;
|
|
4899
|
+
}
|
|
4263
4900
|
async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
|
|
4264
4901
|
const collection = this.getCollection(collectionName);
|
|
4265
4902
|
const raw = collection.get(id);
|
|
@@ -4299,15 +4936,26 @@ var require_dignity_p2p = __commonJS({
|
|
|
4299
4936
|
publisherId
|
|
4300
4937
|
});
|
|
4301
4938
|
const group = this.peerGroups.get(groupId);
|
|
4939
|
+
const deliveryTier = payload.deliveryTier || "live";
|
|
4940
|
+
if (group && group.tiered && group.peerGroupTier === "bulk" && deliveryTier !== "bulk") {
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4302
4943
|
const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
|
|
4303
4944
|
const maxHop = typeof payloadMaxHop === "number" ? Math.min(payloadMaxHop, configuredMaxHop) : configuredMaxHop;
|
|
4304
4945
|
if (!group || group.relayEnabled === false || hop >= maxHop) {
|
|
4305
4946
|
return;
|
|
4306
4947
|
}
|
|
4948
|
+
const relayOptions = {};
|
|
4949
|
+
if (group.tiered) {
|
|
4950
|
+
relayOptions.tier = deliveryTier === "bulk" ? "bulk" : "live";
|
|
4951
|
+
if (deliveryTier === "bulk") {
|
|
4952
|
+
relayOptions.bulkRelayOnly = true;
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4307
4955
|
const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
|
|
4308
4956
|
decrypted.senderId,
|
|
4309
4957
|
this.nodeId
|
|
4310
|
-
]);
|
|
4958
|
+
], relayOptions);
|
|
4311
4959
|
if (relayPeers.length === 0) {
|
|
4312
4960
|
return;
|
|
4313
4961
|
}
|
|
@@ -4319,6 +4967,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
4319
4967
|
publisherId,
|
|
4320
4968
|
hop: hop + 1,
|
|
4321
4969
|
maxHop,
|
|
4970
|
+
deliveryTier,
|
|
4322
4971
|
innerMessageType,
|
|
4323
4972
|
innerPayload
|
|
4324
4973
|
}, {
|
|
@@ -4361,6 +5010,19 @@ var require_dignity_p2p = __commonJS({
|
|
|
4361
5010
|
}
|
|
4362
5011
|
return;
|
|
4363
5012
|
}
|
|
5013
|
+
if (innerMessageType === "domain:event") {
|
|
5014
|
+
this.ingestRemoteDomainEvent(innerPayload, context);
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
if (innerMessageType === "domain:checkpoint") {
|
|
5018
|
+
this.emit("peergroupmessage", {
|
|
5019
|
+
groupId: context.groupId,
|
|
5020
|
+
senderId: context.senderId,
|
|
5021
|
+
type: "domain:checkpoint",
|
|
5022
|
+
payload: innerPayload
|
|
5023
|
+
});
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
4364
5026
|
if (innerMessageType === "record:snapshot") {
|
|
4365
5027
|
const { collectionName, record } = innerPayload || {};
|
|
4366
5028
|
if (collectionName && record) {
|
|
@@ -4406,6 +5068,13 @@ var require_dignity_p2p = __commonJS({
|
|
|
4406
5068
|
};
|
|
4407
5069
|
map.set(peerId, next);
|
|
4408
5070
|
this.trustPeerFromMetadata(peerId, next.metadata);
|
|
5071
|
+
const groupId = parsePeerGroupScope(scope);
|
|
5072
|
+
if (groupId && this.peerGroups.has(groupId)) {
|
|
5073
|
+
this.refreshBulkRelays(groupId);
|
|
5074
|
+
if (peerId !== this.nodeId) {
|
|
5075
|
+
this.recalculateOwnPeerGroupTier(groupId);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
4409
5078
|
if (!existing) {
|
|
4410
5079
|
this.emit("peerdiscovered", { scope, peerId, metadata: next.metadata });
|
|
4411
5080
|
}
|
|
@@ -12442,6 +13111,195 @@ var require_indexeddb_persistence = __commonJS({
|
|
|
12442
13111
|
}
|
|
12443
13112
|
});
|
|
12444
13113
|
|
|
13114
|
+
// src/cqrs/query-replica.js
|
|
13115
|
+
var require_query_replica = __commonJS({
|
|
13116
|
+
"src/cqrs/query-replica.js"(exports, module) {
|
|
13117
|
+
var EventEmitter = require_event_emitter();
|
|
13118
|
+
var {
|
|
13119
|
+
createEmptyView,
|
|
13120
|
+
applyDomainEventToView,
|
|
13121
|
+
verifyEventChain,
|
|
13122
|
+
verifyDomainEvent,
|
|
13123
|
+
DOMAIN_EVENT_SCHEMA_VERSION
|
|
13124
|
+
} = require_domain_events();
|
|
13125
|
+
var DignityQueryReplica = class extends EventEmitter {
|
|
13126
|
+
constructor(dignityP2P, { groupId, collections = [], tierMode = "auto", publisherId = null } = {}) {
|
|
13127
|
+
super();
|
|
13128
|
+
if (!dignityP2P) {
|
|
13129
|
+
throw new Error("DignityQueryReplica requires dignityP2P");
|
|
13130
|
+
}
|
|
13131
|
+
if (!groupId) {
|
|
13132
|
+
throw new Error("DignityQueryReplica requires groupId");
|
|
13133
|
+
}
|
|
13134
|
+
this.dignity = dignityP2P;
|
|
13135
|
+
this.groupId = groupId;
|
|
13136
|
+
this.collections = [...collections];
|
|
13137
|
+
this.tierMode = tierMode;
|
|
13138
|
+
this.publisherId = publisherId;
|
|
13139
|
+
this.view = createEmptyView(this.collections);
|
|
13140
|
+
this.eventLog = [];
|
|
13141
|
+
this.started = false;
|
|
13142
|
+
this.boundDomainHandler = this.handleDomainEvent.bind(this);
|
|
13143
|
+
this.boundPeerGroupHandler = this.handlePeerGroupMessage.bind(this);
|
|
13144
|
+
}
|
|
13145
|
+
async start(options = {}) {
|
|
13146
|
+
if (this.started) {
|
|
13147
|
+
return this;
|
|
13148
|
+
}
|
|
13149
|
+
await this.dignity.joinPeerGroup(this.groupId, {
|
|
13150
|
+
tierMode: this.tierMode,
|
|
13151
|
+
role: "subscriber",
|
|
13152
|
+
commandCapable: false,
|
|
13153
|
+
domainEvents: true,
|
|
13154
|
+
publisherId: this.publisherId,
|
|
13155
|
+
liveCap: options.liveCap,
|
|
13156
|
+
bulkIntervalMs: options.bulkIntervalMs,
|
|
13157
|
+
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
13158
|
+
metadata: { role: "subscriber", replica: true }
|
|
13159
|
+
});
|
|
13160
|
+
this.dignity.on("domainevent", this.boundDomainHandler);
|
|
13161
|
+
this.dignity.on("peergroupmessage", this.boundPeerGroupHandler);
|
|
13162
|
+
this.started = true;
|
|
13163
|
+
this.emit("started", { groupId: this.groupId });
|
|
13164
|
+
return this;
|
|
13165
|
+
}
|
|
13166
|
+
async stop() {
|
|
13167
|
+
if (!this.started) {
|
|
13168
|
+
return;
|
|
13169
|
+
}
|
|
13170
|
+
this.dignity.off("domainevent", this.boundDomainHandler);
|
|
13171
|
+
this.dignity.off("peergroupmessage", this.boundPeerGroupHandler);
|
|
13172
|
+
await this.dignity.leavePeerGroup(this.groupId);
|
|
13173
|
+
this.started = false;
|
|
13174
|
+
this.emit("stopped", { groupId: this.groupId });
|
|
13175
|
+
}
|
|
13176
|
+
handleDomainEvent(event) {
|
|
13177
|
+
if (!event || event.groupId !== this.groupId) {
|
|
13178
|
+
return;
|
|
13179
|
+
}
|
|
13180
|
+
if (this.publisherId && event.publisherId !== this.publisherId) {
|
|
13181
|
+
return;
|
|
13182
|
+
}
|
|
13183
|
+
this.ingestEvent(event);
|
|
13184
|
+
}
|
|
13185
|
+
handlePeerGroupMessage(message) {
|
|
13186
|
+
if (!message || message.groupId !== this.groupId) {
|
|
13187
|
+
return;
|
|
13188
|
+
}
|
|
13189
|
+
if (message.type === "domain:checkpoint") {
|
|
13190
|
+
this.emit("checkpoint", message.payload);
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13193
|
+
ingestEvent(event, { skipChainCheck = false } = {}) {
|
|
13194
|
+
const verified = verifyDomainEvent(event, {
|
|
13195
|
+
supportedVersions: [DOMAIN_EVENT_SCHEMA_VERSION]
|
|
13196
|
+
});
|
|
13197
|
+
if (!verified.ok) {
|
|
13198
|
+
this.emit("warning", { type: "domain-event-rejected", reason: verified.reason, event });
|
|
13199
|
+
return false;
|
|
13200
|
+
}
|
|
13201
|
+
if (!skipChainCheck && this.eventLog.length > 0) {
|
|
13202
|
+
const lastHash = this.eventLog[this.eventLog.length - 1].eventHash;
|
|
13203
|
+
if (event.prevHash !== lastHash) {
|
|
13204
|
+
this.emit("chainbroken", {
|
|
13205
|
+
groupId: this.groupId,
|
|
13206
|
+
expectedPrev: lastHash,
|
|
13207
|
+
actualPrev: event.prevHash,
|
|
13208
|
+
eventId: event.eventId
|
|
13209
|
+
});
|
|
13210
|
+
return false;
|
|
13211
|
+
}
|
|
13212
|
+
} else if (!skipChainCheck && this.eventLog.length === 0 && event.prevHash) {
|
|
13213
|
+
this.emit("chainbroken", {
|
|
13214
|
+
groupId: this.groupId,
|
|
13215
|
+
expectedPrev: null,
|
|
13216
|
+
actualPrev: event.prevHash,
|
|
13217
|
+
eventId: event.eventId
|
|
13218
|
+
});
|
|
13219
|
+
return false;
|
|
13220
|
+
}
|
|
13221
|
+
const duplicate = this.eventLog.some((entry) => entry.eventId === event.eventId);
|
|
13222
|
+
if (duplicate) {
|
|
13223
|
+
return false;
|
|
13224
|
+
}
|
|
13225
|
+
const result = applyDomainEventToView(this.view, event, {
|
|
13226
|
+
collectionsFilter: this.collections.length > 0 ? this.collections : null
|
|
13227
|
+
});
|
|
13228
|
+
if (result.applied || result.reason === "collection-filtered") {
|
|
13229
|
+
this.eventLog.push({ ...event });
|
|
13230
|
+
this.emit("change", { event, result });
|
|
13231
|
+
return true;
|
|
13232
|
+
}
|
|
13233
|
+
this.emit("warning", { type: "domain-event-not-applied", reason: result.reason, event });
|
|
13234
|
+
return false;
|
|
13235
|
+
}
|
|
13236
|
+
read(collectionName, id) {
|
|
13237
|
+
const collection = this.view.get(collectionName);
|
|
13238
|
+
if (!collection) {
|
|
13239
|
+
return null;
|
|
13240
|
+
}
|
|
13241
|
+
const record = collection.get(id);
|
|
13242
|
+
if (!record || record.deletedAt) {
|
|
13243
|
+
return null;
|
|
13244
|
+
}
|
|
13245
|
+
return { ...record, data: { ...record.data } };
|
|
13246
|
+
}
|
|
13247
|
+
list(collectionName, options = {}) {
|
|
13248
|
+
const collection = this.view.get(collectionName);
|
|
13249
|
+
if (!collection) {
|
|
13250
|
+
return [];
|
|
13251
|
+
}
|
|
13252
|
+
const includeDeleted = options.includeDeleted || false;
|
|
13253
|
+
const records = [];
|
|
13254
|
+
for (const record of collection.values()) {
|
|
13255
|
+
if (record.deletedAt && !includeDeleted) {
|
|
13256
|
+
continue;
|
|
13257
|
+
}
|
|
13258
|
+
if (record.deletedAt && includeDeleted) {
|
|
13259
|
+
records.push({
|
|
13260
|
+
id: record.id,
|
|
13261
|
+
ownerId: record.ownerId,
|
|
13262
|
+
deletedAt: record.deletedAt,
|
|
13263
|
+
version: record.version
|
|
13264
|
+
});
|
|
13265
|
+
continue;
|
|
13266
|
+
}
|
|
13267
|
+
records.push({ ...record, data: { ...record.data } });
|
|
13268
|
+
}
|
|
13269
|
+
return records;
|
|
13270
|
+
}
|
|
13271
|
+
verifyChain() {
|
|
13272
|
+
const result = verifyEventChain(this.eventLog);
|
|
13273
|
+
if (!result.ok) {
|
|
13274
|
+
this.emit("chainbroken", { groupId: this.groupId, ...result });
|
|
13275
|
+
}
|
|
13276
|
+
return result;
|
|
13277
|
+
}
|
|
13278
|
+
getViewStats() {
|
|
13279
|
+
const stats = {
|
|
13280
|
+
groupId: this.groupId,
|
|
13281
|
+
eventCount: this.eventLog.length,
|
|
13282
|
+
collections: {}
|
|
13283
|
+
};
|
|
13284
|
+
for (const [name, collection] of this.view.entries()) {
|
|
13285
|
+
let active = 0;
|
|
13286
|
+
let deleted = 0;
|
|
13287
|
+
for (const record of collection.values()) {
|
|
13288
|
+
if (record.deletedAt) {
|
|
13289
|
+
deleted += 1;
|
|
13290
|
+
} else {
|
|
13291
|
+
active += 1;
|
|
13292
|
+
}
|
|
13293
|
+
}
|
|
13294
|
+
stats.collections[name] = { active, deleted };
|
|
13295
|
+
}
|
|
13296
|
+
return stats;
|
|
13297
|
+
}
|
|
13298
|
+
};
|
|
13299
|
+
module.exports = DignityQueryReplica;
|
|
13300
|
+
}
|
|
13301
|
+
});
|
|
13302
|
+
|
|
12445
13303
|
// src/index.js
|
|
12446
13304
|
var require_index = __commonJS({
|
|
12447
13305
|
"src/index.js"(exports, module) {
|
|
@@ -12488,6 +13346,24 @@ var require_index = __commonJS({
|
|
|
12488
13346
|
parsePeerGroupScope,
|
|
12489
13347
|
selectFanoutPeers
|
|
12490
13348
|
} = require_peer_group();
|
|
13349
|
+
var {
|
|
13350
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
13351
|
+
operationToDomainEvent,
|
|
13352
|
+
signDomainEvent,
|
|
13353
|
+
verifyDomainEvent,
|
|
13354
|
+
verifyEventChain,
|
|
13355
|
+
buildCheckpoint,
|
|
13356
|
+
createEmptyView,
|
|
13357
|
+
applyDomainEventToView
|
|
13358
|
+
} = require_domain_events();
|
|
13359
|
+
var {
|
|
13360
|
+
DEFAULT_LIVE_CAP,
|
|
13361
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
13362
|
+
assignPeerGroupTier,
|
|
13363
|
+
filterPeersByTier
|
|
13364
|
+
} = require_peer_group_tiers();
|
|
13365
|
+
var { electBulkRelays, DEFAULT_BULK_RELAY_COUNT } = require_bulk_relay();
|
|
13366
|
+
var DignityQueryReplica = require_query_replica();
|
|
12491
13367
|
module.exports = {
|
|
12492
13368
|
DignityP2P,
|
|
12493
13369
|
createDefaultSignalingPool,
|
|
@@ -12521,7 +13397,22 @@ var require_index = __commonJS({
|
|
|
12521
13397
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
12522
13398
|
peerGroupScope,
|
|
12523
13399
|
parsePeerGroupScope,
|
|
12524
|
-
selectFanoutPeers
|
|
13400
|
+
selectFanoutPeers,
|
|
13401
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
13402
|
+
operationToDomainEvent,
|
|
13403
|
+
signDomainEvent,
|
|
13404
|
+
verifyDomainEvent,
|
|
13405
|
+
verifyEventChain,
|
|
13406
|
+
buildCheckpoint,
|
|
13407
|
+
createEmptyView,
|
|
13408
|
+
applyDomainEventToView,
|
|
13409
|
+
DEFAULT_LIVE_CAP,
|
|
13410
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
13411
|
+
assignPeerGroupTier,
|
|
13412
|
+
filterPeersByTier,
|
|
13413
|
+
electBulkRelays,
|
|
13414
|
+
DEFAULT_BULK_RELAY_COUNT,
|
|
13415
|
+
DignityQueryReplica
|
|
12525
13416
|
};
|
|
12526
13417
|
}
|
|
12527
13418
|
});
|