@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
@@ -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((s) => s.status === "active");
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((sum, s) => sum + s.itemsSynced, 0);
407
- const totalConflicts = sessions.reduce((sum, s) => sum + s.conflictsDetected, 0);
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", { interval });
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(dataBytes, targetReplicaDID);
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("[ReplicationManager] Replica capability verification failed", {
659
- replicaDID,
660
- error: result.error
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((r) => r.nodeId === nodeId);
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((r) => r.status === "syncing");
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((r) => r.status === "failed");
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((r) => r.lagMillis);
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(payloadBytes, message.receiver);
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
- try {
1321
- new Date(message.timestamp);
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(`Failed to serialize message: ${error instanceof Error ? error.message : String(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(`Failed to deserialize message: ${error instanceof Error ? error.message : String(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((e) => e.error.recoverable).length;
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((r) => r.success).length,
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,