@affectively/aeon 1.0.0 → 1.2.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 +10 -0
- package/dist/compression/index.cjs +580 -0
- package/dist/compression/index.cjs.map +1 -0
- package/dist/compression/index.d.cts +189 -0
- package/dist/compression/index.d.ts +189 -0
- package/dist/compression/index.js +573 -0
- package/dist/compression/index.js.map +1 -0
- package/dist/core/index.d.cts +70 -5
- package/dist/core/index.d.ts +70 -5
- package/dist/crypto/index.cjs +100 -0
- package/dist/crypto/index.cjs.map +1 -0
- package/dist/crypto/index.d.cts +407 -0
- package/dist/crypto/index.d.ts +407 -0
- package/dist/crypto/index.js +96 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/distributed/index.cjs +420 -23
- package/dist/distributed/index.cjs.map +1 -1
- package/dist/distributed/index.d.cts +901 -2
- package/dist/distributed/index.d.ts +901 -2
- package/dist/distributed/index.js +420 -23
- package/dist/distributed/index.js.map +1 -1
- package/dist/index.cjs +1222 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -811
- package/dist/index.d.ts +11 -811
- package/dist/index.js +1221 -56
- package/dist/index.js.map +1 -1
- package/dist/offline/index.cjs +419 -0
- package/dist/offline/index.cjs.map +1 -0
- package/dist/offline/index.d.cts +148 -0
- package/dist/offline/index.d.ts +148 -0
- package/dist/offline/index.js +415 -0
- package/dist/offline/index.js.map +1 -0
- package/dist/optimization/index.cjs +797 -0
- package/dist/optimization/index.cjs.map +1 -0
- package/dist/optimization/index.d.cts +347 -0
- package/dist/optimization/index.d.ts +347 -0
- package/dist/optimization/index.js +787 -0
- package/dist/optimization/index.js.map +1 -0
- package/dist/persistence/index.cjs +145 -0
- package/dist/persistence/index.cjs.map +1 -0
- package/dist/persistence/index.d.cts +63 -0
- package/dist/persistence/index.d.ts +63 -0
- package/dist/persistence/index.js +142 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/presence/index.cjs +489 -0
- package/dist/presence/index.cjs.map +1 -0
- package/dist/presence/index.d.cts +283 -0
- package/dist/presence/index.d.ts +283 -0
- package/dist/presence/index.js +485 -0
- package/dist/presence/index.js.map +1 -0
- package/dist/types-CMxO7QF0.d.cts +33 -0
- package/dist/types-CMxO7QF0.d.ts +33 -0
- package/dist/versioning/index.cjs +296 -14
- package/dist/versioning/index.cjs.map +1 -1
- package/dist/versioning/index.d.cts +66 -1
- package/dist/versioning/index.d.ts +66 -1
- package/dist/versioning/index.js +296 -14
- package/dist/versioning/index.js.map +1 -1
- package/package.json +51 -1
- package/dist/index-C_4CMV5c.d.cts +0 -1207
- package/dist/index-C_4CMV5c.d.ts +0 -1207
|
@@ -387,7 +387,9 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
|
|
|
387
387
|
* Get active sync sessions
|
|
388
388
|
*/
|
|
389
389
|
getActiveSyncSessions() {
|
|
390
|
-
return Array.from(this.sessions.values()).filter(
|
|
390
|
+
return Array.from(this.sessions.values()).filter(
|
|
391
|
+
(s) => s.status === "active"
|
|
392
|
+
);
|
|
391
393
|
}
|
|
392
394
|
/**
|
|
393
395
|
* Get sessions for a node
|
|
@@ -405,8 +407,14 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
|
|
|
405
407
|
const completed = sessions.filter((s) => s.status === "completed").length;
|
|
406
408
|
const failed = sessions.filter((s) => s.status === "failed").length;
|
|
407
409
|
const active = sessions.filter((s) => s.status === "active").length;
|
|
408
|
-
const totalItemsSynced = sessions.reduce(
|
|
409
|
-
|
|
410
|
+
const totalItemsSynced = sessions.reduce(
|
|
411
|
+
(sum, s) => sum + s.itemsSynced,
|
|
412
|
+
0
|
|
413
|
+
);
|
|
414
|
+
const totalConflicts = sessions.reduce(
|
|
415
|
+
(sum, s) => sum + s.conflictsDetected,
|
|
416
|
+
0
|
|
417
|
+
);
|
|
410
418
|
return {
|
|
411
419
|
totalNodes: this.nodes.size,
|
|
412
420
|
onlineNodes: this.getOnlineNodes().length,
|
|
@@ -473,7 +481,9 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
|
|
|
473
481
|
}
|
|
474
482
|
}
|
|
475
483
|
}, interval);
|
|
476
|
-
logger.debug("[SyncCoordinator] Heartbeat monitoring started", {
|
|
484
|
+
logger.debug("[SyncCoordinator] Heartbeat monitoring started", {
|
|
485
|
+
interval
|
|
486
|
+
});
|
|
477
487
|
}
|
|
478
488
|
/**
|
|
479
489
|
* Stop heartbeat monitoring
|
|
@@ -506,7 +516,8 @@ var SyncCoordinator = class extends eventemitter3.EventEmitter {
|
|
|
506
516
|
};
|
|
507
517
|
|
|
508
518
|
// src/distributed/ReplicationManager.ts
|
|
509
|
-
var ReplicationManager = class {
|
|
519
|
+
var ReplicationManager = class _ReplicationManager {
|
|
520
|
+
static DEFAULT_PERSIST_KEY = "aeon:replication-state:v1";
|
|
510
521
|
replicas = /* @__PURE__ */ new Map();
|
|
511
522
|
policies = /* @__PURE__ */ new Map();
|
|
512
523
|
replicationEvents = [];
|
|
@@ -515,6 +526,29 @@ var ReplicationManager = class {
|
|
|
515
526
|
cryptoProvider = null;
|
|
516
527
|
replicasByDID = /* @__PURE__ */ new Map();
|
|
517
528
|
// DID -> replicaId
|
|
529
|
+
persistence = null;
|
|
530
|
+
persistTimer = null;
|
|
531
|
+
persistInFlight = false;
|
|
532
|
+
persistPending = false;
|
|
533
|
+
constructor(options) {
|
|
534
|
+
if (options?.persistence) {
|
|
535
|
+
this.persistence = {
|
|
536
|
+
...options.persistence,
|
|
537
|
+
key: options.persistence.key ?? _ReplicationManager.DEFAULT_PERSIST_KEY,
|
|
538
|
+
autoPersist: options.persistence.autoPersist ?? true,
|
|
539
|
+
autoLoad: options.persistence.autoLoad ?? false,
|
|
540
|
+
persistDebounceMs: options.persistence.persistDebounceMs ?? 25
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (this.persistence?.autoLoad) {
|
|
544
|
+
void this.loadFromPersistence().catch((error) => {
|
|
545
|
+
logger.error("[ReplicationManager] Failed to load persistence", {
|
|
546
|
+
key: this.persistence?.key,
|
|
547
|
+
error: error instanceof Error ? error.message : String(error)
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
518
552
|
/**
|
|
519
553
|
* Configure cryptographic provider for encrypted replication
|
|
520
554
|
*/
|
|
@@ -559,6 +593,7 @@ var ReplicationManager = class {
|
|
|
559
593
|
details: { did: replica.did, encrypted, authenticated: true }
|
|
560
594
|
};
|
|
561
595
|
this.replicationEvents.push(event);
|
|
596
|
+
this.schedulePersist();
|
|
562
597
|
logger.debug("[ReplicationManager] Authenticated replica registered", {
|
|
563
598
|
replicaId: replica.id,
|
|
564
599
|
did: replica.did,
|
|
@@ -588,7 +623,10 @@ var ReplicationManager = class {
|
|
|
588
623
|
throw new Error("Crypto provider not initialized");
|
|
589
624
|
}
|
|
590
625
|
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
591
|
-
const encrypted = await this.cryptoProvider.encrypt(
|
|
626
|
+
const encrypted = await this.cryptoProvider.encrypt(
|
|
627
|
+
dataBytes,
|
|
628
|
+
targetReplicaDID
|
|
629
|
+
);
|
|
592
630
|
const localDID = this.cryptoProvider.getLocalDID();
|
|
593
631
|
return {
|
|
594
632
|
ct: encrypted.ct,
|
|
@@ -657,10 +695,13 @@ var ReplicationManager = class {
|
|
|
657
695
|
}))
|
|
658
696
|
});
|
|
659
697
|
if (!result.authorized) {
|
|
660
|
-
logger.warn(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
698
|
+
logger.warn(
|
|
699
|
+
"[ReplicationManager] Replica capability verification failed",
|
|
700
|
+
{
|
|
701
|
+
replicaDID,
|
|
702
|
+
error: result.error
|
|
703
|
+
}
|
|
704
|
+
);
|
|
664
705
|
}
|
|
665
706
|
return result;
|
|
666
707
|
}
|
|
@@ -679,6 +720,7 @@ var ReplicationManager = class {
|
|
|
679
720
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
680
721
|
};
|
|
681
722
|
this.replicationEvents.push(event);
|
|
723
|
+
this.schedulePersist();
|
|
682
724
|
logger.debug("[ReplicationManager] Replica registered", {
|
|
683
725
|
replicaId: replica.id,
|
|
684
726
|
nodeId: replica.nodeId,
|
|
@@ -701,6 +743,7 @@ var ReplicationManager = class {
|
|
|
701
743
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
702
744
|
};
|
|
703
745
|
this.replicationEvents.push(event);
|
|
746
|
+
this.schedulePersist();
|
|
704
747
|
logger.debug("[ReplicationManager] Replica removed", { replicaId });
|
|
705
748
|
}
|
|
706
749
|
/**
|
|
@@ -716,6 +759,7 @@ var ReplicationManager = class {
|
|
|
716
759
|
maxReplicationLag
|
|
717
760
|
};
|
|
718
761
|
this.policies.set(policy.id, policy);
|
|
762
|
+
this.schedulePersist();
|
|
719
763
|
logger.debug("[ReplicationManager] Policy created", {
|
|
720
764
|
policyId: policy.id,
|
|
721
765
|
name,
|
|
@@ -758,12 +802,15 @@ var ReplicationManager = class {
|
|
|
758
802
|
lagBytes,
|
|
759
803
|
lagMillis
|
|
760
804
|
});
|
|
805
|
+
this.schedulePersist();
|
|
761
806
|
}
|
|
762
807
|
/**
|
|
763
808
|
* Get replicas for node
|
|
764
809
|
*/
|
|
765
810
|
getReplicasForNode(nodeId) {
|
|
766
|
-
return Array.from(this.replicas.values()).filter(
|
|
811
|
+
return Array.from(this.replicas.values()).filter(
|
|
812
|
+
(r) => r.nodeId === nodeId
|
|
813
|
+
);
|
|
767
814
|
}
|
|
768
815
|
/**
|
|
769
816
|
* Get healthy replicas
|
|
@@ -777,13 +824,17 @@ var ReplicationManager = class {
|
|
|
777
824
|
* Get syncing replicas
|
|
778
825
|
*/
|
|
779
826
|
getSyncingReplicas() {
|
|
780
|
-
return Array.from(this.replicas.values()).filter(
|
|
827
|
+
return Array.from(this.replicas.values()).filter(
|
|
828
|
+
(r) => r.status === "syncing"
|
|
829
|
+
);
|
|
781
830
|
}
|
|
782
831
|
/**
|
|
783
832
|
* Get failed replicas
|
|
784
833
|
*/
|
|
785
834
|
getFailedReplicas() {
|
|
786
|
-
return Array.from(this.replicas.values()).filter(
|
|
835
|
+
return Array.from(this.replicas.values()).filter(
|
|
836
|
+
(r) => r.status === "failed"
|
|
837
|
+
);
|
|
787
838
|
}
|
|
788
839
|
/**
|
|
789
840
|
* Check replication health for policy
|
|
@@ -844,7 +895,9 @@ var ReplicationManager = class {
|
|
|
844
895
|
const syncing = this.getSyncingReplicas().length;
|
|
845
896
|
const failed = this.getFailedReplicas().length;
|
|
846
897
|
const total = this.replicas.size;
|
|
847
|
-
const replicationLags = Array.from(this.replicas.values()).map(
|
|
898
|
+
const replicationLags = Array.from(this.replicas.values()).map(
|
|
899
|
+
(r) => r.lagMillis
|
|
900
|
+
);
|
|
848
901
|
const avgLag = replicationLags.length > 0 ? replicationLags.reduce((a, b) => a + b) / replicationLags.length : 0;
|
|
849
902
|
const maxLag = replicationLags.length > 0 ? Math.max(...replicationLags) : 0;
|
|
850
903
|
return {
|
|
@@ -918,6 +971,155 @@ var ReplicationManager = class {
|
|
|
918
971
|
return false;
|
|
919
972
|
}
|
|
920
973
|
}
|
|
974
|
+
/**
|
|
975
|
+
* Persist current replication state snapshot.
|
|
976
|
+
*/
|
|
977
|
+
async saveToPersistence() {
|
|
978
|
+
if (!this.persistence) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const data = {
|
|
982
|
+
replicas: this.getAllReplicas(),
|
|
983
|
+
policies: this.getAllPolicies(),
|
|
984
|
+
syncStatus: Array.from(this.syncStatus.entries()).map(
|
|
985
|
+
([nodeId, state]) => ({
|
|
986
|
+
nodeId,
|
|
987
|
+
synced: state.synced,
|
|
988
|
+
failed: state.failed
|
|
989
|
+
})
|
|
990
|
+
)
|
|
991
|
+
};
|
|
992
|
+
const envelope = {
|
|
993
|
+
version: 1,
|
|
994
|
+
updatedAt: Date.now(),
|
|
995
|
+
data
|
|
996
|
+
};
|
|
997
|
+
const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
|
|
998
|
+
await this.persistence.adapter.setItem(this.persistence.key, serialize(envelope));
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Load replication snapshot from persistence.
|
|
1002
|
+
*/
|
|
1003
|
+
async loadFromPersistence() {
|
|
1004
|
+
if (!this.persistence) {
|
|
1005
|
+
return { replicas: 0, policies: 0, syncStatus: 0 };
|
|
1006
|
+
}
|
|
1007
|
+
const raw = await this.persistence.adapter.getItem(this.persistence.key);
|
|
1008
|
+
if (!raw) {
|
|
1009
|
+
return { replicas: 0, policies: 0, syncStatus: 0 };
|
|
1010
|
+
}
|
|
1011
|
+
const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
|
|
1012
|
+
const envelope = deserialize(raw);
|
|
1013
|
+
if (envelope.version !== 1 || !envelope.data) {
|
|
1014
|
+
throw new Error("Invalid replication persistence payload");
|
|
1015
|
+
}
|
|
1016
|
+
if (!Array.isArray(envelope.data.replicas) || !Array.isArray(envelope.data.policies) || !Array.isArray(envelope.data.syncStatus)) {
|
|
1017
|
+
throw new Error("Invalid replication persistence structure");
|
|
1018
|
+
}
|
|
1019
|
+
this.replicas.clear();
|
|
1020
|
+
this.policies.clear();
|
|
1021
|
+
this.syncStatus.clear();
|
|
1022
|
+
this.replicasByDID.clear();
|
|
1023
|
+
let importedReplicas = 0;
|
|
1024
|
+
for (const replica of envelope.data.replicas) {
|
|
1025
|
+
if (this.isValidReplica(replica)) {
|
|
1026
|
+
this.replicas.set(replica.id, replica);
|
|
1027
|
+
if (replica.did) {
|
|
1028
|
+
this.replicasByDID.set(replica.did, replica.id);
|
|
1029
|
+
}
|
|
1030
|
+
importedReplicas++;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
let importedPolicies = 0;
|
|
1034
|
+
for (const policy of envelope.data.policies) {
|
|
1035
|
+
if (this.isValidPolicy(policy)) {
|
|
1036
|
+
this.policies.set(policy.id, policy);
|
|
1037
|
+
importedPolicies++;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
let importedSyncStatus = 0;
|
|
1041
|
+
for (const status of envelope.data.syncStatus) {
|
|
1042
|
+
if (typeof status.nodeId === "string" && typeof status.synced === "number" && typeof status.failed === "number") {
|
|
1043
|
+
this.syncStatus.set(status.nodeId, {
|
|
1044
|
+
synced: status.synced,
|
|
1045
|
+
failed: status.failed
|
|
1046
|
+
});
|
|
1047
|
+
importedSyncStatus++;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
logger.debug("[ReplicationManager] Loaded from persistence", {
|
|
1051
|
+
key: this.persistence.key,
|
|
1052
|
+
replicas: importedReplicas,
|
|
1053
|
+
policies: importedPolicies,
|
|
1054
|
+
syncStatus: importedSyncStatus
|
|
1055
|
+
});
|
|
1056
|
+
return {
|
|
1057
|
+
replicas: importedReplicas,
|
|
1058
|
+
policies: importedPolicies,
|
|
1059
|
+
syncStatus: importedSyncStatus
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Remove persisted replication snapshot.
|
|
1064
|
+
*/
|
|
1065
|
+
async clearPersistence() {
|
|
1066
|
+
if (!this.persistence) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
await this.persistence.adapter.removeItem(this.persistence.key);
|
|
1070
|
+
}
|
|
1071
|
+
schedulePersist() {
|
|
1072
|
+
if (!this.persistence || this.persistence.autoPersist === false) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (this.persistTimer) {
|
|
1076
|
+
clearTimeout(this.persistTimer);
|
|
1077
|
+
}
|
|
1078
|
+
this.persistTimer = setTimeout(() => {
|
|
1079
|
+
void this.persistSafely();
|
|
1080
|
+
}, this.persistence.persistDebounceMs ?? 25);
|
|
1081
|
+
}
|
|
1082
|
+
async persistSafely() {
|
|
1083
|
+
if (!this.persistence) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (this.persistInFlight) {
|
|
1087
|
+
this.persistPending = true;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
this.persistInFlight = true;
|
|
1091
|
+
try {
|
|
1092
|
+
await this.saveToPersistence();
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
logger.error("[ReplicationManager] Persistence write failed", {
|
|
1095
|
+
key: this.persistence.key,
|
|
1096
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1097
|
+
});
|
|
1098
|
+
} finally {
|
|
1099
|
+
this.persistInFlight = false;
|
|
1100
|
+
const shouldRunAgain = this.persistPending;
|
|
1101
|
+
this.persistPending = false;
|
|
1102
|
+
if (shouldRunAgain) {
|
|
1103
|
+
void this.persistSafely();
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
isValidReplica(value) {
|
|
1108
|
+
if (typeof value !== "object" || value === null) {
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
const candidate = value;
|
|
1112
|
+
const validStatus = candidate.status === "primary" || candidate.status === "secondary" || candidate.status === "syncing" || candidate.status === "failed";
|
|
1113
|
+
return typeof candidate.id === "string" && typeof candidate.nodeId === "string" && validStatus && typeof candidate.lastSyncTime === "string" && typeof candidate.lagBytes === "number" && typeof candidate.lagMillis === "number";
|
|
1114
|
+
}
|
|
1115
|
+
isValidPolicy(value) {
|
|
1116
|
+
if (typeof value !== "object" || value === null) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
const candidate = value;
|
|
1120
|
+
const validConsistency = candidate.consistencyLevel === "eventual" || candidate.consistencyLevel === "read-after-write" || candidate.consistencyLevel === "strong";
|
|
1121
|
+
return typeof candidate.id === "string" && typeof candidate.name === "string" && typeof candidate.replicationFactor === "number" && validConsistency && typeof candidate.syncInterval === "number" && typeof candidate.maxReplicationLag === "number";
|
|
1122
|
+
}
|
|
921
1123
|
/**
|
|
922
1124
|
* Clear all state (for testing)
|
|
923
1125
|
*/
|
|
@@ -928,6 +1130,7 @@ var ReplicationManager = class {
|
|
|
928
1130
|
this.syncStatus.clear();
|
|
929
1131
|
this.replicasByDID.clear();
|
|
930
1132
|
this.cryptoProvider = null;
|
|
1133
|
+
this.schedulePersist();
|
|
931
1134
|
}
|
|
932
1135
|
/**
|
|
933
1136
|
* Get the crypto provider (for advanced usage)
|
|
@@ -938,7 +1141,8 @@ var ReplicationManager = class {
|
|
|
938
1141
|
};
|
|
939
1142
|
|
|
940
1143
|
// src/distributed/SyncProtocol.ts
|
|
941
|
-
var SyncProtocol = class {
|
|
1144
|
+
var SyncProtocol = class _SyncProtocol {
|
|
1145
|
+
static DEFAULT_PERSIST_KEY = "aeon:sync-protocol:v1";
|
|
942
1146
|
version = "1.0.0";
|
|
943
1147
|
messageQueue = [];
|
|
944
1148
|
messageMap = /* @__PURE__ */ new Map();
|
|
@@ -948,6 +1152,29 @@ var SyncProtocol = class {
|
|
|
948
1152
|
// Crypto support
|
|
949
1153
|
cryptoProvider = null;
|
|
950
1154
|
cryptoConfig = null;
|
|
1155
|
+
persistence = null;
|
|
1156
|
+
persistTimer = null;
|
|
1157
|
+
persistInFlight = false;
|
|
1158
|
+
persistPending = false;
|
|
1159
|
+
constructor(options) {
|
|
1160
|
+
if (options?.persistence) {
|
|
1161
|
+
this.persistence = {
|
|
1162
|
+
...options.persistence,
|
|
1163
|
+
key: options.persistence.key ?? _SyncProtocol.DEFAULT_PERSIST_KEY,
|
|
1164
|
+
autoPersist: options.persistence.autoPersist ?? true,
|
|
1165
|
+
autoLoad: options.persistence.autoLoad ?? false,
|
|
1166
|
+
persistDebounceMs: options.persistence.persistDebounceMs ?? 25
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
if (this.persistence?.autoLoad) {
|
|
1170
|
+
void this.loadFromPersistence().catch((error) => {
|
|
1171
|
+
logger.error("[SyncProtocol] Failed to load persistence", {
|
|
1172
|
+
key: this.persistence?.key,
|
|
1173
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
951
1178
|
/**
|
|
952
1179
|
* Configure cryptographic provider for authenticated/encrypted messages
|
|
953
1180
|
*/
|
|
@@ -1035,6 +1262,7 @@ var SyncProtocol = class {
|
|
|
1035
1262
|
}
|
|
1036
1263
|
this.messageMap.set(message.messageId, message);
|
|
1037
1264
|
this.messageQueue.push(message);
|
|
1265
|
+
this.schedulePersist();
|
|
1038
1266
|
logger.debug("[SyncProtocol] Authenticated handshake created", {
|
|
1039
1267
|
messageId: message.messageId,
|
|
1040
1268
|
did: localDID,
|
|
@@ -1053,6 +1281,7 @@ var SyncProtocol = class {
|
|
|
1053
1281
|
const handshake = message.payload;
|
|
1054
1282
|
if (!this.cryptoProvider || !this.cryptoConfig) {
|
|
1055
1283
|
this.handshakes.set(message.sender, handshake);
|
|
1284
|
+
this.schedulePersist();
|
|
1056
1285
|
return { valid: true, handshake };
|
|
1057
1286
|
}
|
|
1058
1287
|
if (handshake.did && handshake.publicSigningKey) {
|
|
@@ -1095,6 +1324,7 @@ var SyncProtocol = class {
|
|
|
1095
1324
|
}
|
|
1096
1325
|
}
|
|
1097
1326
|
this.handshakes.set(message.sender, handshake);
|
|
1327
|
+
this.schedulePersist();
|
|
1098
1328
|
logger.debug("[SyncProtocol] Authenticated handshake verified", {
|
|
1099
1329
|
messageId: message.messageId,
|
|
1100
1330
|
did: handshake.did
|
|
@@ -1118,7 +1348,10 @@ var SyncProtocol = class {
|
|
|
1118
1348
|
};
|
|
1119
1349
|
if (encrypt && message.receiver && this.cryptoConfig?.encryptionMode !== "none") {
|
|
1120
1350
|
const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
1121
|
-
const encrypted = await this.cryptoProvider.encrypt(
|
|
1351
|
+
const encrypted = await this.cryptoProvider.encrypt(
|
|
1352
|
+
payloadBytes,
|
|
1353
|
+
message.receiver
|
|
1354
|
+
);
|
|
1122
1355
|
message.payload = encrypted;
|
|
1123
1356
|
message.auth.encrypted = true;
|
|
1124
1357
|
logger.debug("[SyncProtocol] Message encrypted", {
|
|
@@ -1191,6 +1424,7 @@ var SyncProtocol = class {
|
|
|
1191
1424
|
};
|
|
1192
1425
|
this.messageMap.set(message.messageId, message);
|
|
1193
1426
|
this.messageQueue.push(message);
|
|
1427
|
+
this.schedulePersist();
|
|
1194
1428
|
logger.debug("[SyncProtocol] Handshake message created", {
|
|
1195
1429
|
messageId: message.messageId,
|
|
1196
1430
|
nodeId,
|
|
@@ -1218,6 +1452,7 @@ var SyncProtocol = class {
|
|
|
1218
1452
|
};
|
|
1219
1453
|
this.messageMap.set(message.messageId, message);
|
|
1220
1454
|
this.messageQueue.push(message);
|
|
1455
|
+
this.schedulePersist();
|
|
1221
1456
|
logger.debug("[SyncProtocol] Sync request created", {
|
|
1222
1457
|
messageId: message.messageId,
|
|
1223
1458
|
sessionId,
|
|
@@ -1248,6 +1483,7 @@ var SyncProtocol = class {
|
|
|
1248
1483
|
};
|
|
1249
1484
|
this.messageMap.set(message.messageId, message);
|
|
1250
1485
|
this.messageQueue.push(message);
|
|
1486
|
+
this.schedulePersist();
|
|
1251
1487
|
logger.debug("[SyncProtocol] Sync response created", {
|
|
1252
1488
|
messageId: message.messageId,
|
|
1253
1489
|
sessionId,
|
|
@@ -1271,6 +1507,7 @@ var SyncProtocol = class {
|
|
|
1271
1507
|
};
|
|
1272
1508
|
this.messageMap.set(message.messageId, message);
|
|
1273
1509
|
this.messageQueue.push(message);
|
|
1510
|
+
this.schedulePersist();
|
|
1274
1511
|
return message;
|
|
1275
1512
|
}
|
|
1276
1513
|
/**
|
|
@@ -1295,6 +1532,7 @@ var SyncProtocol = class {
|
|
|
1295
1532
|
error,
|
|
1296
1533
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1297
1534
|
});
|
|
1535
|
+
this.schedulePersist();
|
|
1298
1536
|
logger.error("[SyncProtocol] Error message created", {
|
|
1299
1537
|
messageId: message.messageId,
|
|
1300
1538
|
errorCode: error.code,
|
|
@@ -1319,9 +1557,8 @@ var SyncProtocol = class {
|
|
|
1319
1557
|
if (!message.timestamp) {
|
|
1320
1558
|
errors.push("Timestamp is required");
|
|
1321
1559
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
} catch {
|
|
1560
|
+
const timestampValue = new Date(message.timestamp);
|
|
1561
|
+
if (Number.isNaN(timestampValue.getTime())) {
|
|
1325
1562
|
errors.push("Invalid timestamp format");
|
|
1326
1563
|
}
|
|
1327
1564
|
return {
|
|
@@ -1340,7 +1577,9 @@ var SyncProtocol = class {
|
|
|
1340
1577
|
messageId: message.messageId,
|
|
1341
1578
|
error: error instanceof Error ? error.message : String(error)
|
|
1342
1579
|
});
|
|
1343
|
-
throw new Error(
|
|
1580
|
+
throw new Error(
|
|
1581
|
+
`Failed to serialize message: ${error instanceof Error ? error.message : String(error)}`
|
|
1582
|
+
);
|
|
1344
1583
|
}
|
|
1345
1584
|
}
|
|
1346
1585
|
/**
|
|
@@ -1358,7 +1597,9 @@ var SyncProtocol = class {
|
|
|
1358
1597
|
logger.error("[SyncProtocol] Message deserialization failed", {
|
|
1359
1598
|
error: error instanceof Error ? error.message : String(error)
|
|
1360
1599
|
});
|
|
1361
|
-
throw new Error(
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
`Failed to deserialize message: ${error instanceof Error ? error.message : String(error)}`
|
|
1602
|
+
);
|
|
1362
1603
|
}
|
|
1363
1604
|
}
|
|
1364
1605
|
/**
|
|
@@ -1371,6 +1612,7 @@ var SyncProtocol = class {
|
|
|
1371
1612
|
const handshake = message.payload;
|
|
1372
1613
|
const nodeId = message.sender;
|
|
1373
1614
|
this.handshakes.set(nodeId, handshake);
|
|
1615
|
+
this.schedulePersist();
|
|
1374
1616
|
logger.debug("[SyncProtocol] Handshake processed", {
|
|
1375
1617
|
nodeId,
|
|
1376
1618
|
protocolVersion: handshake.protocolVersion,
|
|
@@ -1423,7 +1665,9 @@ var SyncProtocol = class {
|
|
|
1423
1665
|
messagesByType[message.type] = (messagesByType[message.type] || 0) + 1;
|
|
1424
1666
|
}
|
|
1425
1667
|
const errorCount = this.protocolErrors.length;
|
|
1426
|
-
const recoverableErrors = this.protocolErrors.filter(
|
|
1668
|
+
const recoverableErrors = this.protocolErrors.filter(
|
|
1669
|
+
(e) => e.error.recoverable
|
|
1670
|
+
).length;
|
|
1427
1671
|
return {
|
|
1428
1672
|
totalMessages: this.messageQueue.length,
|
|
1429
1673
|
messagesByType,
|
|
@@ -1439,6 +1683,156 @@ var SyncProtocol = class {
|
|
|
1439
1683
|
getErrors() {
|
|
1440
1684
|
return [...this.protocolErrors];
|
|
1441
1685
|
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Persist protocol state for reconnect/replay.
|
|
1688
|
+
*/
|
|
1689
|
+
async saveToPersistence() {
|
|
1690
|
+
if (!this.persistence) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const data = {
|
|
1694
|
+
protocolVersion: this.version,
|
|
1695
|
+
messageCounter: this.messageCounter,
|
|
1696
|
+
messageQueue: this.getAllMessages(),
|
|
1697
|
+
handshakes: Array.from(this.handshakes.entries()).map(
|
|
1698
|
+
([nodeId, handshake]) => ({
|
|
1699
|
+
nodeId,
|
|
1700
|
+
handshake
|
|
1701
|
+
})
|
|
1702
|
+
),
|
|
1703
|
+
protocolErrors: this.getErrors()
|
|
1704
|
+
};
|
|
1705
|
+
const envelope = {
|
|
1706
|
+
version: 1,
|
|
1707
|
+
updatedAt: Date.now(),
|
|
1708
|
+
data
|
|
1709
|
+
};
|
|
1710
|
+
const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
|
|
1711
|
+
await this.persistence.adapter.setItem(this.persistence.key, serialize(envelope));
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Load protocol state from persistence.
|
|
1715
|
+
*/
|
|
1716
|
+
async loadFromPersistence() {
|
|
1717
|
+
if (!this.persistence) {
|
|
1718
|
+
return { messages: 0, handshakes: 0, errors: 0 };
|
|
1719
|
+
}
|
|
1720
|
+
const raw = await this.persistence.adapter.getItem(this.persistence.key);
|
|
1721
|
+
if (!raw) {
|
|
1722
|
+
return { messages: 0, handshakes: 0, errors: 0 };
|
|
1723
|
+
}
|
|
1724
|
+
const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
|
|
1725
|
+
const envelope = deserialize(raw);
|
|
1726
|
+
if (envelope.version !== 1 || !envelope.data) {
|
|
1727
|
+
throw new Error("Invalid sync protocol persistence payload");
|
|
1728
|
+
}
|
|
1729
|
+
if (!Array.isArray(envelope.data.messageQueue) || !Array.isArray(envelope.data.handshakes) || !Array.isArray(envelope.data.protocolErrors)) {
|
|
1730
|
+
throw new Error("Invalid sync protocol persistence structure");
|
|
1731
|
+
}
|
|
1732
|
+
const nextMessages = [];
|
|
1733
|
+
for (const message of envelope.data.messageQueue) {
|
|
1734
|
+
const validation = this.validateMessage(message);
|
|
1735
|
+
if (!validation.valid) {
|
|
1736
|
+
throw new Error(
|
|
1737
|
+
`Invalid persisted message ${message?.messageId ?? "unknown"}: ${validation.errors.join(", ")}`
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
nextMessages.push(message);
|
|
1741
|
+
}
|
|
1742
|
+
const nextHandshakes = /* @__PURE__ */ new Map();
|
|
1743
|
+
for (const entry of envelope.data.handshakes) {
|
|
1744
|
+
if (typeof entry.nodeId !== "string" || !this.isValidHandshake(entry.handshake)) {
|
|
1745
|
+
throw new Error("Invalid persisted handshake payload");
|
|
1746
|
+
}
|
|
1747
|
+
nextHandshakes.set(entry.nodeId, entry.handshake);
|
|
1748
|
+
}
|
|
1749
|
+
const nextErrors = [];
|
|
1750
|
+
for (const entry of envelope.data.protocolErrors) {
|
|
1751
|
+
if (!this.isValidProtocolErrorEntry(entry)) {
|
|
1752
|
+
throw new Error("Invalid persisted protocol error payload");
|
|
1753
|
+
}
|
|
1754
|
+
nextErrors.push(entry);
|
|
1755
|
+
}
|
|
1756
|
+
this.messageQueue = nextMessages;
|
|
1757
|
+
this.messageMap = new Map(nextMessages.map((m) => [m.messageId, m]));
|
|
1758
|
+
this.handshakes = nextHandshakes;
|
|
1759
|
+
this.protocolErrors = nextErrors;
|
|
1760
|
+
this.messageCounter = Math.max(
|
|
1761
|
+
envelope.data.messageCounter || 0,
|
|
1762
|
+
this.messageQueue.length
|
|
1763
|
+
);
|
|
1764
|
+
logger.debug("[SyncProtocol] Loaded from persistence", {
|
|
1765
|
+
key: this.persistence.key,
|
|
1766
|
+
messages: this.messageQueue.length,
|
|
1767
|
+
handshakes: this.handshakes.size,
|
|
1768
|
+
errors: this.protocolErrors.length
|
|
1769
|
+
});
|
|
1770
|
+
return {
|
|
1771
|
+
messages: this.messageQueue.length,
|
|
1772
|
+
handshakes: this.handshakes.size,
|
|
1773
|
+
errors: this.protocolErrors.length
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Clear persisted protocol checkpoint.
|
|
1778
|
+
*/
|
|
1779
|
+
async clearPersistence() {
|
|
1780
|
+
if (!this.persistence) {
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
await this.persistence.adapter.removeItem(this.persistence.key);
|
|
1784
|
+
}
|
|
1785
|
+
schedulePersist() {
|
|
1786
|
+
if (!this.persistence || this.persistence.autoPersist === false) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (this.persistTimer) {
|
|
1790
|
+
clearTimeout(this.persistTimer);
|
|
1791
|
+
}
|
|
1792
|
+
this.persistTimer = setTimeout(() => {
|
|
1793
|
+
void this.persistSafely();
|
|
1794
|
+
}, this.persistence.persistDebounceMs ?? 25);
|
|
1795
|
+
}
|
|
1796
|
+
async persistSafely() {
|
|
1797
|
+
if (!this.persistence) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (this.persistInFlight) {
|
|
1801
|
+
this.persistPending = true;
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
this.persistInFlight = true;
|
|
1805
|
+
try {
|
|
1806
|
+
await this.saveToPersistence();
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
logger.error("[SyncProtocol] Persistence write failed", {
|
|
1809
|
+
key: this.persistence.key,
|
|
1810
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1811
|
+
});
|
|
1812
|
+
} finally {
|
|
1813
|
+
this.persistInFlight = false;
|
|
1814
|
+
const shouldRunAgain = this.persistPending;
|
|
1815
|
+
this.persistPending = false;
|
|
1816
|
+
if (shouldRunAgain) {
|
|
1817
|
+
void this.persistSafely();
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
isValidHandshake(value) {
|
|
1822
|
+
if (typeof value !== "object" || value === null) {
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
const handshake = value;
|
|
1826
|
+
const validState = handshake.state === "initiating" || handshake.state === "responding" || handshake.state === "completed";
|
|
1827
|
+
return typeof handshake.protocolVersion === "string" && typeof handshake.nodeId === "string" && Array.isArray(handshake.capabilities) && handshake.capabilities.every((cap) => typeof cap === "string") && validState;
|
|
1828
|
+
}
|
|
1829
|
+
isValidProtocolErrorEntry(entry) {
|
|
1830
|
+
if (typeof entry !== "object" || entry === null) {
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
const candidate = entry;
|
|
1834
|
+
return typeof candidate.timestamp === "string" && typeof candidate.error?.code === "string" && typeof candidate.error.message === "string" && typeof candidate.error.recoverable === "boolean";
|
|
1835
|
+
}
|
|
1442
1836
|
/**
|
|
1443
1837
|
* Generate message ID
|
|
1444
1838
|
*/
|
|
@@ -1457,6 +1851,7 @@ var SyncProtocol = class {
|
|
|
1457
1851
|
this.messageCounter = 0;
|
|
1458
1852
|
this.cryptoProvider = null;
|
|
1459
1853
|
this.cryptoConfig = null;
|
|
1854
|
+
this.schedulePersist();
|
|
1460
1855
|
}
|
|
1461
1856
|
/**
|
|
1462
1857
|
* Get the crypto provider (for advanced usage)
|
|
@@ -1842,7 +2237,9 @@ var StateReconciler = class {
|
|
|
1842
2237
|
}
|
|
1843
2238
|
return {
|
|
1844
2239
|
totalReconciliations: this.reconciliationHistory.length,
|
|
1845
|
-
successfulReconciliations: this.reconciliationHistory.filter(
|
|
2240
|
+
successfulReconciliations: this.reconciliationHistory.filter(
|
|
2241
|
+
(r) => r.success
|
|
2242
|
+
).length,
|
|
1846
2243
|
totalConflictsResolved: resolvedConflicts,
|
|
1847
2244
|
averageConflictsPerReconciliation: this.reconciliationHistory.length > 0 ? resolvedConflicts / this.reconciliationHistory.length : 0,
|
|
1848
2245
|
strategyUsage,
|