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
package/dist/dignity.cjs.js
CHANGED
|
@@ -3429,7 +3429,7 @@ var require_peer_group = __commonJS({
|
|
|
3429
3429
|
var DEFAULT_PEER_GROUP_OPTIONS2 = {
|
|
3430
3430
|
fanout: 3,
|
|
3431
3431
|
maxActivePeers: 8,
|
|
3432
|
-
maxHops:
|
|
3432
|
+
maxHops: 64,
|
|
3433
3433
|
relayEnabled: true
|
|
3434
3434
|
};
|
|
3435
3435
|
function peerGroupScope2(groupId) {
|
|
@@ -3481,6 +3481,327 @@ var require_peer_group = __commonJS({
|
|
|
3481
3481
|
}
|
|
3482
3482
|
});
|
|
3483
3483
|
|
|
3484
|
+
// src/cqrs/domain-events.js
|
|
3485
|
+
var require_domain_events = __commonJS({
|
|
3486
|
+
"src/cqrs/domain-events.js"(exports2, module2) {
|
|
3487
|
+
var nacl = require_nacl_fast();
|
|
3488
|
+
var naclUtil = require_nacl_util();
|
|
3489
|
+
var { stableStringify } = require_message_security_service();
|
|
3490
|
+
var DOMAIN_EVENT_SCHEMA_VERSION2 = 1;
|
|
3491
|
+
var OPERATION_KIND_TO_EVENT_KIND = {
|
|
3492
|
+
create: "record:created",
|
|
3493
|
+
update: "record:updated",
|
|
3494
|
+
delete: "record:removed",
|
|
3495
|
+
"transfer-ownership": "ownership:transferred"
|
|
3496
|
+
};
|
|
3497
|
+
function computeContentHash(data) {
|
|
3498
|
+
const canonical = stableStringify(data || {});
|
|
3499
|
+
const bytes = naclUtil.decodeUTF8(canonical);
|
|
3500
|
+
const hash = nacl.hash(bytes);
|
|
3501
|
+
const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3502
|
+
return `sha512:${hex}`;
|
|
3503
|
+
}
|
|
3504
|
+
function canonicalEventBody(event) {
|
|
3505
|
+
return stableStringify({
|
|
3506
|
+
schemaVersion: event.schemaVersion,
|
|
3507
|
+
eventId: event.eventId,
|
|
3508
|
+
groupId: event.groupId,
|
|
3509
|
+
publisherId: event.publisherId,
|
|
3510
|
+
kind: event.kind,
|
|
3511
|
+
collectionName: event.collectionName,
|
|
3512
|
+
id: event.id,
|
|
3513
|
+
payload: event.payload,
|
|
3514
|
+
timestamp: event.timestamp,
|
|
3515
|
+
baseVersion: event.baseVersion,
|
|
3516
|
+
prevHash: event.prevHash || null,
|
|
3517
|
+
newOwnerId: event.newOwnerId || null
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
function computeEventHash(event) {
|
|
3521
|
+
const canonical = canonicalEventBody(event);
|
|
3522
|
+
const bytes = naclUtil.decodeUTF8(canonical);
|
|
3523
|
+
const hash = nacl.hash(bytes);
|
|
3524
|
+
const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3525
|
+
return `sha512:${hex}`;
|
|
3526
|
+
}
|
|
3527
|
+
function operationToDomainEvent2(operation, { publisherId, groupId, prevHash, eventIdGenerator }) {
|
|
3528
|
+
if (!operation || !publisherId || !groupId) {
|
|
3529
|
+
throw new Error("operationToDomainEvent requires operation, publisherId, and groupId");
|
|
3530
|
+
}
|
|
3531
|
+
const kind = OPERATION_KIND_TO_EVENT_KIND[operation.kind];
|
|
3532
|
+
if (!kind) {
|
|
3533
|
+
throw new Error(`Unsupported operation kind for domain event: ${operation.kind}`);
|
|
3534
|
+
}
|
|
3535
|
+
const event = {
|
|
3536
|
+
schemaVersion: DOMAIN_EVENT_SCHEMA_VERSION2,
|
|
3537
|
+
eventId: eventIdGenerator ? eventIdGenerator() : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
3538
|
+
groupId,
|
|
3539
|
+
publisherId,
|
|
3540
|
+
kind,
|
|
3541
|
+
collectionName: operation.collectionName,
|
|
3542
|
+
id: operation.id,
|
|
3543
|
+
payload: operation.payload || {},
|
|
3544
|
+
timestamp: operation.timestamp,
|
|
3545
|
+
baseVersion: operation.baseVersion || null,
|
|
3546
|
+
prevHash: prevHash || null,
|
|
3547
|
+
newOwnerId: operation.newOwnerId || null,
|
|
3548
|
+
eventHash: null,
|
|
3549
|
+
signature: null
|
|
3550
|
+
};
|
|
3551
|
+
event.eventHash = computeEventHash(event);
|
|
3552
|
+
return event;
|
|
3553
|
+
}
|
|
3554
|
+
function signDomainEvent2(event, signingSecretKey) {
|
|
3555
|
+
if (!signingSecretKey) {
|
|
3556
|
+
return { ...event };
|
|
3557
|
+
}
|
|
3558
|
+
const unsigned = { ...event, signature: null };
|
|
3559
|
+
const eventHash = computeEventHash(unsigned);
|
|
3560
|
+
const signature = nacl.sign.detached(
|
|
3561
|
+
naclUtil.decodeUTF8(eventHash),
|
|
3562
|
+
signingSecretKey
|
|
3563
|
+
);
|
|
3564
|
+
return {
|
|
3565
|
+
...unsigned,
|
|
3566
|
+
eventHash,
|
|
3567
|
+
signature: naclUtil.encodeBase64(signature)
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
function verifyDomainEventSignature(event, signingPublicKey) {
|
|
3571
|
+
if (!event || !event.eventHash) {
|
|
3572
|
+
return { ok: false, reason: "missing-event-hash" };
|
|
3573
|
+
}
|
|
3574
|
+
const recomputed = computeEventHash({ ...event, signature: null });
|
|
3575
|
+
if (recomputed !== event.eventHash) {
|
|
3576
|
+
return { ok: false, reason: "event-hash-mismatch" };
|
|
3577
|
+
}
|
|
3578
|
+
if (!event.signature) {
|
|
3579
|
+
return { ok: true, unsigned: true };
|
|
3580
|
+
}
|
|
3581
|
+
if (!signingPublicKey) {
|
|
3582
|
+
return { ok: false, reason: "missing-public-key" };
|
|
3583
|
+
}
|
|
3584
|
+
const keyBytes = typeof signingPublicKey === "string" ? naclUtil.decodeBase64(signingPublicKey) : signingPublicKey;
|
|
3585
|
+
const valid = nacl.sign.detached.verify(
|
|
3586
|
+
naclUtil.decodeUTF8(event.eventHash),
|
|
3587
|
+
naclUtil.decodeBase64(event.signature),
|
|
3588
|
+
keyBytes
|
|
3589
|
+
);
|
|
3590
|
+
return valid ? { ok: true } : { ok: false, reason: "invalid-signature" };
|
|
3591
|
+
}
|
|
3592
|
+
function verifyDomainEvent2(event, { signingPublicKey, supportedVersions } = {}) {
|
|
3593
|
+
if (!event || typeof event !== "object") {
|
|
3594
|
+
return { ok: false, reason: "invalid-event" };
|
|
3595
|
+
}
|
|
3596
|
+
const versions = supportedVersions || [DOMAIN_EVENT_SCHEMA_VERSION2];
|
|
3597
|
+
if (!versions.includes(event.schemaVersion)) {
|
|
3598
|
+
return { ok: false, reason: "unsupported-schema-version", schemaVersion: event.schemaVersion };
|
|
3599
|
+
}
|
|
3600
|
+
if (!event.eventId || !event.groupId || !event.publisherId || !event.kind) {
|
|
3601
|
+
return { ok: false, reason: "missing-required-fields" };
|
|
3602
|
+
}
|
|
3603
|
+
return verifyDomainEventSignature(event, signingPublicKey);
|
|
3604
|
+
}
|
|
3605
|
+
function createEmptyView2(collections = []) {
|
|
3606
|
+
const view = /* @__PURE__ */ new Map();
|
|
3607
|
+
for (const name of collections) {
|
|
3608
|
+
view.set(name, /* @__PURE__ */ new Map());
|
|
3609
|
+
}
|
|
3610
|
+
return view;
|
|
3611
|
+
}
|
|
3612
|
+
function ensureCollectionView(view, collectionName) {
|
|
3613
|
+
if (!view.has(collectionName)) {
|
|
3614
|
+
view.set(collectionName, /* @__PURE__ */ new Map());
|
|
3615
|
+
}
|
|
3616
|
+
return view.get(collectionName);
|
|
3617
|
+
}
|
|
3618
|
+
function applyDomainEventToView2(view, event, { collectionsFilter } = {}) {
|
|
3619
|
+
if (!event || !event.collectionName) {
|
|
3620
|
+
return { applied: false, reason: "invalid-event" };
|
|
3621
|
+
}
|
|
3622
|
+
if (Array.isArray(collectionsFilter) && collectionsFilter.length > 0 && !collectionsFilter.includes(event.collectionName)) {
|
|
3623
|
+
return { applied: false, reason: "collection-filtered" };
|
|
3624
|
+
}
|
|
3625
|
+
const collection = ensureCollectionView(view, event.collectionName);
|
|
3626
|
+
if (event.kind === "record:created") {
|
|
3627
|
+
if (collection.has(event.id)) {
|
|
3628
|
+
return { applied: false, reason: "already-exists" };
|
|
3629
|
+
}
|
|
3630
|
+
collection.set(event.id, {
|
|
3631
|
+
id: event.id,
|
|
3632
|
+
ownerId: event.publisherId,
|
|
3633
|
+
data: { ...event.payload || {} },
|
|
3634
|
+
hash: computeContentHash(event.payload || {}),
|
|
3635
|
+
createdAt: event.timestamp,
|
|
3636
|
+
updatedAt: event.timestamp,
|
|
3637
|
+
deletedAt: null,
|
|
3638
|
+
version: 1
|
|
3639
|
+
});
|
|
3640
|
+
return { applied: true, kind: event.kind };
|
|
3641
|
+
}
|
|
3642
|
+
const current = collection.get(event.id);
|
|
3643
|
+
if (!current || current.deletedAt) {
|
|
3644
|
+
if (event.kind === "record:removed") {
|
|
3645
|
+
return { applied: false, reason: "not-found" };
|
|
3646
|
+
}
|
|
3647
|
+
return { applied: false, reason: "missing-record" };
|
|
3648
|
+
}
|
|
3649
|
+
if (event.kind === "record:updated") {
|
|
3650
|
+
if (typeof event.baseVersion === "number" && current.version !== event.baseVersion) {
|
|
3651
|
+
return { applied: false, reason: "version-conflict", currentVersion: current.version };
|
|
3652
|
+
}
|
|
3653
|
+
current.data = { ...current.data, ...event.payload || {} };
|
|
3654
|
+
current.hash = computeContentHash(current.data);
|
|
3655
|
+
current.updatedAt = event.timestamp;
|
|
3656
|
+
current.version += 1;
|
|
3657
|
+
return { applied: true, kind: event.kind };
|
|
3658
|
+
}
|
|
3659
|
+
if (event.kind === "record:removed") {
|
|
3660
|
+
if (typeof event.baseVersion === "number" && current.version !== event.baseVersion) {
|
|
3661
|
+
return { applied: false, reason: "version-conflict", currentVersion: current.version };
|
|
3662
|
+
}
|
|
3663
|
+
current.deletedAt = event.timestamp;
|
|
3664
|
+
current.version += 1;
|
|
3665
|
+
return { applied: true, kind: event.kind };
|
|
3666
|
+
}
|
|
3667
|
+
if (event.kind === "ownership:transferred") {
|
|
3668
|
+
if (typeof event.baseVersion === "number" && current.version !== event.baseVersion) {
|
|
3669
|
+
return { applied: false, reason: "version-conflict", currentVersion: current.version };
|
|
3670
|
+
}
|
|
3671
|
+
current.ownerId = event.newOwnerId;
|
|
3672
|
+
current.updatedAt = event.timestamp;
|
|
3673
|
+
current.version += 1;
|
|
3674
|
+
return { applied: true, kind: event.kind };
|
|
3675
|
+
}
|
|
3676
|
+
return { applied: false, reason: "unknown-kind" };
|
|
3677
|
+
}
|
|
3678
|
+
function verifyEventChain2(events, { genesisHash = null } = {}) {
|
|
3679
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
3680
|
+
return { ok: true, length: 0 };
|
|
3681
|
+
}
|
|
3682
|
+
let expectedPrev = genesisHash;
|
|
3683
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
3684
|
+
const event = events[index];
|
|
3685
|
+
const prevHash = event.prevHash || null;
|
|
3686
|
+
if (prevHash !== expectedPrev) {
|
|
3687
|
+
return {
|
|
3688
|
+
ok: false,
|
|
3689
|
+
reason: "chain-break",
|
|
3690
|
+
index,
|
|
3691
|
+
expectedPrev,
|
|
3692
|
+
actualPrev: prevHash
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
const hashCheck = verifyDomainEventSignature(event, null);
|
|
3696
|
+
if (!hashCheck.ok) {
|
|
3697
|
+
return { ok: false, reason: hashCheck.reason, index };
|
|
3698
|
+
}
|
|
3699
|
+
expectedPrev = event.eventHash;
|
|
3700
|
+
}
|
|
3701
|
+
return { ok: true, length: events.length, lastHash: expectedPrev };
|
|
3702
|
+
}
|
|
3703
|
+
function buildCheckpoint2(groupId, events, { publisherId } = {}) {
|
|
3704
|
+
const chain = verifyEventChain2(events);
|
|
3705
|
+
return {
|
|
3706
|
+
schemaVersion: DOMAIN_EVENT_SCHEMA_VERSION2,
|
|
3707
|
+
groupId,
|
|
3708
|
+
publisherId: publisherId || null,
|
|
3709
|
+
lastEventHash: chain.lastHash || null,
|
|
3710
|
+
recordCount: events.length,
|
|
3711
|
+
timestamp: Date.now()
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
module2.exports = {
|
|
3715
|
+
DOMAIN_EVENT_SCHEMA_VERSION: DOMAIN_EVENT_SCHEMA_VERSION2,
|
|
3716
|
+
OPERATION_KIND_TO_EVENT_KIND,
|
|
3717
|
+
computeEventHash,
|
|
3718
|
+
operationToDomainEvent: operationToDomainEvent2,
|
|
3719
|
+
signDomainEvent: signDomainEvent2,
|
|
3720
|
+
verifyDomainEvent: verifyDomainEvent2,
|
|
3721
|
+
verifyDomainEventSignature,
|
|
3722
|
+
createEmptyView: createEmptyView2,
|
|
3723
|
+
applyDomainEventToView: applyDomainEventToView2,
|
|
3724
|
+
verifyEventChain: verifyEventChain2,
|
|
3725
|
+
buildCheckpoint: buildCheckpoint2
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
|
|
3730
|
+
// src/cqrs/peer-group-tiers.js
|
|
3731
|
+
var require_peer_group_tiers = __commonJS({
|
|
3732
|
+
"src/cqrs/peer-group-tiers.js"(exports2, module2) {
|
|
3733
|
+
var DEFAULT_LIVE_CAP2 = 5e3;
|
|
3734
|
+
var DEFAULT_BULK_INTERVAL_MS2 = 3e4;
|
|
3735
|
+
function assignPeerGroupTier2({ joinIndex, liveCap = DEFAULT_LIVE_CAP2, requestedTier, role }) {
|
|
3736
|
+
if (role === "publisher") {
|
|
3737
|
+
return "live";
|
|
3738
|
+
}
|
|
3739
|
+
if (requestedTier === "live" || requestedTier === "bulk") {
|
|
3740
|
+
if (requestedTier === "live" && joinIndex >= liveCap) {
|
|
3741
|
+
return "bulk";
|
|
3742
|
+
}
|
|
3743
|
+
return requestedTier;
|
|
3744
|
+
}
|
|
3745
|
+
return joinIndex < liveCap ? "live" : "bulk";
|
|
3746
|
+
}
|
|
3747
|
+
function getPeerTier(peer) {
|
|
3748
|
+
return peer?.metadata?.peerGroupTier || peer?.peerGroupTier || null;
|
|
3749
|
+
}
|
|
3750
|
+
function filterPeersByTier2(peers, tier) {
|
|
3751
|
+
if (!tier) {
|
|
3752
|
+
return peers;
|
|
3753
|
+
}
|
|
3754
|
+
return peers.filter((peer) => getPeerTier(peer) === tier);
|
|
3755
|
+
}
|
|
3756
|
+
function countLivePeers(peers) {
|
|
3757
|
+
return peers.filter((peer) => getPeerTier(peer) === "live").length;
|
|
3758
|
+
}
|
|
3759
|
+
function countBulkPeers(peers) {
|
|
3760
|
+
return peers.filter((peer) => getPeerTier(peer) === "bulk").length;
|
|
3761
|
+
}
|
|
3762
|
+
module2.exports = {
|
|
3763
|
+
DEFAULT_LIVE_CAP: DEFAULT_LIVE_CAP2,
|
|
3764
|
+
DEFAULT_BULK_INTERVAL_MS: DEFAULT_BULK_INTERVAL_MS2,
|
|
3765
|
+
assignPeerGroupTier: assignPeerGroupTier2,
|
|
3766
|
+
getPeerTier,
|
|
3767
|
+
filterPeersByTier: filterPeersByTier2,
|
|
3768
|
+
countLivePeers,
|
|
3769
|
+
countBulkPeers
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
});
|
|
3773
|
+
|
|
3774
|
+
// src/cqrs/bulk-relay.js
|
|
3775
|
+
var require_bulk_relay = __commonJS({
|
|
3776
|
+
"src/cqrs/bulk-relay.js"(exports2, module2) {
|
|
3777
|
+
var { getPeerTier } = require_peer_group_tiers();
|
|
3778
|
+
var DEFAULT_BULK_RELAY_COUNT2 = 3;
|
|
3779
|
+
function electBulkRelays2(peers, { count = DEFAULT_BULK_RELAY_COUNT2 } = {}) {
|
|
3780
|
+
const bulkPeers = peers.filter((peer) => getPeerTier(peer) === "bulk").map((peer) => peer.peerId || peer).filter(Boolean).sort();
|
|
3781
|
+
return bulkPeers.slice(0, Math.max(0, count));
|
|
3782
|
+
}
|
|
3783
|
+
function isBulkRelay(metadata) {
|
|
3784
|
+
return metadata?.bulkRelay === true;
|
|
3785
|
+
}
|
|
3786
|
+
function applyBulkRelayFlags(peers, relayPeerIds) {
|
|
3787
|
+
const relaySet = new Set(relayPeerIds);
|
|
3788
|
+
return peers.map((peer) => ({
|
|
3789
|
+
...peer,
|
|
3790
|
+
metadata: {
|
|
3791
|
+
...peer.metadata || {},
|
|
3792
|
+
bulkRelay: relaySet.has(peer.peerId)
|
|
3793
|
+
}
|
|
3794
|
+
}));
|
|
3795
|
+
}
|
|
3796
|
+
module2.exports = {
|
|
3797
|
+
DEFAULT_BULK_RELAY_COUNT: DEFAULT_BULK_RELAY_COUNT2,
|
|
3798
|
+
electBulkRelays: electBulkRelays2,
|
|
3799
|
+
isBulkRelay,
|
|
3800
|
+
applyBulkRelayFlags
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
});
|
|
3804
|
+
|
|
3484
3805
|
// src/core/dignity-p2p.js
|
|
3485
3806
|
var require_dignity_p2p = __commonJS({
|
|
3486
3807
|
"src/core/dignity-p2p.js"(exports2, module2) {
|
|
@@ -3501,8 +3822,24 @@ var require_dignity_p2p = __commonJS({
|
|
|
3501
3822
|
var {
|
|
3502
3823
|
DEFAULT_PEER_GROUP_OPTIONS: DEFAULT_PEER_GROUP_OPTIONS2,
|
|
3503
3824
|
peerGroupScope: peerGroupScope2,
|
|
3825
|
+
parsePeerGroupScope: parsePeerGroupScope2,
|
|
3504
3826
|
selectFanoutPeers: selectFanoutPeers2
|
|
3505
3827
|
} = require_peer_group();
|
|
3828
|
+
var {
|
|
3829
|
+
operationToDomainEvent: operationToDomainEvent2,
|
|
3830
|
+
signDomainEvent: signDomainEvent2,
|
|
3831
|
+
verifyDomainEvent: verifyDomainEvent2,
|
|
3832
|
+
applyDomainEventToView: applyDomainEventToView2,
|
|
3833
|
+
createEmptyView: createEmptyView2,
|
|
3834
|
+
buildCheckpoint: buildCheckpoint2
|
|
3835
|
+
} = require_domain_events();
|
|
3836
|
+
var {
|
|
3837
|
+
DEFAULT_LIVE_CAP: DEFAULT_LIVE_CAP2,
|
|
3838
|
+
DEFAULT_BULK_INTERVAL_MS: DEFAULT_BULK_INTERVAL_MS2,
|
|
3839
|
+
assignPeerGroupTier: assignPeerGroupTier2,
|
|
3840
|
+
filterPeersByTier: filterPeersByTier2
|
|
3841
|
+
} = require_peer_group_tiers();
|
|
3842
|
+
var { electBulkRelays: electBulkRelays2 } = require_bulk_relay();
|
|
3506
3843
|
function computeContentHash(data) {
|
|
3507
3844
|
const canonical = stableStringify(data || {});
|
|
3508
3845
|
const bytes = naclUtil.decodeUTF8(canonical);
|
|
@@ -3546,6 +3883,10 @@ var require_dignity_p2p = __commonJS({
|
|
|
3546
3883
|
this.gossipPublishMinIntervalMs = security && typeof security.gossipPublishMinIntervalMs === "number" ? security.gossipPublishMinIntervalMs : 0;
|
|
3547
3884
|
this.lastGossipPublishAt = /* @__PURE__ */ new Map();
|
|
3548
3885
|
this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
|
|
3886
|
+
this.domainEventLogs = /* @__PURE__ */ new Map();
|
|
3887
|
+
this.lastEventHashByGroup = /* @__PURE__ */ new Map();
|
|
3888
|
+
this.bulkRelayByGroup = /* @__PURE__ */ new Map();
|
|
3889
|
+
this.replicaViews = /* @__PURE__ */ new Map();
|
|
3549
3890
|
this.state = /* @__PURE__ */ new Map();
|
|
3550
3891
|
this.appliedOperations = /* @__PURE__ */ new Map();
|
|
3551
3892
|
this.boundMessageHandler = this.handleIncomingMessage.bind(this);
|
|
@@ -3684,6 +4025,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3684
4025
|
payload: { ...data || {} }
|
|
3685
4026
|
};
|
|
3686
4027
|
this.applyOperation(operation);
|
|
4028
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3687
4029
|
await this.broadcastMessage("operation", operation, {
|
|
3688
4030
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3689
4031
|
messageType: "operation",
|
|
@@ -3761,6 +4103,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3761
4103
|
operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
|
|
3762
4104
|
}
|
|
3763
4105
|
this.applyOperation(operation);
|
|
4106
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3764
4107
|
await this.broadcastMessage("operation", operation, {
|
|
3765
4108
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3766
4109
|
messageType: "operation",
|
|
@@ -3815,6 +4158,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3815
4158
|
keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
|
|
3816
4159
|
};
|
|
3817
4160
|
this.applyOperation(operation);
|
|
4161
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3818
4162
|
await this.broadcastMessage("operation", operation, {
|
|
3819
4163
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3820
4164
|
messageType: "operation",
|
|
@@ -3846,6 +4190,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3846
4190
|
baseVersion: existing.version
|
|
3847
4191
|
};
|
|
3848
4192
|
this.applyOperation(operation);
|
|
4193
|
+
await this.maybePublishDomainEvent(operation, options);
|
|
3849
4194
|
await this.broadcastMessage("operation", operation, {
|
|
3850
4195
|
broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
|
|
3851
4196
|
messageType: "operation",
|
|
@@ -4136,9 +4481,16 @@ var require_dignity_p2p = __commonJS({
|
|
|
4136
4481
|
}
|
|
4137
4482
|
return [];
|
|
4138
4483
|
}
|
|
4139
|
-
selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
|
|
4484
|
+
selectPeerGroupFanout(groupId, count, excludePeerIds = [], fanoutOptions = {}) {
|
|
4140
4485
|
const scope = this.peerGroupScopeFor(groupId);
|
|
4141
|
-
const
|
|
4486
|
+
const group = this.peerGroups.get(groupId);
|
|
4487
|
+
let peers = this.listPeers(scope, { includeSelf: false });
|
|
4488
|
+
if (group && group.tiered && fanoutOptions.tier) {
|
|
4489
|
+
peers = filterPeersByTier2(peers, fanoutOptions.tier);
|
|
4490
|
+
}
|
|
4491
|
+
if (fanoutOptions.bulkRelayOnly) {
|
|
4492
|
+
peers = peers.filter((peer) => peer.metadata?.bulkRelay === true);
|
|
4493
|
+
}
|
|
4142
4494
|
return selectFanoutPeers2({
|
|
4143
4495
|
peers,
|
|
4144
4496
|
count,
|
|
@@ -4170,15 +4522,45 @@ var require_dignity_p2p = __commonJS({
|
|
|
4170
4522
|
throw new Error("joinPeerGroup requires groupId");
|
|
4171
4523
|
}
|
|
4172
4524
|
const scope = this.peerGroupScopeFor(groupId);
|
|
4525
|
+
const role = options.role || options.metadata?.role || "subscriber";
|
|
4526
|
+
const tierMode = options.tierMode || "auto";
|
|
4527
|
+
const tiered = options.tiered === true || options.tierMode !== void 0 || options.role !== void 0 || typeof options.liveCap === "number";
|
|
4528
|
+
const liveCap = typeof options.liveCap === "number" ? options.liveCap : DEFAULT_LIVE_CAP2;
|
|
4529
|
+
const existingMembers = this.listPeerGroupMembers(groupId, { includeSelf: false });
|
|
4530
|
+
const existingSubscriberCount = existingMembers.filter((member) => {
|
|
4531
|
+
const memberRole = member.metadata?.peerGroupRole || member.metadata?.role;
|
|
4532
|
+
return memberRole !== "publisher";
|
|
4533
|
+
}).length;
|
|
4534
|
+
let assignedTier = tiered ? assignPeerGroupTier2({
|
|
4535
|
+
joinIndex: existingSubscriberCount,
|
|
4536
|
+
liveCap,
|
|
4537
|
+
requestedTier: tierMode === "auto" ? null : tierMode,
|
|
4538
|
+
role
|
|
4539
|
+
}) : null;
|
|
4540
|
+
const publisherId = options.publisherId || (role === "publisher" ? this.nodeId : null);
|
|
4173
4541
|
const config = {
|
|
4174
4542
|
fanout: typeof options.fanout === "number" ? options.fanout : this.defaultPeerGroupFanout,
|
|
4175
4543
|
maxActivePeers: typeof options.maxActivePeers === "number" ? options.maxActivePeers : this.defaultPeerGroupMaxActivePeers,
|
|
4176
4544
|
maxHops: typeof options.maxHops === "number" ? options.maxHops : this.defaultGossipMaxHops,
|
|
4177
|
-
relayEnabled: options.relayEnabled !== false
|
|
4545
|
+
relayEnabled: options.relayEnabled !== false,
|
|
4546
|
+
tiered,
|
|
4547
|
+
tierMode,
|
|
4548
|
+
liveCap,
|
|
4549
|
+
bulkIntervalMs: typeof options.bulkIntervalMs === "number" ? options.bulkIntervalMs : DEFAULT_BULK_INTERVAL_MS2,
|
|
4550
|
+
domainEvents: options.domainEvents !== false,
|
|
4551
|
+
autoPublishDomainEvents: options.autoPublishDomainEvents !== false,
|
|
4552
|
+
role,
|
|
4553
|
+
publisherId,
|
|
4554
|
+
commandCapable: options.commandCapable !== false,
|
|
4555
|
+
peerGroupTier: assignedTier
|
|
4178
4556
|
};
|
|
4179
4557
|
await this.joinDiscovery(scope, {
|
|
4180
4558
|
metadata: {
|
|
4181
4559
|
peerGroup: groupId,
|
|
4560
|
+
...assignedTier ? { peerGroupTier: assignedTier } : {},
|
|
4561
|
+
peerGroupRole: role,
|
|
4562
|
+
...publisherId ? { publisherId } : {},
|
|
4563
|
+
bulkRelay: false,
|
|
4182
4564
|
...options.metadata || {}
|
|
4183
4565
|
},
|
|
4184
4566
|
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
@@ -4186,9 +4568,61 @@ var require_dignity_p2p = __commonJS({
|
|
|
4186
4568
|
ttlMs: options.ttlMs
|
|
4187
4569
|
});
|
|
4188
4570
|
this.peerGroups.set(groupId, config);
|
|
4189
|
-
this.
|
|
4571
|
+
if (!this.domainEventLogs.has(groupId)) {
|
|
4572
|
+
this.domainEventLogs.set(groupId, []);
|
|
4573
|
+
}
|
|
4574
|
+
if (!this.replicaViews.has(groupId)) {
|
|
4575
|
+
this.replicaViews.set(groupId, createEmptyView2());
|
|
4576
|
+
}
|
|
4577
|
+
this.refreshBulkRelays(groupId);
|
|
4578
|
+
if (tiered && tierMode === "auto" && role === "subscriber") {
|
|
4579
|
+
assignedTier = this.recalculateOwnPeerGroupTier(groupId) || assignedTier;
|
|
4580
|
+
config.peerGroupTier = assignedTier;
|
|
4581
|
+
}
|
|
4582
|
+
this.emit("peergroupjoined", { groupId, config, tier: assignedTier });
|
|
4190
4583
|
return config;
|
|
4191
4584
|
}
|
|
4585
|
+
recalculateOwnPeerGroupTier(groupId) {
|
|
4586
|
+
const group = this.peerGroups.get(groupId);
|
|
4587
|
+
if (!group || !group.tiered || group.role !== "subscriber") {
|
|
4588
|
+
return group ? group.peerGroupTier : null;
|
|
4589
|
+
}
|
|
4590
|
+
if (group.tierMode !== "auto") {
|
|
4591
|
+
return group.peerGroupTier;
|
|
4592
|
+
}
|
|
4593
|
+
const scope = this.peerGroupScopeFor(groupId);
|
|
4594
|
+
const members = this.listPeerGroupMembers(groupId, { includeSelf: true });
|
|
4595
|
+
const subscribers = members.filter((member) => {
|
|
4596
|
+
const memberRole = member.metadata?.peerGroupRole || member.metadata?.role;
|
|
4597
|
+
return memberRole !== "publisher";
|
|
4598
|
+
}).map((member) => member.peerId).sort();
|
|
4599
|
+
const joinIndex = subscribers.indexOf(this.nodeId);
|
|
4600
|
+
if (joinIndex < 0) {
|
|
4601
|
+
return group.peerGroupTier;
|
|
4602
|
+
}
|
|
4603
|
+
const newTier = assignPeerGroupTier2({
|
|
4604
|
+
joinIndex,
|
|
4605
|
+
liveCap: group.liveCap,
|
|
4606
|
+
requestedTier: null,
|
|
4607
|
+
role: "subscriber"
|
|
4608
|
+
});
|
|
4609
|
+
if (newTier === group.peerGroupTier) {
|
|
4610
|
+
return newTier;
|
|
4611
|
+
}
|
|
4612
|
+
group.peerGroupTier = newTier;
|
|
4613
|
+
const room = this.discoveryRooms.get(scope);
|
|
4614
|
+
if (room) {
|
|
4615
|
+
room.metadata = {
|
|
4616
|
+
...room.metadata || {},
|
|
4617
|
+
peerGroupTier: newTier
|
|
4618
|
+
};
|
|
4619
|
+
this.upsertPresence(scope, this.nodeId, room.metadata, room.ttlMs, this.now());
|
|
4620
|
+
this.announcePresence(scope).catch((error) => {
|
|
4621
|
+
this.emit("warning", { type: "tier-announce-failed", groupId, error });
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
return newTier;
|
|
4625
|
+
}
|
|
4192
4626
|
async leavePeerGroup(groupId) {
|
|
4193
4627
|
if (!groupId) {
|
|
4194
4628
|
return;
|
|
@@ -4196,6 +4630,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
4196
4630
|
const scope = this.peerGroupScopeFor(groupId);
|
|
4197
4631
|
await this.leaveDiscovery(scope);
|
|
4198
4632
|
this.peerGroups.delete(groupId);
|
|
4633
|
+
this.bulkRelayByGroup.delete(groupId);
|
|
4199
4634
|
this.emit("peergroupleft", { groupId });
|
|
4200
4635
|
}
|
|
4201
4636
|
async publishToPeerGroup(groupId, innerMessageType, innerPayload, options = {}) {
|
|
@@ -4218,7 +4653,11 @@ var require_dignity_p2p = __commonJS({
|
|
|
4218
4653
|
const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
|
|
4219
4654
|
const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
|
|
4220
4655
|
const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
|
|
4221
|
-
const
|
|
4656
|
+
const fanoutOptions = {};
|
|
4657
|
+
if (group && group.tiered && options.tier !== "bulk") {
|
|
4658
|
+
fanoutOptions.tier = options.tier || "live";
|
|
4659
|
+
}
|
|
4660
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId], fanoutOptions);
|
|
4222
4661
|
if (fanoutPeerIds.length > 0) {
|
|
4223
4662
|
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
4224
4663
|
await this.enforceConnectionBudget();
|
|
@@ -4240,6 +4679,204 @@ var require_dignity_p2p = __commonJS({
|
|
|
4240
4679
|
});
|
|
4241
4680
|
return { gossipId, fanoutPeerIds };
|
|
4242
4681
|
}
|
|
4682
|
+
async publishPeerGroupBulk(groupId, innerMessageType, innerPayload, options = {}) {
|
|
4683
|
+
const group = this.peerGroups.get(groupId);
|
|
4684
|
+
if (!group && options.allowUnjoined !== true) {
|
|
4685
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
4686
|
+
}
|
|
4687
|
+
if (group && group.role !== "publisher") {
|
|
4688
|
+
throw new Error(`Only publisher can bulk-publish to PeerGroup ${groupId}`);
|
|
4689
|
+
}
|
|
4690
|
+
const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
|
|
4691
|
+
const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
|
|
4692
|
+
const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
|
|
4693
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(
|
|
4694
|
+
groupId,
|
|
4695
|
+
fanout,
|
|
4696
|
+
[this.nodeId],
|
|
4697
|
+
{ tier: "bulk", bulkRelayOnly: group?.tiered === true }
|
|
4698
|
+
);
|
|
4699
|
+
if (fanoutPeerIds.length > 0) {
|
|
4700
|
+
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
4701
|
+
await this.enforceConnectionBudget();
|
|
4702
|
+
}
|
|
4703
|
+
const gossipId = options.gossipId || this.idGenerator();
|
|
4704
|
+
this.markSeenGossip(gossipId);
|
|
4705
|
+
await this.broadcastMessage("peer-group:gossip", {
|
|
4706
|
+
groupId,
|
|
4707
|
+
gossipId,
|
|
4708
|
+
publisherId: this.nodeId,
|
|
4709
|
+
hop: 0,
|
|
4710
|
+
maxHop,
|
|
4711
|
+
deliveryTier: "bulk",
|
|
4712
|
+
innerMessageType,
|
|
4713
|
+
innerPayload
|
|
4714
|
+
}, {
|
|
4715
|
+
broadcastScope: this.peerGroupScopeFor(groupId),
|
|
4716
|
+
fanoutPeerIds
|
|
4717
|
+
});
|
|
4718
|
+
return { gossipId, fanoutPeerIds };
|
|
4719
|
+
}
|
|
4720
|
+
async publishPeerGroupCheckpoint(groupId, options = {}) {
|
|
4721
|
+
const group = this.peerGroups.get(groupId);
|
|
4722
|
+
if (!group) {
|
|
4723
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
4724
|
+
}
|
|
4725
|
+
const events = this.domainEventLogs.get(groupId) || [];
|
|
4726
|
+
const checkpoint = buildCheckpoint2(groupId, events, {
|
|
4727
|
+
publisherId: options.publisherId || group.publisherId || this.nodeId
|
|
4728
|
+
});
|
|
4729
|
+
await this.publishPeerGroupBulk(groupId, "domain:checkpoint", checkpoint, options);
|
|
4730
|
+
this.emit("checkpointpublished", { groupId, checkpoint });
|
|
4731
|
+
return checkpoint;
|
|
4732
|
+
}
|
|
4733
|
+
resolvePublisherGroupIds(options = {}) {
|
|
4734
|
+
if (options.peerGroupId) {
|
|
4735
|
+
return [options.peerGroupId];
|
|
4736
|
+
}
|
|
4737
|
+
const groups = [];
|
|
4738
|
+
for (const [groupId, config] of this.peerGroups.entries()) {
|
|
4739
|
+
if (config.domainEvents && config.autoPublishDomainEvents && config.role === "publisher") {
|
|
4740
|
+
groups.push(groupId);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
return groups;
|
|
4744
|
+
}
|
|
4745
|
+
async maybePublishDomainEvent(operation, options = {}) {
|
|
4746
|
+
const groupIds = this.resolvePublisherGroupIds(options);
|
|
4747
|
+
if (groupIds.length === 0) {
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4750
|
+
for (const groupId of groupIds) {
|
|
4751
|
+
await this.publishDomainEventForOperation(groupId, operation);
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
async publishDomainEventForOperation(groupId, operation) {
|
|
4755
|
+
const group = this.peerGroups.get(groupId);
|
|
4756
|
+
if (!group || !group.domainEvents) {
|
|
4757
|
+
return null;
|
|
4758
|
+
}
|
|
4759
|
+
if (group.role !== "publisher") {
|
|
4760
|
+
throw new Error(`Only publisher can emit domain events for PeerGroup ${groupId}`);
|
|
4761
|
+
}
|
|
4762
|
+
const prevHash = this.lastEventHashByGroup.get(groupId) || null;
|
|
4763
|
+
let event = operationToDomainEvent2(operation, {
|
|
4764
|
+
publisherId: this.nodeId,
|
|
4765
|
+
groupId,
|
|
4766
|
+
prevHash,
|
|
4767
|
+
eventIdGenerator: () => this.idGenerator()
|
|
4768
|
+
});
|
|
4769
|
+
if (this.securityService.options.signingEnabled && this.securityService.signingSecretKey) {
|
|
4770
|
+
event = signDomainEvent2(event, this.securityService.signingSecretKey);
|
|
4771
|
+
}
|
|
4772
|
+
const log = this.domainEventLogs.get(groupId) || [];
|
|
4773
|
+
log.push(event);
|
|
4774
|
+
this.domainEventLogs.set(groupId, log);
|
|
4775
|
+
this.lastEventHashByGroup.set(groupId, event.eventHash);
|
|
4776
|
+
this.emit("domainevent", event);
|
|
4777
|
+
if (group.autoPublishDomainEvents) {
|
|
4778
|
+
await this.publishToPeerGroup(groupId, "domain:event", event, { tier: "live" });
|
|
4779
|
+
if (group.tiered) {
|
|
4780
|
+
await this.publishPeerGroupBulk(groupId, "domain:event", event);
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
return event;
|
|
4784
|
+
}
|
|
4785
|
+
refreshBulkRelays(groupId) {
|
|
4786
|
+
const group = this.peerGroups.get(groupId);
|
|
4787
|
+
if (!group || !group.tiered) {
|
|
4788
|
+
return [];
|
|
4789
|
+
}
|
|
4790
|
+
const peers = this.listPeerGroupMembers(groupId, { includeSelf: false });
|
|
4791
|
+
const relays = electBulkRelays2(peers);
|
|
4792
|
+
const previous = this.bulkRelayByGroup.get(groupId) || [];
|
|
4793
|
+
this.bulkRelayByGroup.set(groupId, relays);
|
|
4794
|
+
const changed = previous.length !== relays.length || previous.some((id, index) => id !== relays[index]);
|
|
4795
|
+
if (changed) {
|
|
4796
|
+
this.emit("bulkrelaychanged", { groupId, relays, previous });
|
|
4797
|
+
}
|
|
4798
|
+
return relays;
|
|
4799
|
+
}
|
|
4800
|
+
ingestRemoteDomainEvent(event, context = {}) {
|
|
4801
|
+
const groupId = event.groupId || context.groupId;
|
|
4802
|
+
if (!groupId) {
|
|
4803
|
+
return false;
|
|
4804
|
+
}
|
|
4805
|
+
const group = this.peerGroups.get(groupId);
|
|
4806
|
+
if (!group) {
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
const publisherId = event.publisherId || context.publisherId;
|
|
4810
|
+
if (group.publisherId && publisherId !== group.publisherId) {
|
|
4811
|
+
this.emit("warning", {
|
|
4812
|
+
type: "domain-event-rejected",
|
|
4813
|
+
groupId,
|
|
4814
|
+
reason: "publisher-mismatch",
|
|
4815
|
+
eventId: event.eventId,
|
|
4816
|
+
expectedPublisher: group.publisherId,
|
|
4817
|
+
actualPublisher: publisherId
|
|
4818
|
+
});
|
|
4819
|
+
return false;
|
|
4820
|
+
}
|
|
4821
|
+
let signingPublicKey = null;
|
|
4822
|
+
if (this.securityService.options.signingEnabled && publisherId) {
|
|
4823
|
+
const peerKey = this.securityService.resolvePeerPublicKey(publisherId, null);
|
|
4824
|
+
signingPublicKey = peerKey ? peerKey.signingPublicKey : null;
|
|
4825
|
+
if (!signingPublicKey) {
|
|
4826
|
+
this.emit("warning", {
|
|
4827
|
+
type: "domain-event-rejected",
|
|
4828
|
+
groupId,
|
|
4829
|
+
reason: "missing-publisher-key",
|
|
4830
|
+
eventId: event.eventId,
|
|
4831
|
+
publisherId
|
|
4832
|
+
});
|
|
4833
|
+
return false;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
const verified = verifyDomainEvent2(event, { signingPublicKey });
|
|
4837
|
+
if (!verified.ok) {
|
|
4838
|
+
this.emit("warning", {
|
|
4839
|
+
type: "domain-event-rejected",
|
|
4840
|
+
groupId,
|
|
4841
|
+
reason: verified.reason,
|
|
4842
|
+
eventId: event.eventId
|
|
4843
|
+
});
|
|
4844
|
+
return false;
|
|
4845
|
+
}
|
|
4846
|
+
if (this.securityService.options.signingEnabled && verified.unsigned) {
|
|
4847
|
+
this.emit("warning", {
|
|
4848
|
+
type: "domain-event-rejected",
|
|
4849
|
+
groupId,
|
|
4850
|
+
reason: "unsigned-event",
|
|
4851
|
+
eventId: event.eventId
|
|
4852
|
+
});
|
|
4853
|
+
return false;
|
|
4854
|
+
}
|
|
4855
|
+
const log = this.domainEventLogs.get(groupId) || [];
|
|
4856
|
+
if (log.some((entry) => entry.eventId === event.eventId)) {
|
|
4857
|
+
return false;
|
|
4858
|
+
}
|
|
4859
|
+
const expectedPrev = log.length > 0 ? log[log.length - 1].eventHash : null;
|
|
4860
|
+
if (event.prevHash !== expectedPrev) {
|
|
4861
|
+
this.emit("chainbroken", {
|
|
4862
|
+
groupId,
|
|
4863
|
+
expectedPrev,
|
|
4864
|
+
actualPrev: event.prevHash,
|
|
4865
|
+
eventId: event.eventId
|
|
4866
|
+
});
|
|
4867
|
+
return false;
|
|
4868
|
+
}
|
|
4869
|
+
log.push(event);
|
|
4870
|
+
this.domainEventLogs.set(groupId, log);
|
|
4871
|
+
this.lastEventHashByGroup.set(groupId, event.eventHash);
|
|
4872
|
+
if (!group.commandCapable) {
|
|
4873
|
+
const view = this.replicaViews.get(groupId) || createEmptyView2();
|
|
4874
|
+
applyDomainEventToView2(view, event);
|
|
4875
|
+
this.replicaViews.set(groupId, view);
|
|
4876
|
+
}
|
|
4877
|
+
this.emit("domainevent", event);
|
|
4878
|
+
return true;
|
|
4879
|
+
}
|
|
4243
4880
|
async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
|
|
4244
4881
|
const collection = this.getCollection(collectionName);
|
|
4245
4882
|
const raw = collection.get(id);
|
|
@@ -4279,15 +4916,26 @@ var require_dignity_p2p = __commonJS({
|
|
|
4279
4916
|
publisherId
|
|
4280
4917
|
});
|
|
4281
4918
|
const group = this.peerGroups.get(groupId);
|
|
4919
|
+
const deliveryTier = payload.deliveryTier || "live";
|
|
4920
|
+
if (group && group.tiered && group.peerGroupTier === "bulk" && deliveryTier !== "bulk") {
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4282
4923
|
const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
|
|
4283
4924
|
const maxHop = typeof payloadMaxHop === "number" ? Math.min(payloadMaxHop, configuredMaxHop) : configuredMaxHop;
|
|
4284
4925
|
if (!group || group.relayEnabled === false || hop >= maxHop) {
|
|
4285
4926
|
return;
|
|
4286
4927
|
}
|
|
4928
|
+
const relayOptions = {};
|
|
4929
|
+
if (group.tiered) {
|
|
4930
|
+
relayOptions.tier = deliveryTier === "bulk" ? "bulk" : "live";
|
|
4931
|
+
if (deliveryTier === "bulk") {
|
|
4932
|
+
relayOptions.bulkRelayOnly = true;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4287
4935
|
const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
|
|
4288
4936
|
decrypted.senderId,
|
|
4289
4937
|
this.nodeId
|
|
4290
|
-
]);
|
|
4938
|
+
], relayOptions);
|
|
4291
4939
|
if (relayPeers.length === 0) {
|
|
4292
4940
|
return;
|
|
4293
4941
|
}
|
|
@@ -4299,6 +4947,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
4299
4947
|
publisherId,
|
|
4300
4948
|
hop: hop + 1,
|
|
4301
4949
|
maxHop,
|
|
4950
|
+
deliveryTier,
|
|
4302
4951
|
innerMessageType,
|
|
4303
4952
|
innerPayload
|
|
4304
4953
|
}, {
|
|
@@ -4341,6 +4990,19 @@ var require_dignity_p2p = __commonJS({
|
|
|
4341
4990
|
}
|
|
4342
4991
|
return;
|
|
4343
4992
|
}
|
|
4993
|
+
if (innerMessageType === "domain:event") {
|
|
4994
|
+
this.ingestRemoteDomainEvent(innerPayload, context);
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
if (innerMessageType === "domain:checkpoint") {
|
|
4998
|
+
this.emit("peergroupmessage", {
|
|
4999
|
+
groupId: context.groupId,
|
|
5000
|
+
senderId: context.senderId,
|
|
5001
|
+
type: "domain:checkpoint",
|
|
5002
|
+
payload: innerPayload
|
|
5003
|
+
});
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
4344
5006
|
if (innerMessageType === "record:snapshot") {
|
|
4345
5007
|
const { collectionName, record } = innerPayload || {};
|
|
4346
5008
|
if (collectionName && record) {
|
|
@@ -4386,6 +5048,13 @@ var require_dignity_p2p = __commonJS({
|
|
|
4386
5048
|
};
|
|
4387
5049
|
map.set(peerId, next);
|
|
4388
5050
|
this.trustPeerFromMetadata(peerId, next.metadata);
|
|
5051
|
+
const groupId = parsePeerGroupScope2(scope);
|
|
5052
|
+
if (groupId && this.peerGroups.has(groupId)) {
|
|
5053
|
+
this.refreshBulkRelays(groupId);
|
|
5054
|
+
if (peerId !== this.nodeId) {
|
|
5055
|
+
this.recalculateOwnPeerGroupTier(groupId);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
4389
5058
|
if (!existing) {
|
|
4390
5059
|
this.emit("peerdiscovered", { scope, peerId, metadata: next.metadata });
|
|
4391
5060
|
}
|
|
@@ -12412,6 +13081,195 @@ var require_indexeddb_persistence = __commonJS({
|
|
|
12412
13081
|
}
|
|
12413
13082
|
});
|
|
12414
13083
|
|
|
13084
|
+
// src/cqrs/query-replica.js
|
|
13085
|
+
var require_query_replica = __commonJS({
|
|
13086
|
+
"src/cqrs/query-replica.js"(exports2, module2) {
|
|
13087
|
+
var EventEmitter = require_event_emitter();
|
|
13088
|
+
var {
|
|
13089
|
+
createEmptyView: createEmptyView2,
|
|
13090
|
+
applyDomainEventToView: applyDomainEventToView2,
|
|
13091
|
+
verifyEventChain: verifyEventChain2,
|
|
13092
|
+
verifyDomainEvent: verifyDomainEvent2,
|
|
13093
|
+
DOMAIN_EVENT_SCHEMA_VERSION: DOMAIN_EVENT_SCHEMA_VERSION2
|
|
13094
|
+
} = require_domain_events();
|
|
13095
|
+
var DignityQueryReplica2 = class extends EventEmitter {
|
|
13096
|
+
constructor(dignityP2P, { groupId, collections = [], tierMode = "auto", publisherId = null } = {}) {
|
|
13097
|
+
super();
|
|
13098
|
+
if (!dignityP2P) {
|
|
13099
|
+
throw new Error("DignityQueryReplica requires dignityP2P");
|
|
13100
|
+
}
|
|
13101
|
+
if (!groupId) {
|
|
13102
|
+
throw new Error("DignityQueryReplica requires groupId");
|
|
13103
|
+
}
|
|
13104
|
+
this.dignity = dignityP2P;
|
|
13105
|
+
this.groupId = groupId;
|
|
13106
|
+
this.collections = [...collections];
|
|
13107
|
+
this.tierMode = tierMode;
|
|
13108
|
+
this.publisherId = publisherId;
|
|
13109
|
+
this.view = createEmptyView2(this.collections);
|
|
13110
|
+
this.eventLog = [];
|
|
13111
|
+
this.started = false;
|
|
13112
|
+
this.boundDomainHandler = this.handleDomainEvent.bind(this);
|
|
13113
|
+
this.boundPeerGroupHandler = this.handlePeerGroupMessage.bind(this);
|
|
13114
|
+
}
|
|
13115
|
+
async start(options = {}) {
|
|
13116
|
+
if (this.started) {
|
|
13117
|
+
return this;
|
|
13118
|
+
}
|
|
13119
|
+
await this.dignity.joinPeerGroup(this.groupId, {
|
|
13120
|
+
tierMode: this.tierMode,
|
|
13121
|
+
role: "subscriber",
|
|
13122
|
+
commandCapable: false,
|
|
13123
|
+
domainEvents: true,
|
|
13124
|
+
publisherId: this.publisherId,
|
|
13125
|
+
liveCap: options.liveCap,
|
|
13126
|
+
bulkIntervalMs: options.bulkIntervalMs,
|
|
13127
|
+
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
13128
|
+
metadata: { role: "subscriber", replica: true }
|
|
13129
|
+
});
|
|
13130
|
+
this.dignity.on("domainevent", this.boundDomainHandler);
|
|
13131
|
+
this.dignity.on("peergroupmessage", this.boundPeerGroupHandler);
|
|
13132
|
+
this.started = true;
|
|
13133
|
+
this.emit("started", { groupId: this.groupId });
|
|
13134
|
+
return this;
|
|
13135
|
+
}
|
|
13136
|
+
async stop() {
|
|
13137
|
+
if (!this.started) {
|
|
13138
|
+
return;
|
|
13139
|
+
}
|
|
13140
|
+
this.dignity.off("domainevent", this.boundDomainHandler);
|
|
13141
|
+
this.dignity.off("peergroupmessage", this.boundPeerGroupHandler);
|
|
13142
|
+
await this.dignity.leavePeerGroup(this.groupId);
|
|
13143
|
+
this.started = false;
|
|
13144
|
+
this.emit("stopped", { groupId: this.groupId });
|
|
13145
|
+
}
|
|
13146
|
+
handleDomainEvent(event) {
|
|
13147
|
+
if (!event || event.groupId !== this.groupId) {
|
|
13148
|
+
return;
|
|
13149
|
+
}
|
|
13150
|
+
if (this.publisherId && event.publisherId !== this.publisherId) {
|
|
13151
|
+
return;
|
|
13152
|
+
}
|
|
13153
|
+
this.ingestEvent(event);
|
|
13154
|
+
}
|
|
13155
|
+
handlePeerGroupMessage(message) {
|
|
13156
|
+
if (!message || message.groupId !== this.groupId) {
|
|
13157
|
+
return;
|
|
13158
|
+
}
|
|
13159
|
+
if (message.type === "domain:checkpoint") {
|
|
13160
|
+
this.emit("checkpoint", message.payload);
|
|
13161
|
+
}
|
|
13162
|
+
}
|
|
13163
|
+
ingestEvent(event, { skipChainCheck = false } = {}) {
|
|
13164
|
+
const verified = verifyDomainEvent2(event, {
|
|
13165
|
+
supportedVersions: [DOMAIN_EVENT_SCHEMA_VERSION2]
|
|
13166
|
+
});
|
|
13167
|
+
if (!verified.ok) {
|
|
13168
|
+
this.emit("warning", { type: "domain-event-rejected", reason: verified.reason, event });
|
|
13169
|
+
return false;
|
|
13170
|
+
}
|
|
13171
|
+
if (!skipChainCheck && this.eventLog.length > 0) {
|
|
13172
|
+
const lastHash = this.eventLog[this.eventLog.length - 1].eventHash;
|
|
13173
|
+
if (event.prevHash !== lastHash) {
|
|
13174
|
+
this.emit("chainbroken", {
|
|
13175
|
+
groupId: this.groupId,
|
|
13176
|
+
expectedPrev: lastHash,
|
|
13177
|
+
actualPrev: event.prevHash,
|
|
13178
|
+
eventId: event.eventId
|
|
13179
|
+
});
|
|
13180
|
+
return false;
|
|
13181
|
+
}
|
|
13182
|
+
} else if (!skipChainCheck && this.eventLog.length === 0 && event.prevHash) {
|
|
13183
|
+
this.emit("chainbroken", {
|
|
13184
|
+
groupId: this.groupId,
|
|
13185
|
+
expectedPrev: null,
|
|
13186
|
+
actualPrev: event.prevHash,
|
|
13187
|
+
eventId: event.eventId
|
|
13188
|
+
});
|
|
13189
|
+
return false;
|
|
13190
|
+
}
|
|
13191
|
+
const duplicate = this.eventLog.some((entry) => entry.eventId === event.eventId);
|
|
13192
|
+
if (duplicate) {
|
|
13193
|
+
return false;
|
|
13194
|
+
}
|
|
13195
|
+
const result = applyDomainEventToView2(this.view, event, {
|
|
13196
|
+
collectionsFilter: this.collections.length > 0 ? this.collections : null
|
|
13197
|
+
});
|
|
13198
|
+
if (result.applied || result.reason === "collection-filtered") {
|
|
13199
|
+
this.eventLog.push({ ...event });
|
|
13200
|
+
this.emit("change", { event, result });
|
|
13201
|
+
return true;
|
|
13202
|
+
}
|
|
13203
|
+
this.emit("warning", { type: "domain-event-not-applied", reason: result.reason, event });
|
|
13204
|
+
return false;
|
|
13205
|
+
}
|
|
13206
|
+
read(collectionName, id) {
|
|
13207
|
+
const collection = this.view.get(collectionName);
|
|
13208
|
+
if (!collection) {
|
|
13209
|
+
return null;
|
|
13210
|
+
}
|
|
13211
|
+
const record = collection.get(id);
|
|
13212
|
+
if (!record || record.deletedAt) {
|
|
13213
|
+
return null;
|
|
13214
|
+
}
|
|
13215
|
+
return { ...record, data: { ...record.data } };
|
|
13216
|
+
}
|
|
13217
|
+
list(collectionName, options = {}) {
|
|
13218
|
+
const collection = this.view.get(collectionName);
|
|
13219
|
+
if (!collection) {
|
|
13220
|
+
return [];
|
|
13221
|
+
}
|
|
13222
|
+
const includeDeleted = options.includeDeleted || false;
|
|
13223
|
+
const records = [];
|
|
13224
|
+
for (const record of collection.values()) {
|
|
13225
|
+
if (record.deletedAt && !includeDeleted) {
|
|
13226
|
+
continue;
|
|
13227
|
+
}
|
|
13228
|
+
if (record.deletedAt && includeDeleted) {
|
|
13229
|
+
records.push({
|
|
13230
|
+
id: record.id,
|
|
13231
|
+
ownerId: record.ownerId,
|
|
13232
|
+
deletedAt: record.deletedAt,
|
|
13233
|
+
version: record.version
|
|
13234
|
+
});
|
|
13235
|
+
continue;
|
|
13236
|
+
}
|
|
13237
|
+
records.push({ ...record, data: { ...record.data } });
|
|
13238
|
+
}
|
|
13239
|
+
return records;
|
|
13240
|
+
}
|
|
13241
|
+
verifyChain() {
|
|
13242
|
+
const result = verifyEventChain2(this.eventLog);
|
|
13243
|
+
if (!result.ok) {
|
|
13244
|
+
this.emit("chainbroken", { groupId: this.groupId, ...result });
|
|
13245
|
+
}
|
|
13246
|
+
return result;
|
|
13247
|
+
}
|
|
13248
|
+
getViewStats() {
|
|
13249
|
+
const stats = {
|
|
13250
|
+
groupId: this.groupId,
|
|
13251
|
+
eventCount: this.eventLog.length,
|
|
13252
|
+
collections: {}
|
|
13253
|
+
};
|
|
13254
|
+
for (const [name, collection] of this.view.entries()) {
|
|
13255
|
+
let active = 0;
|
|
13256
|
+
let deleted = 0;
|
|
13257
|
+
for (const record of collection.values()) {
|
|
13258
|
+
if (record.deletedAt) {
|
|
13259
|
+
deleted += 1;
|
|
13260
|
+
} else {
|
|
13261
|
+
active += 1;
|
|
13262
|
+
}
|
|
13263
|
+
}
|
|
13264
|
+
stats.collections[name] = { active, deleted };
|
|
13265
|
+
}
|
|
13266
|
+
return stats;
|
|
13267
|
+
}
|
|
13268
|
+
};
|
|
13269
|
+
module2.exports = DignityQueryReplica2;
|
|
13270
|
+
}
|
|
13271
|
+
});
|
|
13272
|
+
|
|
12415
13273
|
// src/index.js
|
|
12416
13274
|
var DignityP2P = require_dignity_p2p();
|
|
12417
13275
|
var createDefaultSignalingPool = require_create_default_signaling_pool();
|
|
@@ -12456,6 +13314,24 @@ var {
|
|
|
12456
13314
|
parsePeerGroupScope,
|
|
12457
13315
|
selectFanoutPeers
|
|
12458
13316
|
} = require_peer_group();
|
|
13317
|
+
var {
|
|
13318
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
13319
|
+
operationToDomainEvent,
|
|
13320
|
+
signDomainEvent,
|
|
13321
|
+
verifyDomainEvent,
|
|
13322
|
+
verifyEventChain,
|
|
13323
|
+
buildCheckpoint,
|
|
13324
|
+
createEmptyView,
|
|
13325
|
+
applyDomainEventToView
|
|
13326
|
+
} = require_domain_events();
|
|
13327
|
+
var {
|
|
13328
|
+
DEFAULT_LIVE_CAP,
|
|
13329
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
13330
|
+
assignPeerGroupTier,
|
|
13331
|
+
filterPeersByTier
|
|
13332
|
+
} = require_peer_group_tiers();
|
|
13333
|
+
var { electBulkRelays, DEFAULT_BULK_RELAY_COUNT } = require_bulk_relay();
|
|
13334
|
+
var DignityQueryReplica = require_query_replica();
|
|
12459
13335
|
module.exports = {
|
|
12460
13336
|
DignityP2P,
|
|
12461
13337
|
createDefaultSignalingPool,
|
|
@@ -12489,6 +13365,21 @@ module.exports = {
|
|
|
12489
13365
|
DEFAULT_PEER_GROUP_OPTIONS,
|
|
12490
13366
|
peerGroupScope,
|
|
12491
13367
|
parsePeerGroupScope,
|
|
12492
|
-
selectFanoutPeers
|
|
13368
|
+
selectFanoutPeers,
|
|
13369
|
+
DOMAIN_EVENT_SCHEMA_VERSION,
|
|
13370
|
+
operationToDomainEvent,
|
|
13371
|
+
signDomainEvent,
|
|
13372
|
+
verifyDomainEvent,
|
|
13373
|
+
verifyEventChain,
|
|
13374
|
+
buildCheckpoint,
|
|
13375
|
+
createEmptyView,
|
|
13376
|
+
applyDomainEventToView,
|
|
13377
|
+
DEFAULT_LIVE_CAP,
|
|
13378
|
+
DEFAULT_BULK_INTERVAL_MS,
|
|
13379
|
+
assignPeerGroupTier,
|
|
13380
|
+
filterPeersByTier,
|
|
13381
|
+
electBulkRelays,
|
|
13382
|
+
DEFAULT_BULK_RELAY_COUNT,
|
|
13383
|
+
DignityQueryReplica
|
|
12493
13384
|
};
|
|
12494
13385
|
//# sourceMappingURL=dignity.cjs.js.map
|