@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.
Files changed (62) hide show
  1. package/README.md +10 -0
  2. package/dist/compression/index.cjs +580 -0
  3. package/dist/compression/index.cjs.map +1 -0
  4. package/dist/compression/index.d.cts +189 -0
  5. package/dist/compression/index.d.ts +189 -0
  6. package/dist/compression/index.js +573 -0
  7. package/dist/compression/index.js.map +1 -0
  8. package/dist/core/index.d.cts +70 -5
  9. package/dist/core/index.d.ts +70 -5
  10. package/dist/crypto/index.cjs +100 -0
  11. package/dist/crypto/index.cjs.map +1 -0
  12. package/dist/crypto/index.d.cts +407 -0
  13. package/dist/crypto/index.d.ts +407 -0
  14. package/dist/crypto/index.js +96 -0
  15. package/dist/crypto/index.js.map +1 -0
  16. package/dist/distributed/index.cjs +420 -23
  17. package/dist/distributed/index.cjs.map +1 -1
  18. package/dist/distributed/index.d.cts +901 -2
  19. package/dist/distributed/index.d.ts +901 -2
  20. package/dist/distributed/index.js +420 -23
  21. package/dist/distributed/index.js.map +1 -1
  22. package/dist/index.cjs +1222 -55
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +11 -811
  25. package/dist/index.d.ts +11 -811
  26. package/dist/index.js +1221 -56
  27. package/dist/index.js.map +1 -1
  28. package/dist/offline/index.cjs +419 -0
  29. package/dist/offline/index.cjs.map +1 -0
  30. package/dist/offline/index.d.cts +148 -0
  31. package/dist/offline/index.d.ts +148 -0
  32. package/dist/offline/index.js +415 -0
  33. package/dist/offline/index.js.map +1 -0
  34. package/dist/optimization/index.cjs +797 -0
  35. package/dist/optimization/index.cjs.map +1 -0
  36. package/dist/optimization/index.d.cts +347 -0
  37. package/dist/optimization/index.d.ts +347 -0
  38. package/dist/optimization/index.js +787 -0
  39. package/dist/optimization/index.js.map +1 -0
  40. package/dist/persistence/index.cjs +145 -0
  41. package/dist/persistence/index.cjs.map +1 -0
  42. package/dist/persistence/index.d.cts +63 -0
  43. package/dist/persistence/index.d.ts +63 -0
  44. package/dist/persistence/index.js +142 -0
  45. package/dist/persistence/index.js.map +1 -0
  46. package/dist/presence/index.cjs +489 -0
  47. package/dist/presence/index.cjs.map +1 -0
  48. package/dist/presence/index.d.cts +283 -0
  49. package/dist/presence/index.d.ts +283 -0
  50. package/dist/presence/index.js +485 -0
  51. package/dist/presence/index.js.map +1 -0
  52. package/dist/types-CMxO7QF0.d.cts +33 -0
  53. package/dist/types-CMxO7QF0.d.ts +33 -0
  54. package/dist/versioning/index.cjs +296 -14
  55. package/dist/versioning/index.cjs.map +1 -1
  56. package/dist/versioning/index.d.cts +66 -1
  57. package/dist/versioning/index.d.ts +66 -1
  58. package/dist/versioning/index.js +296 -14
  59. package/dist/versioning/index.js.map +1 -1
  60. package/package.json +51 -1
  61. package/dist/index-C_4CMV5c.d.cts +0 -1207
  62. 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((s) => s.status === "active");
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((sum, s) => sum + s.itemsSynced, 0);
409
- const totalConflicts = sessions.reduce((sum, s) => sum + s.conflictsDetected, 0);
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", { interval });
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(dataBytes, targetReplicaDID);
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("[ReplicationManager] Replica capability verification failed", {
661
- replicaDID,
662
- error: result.error
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((r) => r.nodeId === nodeId);
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((r) => r.status === "syncing");
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((r) => r.status === "failed");
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((r) => r.lagMillis);
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(payloadBytes, message.receiver);
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
- try {
1323
- new Date(message.timestamp);
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(`Failed to serialize message: ${error instanceof Error ? error.message : String(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(`Failed to deserialize message: ${error instanceof Error ? error.message : String(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((e) => e.error.recoverable).length;
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((r) => r.success).length,
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,