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