@affectively/aeon 1.0.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/LICENSE +21 -0
- package/README.md +332 -0
- package/dist/core/index.cjs +4 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +146 -0
- package/dist/core/index.d.ts +146 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/distributed/index.cjs +1874 -0
- package/dist/distributed/index.cjs.map +1 -0
- package/dist/distributed/index.d.cts +2 -0
- package/dist/distributed/index.d.ts +2 -0
- package/dist/distributed/index.js +1869 -0
- package/dist/distributed/index.js.map +1 -0
- package/dist/index-C_4CMV5c.d.cts +1207 -0
- package/dist/index-C_4CMV5c.d.ts +1207 -0
- package/dist/index.cjs +4671 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +812 -0
- package/dist/index.d.ts +812 -0
- package/dist/index.js +4632 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.cjs +64 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +38 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +57 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/versioning/index.cjs +871 -0
- package/dist/versioning/index.cjs.map +1 -0
- package/dist/versioning/index.d.cts +472 -0
- package/dist/versioning/index.d.ts +472 -0
- package/dist/versioning/index.js +866 -0
- package/dist/versioning/index.js.map +1 -0
- package/package.json +142 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4671 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var eventemitter3 = require('eventemitter3');
|
|
4
|
+
|
|
5
|
+
// src/utils/logger.ts
|
|
6
|
+
var consoleLogger = {
|
|
7
|
+
debug: (...args) => {
|
|
8
|
+
console.debug("[AEON:DEBUG]", ...args);
|
|
9
|
+
},
|
|
10
|
+
info: (...args) => {
|
|
11
|
+
console.info("[AEON:INFO]", ...args);
|
|
12
|
+
},
|
|
13
|
+
warn: (...args) => {
|
|
14
|
+
console.warn("[AEON:WARN]", ...args);
|
|
15
|
+
},
|
|
16
|
+
error: (...args) => {
|
|
17
|
+
console.error("[AEON:ERROR]", ...args);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var noopLogger = {
|
|
21
|
+
debug: () => {
|
|
22
|
+
},
|
|
23
|
+
info: () => {
|
|
24
|
+
},
|
|
25
|
+
warn: () => {
|
|
26
|
+
},
|
|
27
|
+
error: () => {
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var currentLogger = consoleLogger;
|
|
31
|
+
function getLogger() {
|
|
32
|
+
return currentLogger;
|
|
33
|
+
}
|
|
34
|
+
function setLogger(logger9) {
|
|
35
|
+
currentLogger = logger9;
|
|
36
|
+
}
|
|
37
|
+
function resetLogger() {
|
|
38
|
+
currentLogger = consoleLogger;
|
|
39
|
+
}
|
|
40
|
+
function disableLogging() {
|
|
41
|
+
currentLogger = noopLogger;
|
|
42
|
+
}
|
|
43
|
+
function createNamespacedLogger(namespace) {
|
|
44
|
+
const logger9 = getLogger();
|
|
45
|
+
return {
|
|
46
|
+
debug: (...args) => logger9.debug(`[${namespace}]`, ...args),
|
|
47
|
+
info: (...args) => logger9.info(`[${namespace}]`, ...args),
|
|
48
|
+
warn: (...args) => logger9.warn(`[${namespace}]`, ...args),
|
|
49
|
+
error: (...args) => logger9.error(`[${namespace}]`, ...args)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var logger = {
|
|
53
|
+
debug: (...args) => getLogger().debug(...args),
|
|
54
|
+
info: (...args) => getLogger().info(...args),
|
|
55
|
+
warn: (...args) => getLogger().warn(...args),
|
|
56
|
+
error: (...args) => getLogger().error(...args)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/versioning/SchemaVersionManager.ts
|
|
60
|
+
var SchemaVersionManager = class {
|
|
61
|
+
versions = /* @__PURE__ */ new Map();
|
|
62
|
+
versionHistory = [];
|
|
63
|
+
compatibilityMatrix = /* @__PURE__ */ new Map();
|
|
64
|
+
currentVersion = null;
|
|
65
|
+
constructor() {
|
|
66
|
+
this.initializeDefaultVersions();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Initialize default versions
|
|
70
|
+
*/
|
|
71
|
+
initializeDefaultVersions() {
|
|
72
|
+
const v1_0_0 = {
|
|
73
|
+
major: 1,
|
|
74
|
+
minor: 0,
|
|
75
|
+
patch: 0,
|
|
76
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
77
|
+
description: "Initial schema version",
|
|
78
|
+
breaking: false
|
|
79
|
+
};
|
|
80
|
+
this.registerVersion(v1_0_0);
|
|
81
|
+
this.currentVersion = v1_0_0;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Register a new schema version
|
|
85
|
+
*/
|
|
86
|
+
registerVersion(version) {
|
|
87
|
+
const versionString = this.versionToString(version);
|
|
88
|
+
this.versions.set(versionString, version);
|
|
89
|
+
this.versionHistory.push(version);
|
|
90
|
+
logger.debug("[SchemaVersionManager] Version registered", {
|
|
91
|
+
version: versionString,
|
|
92
|
+
breaking: version.breaking,
|
|
93
|
+
description: version.description
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get current version
|
|
98
|
+
*/
|
|
99
|
+
getCurrentVersion() {
|
|
100
|
+
if (!this.currentVersion) {
|
|
101
|
+
throw new Error("No current version set");
|
|
102
|
+
}
|
|
103
|
+
return this.currentVersion;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set current version
|
|
107
|
+
*/
|
|
108
|
+
setCurrentVersion(version) {
|
|
109
|
+
if (!this.versions.has(this.versionToString(version))) {
|
|
110
|
+
throw new Error(`Version ${this.versionToString(version)} not registered`);
|
|
111
|
+
}
|
|
112
|
+
this.currentVersion = version;
|
|
113
|
+
logger.debug("[SchemaVersionManager] Current version set", {
|
|
114
|
+
version: this.versionToString(version)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get version history
|
|
119
|
+
*/
|
|
120
|
+
getVersionHistory() {
|
|
121
|
+
return [...this.versionHistory];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Check if version exists
|
|
125
|
+
*/
|
|
126
|
+
hasVersion(version) {
|
|
127
|
+
return this.versions.has(this.versionToString(version));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get version by string (e.g., "1.2.3")
|
|
131
|
+
*/
|
|
132
|
+
getVersion(versionString) {
|
|
133
|
+
return this.versions.get(versionString);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Register compatibility rule
|
|
137
|
+
*/
|
|
138
|
+
registerCompatibility(rule) {
|
|
139
|
+
if (!this.compatibilityMatrix.has(rule.from)) {
|
|
140
|
+
this.compatibilityMatrix.set(rule.from, []);
|
|
141
|
+
}
|
|
142
|
+
const rules = this.compatibilityMatrix.get(rule.from);
|
|
143
|
+
if (rules) {
|
|
144
|
+
rules.push(rule);
|
|
145
|
+
}
|
|
146
|
+
logger.debug("[SchemaVersionManager] Compatibility rule registered", {
|
|
147
|
+
from: rule.from,
|
|
148
|
+
to: rule.to,
|
|
149
|
+
compatible: rule.compatible,
|
|
150
|
+
requiresMigration: rule.requiresMigration
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if migration path exists
|
|
155
|
+
*/
|
|
156
|
+
canMigrate(fromVersion, toVersion) {
|
|
157
|
+
const fromStr = typeof fromVersion === "string" ? fromVersion : this.versionToString(fromVersion);
|
|
158
|
+
const toStr = typeof toVersion === "string" ? toVersion : this.versionToString(toVersion);
|
|
159
|
+
const rules = this.compatibilityMatrix.get(fromStr) || [];
|
|
160
|
+
return rules.some((r) => r.to === toStr && r.requiresMigration);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get migration path
|
|
164
|
+
*/
|
|
165
|
+
getMigrationPath(fromVersion, toVersion) {
|
|
166
|
+
const path = [];
|
|
167
|
+
let current = fromVersion;
|
|
168
|
+
const maxSteps = 100;
|
|
169
|
+
let steps = 0;
|
|
170
|
+
while (this.compareVersions(current, toVersion) !== 0 && steps < maxSteps) {
|
|
171
|
+
const fromStr = this.versionToString(current);
|
|
172
|
+
const rules = this.compatibilityMatrix.get(fromStr) || [];
|
|
173
|
+
let found = false;
|
|
174
|
+
for (const rule of rules) {
|
|
175
|
+
const nextVersion = this.getVersion(rule.to);
|
|
176
|
+
if (nextVersion) {
|
|
177
|
+
if (this.compareVersions(nextVersion, toVersion) <= 0 || this.compareVersions(current, nextVersion) < this.compareVersions(current, toVersion)) {
|
|
178
|
+
current = nextVersion;
|
|
179
|
+
path.push(current);
|
|
180
|
+
found = true;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!found) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
steps++;
|
|
189
|
+
}
|
|
190
|
+
return path;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Compare two versions
|
|
194
|
+
* Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
195
|
+
*/
|
|
196
|
+
compareVersions(v1, v2) {
|
|
197
|
+
const ver1 = typeof v1 === "string" ? this.parseVersion(v1) : v1;
|
|
198
|
+
const ver2 = typeof v2 === "string" ? this.parseVersion(v2) : v2;
|
|
199
|
+
if (ver1.major !== ver2.major) {
|
|
200
|
+
return ver1.major < ver2.major ? -1 : 1;
|
|
201
|
+
}
|
|
202
|
+
if (ver1.minor !== ver2.minor) {
|
|
203
|
+
return ver1.minor < ver2.minor ? -1 : 1;
|
|
204
|
+
}
|
|
205
|
+
if (ver1.patch !== ver2.patch) {
|
|
206
|
+
return ver1.patch < ver2.patch ? -1 : 1;
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Parse version string to SchemaVersion
|
|
212
|
+
*/
|
|
213
|
+
parseVersion(versionString) {
|
|
214
|
+
const parts = versionString.split(".").map(Number);
|
|
215
|
+
return {
|
|
216
|
+
major: parts[0] || 0,
|
|
217
|
+
minor: parts[1] || 0,
|
|
218
|
+
patch: parts[2] || 0,
|
|
219
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
220
|
+
description: "",
|
|
221
|
+
breaking: false
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Create new version
|
|
226
|
+
*/
|
|
227
|
+
createVersion(major, minor, patch, description, breaking = false) {
|
|
228
|
+
return {
|
|
229
|
+
major,
|
|
230
|
+
minor,
|
|
231
|
+
patch,
|
|
232
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
233
|
+
description,
|
|
234
|
+
breaking
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Convert version to string
|
|
239
|
+
*/
|
|
240
|
+
versionToString(version) {
|
|
241
|
+
return `${version.major}.${version.minor}.${version.patch}`;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get version metadata
|
|
245
|
+
*/
|
|
246
|
+
getVersionMetadata(version) {
|
|
247
|
+
const history = this.versionHistory;
|
|
248
|
+
const currentIndex = history.findIndex(
|
|
249
|
+
(v) => this.versionToString(v) === this.versionToString(version)
|
|
250
|
+
);
|
|
251
|
+
return {
|
|
252
|
+
version,
|
|
253
|
+
previousVersion: currentIndex > 0 ? history[currentIndex - 1] : void 0,
|
|
254
|
+
changes: [version.description],
|
|
255
|
+
migrationsRequired: this.canMigrate(
|
|
256
|
+
this.currentVersion || version,
|
|
257
|
+
version
|
|
258
|
+
) ? [this.versionToString(version)] : [],
|
|
259
|
+
rollbackPossible: currentIndex > 0
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get all registered versions
|
|
264
|
+
*/
|
|
265
|
+
getAllVersions() {
|
|
266
|
+
return Array.from(this.versions.values()).sort(
|
|
267
|
+
(a, b) => this.compareVersions(a, b)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Clear all versions (for testing)
|
|
272
|
+
*/
|
|
273
|
+
clear() {
|
|
274
|
+
this.versions.clear();
|
|
275
|
+
this.versionHistory = [];
|
|
276
|
+
this.compatibilityMatrix.clear();
|
|
277
|
+
this.currentVersion = null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/versioning/MigrationEngine.ts
|
|
282
|
+
var MigrationEngine = class {
|
|
283
|
+
migrations = /* @__PURE__ */ new Map();
|
|
284
|
+
executedMigrations = [];
|
|
285
|
+
state = {
|
|
286
|
+
currentVersion: "1.0.0",
|
|
287
|
+
appliedMigrations: [],
|
|
288
|
+
failedMigrations: [],
|
|
289
|
+
lastMigrationTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
totalMigrationsRun: 0
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Register a migration
|
|
294
|
+
*/
|
|
295
|
+
registerMigration(migration) {
|
|
296
|
+
this.migrations.set(migration.id, migration);
|
|
297
|
+
logger.debug("[MigrationEngine] Migration registered", {
|
|
298
|
+
id: migration.id,
|
|
299
|
+
version: migration.version,
|
|
300
|
+
name: migration.name
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Execute a migration
|
|
305
|
+
*/
|
|
306
|
+
async executeMigration(migrationId, data) {
|
|
307
|
+
const migration = this.migrations.get(migrationId);
|
|
308
|
+
if (!migration) {
|
|
309
|
+
throw new Error(`Migration ${migrationId} not found`);
|
|
310
|
+
}
|
|
311
|
+
const startTime = Date.now();
|
|
312
|
+
const result = {
|
|
313
|
+
migrationId,
|
|
314
|
+
success: false,
|
|
315
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
316
|
+
duration: 0,
|
|
317
|
+
itemsAffected: 0,
|
|
318
|
+
errors: []
|
|
319
|
+
};
|
|
320
|
+
try {
|
|
321
|
+
logger.debug("[MigrationEngine] Executing migration", {
|
|
322
|
+
id: migrationId,
|
|
323
|
+
version: migration.version
|
|
324
|
+
});
|
|
325
|
+
migration.up(data);
|
|
326
|
+
result.success = true;
|
|
327
|
+
result.itemsAffected = Array.isArray(data) ? data.length : 1;
|
|
328
|
+
result.duration = Date.now() - startTime;
|
|
329
|
+
this.state.appliedMigrations.push(migrationId);
|
|
330
|
+
this.state.currentVersion = migration.version;
|
|
331
|
+
this.state.totalMigrationsRun++;
|
|
332
|
+
this.state.lastMigrationTime = result.timestamp;
|
|
333
|
+
this.executedMigrations.push(result);
|
|
334
|
+
logger.debug("[MigrationEngine] Migration executed successfully", {
|
|
335
|
+
id: migrationId,
|
|
336
|
+
duration: result.duration,
|
|
337
|
+
itemsAffected: result.itemsAffected
|
|
338
|
+
});
|
|
339
|
+
return result;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
result.errors = [error instanceof Error ? error.message : String(error)];
|
|
342
|
+
this.state.failedMigrations.push(migrationId);
|
|
343
|
+
this.executedMigrations.push(result);
|
|
344
|
+
logger.error("[MigrationEngine] Migration failed", {
|
|
345
|
+
id: migrationId,
|
|
346
|
+
error: result.errors[0]
|
|
347
|
+
});
|
|
348
|
+
throw new Error(`Migration ${migrationId} failed: ${result.errors[0]}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Rollback a migration
|
|
353
|
+
*/
|
|
354
|
+
async rollbackMigration(migrationId, data) {
|
|
355
|
+
const migration = this.migrations.get(migrationId);
|
|
356
|
+
if (!migration) {
|
|
357
|
+
throw new Error(`Migration ${migrationId} not found`);
|
|
358
|
+
}
|
|
359
|
+
if (!migration.down) {
|
|
360
|
+
throw new Error(`Migration ${migrationId} does not support rollback`);
|
|
361
|
+
}
|
|
362
|
+
const startTime = Date.now();
|
|
363
|
+
const result = {
|
|
364
|
+
migrationId,
|
|
365
|
+
success: false,
|
|
366
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
367
|
+
duration: 0,
|
|
368
|
+
itemsAffected: 0,
|
|
369
|
+
errors: []
|
|
370
|
+
};
|
|
371
|
+
try {
|
|
372
|
+
logger.debug("[MigrationEngine] Rolling back migration", {
|
|
373
|
+
id: migrationId,
|
|
374
|
+
version: migration.version
|
|
375
|
+
});
|
|
376
|
+
migration.down(data);
|
|
377
|
+
result.success = true;
|
|
378
|
+
result.itemsAffected = Array.isArray(data) ? data.length : 1;
|
|
379
|
+
result.duration = Date.now() - startTime;
|
|
380
|
+
this.state.appliedMigrations = this.state.appliedMigrations.filter(
|
|
381
|
+
(id) => id !== migrationId
|
|
382
|
+
);
|
|
383
|
+
this.executedMigrations.push(result);
|
|
384
|
+
logger.debug("[MigrationEngine] Migration rolled back", {
|
|
385
|
+
id: migrationId,
|
|
386
|
+
duration: result.duration
|
|
387
|
+
});
|
|
388
|
+
return result;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
result.errors = [error instanceof Error ? error.message : String(error)];
|
|
391
|
+
this.executedMigrations.push(result);
|
|
392
|
+
logger.error("[MigrationEngine] Rollback failed", {
|
|
393
|
+
id: migrationId,
|
|
394
|
+
error: result.errors[0]
|
|
395
|
+
});
|
|
396
|
+
throw new Error(`Rollback for ${migrationId} failed: ${result.errors[0]}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get migration state
|
|
401
|
+
*/
|
|
402
|
+
getState() {
|
|
403
|
+
return { ...this.state };
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get migration execution history
|
|
407
|
+
*/
|
|
408
|
+
getExecutionHistory() {
|
|
409
|
+
return [...this.executedMigrations];
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Get migration by ID
|
|
413
|
+
*/
|
|
414
|
+
getMigration(migrationId) {
|
|
415
|
+
return this.migrations.get(migrationId);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Get all registered migrations
|
|
419
|
+
*/
|
|
420
|
+
getAllMigrations() {
|
|
421
|
+
return Array.from(this.migrations.values());
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get applied migrations
|
|
425
|
+
*/
|
|
426
|
+
getAppliedMigrations() {
|
|
427
|
+
return [...this.state.appliedMigrations];
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get failed migrations
|
|
431
|
+
*/
|
|
432
|
+
getFailedMigrations() {
|
|
433
|
+
return [...this.state.failedMigrations];
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get pending migrations
|
|
437
|
+
*/
|
|
438
|
+
getPendingMigrations() {
|
|
439
|
+
return this.getAllMigrations().filter(
|
|
440
|
+
(m) => !this.state.appliedMigrations.includes(m.id)
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Get migration statistics
|
|
445
|
+
*/
|
|
446
|
+
getStatistics() {
|
|
447
|
+
const successful = this.executedMigrations.filter((m) => m.success).length;
|
|
448
|
+
const failed = this.executedMigrations.filter((m) => !m.success).length;
|
|
449
|
+
const totalDuration = this.executedMigrations.reduce((sum, m) => sum + m.duration, 0);
|
|
450
|
+
const totalAffected = this.executedMigrations.reduce((sum, m) => sum + m.itemsAffected, 0);
|
|
451
|
+
return {
|
|
452
|
+
totalExecuted: this.executedMigrations.length,
|
|
453
|
+
successful,
|
|
454
|
+
failed,
|
|
455
|
+
successRate: this.executedMigrations.length > 0 ? successful / this.executedMigrations.length * 100 : 0,
|
|
456
|
+
totalDurationMs: totalDuration,
|
|
457
|
+
averageDurationMs: this.executedMigrations.length > 0 ? totalDuration / this.executedMigrations.length : 0,
|
|
458
|
+
totalAffected
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Clear history (for testing)
|
|
463
|
+
*/
|
|
464
|
+
clear() {
|
|
465
|
+
this.migrations.clear();
|
|
466
|
+
this.executedMigrations = [];
|
|
467
|
+
this.state = {
|
|
468
|
+
currentVersion: "1.0.0",
|
|
469
|
+
appliedMigrations: [],
|
|
470
|
+
failedMigrations: [],
|
|
471
|
+
lastMigrationTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
472
|
+
totalMigrationsRun: 0
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/versioning/DataTransformer.ts
|
|
478
|
+
var DataTransformer = class {
|
|
479
|
+
rules = /* @__PURE__ */ new Map();
|
|
480
|
+
transformationHistory = [];
|
|
481
|
+
/**
|
|
482
|
+
* Register a transformation rule
|
|
483
|
+
*/
|
|
484
|
+
registerRule(rule) {
|
|
485
|
+
this.rules.set(rule.field, rule);
|
|
486
|
+
logger.debug("[DataTransformer] Rule registered", {
|
|
487
|
+
field: rule.field,
|
|
488
|
+
required: rule.required,
|
|
489
|
+
hasDefault: rule.defaultValue !== void 0
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Transform a single field value
|
|
494
|
+
*/
|
|
495
|
+
transformField(field, value) {
|
|
496
|
+
const rule = this.rules.get(field);
|
|
497
|
+
if (!rule) {
|
|
498
|
+
return value;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
return rule.transformer(value);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (rule.required) {
|
|
504
|
+
throw new Error(`Failed to transform required field ${field}: ${error instanceof Error ? error.message : String(error)}`);
|
|
505
|
+
}
|
|
506
|
+
return rule.defaultValue !== void 0 ? rule.defaultValue : value;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Transform a single object
|
|
511
|
+
*/
|
|
512
|
+
transformObject(data) {
|
|
513
|
+
const transformed = {};
|
|
514
|
+
for (const [key, value] of Object.entries(data)) {
|
|
515
|
+
try {
|
|
516
|
+
transformed[key] = this.transformField(key, value);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
logger.warn("[DataTransformer] Field transformation failed", {
|
|
519
|
+
field: key,
|
|
520
|
+
error: error instanceof Error ? error.message : String(error)
|
|
521
|
+
});
|
|
522
|
+
const rule = this.rules.get(key);
|
|
523
|
+
if (!rule || !rule.required) {
|
|
524
|
+
transformed[key] = value;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return transformed;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Transform a collection of items
|
|
532
|
+
*/
|
|
533
|
+
transformCollection(items) {
|
|
534
|
+
const startTime = Date.now();
|
|
535
|
+
const result = {
|
|
536
|
+
success: true,
|
|
537
|
+
itemsTransformed: 0,
|
|
538
|
+
itemsFailed: 0,
|
|
539
|
+
errors: [],
|
|
540
|
+
warnings: [],
|
|
541
|
+
duration: 0
|
|
542
|
+
};
|
|
543
|
+
for (let i = 0; i < items.length; i++) {
|
|
544
|
+
const item = items[i];
|
|
545
|
+
try {
|
|
546
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
547
|
+
this.transformObject(item);
|
|
548
|
+
result.itemsTransformed++;
|
|
549
|
+
} else {
|
|
550
|
+
result.warnings.push(`Item ${i} is not a transformable object`);
|
|
551
|
+
}
|
|
552
|
+
} catch (error) {
|
|
553
|
+
result.errors.push({
|
|
554
|
+
item,
|
|
555
|
+
error: error instanceof Error ? error.message : String(error)
|
|
556
|
+
});
|
|
557
|
+
result.itemsFailed++;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
result.duration = Date.now() - startTime;
|
|
561
|
+
result.success = result.itemsFailed === 0;
|
|
562
|
+
this.transformationHistory.push(result);
|
|
563
|
+
logger.debug("[DataTransformer] Collection transformed", {
|
|
564
|
+
total: items.length,
|
|
565
|
+
transformed: result.itemsTransformed,
|
|
566
|
+
failed: result.itemsFailed,
|
|
567
|
+
duration: result.duration
|
|
568
|
+
});
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Validate transformed data
|
|
573
|
+
*/
|
|
574
|
+
validateTransformation(original, transformed) {
|
|
575
|
+
const issues = [];
|
|
576
|
+
if (original.length !== transformed.length) {
|
|
577
|
+
issues.push(`Item count mismatch: ${original.length} -> ${transformed.length}`);
|
|
578
|
+
}
|
|
579
|
+
for (let i = 0; i < Math.min(original.length, transformed.length); i++) {
|
|
580
|
+
const orig = original[i];
|
|
581
|
+
const trans = transformed[i];
|
|
582
|
+
if (!this.validateItem(orig, trans)) {
|
|
583
|
+
issues.push(`Item ${i} validation failed`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
valid: issues.length === 0,
|
|
588
|
+
issues
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Validate a single item transformation
|
|
593
|
+
*/
|
|
594
|
+
validateItem(original, transformed) {
|
|
595
|
+
if (original === null || original === void 0) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
if (typeof original === "object" && typeof transformed !== "object") {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Get transformation history
|
|
605
|
+
*/
|
|
606
|
+
getTransformationHistory() {
|
|
607
|
+
return [...this.transformationHistory];
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Get transformation statistics
|
|
611
|
+
*/
|
|
612
|
+
getStatistics() {
|
|
613
|
+
const totalTransformed = this.transformationHistory.reduce(
|
|
614
|
+
(sum, r) => sum + r.itemsTransformed,
|
|
615
|
+
0
|
|
616
|
+
);
|
|
617
|
+
const totalFailed = this.transformationHistory.reduce(
|
|
618
|
+
(sum, r) => sum + r.itemsFailed,
|
|
619
|
+
0
|
|
620
|
+
);
|
|
621
|
+
const totalDuration = this.transformationHistory.reduce(
|
|
622
|
+
(sum, r) => sum + r.duration,
|
|
623
|
+
0
|
|
624
|
+
);
|
|
625
|
+
return {
|
|
626
|
+
totalBatches: this.transformationHistory.length,
|
|
627
|
+
totalTransformed,
|
|
628
|
+
totalFailed,
|
|
629
|
+
successRate: totalTransformed + totalFailed > 0 ? totalTransformed / (totalTransformed + totalFailed) * 100 : 0,
|
|
630
|
+
totalDurationMs: totalDuration,
|
|
631
|
+
averageBatchDurationMs: this.transformationHistory.length > 0 ? totalDuration / this.transformationHistory.length : 0
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Get registered rules
|
|
636
|
+
*/
|
|
637
|
+
getRules() {
|
|
638
|
+
return Array.from(this.rules.values());
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Get rule for field
|
|
642
|
+
*/
|
|
643
|
+
getRule(field) {
|
|
644
|
+
return this.rules.get(field);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Clear all rules (for testing)
|
|
648
|
+
*/
|
|
649
|
+
clearRules() {
|
|
650
|
+
this.rules.clear();
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Clear history (for testing)
|
|
654
|
+
*/
|
|
655
|
+
clearHistory() {
|
|
656
|
+
this.transformationHistory = [];
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Clear all state (for testing)
|
|
660
|
+
*/
|
|
661
|
+
clear() {
|
|
662
|
+
this.clearRules();
|
|
663
|
+
this.clearHistory();
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// src/versioning/MigrationTracker.ts
|
|
668
|
+
var MigrationTracker = class {
|
|
669
|
+
migrations = [];
|
|
670
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
671
|
+
/**
|
|
672
|
+
* Track a new migration
|
|
673
|
+
*/
|
|
674
|
+
recordMigration(record) {
|
|
675
|
+
this.migrations.push({ ...record });
|
|
676
|
+
logger.debug("[MigrationTracker] Migration recorded", {
|
|
677
|
+
id: record.id,
|
|
678
|
+
migrationId: record.migrationId,
|
|
679
|
+
version: record.version,
|
|
680
|
+
status: record.status
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Track migration with snapshot
|
|
685
|
+
*/
|
|
686
|
+
trackMigration(migrationId, version, beforeHash, afterHash, itemCount, duration, itemsAffected, appliedBy = "system") {
|
|
687
|
+
const record = {
|
|
688
|
+
id: `${migrationId}-${Date.now()}`,
|
|
689
|
+
migrationId,
|
|
690
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
691
|
+
version,
|
|
692
|
+
direction: "up",
|
|
693
|
+
status: "applied",
|
|
694
|
+
duration,
|
|
695
|
+
itemsAffected,
|
|
696
|
+
dataSnapshot: {
|
|
697
|
+
beforeHash,
|
|
698
|
+
afterHash,
|
|
699
|
+
itemCount
|
|
700
|
+
},
|
|
701
|
+
appliedBy
|
|
702
|
+
};
|
|
703
|
+
this.recordMigration(record);
|
|
704
|
+
this.snapshots.set(record.id, {
|
|
705
|
+
beforeHash,
|
|
706
|
+
afterHash,
|
|
707
|
+
itemCount
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Get all migration records
|
|
712
|
+
*/
|
|
713
|
+
getMigrations() {
|
|
714
|
+
return this.migrations.map((m) => ({ ...m }));
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Get migrations for a specific version
|
|
718
|
+
*/
|
|
719
|
+
getMigrationsForVersion(version) {
|
|
720
|
+
return this.migrations.filter((m) => m.version === version);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Get migration by ID
|
|
724
|
+
*/
|
|
725
|
+
getMigration(id) {
|
|
726
|
+
return this.migrations.find((m) => m.id === id);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Check if can rollback
|
|
730
|
+
*/
|
|
731
|
+
canRollback(fromVersion, toVersion) {
|
|
732
|
+
const fromIndex = this.migrations.findIndex((m) => m.version === fromVersion);
|
|
733
|
+
const toIndex = this.migrations.findIndex((m) => m.version === toVersion);
|
|
734
|
+
if (fromIndex === -1 || toIndex === -1) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
if (toIndex >= fromIndex) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
for (let i = fromIndex; i > toIndex; i--) {
|
|
741
|
+
if (!this.migrations[i]?.dataSnapshot) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Get rollback path
|
|
749
|
+
*/
|
|
750
|
+
getRollbackPath(fromVersion, toVersion) {
|
|
751
|
+
const canRollback = this.canRollback(fromVersion, toVersion);
|
|
752
|
+
const path = [];
|
|
753
|
+
const affectedVersions = [];
|
|
754
|
+
let estimatedDuration = 0;
|
|
755
|
+
if (canRollback) {
|
|
756
|
+
const fromIndex = this.migrations.findIndex((m) => m.version === fromVersion);
|
|
757
|
+
const toIndex = this.migrations.findIndex((m) => m.version === toVersion);
|
|
758
|
+
for (let i = fromIndex; i > toIndex; i--) {
|
|
759
|
+
const migration = this.migrations[i];
|
|
760
|
+
if (migration) {
|
|
761
|
+
path.push(migration.migrationId);
|
|
762
|
+
affectedVersions.push(migration.version);
|
|
763
|
+
estimatedDuration += migration.duration;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
path,
|
|
769
|
+
canRollback,
|
|
770
|
+
affectedVersions,
|
|
771
|
+
estimatedDuration
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Get applied migrations
|
|
776
|
+
*/
|
|
777
|
+
getAppliedMigrations() {
|
|
778
|
+
return this.migrations.filter((m) => m.status === "applied");
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Get failed migrations
|
|
782
|
+
*/
|
|
783
|
+
getFailedMigrations() {
|
|
784
|
+
return this.migrations.filter((m) => m.status === "failed");
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Get pending migrations
|
|
788
|
+
*/
|
|
789
|
+
getPendingMigrations() {
|
|
790
|
+
return this.migrations.filter((m) => m.status === "pending");
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Get latest migration
|
|
794
|
+
*/
|
|
795
|
+
getLatestMigration() {
|
|
796
|
+
return this.migrations[this.migrations.length - 1];
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Get migration timeline
|
|
800
|
+
*/
|
|
801
|
+
getTimeline() {
|
|
802
|
+
return this.migrations.map((m) => ({
|
|
803
|
+
timestamp: m.timestamp,
|
|
804
|
+
version: m.version,
|
|
805
|
+
status: m.status
|
|
806
|
+
}));
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get migration statistics
|
|
810
|
+
*/
|
|
811
|
+
getStatistics() {
|
|
812
|
+
const applied = this.migrations.filter((m) => m.status === "applied").length;
|
|
813
|
+
const failed = this.migrations.filter((m) => m.status === "failed").length;
|
|
814
|
+
const pending = this.migrations.filter((m) => m.status === "pending").length;
|
|
815
|
+
const rolledBack = this.migrations.filter((m) => m.status === "rolled-back").length;
|
|
816
|
+
const totalDuration = this.migrations.reduce((sum, m) => sum + m.duration, 0);
|
|
817
|
+
const totalAffected = this.migrations.reduce((sum, m) => sum + m.itemsAffected, 0);
|
|
818
|
+
return {
|
|
819
|
+
total: this.migrations.length,
|
|
820
|
+
applied,
|
|
821
|
+
failed,
|
|
822
|
+
pending,
|
|
823
|
+
rolledBack,
|
|
824
|
+
successRate: this.migrations.length > 0 ? applied / this.migrations.length * 100 : 0,
|
|
825
|
+
totalDurationMs: totalDuration,
|
|
826
|
+
averageDurationMs: this.migrations.length > 0 ? totalDuration / this.migrations.length : 0,
|
|
827
|
+
totalItemsAffected: totalAffected
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Get audit trail
|
|
832
|
+
*/
|
|
833
|
+
getAuditTrail(migrationId) {
|
|
834
|
+
const filtered = migrationId ? this.migrations.filter((m) => m.migrationId === migrationId) : this.migrations;
|
|
835
|
+
return filtered.map((m) => ({
|
|
836
|
+
id: m.id,
|
|
837
|
+
timestamp: m.timestamp,
|
|
838
|
+
migrationId: m.migrationId,
|
|
839
|
+
version: m.version,
|
|
840
|
+
status: m.status,
|
|
841
|
+
appliedBy: m.appliedBy,
|
|
842
|
+
duration: m.duration,
|
|
843
|
+
itemsAffected: m.itemsAffected,
|
|
844
|
+
error: m.errorMessage
|
|
845
|
+
}));
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get data snapshot for recovery
|
|
849
|
+
*/
|
|
850
|
+
getSnapshot(recordId) {
|
|
851
|
+
return this.snapshots.get(recordId);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Update migration status
|
|
855
|
+
*/
|
|
856
|
+
updateMigrationStatus(recordId, status, error) {
|
|
857
|
+
const migration = this.migrations.find((m) => m.id === recordId);
|
|
858
|
+
if (migration) {
|
|
859
|
+
migration.status = status;
|
|
860
|
+
if (error) {
|
|
861
|
+
migration.errorMessage = error;
|
|
862
|
+
}
|
|
863
|
+
logger.debug("[MigrationTracker] Migration status updated", {
|
|
864
|
+
recordId,
|
|
865
|
+
status,
|
|
866
|
+
hasError: !!error
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Clear history (for testing)
|
|
872
|
+
*/
|
|
873
|
+
clear() {
|
|
874
|
+
this.migrations = [];
|
|
875
|
+
this.snapshots.clear();
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Get total migrations tracked
|
|
879
|
+
*/
|
|
880
|
+
getTotalMigrations() {
|
|
881
|
+
return this.migrations.length;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Find migrations by time range
|
|
885
|
+
*/
|
|
886
|
+
getMigrationsByTimeRange(startTime, endTime) {
|
|
887
|
+
const start = new Date(startTime).getTime();
|
|
888
|
+
const end = new Date(endTime).getTime();
|
|
889
|
+
return this.migrations.filter((m) => {
|
|
890
|
+
const time = new Date(m.timestamp).getTime();
|
|
891
|
+
return time >= start && time <= end;
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
var SyncCoordinator = class extends eventemitter3.EventEmitter {
|
|
896
|
+
nodes = /* @__PURE__ */ new Map();
|
|
897
|
+
sessions = /* @__PURE__ */ new Map();
|
|
898
|
+
syncEvents = [];
|
|
899
|
+
nodeHeartbeats = /* @__PURE__ */ new Map();
|
|
900
|
+
heartbeatInterval = null;
|
|
901
|
+
// Crypto support
|
|
902
|
+
cryptoProvider = null;
|
|
903
|
+
nodesByDID = /* @__PURE__ */ new Map();
|
|
904
|
+
// DID -> nodeId
|
|
905
|
+
constructor() {
|
|
906
|
+
super();
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Configure cryptographic provider for authenticated sync
|
|
910
|
+
*/
|
|
911
|
+
configureCrypto(provider) {
|
|
912
|
+
this.cryptoProvider = provider;
|
|
913
|
+
logger.debug("[SyncCoordinator] Crypto configured", {
|
|
914
|
+
initialized: provider.isInitialized()
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Check if crypto is configured
|
|
919
|
+
*/
|
|
920
|
+
isCryptoEnabled() {
|
|
921
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Register a node with DID-based identity
|
|
925
|
+
*/
|
|
926
|
+
async registerAuthenticatedNode(nodeInfo) {
|
|
927
|
+
const node = {
|
|
928
|
+
...nodeInfo
|
|
929
|
+
};
|
|
930
|
+
this.nodes.set(node.id, node);
|
|
931
|
+
this.nodeHeartbeats.set(node.id, Date.now());
|
|
932
|
+
this.nodesByDID.set(nodeInfo.did, node.id);
|
|
933
|
+
if (this.cryptoProvider) {
|
|
934
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
935
|
+
id: node.id,
|
|
936
|
+
did: nodeInfo.did,
|
|
937
|
+
publicSigningKey: nodeInfo.publicSigningKey,
|
|
938
|
+
publicEncryptionKey: nodeInfo.publicEncryptionKey
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
const event = {
|
|
942
|
+
type: "node-joined",
|
|
943
|
+
nodeId: node.id,
|
|
944
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
945
|
+
data: { did: nodeInfo.did, authenticated: true }
|
|
946
|
+
};
|
|
947
|
+
this.syncEvents.push(event);
|
|
948
|
+
this.emit("node-joined", node);
|
|
949
|
+
logger.debug("[SyncCoordinator] Authenticated node registered", {
|
|
950
|
+
nodeId: node.id,
|
|
951
|
+
did: nodeInfo.did,
|
|
952
|
+
version: node.version
|
|
953
|
+
});
|
|
954
|
+
return node;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Get node by DID
|
|
958
|
+
*/
|
|
959
|
+
getNodeByDID(did) {
|
|
960
|
+
const nodeId = this.nodesByDID.get(did);
|
|
961
|
+
if (!nodeId) return void 0;
|
|
962
|
+
return this.nodes.get(nodeId);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Get all authenticated nodes (nodes with DIDs)
|
|
966
|
+
*/
|
|
967
|
+
getAuthenticatedNodes() {
|
|
968
|
+
return Array.from(this.nodes.values()).filter((n) => n.did);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Create an authenticated sync session with UCAN-based authorization
|
|
972
|
+
*/
|
|
973
|
+
async createAuthenticatedSyncSession(initiatorDID, participantDIDs, options) {
|
|
974
|
+
const initiatorNodeId = this.nodesByDID.get(initiatorDID);
|
|
975
|
+
if (!initiatorNodeId) {
|
|
976
|
+
throw new Error(`Initiator node with DID ${initiatorDID} not found`);
|
|
977
|
+
}
|
|
978
|
+
const participantIds = [];
|
|
979
|
+
for (const did of participantDIDs) {
|
|
980
|
+
const nodeId = this.nodesByDID.get(did);
|
|
981
|
+
if (nodeId) {
|
|
982
|
+
participantIds.push(nodeId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
let sessionToken;
|
|
986
|
+
if (this.cryptoProvider && this.cryptoProvider.isInitialized()) {
|
|
987
|
+
const capabilities = (options?.requiredCapabilities || ["aeon:sync:read", "aeon:sync:write"]).map((cap) => ({ can: cap, with: "*" }));
|
|
988
|
+
if (participantDIDs.length > 0) {
|
|
989
|
+
sessionToken = await this.cryptoProvider.createUCAN(
|
|
990
|
+
participantDIDs[0],
|
|
991
|
+
capabilities,
|
|
992
|
+
{ expirationSeconds: 3600 }
|
|
993
|
+
// 1 hour
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const session = {
|
|
998
|
+
id: `sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
999
|
+
initiatorId: initiatorNodeId,
|
|
1000
|
+
participantIds,
|
|
1001
|
+
status: "pending",
|
|
1002
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1003
|
+
itemsSynced: 0,
|
|
1004
|
+
itemsFailed: 0,
|
|
1005
|
+
conflictsDetected: 0,
|
|
1006
|
+
initiatorDID,
|
|
1007
|
+
participantDIDs,
|
|
1008
|
+
encryptionMode: options?.encryptionMode || "none",
|
|
1009
|
+
requiredCapabilities: options?.requiredCapabilities,
|
|
1010
|
+
sessionToken
|
|
1011
|
+
};
|
|
1012
|
+
this.sessions.set(session.id, session);
|
|
1013
|
+
const event = {
|
|
1014
|
+
type: "sync-started",
|
|
1015
|
+
sessionId: session.id,
|
|
1016
|
+
nodeId: initiatorNodeId,
|
|
1017
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1018
|
+
data: {
|
|
1019
|
+
authenticated: true,
|
|
1020
|
+
initiatorDID,
|
|
1021
|
+
participantCount: participantDIDs.length,
|
|
1022
|
+
encryptionMode: session.encryptionMode
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
this.syncEvents.push(event);
|
|
1026
|
+
this.emit("sync-started", session);
|
|
1027
|
+
logger.debug("[SyncCoordinator] Authenticated sync session created", {
|
|
1028
|
+
sessionId: session.id,
|
|
1029
|
+
initiatorDID,
|
|
1030
|
+
participants: participantDIDs.length,
|
|
1031
|
+
encryptionMode: session.encryptionMode
|
|
1032
|
+
});
|
|
1033
|
+
return session;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Verify a node's UCAN capabilities for a session
|
|
1037
|
+
*/
|
|
1038
|
+
async verifyNodeCapabilities(sessionId, nodeDID, token) {
|
|
1039
|
+
if (!this.cryptoProvider) {
|
|
1040
|
+
return { authorized: true };
|
|
1041
|
+
}
|
|
1042
|
+
const session = this.sessions.get(sessionId);
|
|
1043
|
+
if (!session) {
|
|
1044
|
+
return { authorized: false, error: `Session ${sessionId} not found` };
|
|
1045
|
+
}
|
|
1046
|
+
const result = await this.cryptoProvider.verifyUCAN(token, {
|
|
1047
|
+
requiredCapabilities: session.requiredCapabilities?.map((cap) => ({
|
|
1048
|
+
can: cap,
|
|
1049
|
+
with: "*"
|
|
1050
|
+
}))
|
|
1051
|
+
});
|
|
1052
|
+
if (!result.authorized) {
|
|
1053
|
+
logger.warn("[SyncCoordinator] Node capability verification failed", {
|
|
1054
|
+
sessionId,
|
|
1055
|
+
nodeDID,
|
|
1056
|
+
error: result.error
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Register a node in the cluster
|
|
1063
|
+
*/
|
|
1064
|
+
registerNode(node) {
|
|
1065
|
+
this.nodes.set(node.id, node);
|
|
1066
|
+
this.nodeHeartbeats.set(node.id, Date.now());
|
|
1067
|
+
const event = {
|
|
1068
|
+
type: "node-joined",
|
|
1069
|
+
nodeId: node.id,
|
|
1070
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1071
|
+
};
|
|
1072
|
+
this.syncEvents.push(event);
|
|
1073
|
+
this.emit("node-joined", node);
|
|
1074
|
+
logger.debug("[SyncCoordinator] Node registered", {
|
|
1075
|
+
nodeId: node.id,
|
|
1076
|
+
address: node.address,
|
|
1077
|
+
version: node.version
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Deregister a node from the cluster
|
|
1082
|
+
*/
|
|
1083
|
+
deregisterNode(nodeId) {
|
|
1084
|
+
const node = this.nodes.get(nodeId);
|
|
1085
|
+
if (!node) {
|
|
1086
|
+
throw new Error(`Node ${nodeId} not found`);
|
|
1087
|
+
}
|
|
1088
|
+
this.nodes.delete(nodeId);
|
|
1089
|
+
this.nodeHeartbeats.delete(nodeId);
|
|
1090
|
+
const event = {
|
|
1091
|
+
type: "node-left",
|
|
1092
|
+
nodeId,
|
|
1093
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1094
|
+
};
|
|
1095
|
+
this.syncEvents.push(event);
|
|
1096
|
+
this.emit("node-left", node);
|
|
1097
|
+
logger.debug("[SyncCoordinator] Node deregistered", { nodeId });
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Create a new sync session
|
|
1101
|
+
*/
|
|
1102
|
+
createSyncSession(initiatorId, participantIds) {
|
|
1103
|
+
const node = this.nodes.get(initiatorId);
|
|
1104
|
+
if (!node) {
|
|
1105
|
+
throw new Error(`Initiator node ${initiatorId} not found`);
|
|
1106
|
+
}
|
|
1107
|
+
const session = {
|
|
1108
|
+
id: `sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1109
|
+
initiatorId,
|
|
1110
|
+
participantIds,
|
|
1111
|
+
status: "pending",
|
|
1112
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1113
|
+
itemsSynced: 0,
|
|
1114
|
+
itemsFailed: 0,
|
|
1115
|
+
conflictsDetected: 0
|
|
1116
|
+
};
|
|
1117
|
+
this.sessions.set(session.id, session);
|
|
1118
|
+
const event = {
|
|
1119
|
+
type: "sync-started",
|
|
1120
|
+
sessionId: session.id,
|
|
1121
|
+
nodeId: initiatorId,
|
|
1122
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1123
|
+
};
|
|
1124
|
+
this.syncEvents.push(event);
|
|
1125
|
+
this.emit("sync-started", session);
|
|
1126
|
+
logger.debug("[SyncCoordinator] Sync session created", {
|
|
1127
|
+
sessionId: session.id,
|
|
1128
|
+
initiator: initiatorId,
|
|
1129
|
+
participants: participantIds.length
|
|
1130
|
+
});
|
|
1131
|
+
return session;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Update sync session
|
|
1135
|
+
*/
|
|
1136
|
+
updateSyncSession(sessionId, updates) {
|
|
1137
|
+
const session = this.sessions.get(sessionId);
|
|
1138
|
+
if (!session) {
|
|
1139
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1140
|
+
}
|
|
1141
|
+
Object.assign(session, updates);
|
|
1142
|
+
if (updates.status === "completed" || updates.status === "failed") {
|
|
1143
|
+
session.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1144
|
+
const event = {
|
|
1145
|
+
type: "sync-completed",
|
|
1146
|
+
sessionId,
|
|
1147
|
+
nodeId: session.initiatorId,
|
|
1148
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1149
|
+
data: { status: updates.status, itemsSynced: session.itemsSynced }
|
|
1150
|
+
};
|
|
1151
|
+
this.syncEvents.push(event);
|
|
1152
|
+
this.emit("sync-completed", session);
|
|
1153
|
+
}
|
|
1154
|
+
logger.debug("[SyncCoordinator] Sync session updated", {
|
|
1155
|
+
sessionId,
|
|
1156
|
+
status: session.status,
|
|
1157
|
+
itemsSynced: session.itemsSynced
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Record a conflict during sync
|
|
1162
|
+
*/
|
|
1163
|
+
recordConflict(sessionId, nodeId, conflictData) {
|
|
1164
|
+
const session = this.sessions.get(sessionId);
|
|
1165
|
+
if (session) {
|
|
1166
|
+
session.conflictsDetected++;
|
|
1167
|
+
const event = {
|
|
1168
|
+
type: "conflict-detected",
|
|
1169
|
+
sessionId,
|
|
1170
|
+
nodeId,
|
|
1171
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1172
|
+
data: conflictData
|
|
1173
|
+
};
|
|
1174
|
+
this.syncEvents.push(event);
|
|
1175
|
+
this.emit("conflict-detected", { session, nodeId, conflictData });
|
|
1176
|
+
logger.debug("[SyncCoordinator] Conflict recorded", {
|
|
1177
|
+
sessionId,
|
|
1178
|
+
nodeId,
|
|
1179
|
+
totalConflicts: session.conflictsDetected
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Update node status
|
|
1185
|
+
*/
|
|
1186
|
+
updateNodeStatus(nodeId, status) {
|
|
1187
|
+
const node = this.nodes.get(nodeId);
|
|
1188
|
+
if (!node) {
|
|
1189
|
+
throw new Error(`Node ${nodeId} not found`);
|
|
1190
|
+
}
|
|
1191
|
+
node.status = status;
|
|
1192
|
+
this.nodeHeartbeats.set(nodeId, Date.now());
|
|
1193
|
+
logger.debug("[SyncCoordinator] Node status updated", {
|
|
1194
|
+
nodeId,
|
|
1195
|
+
status
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Record heartbeat from node
|
|
1200
|
+
*/
|
|
1201
|
+
recordHeartbeat(nodeId) {
|
|
1202
|
+
const node = this.nodes.get(nodeId);
|
|
1203
|
+
if (!node) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
node.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
1207
|
+
this.nodeHeartbeats.set(nodeId, Date.now());
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get all nodes
|
|
1211
|
+
*/
|
|
1212
|
+
getNodes() {
|
|
1213
|
+
return Array.from(this.nodes.values());
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Get node by ID
|
|
1217
|
+
*/
|
|
1218
|
+
getNode(nodeId) {
|
|
1219
|
+
return this.nodes.get(nodeId);
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Get online nodes
|
|
1223
|
+
*/
|
|
1224
|
+
getOnlineNodes() {
|
|
1225
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "online");
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Get nodes by capability
|
|
1229
|
+
*/
|
|
1230
|
+
getNodesByCapability(capability) {
|
|
1231
|
+
return Array.from(this.nodes.values()).filter(
|
|
1232
|
+
(n) => n.capabilities.includes(capability)
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Get sync session
|
|
1237
|
+
*/
|
|
1238
|
+
getSyncSession(sessionId) {
|
|
1239
|
+
return this.sessions.get(sessionId);
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Get all sync sessions
|
|
1243
|
+
*/
|
|
1244
|
+
getAllSyncSessions() {
|
|
1245
|
+
return Array.from(this.sessions.values());
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Get active sync sessions
|
|
1249
|
+
*/
|
|
1250
|
+
getActiveSyncSessions() {
|
|
1251
|
+
return Array.from(this.sessions.values()).filter((s) => s.status === "active");
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Get sessions for a node
|
|
1255
|
+
*/
|
|
1256
|
+
getSessionsForNode(nodeId) {
|
|
1257
|
+
return Array.from(this.sessions.values()).filter(
|
|
1258
|
+
(s) => s.initiatorId === nodeId || s.participantIds.includes(nodeId)
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Get sync statistics
|
|
1263
|
+
*/
|
|
1264
|
+
getStatistics() {
|
|
1265
|
+
const sessions = Array.from(this.sessions.values());
|
|
1266
|
+
const completed = sessions.filter((s) => s.status === "completed").length;
|
|
1267
|
+
const failed = sessions.filter((s) => s.status === "failed").length;
|
|
1268
|
+
const active = sessions.filter((s) => s.status === "active").length;
|
|
1269
|
+
const totalItemsSynced = sessions.reduce((sum, s) => sum + s.itemsSynced, 0);
|
|
1270
|
+
const totalConflicts = sessions.reduce((sum, s) => sum + s.conflictsDetected, 0);
|
|
1271
|
+
return {
|
|
1272
|
+
totalNodes: this.nodes.size,
|
|
1273
|
+
onlineNodes: this.getOnlineNodes().length,
|
|
1274
|
+
offlineNodes: this.nodes.size - this.getOnlineNodes().length,
|
|
1275
|
+
totalSessions: sessions.length,
|
|
1276
|
+
activeSessions: active,
|
|
1277
|
+
completedSessions: completed,
|
|
1278
|
+
failedSessions: failed,
|
|
1279
|
+
successRate: sessions.length > 0 ? completed / sessions.length * 100 : 0,
|
|
1280
|
+
totalItemsSynced,
|
|
1281
|
+
totalConflicts,
|
|
1282
|
+
averageConflictsPerSession: sessions.length > 0 ? totalConflicts / sessions.length : 0
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Get sync events
|
|
1287
|
+
*/
|
|
1288
|
+
getSyncEvents(limit) {
|
|
1289
|
+
const events = [...this.syncEvents];
|
|
1290
|
+
if (limit) {
|
|
1291
|
+
return events.slice(-limit);
|
|
1292
|
+
}
|
|
1293
|
+
return events;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Get sync events for session
|
|
1297
|
+
*/
|
|
1298
|
+
getSessionEvents(sessionId) {
|
|
1299
|
+
return this.syncEvents.filter((e) => e.sessionId === sessionId);
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Check node health
|
|
1303
|
+
*/
|
|
1304
|
+
getNodeHealth() {
|
|
1305
|
+
const health = {};
|
|
1306
|
+
for (const [nodeId, lastHeartbeat] of this.nodeHeartbeats) {
|
|
1307
|
+
const now = Date.now();
|
|
1308
|
+
const downtime = now - lastHeartbeat;
|
|
1309
|
+
const isHealthy = downtime < 3e4;
|
|
1310
|
+
health[nodeId] = {
|
|
1311
|
+
isHealthy,
|
|
1312
|
+
downtime
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
return health;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Start heartbeat monitoring
|
|
1319
|
+
*/
|
|
1320
|
+
startHeartbeatMonitoring(interval = 5e3) {
|
|
1321
|
+
if (this.heartbeatInterval) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
this.heartbeatInterval = setInterval(() => {
|
|
1325
|
+
const health = this.getNodeHealth();
|
|
1326
|
+
for (const [nodeId, { isHealthy }] of Object.entries(health)) {
|
|
1327
|
+
const node = this.nodes.get(nodeId);
|
|
1328
|
+
if (!node) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
const newStatus = isHealthy ? "online" : "offline";
|
|
1332
|
+
if (node.status !== newStatus) {
|
|
1333
|
+
this.updateNodeStatus(nodeId, newStatus);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}, interval);
|
|
1337
|
+
logger.debug("[SyncCoordinator] Heartbeat monitoring started", { interval });
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Stop heartbeat monitoring
|
|
1341
|
+
*/
|
|
1342
|
+
stopHeartbeatMonitoring() {
|
|
1343
|
+
if (this.heartbeatInterval) {
|
|
1344
|
+
clearInterval(this.heartbeatInterval);
|
|
1345
|
+
this.heartbeatInterval = null;
|
|
1346
|
+
logger.debug("[SyncCoordinator] Heartbeat monitoring stopped");
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Clear all state (for testing)
|
|
1351
|
+
*/
|
|
1352
|
+
clear() {
|
|
1353
|
+
this.nodes.clear();
|
|
1354
|
+
this.sessions.clear();
|
|
1355
|
+
this.syncEvents = [];
|
|
1356
|
+
this.nodeHeartbeats.clear();
|
|
1357
|
+
this.nodesByDID.clear();
|
|
1358
|
+
this.cryptoProvider = null;
|
|
1359
|
+
this.stopHeartbeatMonitoring();
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Get the crypto provider (for advanced usage)
|
|
1363
|
+
*/
|
|
1364
|
+
getCryptoProvider() {
|
|
1365
|
+
return this.cryptoProvider;
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// src/distributed/ReplicationManager.ts
|
|
1370
|
+
var ReplicationManager = class {
|
|
1371
|
+
replicas = /* @__PURE__ */ new Map();
|
|
1372
|
+
policies = /* @__PURE__ */ new Map();
|
|
1373
|
+
replicationEvents = [];
|
|
1374
|
+
syncStatus = /* @__PURE__ */ new Map();
|
|
1375
|
+
// Crypto support
|
|
1376
|
+
cryptoProvider = null;
|
|
1377
|
+
replicasByDID = /* @__PURE__ */ new Map();
|
|
1378
|
+
// DID -> replicaId
|
|
1379
|
+
/**
|
|
1380
|
+
* Configure cryptographic provider for encrypted replication
|
|
1381
|
+
*/
|
|
1382
|
+
configureCrypto(provider) {
|
|
1383
|
+
this.cryptoProvider = provider;
|
|
1384
|
+
logger.debug("[ReplicationManager] Crypto configured", {
|
|
1385
|
+
initialized: provider.isInitialized()
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Check if crypto is configured
|
|
1390
|
+
*/
|
|
1391
|
+
isCryptoEnabled() {
|
|
1392
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Register an authenticated replica with DID
|
|
1396
|
+
*/
|
|
1397
|
+
async registerAuthenticatedReplica(replica, encrypted = false) {
|
|
1398
|
+
const authenticatedReplica = {
|
|
1399
|
+
...replica,
|
|
1400
|
+
encrypted
|
|
1401
|
+
};
|
|
1402
|
+
this.replicas.set(replica.id, authenticatedReplica);
|
|
1403
|
+
this.replicasByDID.set(replica.did, replica.id);
|
|
1404
|
+
if (!this.syncStatus.has(replica.nodeId)) {
|
|
1405
|
+
this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
|
|
1406
|
+
}
|
|
1407
|
+
if (this.cryptoProvider && replica.publicSigningKey) {
|
|
1408
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
1409
|
+
id: replica.nodeId,
|
|
1410
|
+
did: replica.did,
|
|
1411
|
+
publicSigningKey: replica.publicSigningKey,
|
|
1412
|
+
publicEncryptionKey: replica.publicEncryptionKey
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
const event = {
|
|
1416
|
+
type: "replica-added",
|
|
1417
|
+
replicaId: replica.id,
|
|
1418
|
+
nodeId: replica.nodeId,
|
|
1419
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1420
|
+
details: { did: replica.did, encrypted, authenticated: true }
|
|
1421
|
+
};
|
|
1422
|
+
this.replicationEvents.push(event);
|
|
1423
|
+
logger.debug("[ReplicationManager] Authenticated replica registered", {
|
|
1424
|
+
replicaId: replica.id,
|
|
1425
|
+
did: replica.did,
|
|
1426
|
+
encrypted
|
|
1427
|
+
});
|
|
1428
|
+
return authenticatedReplica;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Get replica by DID
|
|
1432
|
+
*/
|
|
1433
|
+
getReplicaByDID(did) {
|
|
1434
|
+
const replicaId = this.replicasByDID.get(did);
|
|
1435
|
+
if (!replicaId) return void 0;
|
|
1436
|
+
return this.replicas.get(replicaId);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Get all encrypted replicas
|
|
1440
|
+
*/
|
|
1441
|
+
getEncryptedReplicas() {
|
|
1442
|
+
return Array.from(this.replicas.values()).filter((r) => r.encrypted);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Encrypt data for replication to a specific replica
|
|
1446
|
+
*/
|
|
1447
|
+
async encryptForReplica(data, targetReplicaDID) {
|
|
1448
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1449
|
+
throw new Error("Crypto provider not initialized");
|
|
1450
|
+
}
|
|
1451
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
1452
|
+
const encrypted = await this.cryptoProvider.encrypt(dataBytes, targetReplicaDID);
|
|
1453
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1454
|
+
return {
|
|
1455
|
+
ct: encrypted.ct,
|
|
1456
|
+
iv: encrypted.iv,
|
|
1457
|
+
tag: encrypted.tag,
|
|
1458
|
+
epk: encrypted.epk,
|
|
1459
|
+
senderDID: localDID || void 0,
|
|
1460
|
+
targetDID: targetReplicaDID,
|
|
1461
|
+
encryptedAt: encrypted.encryptedAt
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Decrypt data received from replication
|
|
1466
|
+
*/
|
|
1467
|
+
async decryptReplicationData(encrypted) {
|
|
1468
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1469
|
+
throw new Error("Crypto provider not initialized");
|
|
1470
|
+
}
|
|
1471
|
+
const decrypted = await this.cryptoProvider.decrypt(
|
|
1472
|
+
{
|
|
1473
|
+
alg: "ECIES-P256",
|
|
1474
|
+
ct: encrypted.ct,
|
|
1475
|
+
iv: encrypted.iv,
|
|
1476
|
+
tag: encrypted.tag,
|
|
1477
|
+
epk: encrypted.epk
|
|
1478
|
+
},
|
|
1479
|
+
encrypted.senderDID
|
|
1480
|
+
);
|
|
1481
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Create an encrypted replication policy
|
|
1485
|
+
*/
|
|
1486
|
+
createEncryptedPolicy(name, replicationFactor, consistencyLevel, encryptionMode, options) {
|
|
1487
|
+
const policy = {
|
|
1488
|
+
id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1489
|
+
name,
|
|
1490
|
+
replicationFactor,
|
|
1491
|
+
consistencyLevel,
|
|
1492
|
+
syncInterval: options?.syncInterval || 1e3,
|
|
1493
|
+
maxReplicationLag: options?.maxReplicationLag || 1e4,
|
|
1494
|
+
encryptionMode,
|
|
1495
|
+
requiredCapabilities: options?.requiredCapabilities
|
|
1496
|
+
};
|
|
1497
|
+
this.policies.set(policy.id, policy);
|
|
1498
|
+
logger.debug("[ReplicationManager] Encrypted policy created", {
|
|
1499
|
+
policyId: policy.id,
|
|
1500
|
+
name,
|
|
1501
|
+
replicationFactor,
|
|
1502
|
+
encryptionMode
|
|
1503
|
+
});
|
|
1504
|
+
return policy;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Verify a replica's capabilities via UCAN
|
|
1508
|
+
*/
|
|
1509
|
+
async verifyReplicaCapabilities(replicaDID, token, policyId) {
|
|
1510
|
+
if (!this.cryptoProvider) {
|
|
1511
|
+
return { authorized: true };
|
|
1512
|
+
}
|
|
1513
|
+
const policy = policyId ? this.policies.get(policyId) : void 0;
|
|
1514
|
+
const result = await this.cryptoProvider.verifyUCAN(token, {
|
|
1515
|
+
requiredCapabilities: policy?.requiredCapabilities?.map((cap) => ({
|
|
1516
|
+
can: cap,
|
|
1517
|
+
with: "*"
|
|
1518
|
+
}))
|
|
1519
|
+
});
|
|
1520
|
+
if (!result.authorized) {
|
|
1521
|
+
logger.warn("[ReplicationManager] Replica capability verification failed", {
|
|
1522
|
+
replicaDID,
|
|
1523
|
+
error: result.error
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
return result;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Register a replica
|
|
1530
|
+
*/
|
|
1531
|
+
registerReplica(replica) {
|
|
1532
|
+
this.replicas.set(replica.id, replica);
|
|
1533
|
+
if (!this.syncStatus.has(replica.nodeId)) {
|
|
1534
|
+
this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
|
|
1535
|
+
}
|
|
1536
|
+
const event = {
|
|
1537
|
+
type: "replica-added",
|
|
1538
|
+
replicaId: replica.id,
|
|
1539
|
+
nodeId: replica.nodeId,
|
|
1540
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1541
|
+
};
|
|
1542
|
+
this.replicationEvents.push(event);
|
|
1543
|
+
logger.debug("[ReplicationManager] Replica registered", {
|
|
1544
|
+
replicaId: replica.id,
|
|
1545
|
+
nodeId: replica.nodeId,
|
|
1546
|
+
status: replica.status
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Remove a replica
|
|
1551
|
+
*/
|
|
1552
|
+
removeReplica(replicaId) {
|
|
1553
|
+
const replica = this.replicas.get(replicaId);
|
|
1554
|
+
if (!replica) {
|
|
1555
|
+
throw new Error(`Replica ${replicaId} not found`);
|
|
1556
|
+
}
|
|
1557
|
+
this.replicas.delete(replicaId);
|
|
1558
|
+
const event = {
|
|
1559
|
+
type: "replica-removed",
|
|
1560
|
+
replicaId,
|
|
1561
|
+
nodeId: replica.nodeId,
|
|
1562
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1563
|
+
};
|
|
1564
|
+
this.replicationEvents.push(event);
|
|
1565
|
+
logger.debug("[ReplicationManager] Replica removed", { replicaId });
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Create a replication policy
|
|
1569
|
+
*/
|
|
1570
|
+
createPolicy(name, replicationFactor, consistencyLevel, syncInterval = 1e3, maxReplicationLag = 1e4) {
|
|
1571
|
+
const policy = {
|
|
1572
|
+
id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1573
|
+
name,
|
|
1574
|
+
replicationFactor,
|
|
1575
|
+
consistencyLevel,
|
|
1576
|
+
syncInterval,
|
|
1577
|
+
maxReplicationLag
|
|
1578
|
+
};
|
|
1579
|
+
this.policies.set(policy.id, policy);
|
|
1580
|
+
logger.debug("[ReplicationManager] Policy created", {
|
|
1581
|
+
policyId: policy.id,
|
|
1582
|
+
name,
|
|
1583
|
+
replicationFactor,
|
|
1584
|
+
consistencyLevel
|
|
1585
|
+
});
|
|
1586
|
+
return policy;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Update replica status
|
|
1590
|
+
*/
|
|
1591
|
+
updateReplicaStatus(replicaId, status, lagBytes = 0, lagMillis = 0) {
|
|
1592
|
+
const replica = this.replicas.get(replicaId);
|
|
1593
|
+
if (!replica) {
|
|
1594
|
+
throw new Error(`Replica ${replicaId} not found`);
|
|
1595
|
+
}
|
|
1596
|
+
replica.status = status;
|
|
1597
|
+
replica.lagBytes = lagBytes;
|
|
1598
|
+
replica.lagMillis = lagMillis;
|
|
1599
|
+
replica.lastSyncTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1600
|
+
const event = {
|
|
1601
|
+
type: status === "syncing" ? "replica-synced" : "sync-failed",
|
|
1602
|
+
replicaId,
|
|
1603
|
+
nodeId: replica.nodeId,
|
|
1604
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1605
|
+
details: { status, lagBytes, lagMillis }
|
|
1606
|
+
};
|
|
1607
|
+
this.replicationEvents.push(event);
|
|
1608
|
+
const syncStatus = this.syncStatus.get(replica.nodeId);
|
|
1609
|
+
if (syncStatus) {
|
|
1610
|
+
if (status === "syncing" || status === "secondary") {
|
|
1611
|
+
syncStatus.synced++;
|
|
1612
|
+
} else if (status === "failed") {
|
|
1613
|
+
syncStatus.failed++;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
logger.debug("[ReplicationManager] Replica status updated", {
|
|
1617
|
+
replicaId,
|
|
1618
|
+
status,
|
|
1619
|
+
lagBytes,
|
|
1620
|
+
lagMillis
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Get replicas for node
|
|
1625
|
+
*/
|
|
1626
|
+
getReplicasForNode(nodeId) {
|
|
1627
|
+
return Array.from(this.replicas.values()).filter((r) => r.nodeId === nodeId);
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Get healthy replicas
|
|
1631
|
+
*/
|
|
1632
|
+
getHealthyReplicas() {
|
|
1633
|
+
return Array.from(this.replicas.values()).filter(
|
|
1634
|
+
(r) => r.status === "secondary" || r.status === "primary"
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Get syncing replicas
|
|
1639
|
+
*/
|
|
1640
|
+
getSyncingReplicas() {
|
|
1641
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "syncing");
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Get failed replicas
|
|
1645
|
+
*/
|
|
1646
|
+
getFailedReplicas() {
|
|
1647
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "failed");
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Check replication health for policy
|
|
1651
|
+
*/
|
|
1652
|
+
checkReplicationHealth(policyId) {
|
|
1653
|
+
const policy = this.policies.get(policyId);
|
|
1654
|
+
if (!policy) {
|
|
1655
|
+
throw new Error(`Policy ${policyId} not found`);
|
|
1656
|
+
}
|
|
1657
|
+
const healthy = this.getHealthyReplicas();
|
|
1658
|
+
const maxLag = Math.max(0, ...healthy.map((r) => r.lagMillis));
|
|
1659
|
+
return {
|
|
1660
|
+
healthy: healthy.length >= policy.replicationFactor && maxLag <= policy.maxReplicationLag,
|
|
1661
|
+
replicasInPolicy: policy.replicationFactor,
|
|
1662
|
+
healthyReplicas: healthy.length,
|
|
1663
|
+
replicationLag: maxLag
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Get consistency level
|
|
1668
|
+
*/
|
|
1669
|
+
getConsistencyLevel(policyId) {
|
|
1670
|
+
const policy = this.policies.get(policyId);
|
|
1671
|
+
if (!policy) {
|
|
1672
|
+
return "eventual";
|
|
1673
|
+
}
|
|
1674
|
+
return policy.consistencyLevel;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Get replica
|
|
1678
|
+
*/
|
|
1679
|
+
getReplica(replicaId) {
|
|
1680
|
+
return this.replicas.get(replicaId);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Get all replicas
|
|
1684
|
+
*/
|
|
1685
|
+
getAllReplicas() {
|
|
1686
|
+
return Array.from(this.replicas.values());
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Get policy
|
|
1690
|
+
*/
|
|
1691
|
+
getPolicy(policyId) {
|
|
1692
|
+
return this.policies.get(policyId);
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Get all policies
|
|
1696
|
+
*/
|
|
1697
|
+
getAllPolicies() {
|
|
1698
|
+
return Array.from(this.policies.values());
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Get replication statistics
|
|
1702
|
+
*/
|
|
1703
|
+
getStatistics() {
|
|
1704
|
+
const healthy = this.getHealthyReplicas().length;
|
|
1705
|
+
const syncing = this.getSyncingReplicas().length;
|
|
1706
|
+
const failed = this.getFailedReplicas().length;
|
|
1707
|
+
const total = this.replicas.size;
|
|
1708
|
+
const replicationLags = Array.from(this.replicas.values()).map((r) => r.lagMillis);
|
|
1709
|
+
const avgLag = replicationLags.length > 0 ? replicationLags.reduce((a, b) => a + b) / replicationLags.length : 0;
|
|
1710
|
+
const maxLag = replicationLags.length > 0 ? Math.max(...replicationLags) : 0;
|
|
1711
|
+
return {
|
|
1712
|
+
totalReplicas: total,
|
|
1713
|
+
healthyReplicas: healthy,
|
|
1714
|
+
syncingReplicas: syncing,
|
|
1715
|
+
failedReplicas: failed,
|
|
1716
|
+
healthiness: total > 0 ? healthy / total * 100 : 0,
|
|
1717
|
+
averageReplicationLagMs: avgLag,
|
|
1718
|
+
maxReplicationLagMs: maxLag,
|
|
1719
|
+
totalPolicies: this.policies.size
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Get replication events
|
|
1724
|
+
*/
|
|
1725
|
+
getReplicationEvents(limit) {
|
|
1726
|
+
const events = [...this.replicationEvents];
|
|
1727
|
+
if (limit) {
|
|
1728
|
+
return events.slice(-limit);
|
|
1729
|
+
}
|
|
1730
|
+
return events;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Get sync status for node
|
|
1734
|
+
*/
|
|
1735
|
+
getSyncStatus(nodeId) {
|
|
1736
|
+
return this.syncStatus.get(nodeId) || { synced: 0, failed: 0 };
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Get replication lag distribution
|
|
1740
|
+
*/
|
|
1741
|
+
getReplicationLagDistribution() {
|
|
1742
|
+
const distribution = {
|
|
1743
|
+
"0-100ms": 0,
|
|
1744
|
+
"100-500ms": 0,
|
|
1745
|
+
"500-1000ms": 0,
|
|
1746
|
+
"1000+ms": 0
|
|
1747
|
+
};
|
|
1748
|
+
for (const replica of this.replicas.values()) {
|
|
1749
|
+
if (replica.lagMillis <= 100) {
|
|
1750
|
+
distribution["0-100ms"]++;
|
|
1751
|
+
} else if (replica.lagMillis <= 500) {
|
|
1752
|
+
distribution["100-500ms"]++;
|
|
1753
|
+
} else if (replica.lagMillis <= 1e3) {
|
|
1754
|
+
distribution["500-1000ms"]++;
|
|
1755
|
+
} else {
|
|
1756
|
+
distribution["1000+ms"]++;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return distribution;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Check if can satisfy consistency level
|
|
1763
|
+
*/
|
|
1764
|
+
canSatisfyConsistency(policyId, _requiredAcks) {
|
|
1765
|
+
const policy = this.policies.get(policyId);
|
|
1766
|
+
if (!policy) {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
const healthyCount = this.getHealthyReplicas().length;
|
|
1770
|
+
switch (policy.consistencyLevel) {
|
|
1771
|
+
case "eventual":
|
|
1772
|
+
return true;
|
|
1773
|
+
// Always achievable
|
|
1774
|
+
case "read-after-write":
|
|
1775
|
+
return healthyCount >= 1;
|
|
1776
|
+
case "strong":
|
|
1777
|
+
return healthyCount >= policy.replicationFactor;
|
|
1778
|
+
default:
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Clear all state (for testing)
|
|
1784
|
+
*/
|
|
1785
|
+
clear() {
|
|
1786
|
+
this.replicas.clear();
|
|
1787
|
+
this.policies.clear();
|
|
1788
|
+
this.replicationEvents = [];
|
|
1789
|
+
this.syncStatus.clear();
|
|
1790
|
+
this.replicasByDID.clear();
|
|
1791
|
+
this.cryptoProvider = null;
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Get the crypto provider (for advanced usage)
|
|
1795
|
+
*/
|
|
1796
|
+
getCryptoProvider() {
|
|
1797
|
+
return this.cryptoProvider;
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
// src/distributed/SyncProtocol.ts
|
|
1802
|
+
var SyncProtocol = class {
|
|
1803
|
+
version = "1.0.0";
|
|
1804
|
+
messageQueue = [];
|
|
1805
|
+
messageMap = /* @__PURE__ */ new Map();
|
|
1806
|
+
handshakes = /* @__PURE__ */ new Map();
|
|
1807
|
+
protocolErrors = [];
|
|
1808
|
+
messageCounter = 0;
|
|
1809
|
+
// Crypto support
|
|
1810
|
+
cryptoProvider = null;
|
|
1811
|
+
cryptoConfig = null;
|
|
1812
|
+
/**
|
|
1813
|
+
* Configure cryptographic provider for authenticated/encrypted messages
|
|
1814
|
+
*/
|
|
1815
|
+
configureCrypto(provider, config) {
|
|
1816
|
+
this.cryptoProvider = provider;
|
|
1817
|
+
this.cryptoConfig = {
|
|
1818
|
+
encryptionMode: config?.encryptionMode ?? "none",
|
|
1819
|
+
requireSignatures: config?.requireSignatures ?? false,
|
|
1820
|
+
requireCapabilities: config?.requireCapabilities ?? false,
|
|
1821
|
+
requiredCapabilities: config?.requiredCapabilities
|
|
1822
|
+
};
|
|
1823
|
+
logger.debug("[SyncProtocol] Crypto configured", {
|
|
1824
|
+
encryptionMode: this.cryptoConfig.encryptionMode,
|
|
1825
|
+
requireSignatures: this.cryptoConfig.requireSignatures,
|
|
1826
|
+
requireCapabilities: this.cryptoConfig.requireCapabilities
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Check if crypto is configured
|
|
1831
|
+
*/
|
|
1832
|
+
isCryptoEnabled() {
|
|
1833
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Get crypto configuration
|
|
1837
|
+
*/
|
|
1838
|
+
getCryptoConfig() {
|
|
1839
|
+
return this.cryptoConfig ? { ...this.cryptoConfig } : null;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Get protocol version
|
|
1843
|
+
*/
|
|
1844
|
+
getVersion() {
|
|
1845
|
+
return this.version;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Create authenticated handshake message with DID and keys
|
|
1849
|
+
*/
|
|
1850
|
+
async createAuthenticatedHandshake(capabilities, targetDID) {
|
|
1851
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1852
|
+
throw new Error("Crypto provider not initialized");
|
|
1853
|
+
}
|
|
1854
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1855
|
+
if (!localDID) {
|
|
1856
|
+
throw new Error("Local DID not available");
|
|
1857
|
+
}
|
|
1858
|
+
const publicInfo = await this.cryptoProvider.exportPublicIdentity();
|
|
1859
|
+
if (!publicInfo) {
|
|
1860
|
+
throw new Error("Cannot export public identity");
|
|
1861
|
+
}
|
|
1862
|
+
let ucan;
|
|
1863
|
+
if (targetDID && this.cryptoConfig?.requireCapabilities) {
|
|
1864
|
+
const caps = this.cryptoConfig.requiredCapabilities || [
|
|
1865
|
+
{ can: "aeon:sync:read", with: "*" },
|
|
1866
|
+
{ can: "aeon:sync:write", with: "*" }
|
|
1867
|
+
];
|
|
1868
|
+
ucan = await this.cryptoProvider.createUCAN(targetDID, caps);
|
|
1869
|
+
}
|
|
1870
|
+
const handshakePayload = {
|
|
1871
|
+
protocolVersion: this.version,
|
|
1872
|
+
nodeId: localDID,
|
|
1873
|
+
capabilities,
|
|
1874
|
+
state: "initiating",
|
|
1875
|
+
did: localDID,
|
|
1876
|
+
publicSigningKey: publicInfo.publicSigningKey,
|
|
1877
|
+
publicEncryptionKey: publicInfo.publicEncryptionKey,
|
|
1878
|
+
ucan
|
|
1879
|
+
};
|
|
1880
|
+
const message = {
|
|
1881
|
+
type: "handshake",
|
|
1882
|
+
version: this.version,
|
|
1883
|
+
sender: localDID,
|
|
1884
|
+
receiver: targetDID || "",
|
|
1885
|
+
messageId: this.generateMessageId(),
|
|
1886
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1887
|
+
payload: handshakePayload
|
|
1888
|
+
};
|
|
1889
|
+
if (this.cryptoConfig?.requireSignatures) {
|
|
1890
|
+
const signed = await this.cryptoProvider.signData(handshakePayload);
|
|
1891
|
+
message.auth = {
|
|
1892
|
+
senderDID: localDID,
|
|
1893
|
+
receiverDID: targetDID,
|
|
1894
|
+
signature: signed.signature
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
this.messageMap.set(message.messageId, message);
|
|
1898
|
+
this.messageQueue.push(message);
|
|
1899
|
+
logger.debug("[SyncProtocol] Authenticated handshake created", {
|
|
1900
|
+
messageId: message.messageId,
|
|
1901
|
+
did: localDID,
|
|
1902
|
+
capabilities: capabilities.length,
|
|
1903
|
+
hasUCAN: !!ucan
|
|
1904
|
+
});
|
|
1905
|
+
return message;
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Verify and process an authenticated handshake
|
|
1909
|
+
*/
|
|
1910
|
+
async verifyAuthenticatedHandshake(message) {
|
|
1911
|
+
if (message.type !== "handshake") {
|
|
1912
|
+
return { valid: false, error: "Message is not a handshake" };
|
|
1913
|
+
}
|
|
1914
|
+
const handshake = message.payload;
|
|
1915
|
+
if (!this.cryptoProvider || !this.cryptoConfig) {
|
|
1916
|
+
this.handshakes.set(message.sender, handshake);
|
|
1917
|
+
return { valid: true, handshake };
|
|
1918
|
+
}
|
|
1919
|
+
if (handshake.did && handshake.publicSigningKey) {
|
|
1920
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
1921
|
+
id: handshake.nodeId,
|
|
1922
|
+
did: handshake.did,
|
|
1923
|
+
publicSigningKey: handshake.publicSigningKey,
|
|
1924
|
+
publicEncryptionKey: handshake.publicEncryptionKey
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
if (this.cryptoConfig.requireSignatures && message.auth?.signature) {
|
|
1928
|
+
const signed = {
|
|
1929
|
+
payload: handshake,
|
|
1930
|
+
signature: message.auth.signature,
|
|
1931
|
+
signer: message.auth.senderDID || message.sender,
|
|
1932
|
+
algorithm: "ES256",
|
|
1933
|
+
signedAt: Date.now()
|
|
1934
|
+
};
|
|
1935
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
1936
|
+
if (!isValid) {
|
|
1937
|
+
logger.warn("[SyncProtocol] Handshake signature verification failed", {
|
|
1938
|
+
messageId: message.messageId,
|
|
1939
|
+
sender: message.sender
|
|
1940
|
+
});
|
|
1941
|
+
return { valid: false, error: "Invalid signature" };
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if (this.cryptoConfig.requireCapabilities && handshake.ucan) {
|
|
1945
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1946
|
+
const result = await this.cryptoProvider.verifyUCAN(handshake.ucan, {
|
|
1947
|
+
expectedAudience: localDID || void 0,
|
|
1948
|
+
requiredCapabilities: this.cryptoConfig.requiredCapabilities
|
|
1949
|
+
});
|
|
1950
|
+
if (!result.authorized) {
|
|
1951
|
+
logger.warn("[SyncProtocol] Handshake UCAN verification failed", {
|
|
1952
|
+
messageId: message.messageId,
|
|
1953
|
+
error: result.error
|
|
1954
|
+
});
|
|
1955
|
+
return { valid: false, error: result.error || "Unauthorized" };
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
this.handshakes.set(message.sender, handshake);
|
|
1959
|
+
logger.debug("[SyncProtocol] Authenticated handshake verified", {
|
|
1960
|
+
messageId: message.messageId,
|
|
1961
|
+
did: handshake.did
|
|
1962
|
+
});
|
|
1963
|
+
return { valid: true, handshake };
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Sign and optionally encrypt a message payload
|
|
1967
|
+
*/
|
|
1968
|
+
async signMessage(message, payload, encrypt = false) {
|
|
1969
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1970
|
+
throw new Error("Crypto provider not initialized");
|
|
1971
|
+
}
|
|
1972
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1973
|
+
const signed = await this.cryptoProvider.signData(payload);
|
|
1974
|
+
message.auth = {
|
|
1975
|
+
senderDID: localDID || void 0,
|
|
1976
|
+
receiverDID: message.receiver || void 0,
|
|
1977
|
+
signature: signed.signature,
|
|
1978
|
+
encrypted: false
|
|
1979
|
+
};
|
|
1980
|
+
if (encrypt && message.receiver && this.cryptoConfig?.encryptionMode !== "none") {
|
|
1981
|
+
const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
1982
|
+
const encrypted = await this.cryptoProvider.encrypt(payloadBytes, message.receiver);
|
|
1983
|
+
message.payload = encrypted;
|
|
1984
|
+
message.auth.encrypted = true;
|
|
1985
|
+
logger.debug("[SyncProtocol] Message encrypted", {
|
|
1986
|
+
messageId: message.messageId,
|
|
1987
|
+
recipient: message.receiver
|
|
1988
|
+
});
|
|
1989
|
+
} else {
|
|
1990
|
+
message.payload = payload;
|
|
1991
|
+
}
|
|
1992
|
+
return message;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Verify signature and optionally decrypt a message
|
|
1996
|
+
*/
|
|
1997
|
+
async verifyMessage(message) {
|
|
1998
|
+
if (!this.cryptoProvider || !message.auth) {
|
|
1999
|
+
return { valid: true, payload: message.payload };
|
|
2000
|
+
}
|
|
2001
|
+
let payload = message.payload;
|
|
2002
|
+
if (message.auth.encrypted && message.payload) {
|
|
2003
|
+
try {
|
|
2004
|
+
const encrypted = message.payload;
|
|
2005
|
+
const decrypted = await this.cryptoProvider.decrypt(
|
|
2006
|
+
encrypted,
|
|
2007
|
+
message.auth.senderDID
|
|
2008
|
+
);
|
|
2009
|
+
payload = JSON.parse(new TextDecoder().decode(decrypted));
|
|
2010
|
+
logger.debug("[SyncProtocol] Message decrypted", {
|
|
2011
|
+
messageId: message.messageId
|
|
2012
|
+
});
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
return {
|
|
2015
|
+
valid: false,
|
|
2016
|
+
error: `Decryption failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (message.auth.signature && message.auth.senderDID) {
|
|
2021
|
+
const signed = {
|
|
2022
|
+
payload,
|
|
2023
|
+
signature: message.auth.signature,
|
|
2024
|
+
signer: message.auth.senderDID,
|
|
2025
|
+
algorithm: "ES256",
|
|
2026
|
+
signedAt: Date.now()
|
|
2027
|
+
};
|
|
2028
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
2029
|
+
if (!isValid) {
|
|
2030
|
+
return { valid: false, error: "Invalid signature" };
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return { valid: true, payload };
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Create handshake message
|
|
2037
|
+
*/
|
|
2038
|
+
createHandshakeMessage(nodeId, capabilities) {
|
|
2039
|
+
const message = {
|
|
2040
|
+
type: "handshake",
|
|
2041
|
+
version: this.version,
|
|
2042
|
+
sender: nodeId,
|
|
2043
|
+
receiver: "",
|
|
2044
|
+
messageId: this.generateMessageId(),
|
|
2045
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2046
|
+
payload: {
|
|
2047
|
+
protocolVersion: this.version,
|
|
2048
|
+
nodeId,
|
|
2049
|
+
capabilities,
|
|
2050
|
+
state: "initiating"
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
this.messageMap.set(message.messageId, message);
|
|
2054
|
+
this.messageQueue.push(message);
|
|
2055
|
+
logger.debug("[SyncProtocol] Handshake message created", {
|
|
2056
|
+
messageId: message.messageId,
|
|
2057
|
+
nodeId,
|
|
2058
|
+
capabilities: capabilities.length
|
|
2059
|
+
});
|
|
2060
|
+
return message;
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Create sync request message
|
|
2064
|
+
*/
|
|
2065
|
+
createSyncRequestMessage(sender, receiver, sessionId, fromVersion, toVersion, filter) {
|
|
2066
|
+
const message = {
|
|
2067
|
+
type: "sync-request",
|
|
2068
|
+
version: this.version,
|
|
2069
|
+
sender,
|
|
2070
|
+
receiver,
|
|
2071
|
+
messageId: this.generateMessageId(),
|
|
2072
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2073
|
+
payload: {
|
|
2074
|
+
sessionId,
|
|
2075
|
+
fromVersion,
|
|
2076
|
+
toVersion,
|
|
2077
|
+
filter
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
this.messageMap.set(message.messageId, message);
|
|
2081
|
+
this.messageQueue.push(message);
|
|
2082
|
+
logger.debug("[SyncProtocol] Sync request created", {
|
|
2083
|
+
messageId: message.messageId,
|
|
2084
|
+
sessionId,
|
|
2085
|
+
fromVersion,
|
|
2086
|
+
toVersion
|
|
2087
|
+
});
|
|
2088
|
+
return message;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Create sync response message
|
|
2092
|
+
*/
|
|
2093
|
+
createSyncResponseMessage(sender, receiver, sessionId, fromVersion, toVersion, data, hasMore = false, offset = 0) {
|
|
2094
|
+
const message = {
|
|
2095
|
+
type: "sync-response",
|
|
2096
|
+
version: this.version,
|
|
2097
|
+
sender,
|
|
2098
|
+
receiver,
|
|
2099
|
+
messageId: this.generateMessageId(),
|
|
2100
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2101
|
+
payload: {
|
|
2102
|
+
sessionId,
|
|
2103
|
+
fromVersion,
|
|
2104
|
+
toVersion,
|
|
2105
|
+
data,
|
|
2106
|
+
hasMore,
|
|
2107
|
+
offset
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
this.messageMap.set(message.messageId, message);
|
|
2111
|
+
this.messageQueue.push(message);
|
|
2112
|
+
logger.debug("[SyncProtocol] Sync response created", {
|
|
2113
|
+
messageId: message.messageId,
|
|
2114
|
+
sessionId,
|
|
2115
|
+
itemCount: data.length,
|
|
2116
|
+
hasMore
|
|
2117
|
+
});
|
|
2118
|
+
return message;
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Create acknowledgement message
|
|
2122
|
+
*/
|
|
2123
|
+
createAckMessage(sender, receiver, messageId) {
|
|
2124
|
+
const message = {
|
|
2125
|
+
type: "ack",
|
|
2126
|
+
version: this.version,
|
|
2127
|
+
sender,
|
|
2128
|
+
receiver,
|
|
2129
|
+
messageId: this.generateMessageId(),
|
|
2130
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2131
|
+
payload: { acknowledgedMessageId: messageId }
|
|
2132
|
+
};
|
|
2133
|
+
this.messageMap.set(message.messageId, message);
|
|
2134
|
+
this.messageQueue.push(message);
|
|
2135
|
+
return message;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Create error message
|
|
2139
|
+
*/
|
|
2140
|
+
createErrorMessage(sender, receiver, error, relatedMessageId) {
|
|
2141
|
+
const message = {
|
|
2142
|
+
type: "error",
|
|
2143
|
+
version: this.version,
|
|
2144
|
+
sender,
|
|
2145
|
+
receiver,
|
|
2146
|
+
messageId: this.generateMessageId(),
|
|
2147
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2148
|
+
payload: {
|
|
2149
|
+
error,
|
|
2150
|
+
relatedMessageId
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
this.messageMap.set(message.messageId, message);
|
|
2154
|
+
this.messageQueue.push(message);
|
|
2155
|
+
this.protocolErrors.push({
|
|
2156
|
+
error,
|
|
2157
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2158
|
+
});
|
|
2159
|
+
logger.error("[SyncProtocol] Error message created", {
|
|
2160
|
+
messageId: message.messageId,
|
|
2161
|
+
errorCode: error.code,
|
|
2162
|
+
recoverable: error.recoverable
|
|
2163
|
+
});
|
|
2164
|
+
return message;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Validate message
|
|
2168
|
+
*/
|
|
2169
|
+
validateMessage(message) {
|
|
2170
|
+
const errors = [];
|
|
2171
|
+
if (!message.type) {
|
|
2172
|
+
errors.push("Message type is required");
|
|
2173
|
+
}
|
|
2174
|
+
if (!message.sender) {
|
|
2175
|
+
errors.push("Sender is required");
|
|
2176
|
+
}
|
|
2177
|
+
if (!message.messageId) {
|
|
2178
|
+
errors.push("Message ID is required");
|
|
2179
|
+
}
|
|
2180
|
+
if (!message.timestamp) {
|
|
2181
|
+
errors.push("Timestamp is required");
|
|
2182
|
+
}
|
|
2183
|
+
try {
|
|
2184
|
+
new Date(message.timestamp);
|
|
2185
|
+
} catch {
|
|
2186
|
+
errors.push("Invalid timestamp format");
|
|
2187
|
+
}
|
|
2188
|
+
return {
|
|
2189
|
+
valid: errors.length === 0,
|
|
2190
|
+
errors
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Serialize message
|
|
2195
|
+
*/
|
|
2196
|
+
serializeMessage(message) {
|
|
2197
|
+
try {
|
|
2198
|
+
return JSON.stringify(message);
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
logger.error("[SyncProtocol] Message serialization failed", {
|
|
2201
|
+
messageId: message.messageId,
|
|
2202
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2203
|
+
});
|
|
2204
|
+
throw new Error(`Failed to serialize message: ${error instanceof Error ? error.message : String(error)}`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Deserialize message
|
|
2209
|
+
*/
|
|
2210
|
+
deserializeMessage(data) {
|
|
2211
|
+
try {
|
|
2212
|
+
const message = JSON.parse(data);
|
|
2213
|
+
const validation = this.validateMessage(message);
|
|
2214
|
+
if (!validation.valid) {
|
|
2215
|
+
throw new Error(`Invalid message: ${validation.errors.join(", ")}`);
|
|
2216
|
+
}
|
|
2217
|
+
return message;
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
logger.error("[SyncProtocol] Message deserialization failed", {
|
|
2220
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2221
|
+
});
|
|
2222
|
+
throw new Error(`Failed to deserialize message: ${error instanceof Error ? error.message : String(error)}`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Process handshake
|
|
2227
|
+
*/
|
|
2228
|
+
processHandshake(message) {
|
|
2229
|
+
if (message.type !== "handshake") {
|
|
2230
|
+
throw new Error("Message is not a handshake");
|
|
2231
|
+
}
|
|
2232
|
+
const handshake = message.payload;
|
|
2233
|
+
const nodeId = message.sender;
|
|
2234
|
+
this.handshakes.set(nodeId, handshake);
|
|
2235
|
+
logger.debug("[SyncProtocol] Handshake processed", {
|
|
2236
|
+
nodeId,
|
|
2237
|
+
protocolVersion: handshake.protocolVersion,
|
|
2238
|
+
capabilities: handshake.capabilities.length
|
|
2239
|
+
});
|
|
2240
|
+
return handshake;
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Get message
|
|
2244
|
+
*/
|
|
2245
|
+
getMessage(messageId) {
|
|
2246
|
+
return this.messageMap.get(messageId);
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Get all messages
|
|
2250
|
+
*/
|
|
2251
|
+
getAllMessages() {
|
|
2252
|
+
return [...this.messageQueue];
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Get messages by type
|
|
2256
|
+
*/
|
|
2257
|
+
getMessagesByType(type) {
|
|
2258
|
+
return this.messageQueue.filter((m) => m.type === type);
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Get messages from sender
|
|
2262
|
+
*/
|
|
2263
|
+
getMessagesFromSender(sender) {
|
|
2264
|
+
return this.messageQueue.filter((m) => m.sender === sender);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Get pending messages
|
|
2268
|
+
*/
|
|
2269
|
+
getPendingMessages(receiver) {
|
|
2270
|
+
return this.messageQueue.filter((m) => m.receiver === receiver);
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Get handshakes
|
|
2274
|
+
*/
|
|
2275
|
+
getHandshakes() {
|
|
2276
|
+
return new Map(this.handshakes);
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Get protocol statistics
|
|
2280
|
+
*/
|
|
2281
|
+
getStatistics() {
|
|
2282
|
+
const messagesByType = {};
|
|
2283
|
+
for (const message of this.messageQueue) {
|
|
2284
|
+
messagesByType[message.type] = (messagesByType[message.type] || 0) + 1;
|
|
2285
|
+
}
|
|
2286
|
+
const errorCount = this.protocolErrors.length;
|
|
2287
|
+
const recoverableErrors = this.protocolErrors.filter((e) => e.error.recoverable).length;
|
|
2288
|
+
return {
|
|
2289
|
+
totalMessages: this.messageQueue.length,
|
|
2290
|
+
messagesByType,
|
|
2291
|
+
totalHandshakes: this.handshakes.size,
|
|
2292
|
+
totalErrors: errorCount,
|
|
2293
|
+
recoverableErrors,
|
|
2294
|
+
unrecoverableErrors: errorCount - recoverableErrors
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Get protocol errors
|
|
2299
|
+
*/
|
|
2300
|
+
getErrors() {
|
|
2301
|
+
return [...this.protocolErrors];
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Generate message ID
|
|
2305
|
+
*/
|
|
2306
|
+
generateMessageId() {
|
|
2307
|
+
this.messageCounter++;
|
|
2308
|
+
return `msg-${Date.now()}-${this.messageCounter}`;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Clear all state (for testing)
|
|
2312
|
+
*/
|
|
2313
|
+
clear() {
|
|
2314
|
+
this.messageQueue = [];
|
|
2315
|
+
this.messageMap.clear();
|
|
2316
|
+
this.handshakes.clear();
|
|
2317
|
+
this.protocolErrors = [];
|
|
2318
|
+
this.messageCounter = 0;
|
|
2319
|
+
this.cryptoProvider = null;
|
|
2320
|
+
this.cryptoConfig = null;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Get the crypto provider (for advanced usage)
|
|
2324
|
+
*/
|
|
2325
|
+
getCryptoProvider() {
|
|
2326
|
+
return this.cryptoProvider;
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
// src/distributed/StateReconciler.ts
|
|
2331
|
+
var StateReconciler = class {
|
|
2332
|
+
stateVersions = /* @__PURE__ */ new Map();
|
|
2333
|
+
reconciliationHistory = [];
|
|
2334
|
+
cryptoProvider = null;
|
|
2335
|
+
requireSignedVersions = false;
|
|
2336
|
+
/**
|
|
2337
|
+
* Configure cryptographic provider for signed state versions
|
|
2338
|
+
*/
|
|
2339
|
+
configureCrypto(provider, requireSigned = false) {
|
|
2340
|
+
this.cryptoProvider = provider;
|
|
2341
|
+
this.requireSignedVersions = requireSigned;
|
|
2342
|
+
logger.debug("[StateReconciler] Crypto configured", {
|
|
2343
|
+
initialized: provider.isInitialized(),
|
|
2344
|
+
requireSigned
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Check if crypto is configured
|
|
2349
|
+
*/
|
|
2350
|
+
isCryptoEnabled() {
|
|
2351
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Record a signed state version with cryptographic verification
|
|
2355
|
+
*/
|
|
2356
|
+
async recordSignedStateVersion(key, version, data) {
|
|
2357
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
2358
|
+
throw new Error("Crypto provider not initialized");
|
|
2359
|
+
}
|
|
2360
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
2361
|
+
if (!localDID) {
|
|
2362
|
+
throw new Error("Local DID not available");
|
|
2363
|
+
}
|
|
2364
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
2365
|
+
const hashBytes = await this.cryptoProvider.hash(dataBytes);
|
|
2366
|
+
const hash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2367
|
+
const versionData = { version, data, hash };
|
|
2368
|
+
const signed = await this.cryptoProvider.signData(versionData);
|
|
2369
|
+
const stateVersion = {
|
|
2370
|
+
version,
|
|
2371
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2372
|
+
nodeId: localDID,
|
|
2373
|
+
hash,
|
|
2374
|
+
data,
|
|
2375
|
+
signerDID: localDID,
|
|
2376
|
+
signature: signed.signature,
|
|
2377
|
+
signedAt: signed.signedAt
|
|
2378
|
+
};
|
|
2379
|
+
if (!this.stateVersions.has(key)) {
|
|
2380
|
+
this.stateVersions.set(key, []);
|
|
2381
|
+
}
|
|
2382
|
+
this.stateVersions.get(key).push(stateVersion);
|
|
2383
|
+
logger.debug("[StateReconciler] Signed state version recorded", {
|
|
2384
|
+
key,
|
|
2385
|
+
version,
|
|
2386
|
+
signerDID: localDID,
|
|
2387
|
+
hash: hash.slice(0, 16) + "..."
|
|
2388
|
+
});
|
|
2389
|
+
return stateVersion;
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Verify a state version's signature
|
|
2393
|
+
*/
|
|
2394
|
+
async verifyStateVersion(version) {
|
|
2395
|
+
if (!version.signature || !version.signerDID) {
|
|
2396
|
+
if (this.requireSignedVersions) {
|
|
2397
|
+
return { valid: false, error: "Signature required but not present" };
|
|
2398
|
+
}
|
|
2399
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(version.data));
|
|
2400
|
+
if (this.cryptoProvider) {
|
|
2401
|
+
const hashBytes = await this.cryptoProvider.hash(dataBytes);
|
|
2402
|
+
const computedHash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2403
|
+
if (computedHash !== version.hash) {
|
|
2404
|
+
return { valid: false, error: "Hash mismatch" };
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return { valid: true };
|
|
2408
|
+
}
|
|
2409
|
+
if (!this.cryptoProvider) {
|
|
2410
|
+
return { valid: false, error: "Crypto provider not configured" };
|
|
2411
|
+
}
|
|
2412
|
+
const versionData = {
|
|
2413
|
+
version: version.version,
|
|
2414
|
+
data: version.data,
|
|
2415
|
+
hash: version.hash
|
|
2416
|
+
};
|
|
2417
|
+
const signed = {
|
|
2418
|
+
payload: versionData,
|
|
2419
|
+
signature: version.signature,
|
|
2420
|
+
signer: version.signerDID,
|
|
2421
|
+
algorithm: "ES256",
|
|
2422
|
+
signedAt: version.signedAt || Date.now()
|
|
2423
|
+
};
|
|
2424
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
2425
|
+
if (!isValid) {
|
|
2426
|
+
return { valid: false, error: "Invalid signature" };
|
|
2427
|
+
}
|
|
2428
|
+
return { valid: true };
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Reconcile with verification - only accept verified versions
|
|
2432
|
+
*/
|
|
2433
|
+
async reconcileWithVerification(key, strategy = "last-write-wins") {
|
|
2434
|
+
const versions = this.stateVersions.get(key) || [];
|
|
2435
|
+
const verifiedVersions = [];
|
|
2436
|
+
const verificationErrors = [];
|
|
2437
|
+
for (const version of versions) {
|
|
2438
|
+
const result2 = await this.verifyStateVersion(version);
|
|
2439
|
+
if (result2.valid) {
|
|
2440
|
+
verifiedVersions.push(version);
|
|
2441
|
+
} else {
|
|
2442
|
+
verificationErrors.push(
|
|
2443
|
+
`Version ${version.version} from ${version.nodeId}: ${result2.error}`
|
|
2444
|
+
);
|
|
2445
|
+
logger.warn("[StateReconciler] Version verification failed", {
|
|
2446
|
+
version: version.version,
|
|
2447
|
+
nodeId: version.nodeId,
|
|
2448
|
+
error: result2.error
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
if (verifiedVersions.length === 0) {
|
|
2453
|
+
return {
|
|
2454
|
+
success: false,
|
|
2455
|
+
mergedState: null,
|
|
2456
|
+
conflictsResolved: 0,
|
|
2457
|
+
strategy,
|
|
2458
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2459
|
+
verificationErrors
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
let result;
|
|
2463
|
+
switch (strategy) {
|
|
2464
|
+
case "last-write-wins":
|
|
2465
|
+
result = this.reconcileLastWriteWins(verifiedVersions);
|
|
2466
|
+
break;
|
|
2467
|
+
case "vector-clock":
|
|
2468
|
+
result = this.reconcileVectorClock(verifiedVersions);
|
|
2469
|
+
break;
|
|
2470
|
+
case "majority-vote":
|
|
2471
|
+
result = this.reconcileMajorityVote(verifiedVersions);
|
|
2472
|
+
break;
|
|
2473
|
+
default:
|
|
2474
|
+
result = this.reconcileLastWriteWins(verifiedVersions);
|
|
2475
|
+
}
|
|
2476
|
+
return { ...result, verificationErrors };
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Record a state version
|
|
2480
|
+
*/
|
|
2481
|
+
recordStateVersion(key, version, timestamp, nodeId, hash, data) {
|
|
2482
|
+
if (!this.stateVersions.has(key)) {
|
|
2483
|
+
this.stateVersions.set(key, []);
|
|
2484
|
+
}
|
|
2485
|
+
const versions = this.stateVersions.get(key);
|
|
2486
|
+
versions.push({
|
|
2487
|
+
version,
|
|
2488
|
+
timestamp,
|
|
2489
|
+
nodeId,
|
|
2490
|
+
hash,
|
|
2491
|
+
data
|
|
2492
|
+
});
|
|
2493
|
+
logger.debug("[StateReconciler] State version recorded", {
|
|
2494
|
+
key,
|
|
2495
|
+
version,
|
|
2496
|
+
nodeId,
|
|
2497
|
+
hash
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Detect conflicts in state versions
|
|
2502
|
+
*/
|
|
2503
|
+
detectConflicts(key) {
|
|
2504
|
+
const versions = this.stateVersions.get(key);
|
|
2505
|
+
if (!versions || versions.length <= 1) {
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2508
|
+
const hashes = new Set(versions.map((v) => v.hash));
|
|
2509
|
+
return hashes.size > 1;
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Compare two states and generate diff
|
|
2513
|
+
*/
|
|
2514
|
+
compareStates(state1, state2) {
|
|
2515
|
+
const diff = {
|
|
2516
|
+
added: {},
|
|
2517
|
+
modified: {},
|
|
2518
|
+
removed: [],
|
|
2519
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2520
|
+
};
|
|
2521
|
+
for (const [key, value] of Object.entries(state2)) {
|
|
2522
|
+
if (!(key in state1)) {
|
|
2523
|
+
diff.added[key] = value;
|
|
2524
|
+
} else if (JSON.stringify(state1[key]) !== JSON.stringify(value)) {
|
|
2525
|
+
diff.modified[key] = { old: state1[key], new: value };
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
for (const key of Object.keys(state1)) {
|
|
2529
|
+
if (!(key in state2)) {
|
|
2530
|
+
diff.removed.push(key);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return diff;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Reconcile states using last-write-wins strategy
|
|
2537
|
+
*/
|
|
2538
|
+
reconcileLastWriteWins(versions) {
|
|
2539
|
+
if (versions.length === 0) {
|
|
2540
|
+
throw new Error("No versions to reconcile");
|
|
2541
|
+
}
|
|
2542
|
+
const sorted = [...versions].sort(
|
|
2543
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
2544
|
+
);
|
|
2545
|
+
const latest = sorted[0];
|
|
2546
|
+
const conflictsResolved = versions.length - 1;
|
|
2547
|
+
const result = {
|
|
2548
|
+
success: true,
|
|
2549
|
+
mergedState: latest.data,
|
|
2550
|
+
conflictsResolved,
|
|
2551
|
+
strategy: "last-write-wins",
|
|
2552
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2553
|
+
};
|
|
2554
|
+
this.reconciliationHistory.push(result);
|
|
2555
|
+
logger.debug("[StateReconciler] State reconciled (last-write-wins)", {
|
|
2556
|
+
winnerNode: latest.nodeId,
|
|
2557
|
+
conflictsResolved
|
|
2558
|
+
});
|
|
2559
|
+
return result;
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Reconcile states using vector clock strategy
|
|
2563
|
+
*/
|
|
2564
|
+
reconcileVectorClock(versions) {
|
|
2565
|
+
if (versions.length === 0) {
|
|
2566
|
+
throw new Error("No versions to reconcile");
|
|
2567
|
+
}
|
|
2568
|
+
const sorted = [...versions].sort(
|
|
2569
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
2570
|
+
);
|
|
2571
|
+
const latest = sorted[0];
|
|
2572
|
+
let conflictsResolved = 0;
|
|
2573
|
+
for (const v of versions) {
|
|
2574
|
+
const timeDiff = Math.abs(
|
|
2575
|
+
new Date(v.timestamp).getTime() - new Date(latest.timestamp).getTime()
|
|
2576
|
+
);
|
|
2577
|
+
if (timeDiff > 100) {
|
|
2578
|
+
conflictsResolved++;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
const result = {
|
|
2582
|
+
success: true,
|
|
2583
|
+
mergedState: latest.data,
|
|
2584
|
+
conflictsResolved,
|
|
2585
|
+
strategy: "vector-clock",
|
|
2586
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2587
|
+
};
|
|
2588
|
+
this.reconciliationHistory.push(result);
|
|
2589
|
+
logger.debug("[StateReconciler] State reconciled (vector-clock)", {
|
|
2590
|
+
winnerVersion: latest.version,
|
|
2591
|
+
conflictsResolved
|
|
2592
|
+
});
|
|
2593
|
+
return result;
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Reconcile states using majority vote strategy
|
|
2597
|
+
*/
|
|
2598
|
+
reconcileMajorityVote(versions) {
|
|
2599
|
+
if (versions.length === 0) {
|
|
2600
|
+
throw new Error("No versions to reconcile");
|
|
2601
|
+
}
|
|
2602
|
+
const hashGroups = /* @__PURE__ */ new Map();
|
|
2603
|
+
for (const version of versions) {
|
|
2604
|
+
if (!hashGroups.has(version.hash)) {
|
|
2605
|
+
hashGroups.set(version.hash, []);
|
|
2606
|
+
}
|
|
2607
|
+
hashGroups.get(version.hash).push(version);
|
|
2608
|
+
}
|
|
2609
|
+
let majorityVersion = null;
|
|
2610
|
+
let maxCount = 0;
|
|
2611
|
+
for (const [, versionGroup] of hashGroups) {
|
|
2612
|
+
if (versionGroup.length > maxCount) {
|
|
2613
|
+
maxCount = versionGroup.length;
|
|
2614
|
+
majorityVersion = versionGroup[0];
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (!majorityVersion) {
|
|
2618
|
+
majorityVersion = versions[0];
|
|
2619
|
+
}
|
|
2620
|
+
const conflictsResolved = versions.length - maxCount;
|
|
2621
|
+
const result = {
|
|
2622
|
+
success: true,
|
|
2623
|
+
mergedState: majorityVersion.data,
|
|
2624
|
+
conflictsResolved,
|
|
2625
|
+
strategy: "majority-vote",
|
|
2626
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2627
|
+
};
|
|
2628
|
+
this.reconciliationHistory.push(result);
|
|
2629
|
+
logger.debug("[StateReconciler] State reconciled (majority-vote)", {
|
|
2630
|
+
majorityCount: maxCount,
|
|
2631
|
+
conflictsResolved
|
|
2632
|
+
});
|
|
2633
|
+
return result;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Merge multiple states
|
|
2637
|
+
*/
|
|
2638
|
+
mergeStates(states) {
|
|
2639
|
+
if (states.length === 0) {
|
|
2640
|
+
return {};
|
|
2641
|
+
}
|
|
2642
|
+
if (states.length === 1) {
|
|
2643
|
+
return states[0];
|
|
2644
|
+
}
|
|
2645
|
+
const merged = {};
|
|
2646
|
+
for (const state of states) {
|
|
2647
|
+
if (typeof state === "object" && state !== null) {
|
|
2648
|
+
Object.assign(merged, state);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return merged;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Validate state after reconciliation
|
|
2655
|
+
*/
|
|
2656
|
+
validateState(state) {
|
|
2657
|
+
const errors = [];
|
|
2658
|
+
if (state === null) {
|
|
2659
|
+
errors.push("State is null");
|
|
2660
|
+
} else if (state === void 0) {
|
|
2661
|
+
errors.push("State is undefined");
|
|
2662
|
+
} else if (typeof state !== "object") {
|
|
2663
|
+
errors.push("State is not an object");
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
valid: errors.length === 0,
|
|
2667
|
+
errors
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Get state versions for a key
|
|
2672
|
+
*/
|
|
2673
|
+
getStateVersions(key) {
|
|
2674
|
+
return this.stateVersions.get(key) || [];
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Get all state versions
|
|
2678
|
+
*/
|
|
2679
|
+
getAllStateVersions() {
|
|
2680
|
+
const result = {};
|
|
2681
|
+
for (const [key, versions] of this.stateVersions) {
|
|
2682
|
+
result[key] = [...versions];
|
|
2683
|
+
}
|
|
2684
|
+
return result;
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Get reconciliation history
|
|
2688
|
+
*/
|
|
2689
|
+
getReconciliationHistory() {
|
|
2690
|
+
return [...this.reconciliationHistory];
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Get reconciliation statistics
|
|
2694
|
+
*/
|
|
2695
|
+
getStatistics() {
|
|
2696
|
+
const resolvedConflicts = this.reconciliationHistory.reduce(
|
|
2697
|
+
(sum, r) => sum + r.conflictsResolved,
|
|
2698
|
+
0
|
|
2699
|
+
);
|
|
2700
|
+
const strategyUsage = {};
|
|
2701
|
+
for (const result of this.reconciliationHistory) {
|
|
2702
|
+
strategyUsage[result.strategy] = (strategyUsage[result.strategy] || 0) + 1;
|
|
2703
|
+
}
|
|
2704
|
+
return {
|
|
2705
|
+
totalReconciliations: this.reconciliationHistory.length,
|
|
2706
|
+
successfulReconciliations: this.reconciliationHistory.filter((r) => r.success).length,
|
|
2707
|
+
totalConflictsResolved: resolvedConflicts,
|
|
2708
|
+
averageConflictsPerReconciliation: this.reconciliationHistory.length > 0 ? resolvedConflicts / this.reconciliationHistory.length : 0,
|
|
2709
|
+
strategyUsage,
|
|
2710
|
+
trackedKeys: this.stateVersions.size
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Clear all state (for testing)
|
|
2715
|
+
*/
|
|
2716
|
+
clear() {
|
|
2717
|
+
this.stateVersions.clear();
|
|
2718
|
+
this.reconciliationHistory = [];
|
|
2719
|
+
this.cryptoProvider = null;
|
|
2720
|
+
this.requireSignedVersions = false;
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Get the crypto provider (for advanced usage)
|
|
2724
|
+
*/
|
|
2725
|
+
getCryptoProvider() {
|
|
2726
|
+
return this.cryptoProvider;
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
var logger2 = getLogger();
|
|
2730
|
+
var OfflineOperationQueue = class extends eventemitter3.EventEmitter {
|
|
2731
|
+
queue = /* @__PURE__ */ new Map();
|
|
2732
|
+
syncingIds = /* @__PURE__ */ new Set();
|
|
2733
|
+
maxQueueSize = 1e3;
|
|
2734
|
+
defaultMaxRetries = 3;
|
|
2735
|
+
constructor(maxQueueSize = 1e3, defaultMaxRetries = 3) {
|
|
2736
|
+
super();
|
|
2737
|
+
this.maxQueueSize = maxQueueSize;
|
|
2738
|
+
this.defaultMaxRetries = defaultMaxRetries;
|
|
2739
|
+
logger2.debug("[OfflineOperationQueue] Initialized", {
|
|
2740
|
+
maxQueueSize,
|
|
2741
|
+
defaultMaxRetries
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Add operation to the queue
|
|
2746
|
+
*/
|
|
2747
|
+
enqueue(type, data, sessionId, priority = "normal", maxRetries) {
|
|
2748
|
+
if (this.queue.size >= this.maxQueueSize) {
|
|
2749
|
+
const oldest = this.findOldestLowPriority();
|
|
2750
|
+
if (oldest) {
|
|
2751
|
+
this.queue.delete(oldest.id);
|
|
2752
|
+
logger2.warn("[OfflineOperationQueue] Queue full, removed oldest", {
|
|
2753
|
+
removedId: oldest.id
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const operation = {
|
|
2758
|
+
id: `op-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
2759
|
+
type,
|
|
2760
|
+
data,
|
|
2761
|
+
sessionId,
|
|
2762
|
+
priority,
|
|
2763
|
+
createdAt: Date.now(),
|
|
2764
|
+
retryCount: 0,
|
|
2765
|
+
maxRetries: maxRetries ?? this.defaultMaxRetries,
|
|
2766
|
+
status: "pending"
|
|
2767
|
+
};
|
|
2768
|
+
this.queue.set(operation.id, operation);
|
|
2769
|
+
this.emit("operation-added", operation);
|
|
2770
|
+
logger2.debug("[OfflineOperationQueue] Operation enqueued", {
|
|
2771
|
+
id: operation.id,
|
|
2772
|
+
type,
|
|
2773
|
+
priority,
|
|
2774
|
+
queueSize: this.queue.size
|
|
2775
|
+
});
|
|
2776
|
+
return operation;
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Get next operations to sync (by priority)
|
|
2780
|
+
*/
|
|
2781
|
+
getNextBatch(batchSize = 10) {
|
|
2782
|
+
const pending = Array.from(this.queue.values()).filter((op) => op.status === "pending" && !this.syncingIds.has(op.id)).sort((a, b) => {
|
|
2783
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
2784
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
2785
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
2786
|
+
return a.createdAt - b.createdAt;
|
|
2787
|
+
});
|
|
2788
|
+
return pending.slice(0, batchSize);
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Mark operations as syncing
|
|
2792
|
+
*/
|
|
2793
|
+
markSyncing(operationIds) {
|
|
2794
|
+
for (const id of operationIds) {
|
|
2795
|
+
const op = this.queue.get(id);
|
|
2796
|
+
if (op) {
|
|
2797
|
+
op.status = "syncing";
|
|
2798
|
+
this.syncingIds.add(id);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Mark operation as synced
|
|
2804
|
+
*/
|
|
2805
|
+
markSynced(operationId) {
|
|
2806
|
+
const op = this.queue.get(operationId);
|
|
2807
|
+
if (op) {
|
|
2808
|
+
op.status = "synced";
|
|
2809
|
+
this.syncingIds.delete(operationId);
|
|
2810
|
+
this.emit("operation-synced", op);
|
|
2811
|
+
setTimeout(() => {
|
|
2812
|
+
this.queue.delete(operationId);
|
|
2813
|
+
if (this.getPendingCount() === 0) {
|
|
2814
|
+
this.emit("queue-empty");
|
|
2815
|
+
}
|
|
2816
|
+
}, 1e3);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Mark operation as failed
|
|
2821
|
+
*/
|
|
2822
|
+
markFailed(operationId, error) {
|
|
2823
|
+
const op = this.queue.get(operationId);
|
|
2824
|
+
if (op) {
|
|
2825
|
+
op.retryCount++;
|
|
2826
|
+
op.lastError = error.message;
|
|
2827
|
+
this.syncingIds.delete(operationId);
|
|
2828
|
+
if (op.retryCount >= op.maxRetries) {
|
|
2829
|
+
op.status = "failed";
|
|
2830
|
+
this.emit("operation-failed", op, error);
|
|
2831
|
+
logger2.error("[OfflineOperationQueue] Operation permanently failed", {
|
|
2832
|
+
id: operationId,
|
|
2833
|
+
retries: op.retryCount,
|
|
2834
|
+
error: error.message
|
|
2835
|
+
});
|
|
2836
|
+
} else {
|
|
2837
|
+
op.status = "pending";
|
|
2838
|
+
logger2.warn("[OfflineOperationQueue] Operation failed, will retry", {
|
|
2839
|
+
id: operationId,
|
|
2840
|
+
retryCount: op.retryCount,
|
|
2841
|
+
maxRetries: op.maxRetries
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Get operation by ID
|
|
2848
|
+
*/
|
|
2849
|
+
getOperation(operationId) {
|
|
2850
|
+
return this.queue.get(operationId);
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Get all pending operations
|
|
2854
|
+
*/
|
|
2855
|
+
getPendingOperations() {
|
|
2856
|
+
return Array.from(this.queue.values()).filter(
|
|
2857
|
+
(op) => op.status === "pending"
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Get pending count
|
|
2862
|
+
*/
|
|
2863
|
+
getPendingCount() {
|
|
2864
|
+
return Array.from(this.queue.values()).filter(
|
|
2865
|
+
(op) => op.status === "pending"
|
|
2866
|
+
).length;
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Get queue statistics
|
|
2870
|
+
*/
|
|
2871
|
+
getStats() {
|
|
2872
|
+
const operations = Array.from(this.queue.values());
|
|
2873
|
+
const pending = operations.filter((op) => op.status === "pending").length;
|
|
2874
|
+
const syncing = operations.filter((op) => op.status === "syncing").length;
|
|
2875
|
+
const failed = operations.filter((op) => op.status === "failed").length;
|
|
2876
|
+
const synced = operations.filter((op) => op.status === "synced").length;
|
|
2877
|
+
const pendingOps = operations.filter((op) => op.status === "pending");
|
|
2878
|
+
const oldestPendingMs = pendingOps.length > 0 ? Date.now() - Math.min(...pendingOps.map((op) => op.createdAt)) : 0;
|
|
2879
|
+
const averageRetries = operations.length > 0 ? operations.reduce((sum, op) => sum + op.retryCount, 0) / operations.length : 0;
|
|
2880
|
+
return {
|
|
2881
|
+
pending,
|
|
2882
|
+
syncing,
|
|
2883
|
+
failed,
|
|
2884
|
+
synced,
|
|
2885
|
+
totalOperations: operations.length,
|
|
2886
|
+
oldestPendingMs,
|
|
2887
|
+
averageRetries
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Clear all operations
|
|
2892
|
+
*/
|
|
2893
|
+
clear() {
|
|
2894
|
+
this.queue.clear();
|
|
2895
|
+
this.syncingIds.clear();
|
|
2896
|
+
logger2.debug("[OfflineOperationQueue] Queue cleared");
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Clear failed operations
|
|
2900
|
+
*/
|
|
2901
|
+
clearFailed() {
|
|
2902
|
+
for (const [id, op] of this.queue.entries()) {
|
|
2903
|
+
if (op.status === "failed") {
|
|
2904
|
+
this.queue.delete(id);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Retry failed operations
|
|
2910
|
+
*/
|
|
2911
|
+
retryFailed() {
|
|
2912
|
+
for (const op of this.queue.values()) {
|
|
2913
|
+
if (op.status === "failed") {
|
|
2914
|
+
op.status = "pending";
|
|
2915
|
+
op.retryCount = 0;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Find oldest low-priority operation
|
|
2921
|
+
*/
|
|
2922
|
+
findOldestLowPriority() {
|
|
2923
|
+
const lowPriority = Array.from(this.queue.values()).filter((op) => op.priority === "low" && op.status === "pending").sort((a, b) => a.createdAt - b.createdAt);
|
|
2924
|
+
return lowPriority[0] ?? null;
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Export queue for persistence
|
|
2928
|
+
*/
|
|
2929
|
+
export() {
|
|
2930
|
+
return Array.from(this.queue.values());
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Import queue from persistence
|
|
2934
|
+
*/
|
|
2935
|
+
import(operations) {
|
|
2936
|
+
for (const op of operations) {
|
|
2937
|
+
this.queue.set(op.id, op);
|
|
2938
|
+
}
|
|
2939
|
+
logger2.debug("[OfflineOperationQueue] Imported operations", {
|
|
2940
|
+
count: operations.length
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
};
|
|
2944
|
+
var offlineQueueInstance = null;
|
|
2945
|
+
function getOfflineOperationQueue() {
|
|
2946
|
+
if (!offlineQueueInstance) {
|
|
2947
|
+
offlineQueueInstance = new OfflineOperationQueue();
|
|
2948
|
+
}
|
|
2949
|
+
return offlineQueueInstance;
|
|
2950
|
+
}
|
|
2951
|
+
function resetOfflineOperationQueue() {
|
|
2952
|
+
offlineQueueInstance = null;
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// src/compression/CompressionEngine.ts
|
|
2956
|
+
var logger3 = getLogger();
|
|
2957
|
+
var CompressionEngine = class {
|
|
2958
|
+
stats = {
|
|
2959
|
+
totalCompressed: 0,
|
|
2960
|
+
totalDecompressed: 0,
|
|
2961
|
+
totalOriginalBytes: 0,
|
|
2962
|
+
totalCompressedBytes: 0,
|
|
2963
|
+
averageCompressionRatio: 0,
|
|
2964
|
+
compressionTimeMs: 0,
|
|
2965
|
+
decompressionTimeMs: 0
|
|
2966
|
+
};
|
|
2967
|
+
preferredAlgorithm = "gzip";
|
|
2968
|
+
constructor(preferredAlgorithm = "gzip") {
|
|
2969
|
+
this.preferredAlgorithm = preferredAlgorithm;
|
|
2970
|
+
logger3.debug("[CompressionEngine] Initialized", {
|
|
2971
|
+
algorithm: preferredAlgorithm,
|
|
2972
|
+
supportsNative: this.supportsNativeCompression()
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Check if native compression is available
|
|
2977
|
+
*/
|
|
2978
|
+
supportsNativeCompression() {
|
|
2979
|
+
return typeof CompressionStream !== "undefined" && typeof DecompressionStream !== "undefined";
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Compress data
|
|
2983
|
+
*/
|
|
2984
|
+
async compress(data) {
|
|
2985
|
+
const startTime = performance.now();
|
|
2986
|
+
const inputData = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
2987
|
+
const originalSize = inputData.byteLength;
|
|
2988
|
+
let compressed;
|
|
2989
|
+
let algorithm = this.preferredAlgorithm;
|
|
2990
|
+
if (this.supportsNativeCompression()) {
|
|
2991
|
+
try {
|
|
2992
|
+
compressed = await this.compressNative(inputData, this.preferredAlgorithm);
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
logger3.warn("[CompressionEngine] Native compression failed, using fallback", error);
|
|
2995
|
+
compressed = inputData;
|
|
2996
|
+
algorithm = "none";
|
|
2997
|
+
}
|
|
2998
|
+
} else {
|
|
2999
|
+
compressed = inputData;
|
|
3000
|
+
algorithm = "none";
|
|
3001
|
+
}
|
|
3002
|
+
const compressionRatio = originalSize > 0 ? 1 - compressed.byteLength / originalSize : 0;
|
|
3003
|
+
const batch = {
|
|
3004
|
+
id: `batch-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
3005
|
+
compressed,
|
|
3006
|
+
originalSize,
|
|
3007
|
+
compressedSize: compressed.byteLength,
|
|
3008
|
+
compressionRatio,
|
|
3009
|
+
algorithm,
|
|
3010
|
+
timestamp: Date.now()
|
|
3011
|
+
};
|
|
3012
|
+
const elapsed = performance.now() - startTime;
|
|
3013
|
+
this.stats.totalCompressed++;
|
|
3014
|
+
this.stats.totalOriginalBytes += originalSize;
|
|
3015
|
+
this.stats.totalCompressedBytes += compressed.byteLength;
|
|
3016
|
+
this.stats.compressionTimeMs += elapsed;
|
|
3017
|
+
this.updateAverageRatio();
|
|
3018
|
+
logger3.debug("[CompressionEngine] Compressed", {
|
|
3019
|
+
original: originalSize,
|
|
3020
|
+
compressed: compressed.byteLength,
|
|
3021
|
+
ratio: (compressionRatio * 100).toFixed(1) + "%",
|
|
3022
|
+
algorithm,
|
|
3023
|
+
timeMs: elapsed.toFixed(2)
|
|
3024
|
+
});
|
|
3025
|
+
return batch;
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Decompress data
|
|
3029
|
+
*/
|
|
3030
|
+
async decompress(batch) {
|
|
3031
|
+
const startTime = performance.now();
|
|
3032
|
+
let decompressed;
|
|
3033
|
+
if (batch.algorithm === "none") {
|
|
3034
|
+
decompressed = batch.compressed;
|
|
3035
|
+
} else if (this.supportsNativeCompression()) {
|
|
3036
|
+
try {
|
|
3037
|
+
decompressed = await this.decompressNative(
|
|
3038
|
+
batch.compressed,
|
|
3039
|
+
batch.algorithm
|
|
3040
|
+
);
|
|
3041
|
+
} catch (error) {
|
|
3042
|
+
logger3.warn("[CompressionEngine] Native decompression failed", error);
|
|
3043
|
+
throw error;
|
|
3044
|
+
}
|
|
3045
|
+
} else {
|
|
3046
|
+
throw new Error("Native decompression not available");
|
|
3047
|
+
}
|
|
3048
|
+
const elapsed = performance.now() - startTime;
|
|
3049
|
+
this.stats.totalDecompressed++;
|
|
3050
|
+
this.stats.decompressionTimeMs += elapsed;
|
|
3051
|
+
logger3.debug("[CompressionEngine] Decompressed", {
|
|
3052
|
+
compressed: batch.compressedSize,
|
|
3053
|
+
decompressed: decompressed.byteLength,
|
|
3054
|
+
algorithm: batch.algorithm,
|
|
3055
|
+
timeMs: elapsed.toFixed(2)
|
|
3056
|
+
});
|
|
3057
|
+
return decompressed;
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Compress using native CompressionStream
|
|
3061
|
+
*/
|
|
3062
|
+
async compressNative(data, algorithm) {
|
|
3063
|
+
const stream = new CompressionStream(algorithm);
|
|
3064
|
+
const writer = stream.writable.getWriter();
|
|
3065
|
+
const reader = stream.readable.getReader();
|
|
3066
|
+
writer.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
3067
|
+
writer.close();
|
|
3068
|
+
const chunks = [];
|
|
3069
|
+
let done = false;
|
|
3070
|
+
while (!done) {
|
|
3071
|
+
const result = await reader.read();
|
|
3072
|
+
done = result.done;
|
|
3073
|
+
if (result.value) {
|
|
3074
|
+
chunks.push(result.value);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
3078
|
+
const combined = new Uint8Array(totalLength);
|
|
3079
|
+
let offset = 0;
|
|
3080
|
+
for (const chunk of chunks) {
|
|
3081
|
+
combined.set(chunk, offset);
|
|
3082
|
+
offset += chunk.length;
|
|
3083
|
+
}
|
|
3084
|
+
return combined;
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Decompress using native DecompressionStream
|
|
3088
|
+
*/
|
|
3089
|
+
async decompressNative(data, algorithm) {
|
|
3090
|
+
const stream = new DecompressionStream(algorithm);
|
|
3091
|
+
const writer = stream.writable.getWriter();
|
|
3092
|
+
const reader = stream.readable.getReader();
|
|
3093
|
+
writer.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
3094
|
+
writer.close();
|
|
3095
|
+
const chunks = [];
|
|
3096
|
+
let done = false;
|
|
3097
|
+
while (!done) {
|
|
3098
|
+
const result = await reader.read();
|
|
3099
|
+
done = result.done;
|
|
3100
|
+
if (result.value) {
|
|
3101
|
+
chunks.push(result.value);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
3105
|
+
const combined = new Uint8Array(totalLength);
|
|
3106
|
+
let offset = 0;
|
|
3107
|
+
for (const chunk of chunks) {
|
|
3108
|
+
combined.set(chunk, offset);
|
|
3109
|
+
offset += chunk.length;
|
|
3110
|
+
}
|
|
3111
|
+
return combined;
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Split compressed batch into chunks for transmission
|
|
3115
|
+
*/
|
|
3116
|
+
splitIntoChunks(batch, chunkSize = 64 * 1024) {
|
|
3117
|
+
const chunks = [];
|
|
3118
|
+
const data = batch.compressed;
|
|
3119
|
+
const total = Math.ceil(data.byteLength / chunkSize);
|
|
3120
|
+
for (let i = 0; i < total; i++) {
|
|
3121
|
+
const start = i * chunkSize;
|
|
3122
|
+
const end = Math.min(start + chunkSize, data.byteLength);
|
|
3123
|
+
const chunkData = data.slice(start, end);
|
|
3124
|
+
chunks.push({
|
|
3125
|
+
chunkId: `${batch.id}-chunk-${i}`,
|
|
3126
|
+
batchId: batch.id,
|
|
3127
|
+
data: chunkData,
|
|
3128
|
+
index: i,
|
|
3129
|
+
total,
|
|
3130
|
+
checksum: this.simpleChecksum(chunkData)
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
return chunks;
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Reassemble chunks into compressed batch
|
|
3137
|
+
*/
|
|
3138
|
+
reassembleChunks(chunks) {
|
|
3139
|
+
const sorted = [...chunks].sort((a, b) => a.index - b.index);
|
|
3140
|
+
const total = sorted[0]?.total ?? 0;
|
|
3141
|
+
if (sorted.length !== total) {
|
|
3142
|
+
throw new Error(`Missing chunks: got ${sorted.length}, expected ${total}`);
|
|
3143
|
+
}
|
|
3144
|
+
const totalLength = sorted.reduce((sum, chunk) => sum + chunk.data.length, 0);
|
|
3145
|
+
const combined = new Uint8Array(totalLength);
|
|
3146
|
+
let offset = 0;
|
|
3147
|
+
for (const chunk of sorted) {
|
|
3148
|
+
combined.set(chunk.data, offset);
|
|
3149
|
+
offset += chunk.data.length;
|
|
3150
|
+
}
|
|
3151
|
+
return combined;
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Simple checksum for chunk verification
|
|
3155
|
+
*/
|
|
3156
|
+
simpleChecksum(data) {
|
|
3157
|
+
let hash = 0;
|
|
3158
|
+
for (let i = 0; i < data.length; i++) {
|
|
3159
|
+
hash = (hash << 5) - hash + data[i] | 0;
|
|
3160
|
+
}
|
|
3161
|
+
return hash.toString(16);
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Update average compression ratio
|
|
3165
|
+
*/
|
|
3166
|
+
updateAverageRatio() {
|
|
3167
|
+
if (this.stats.totalOriginalBytes > 0) {
|
|
3168
|
+
this.stats.averageCompressionRatio = 1 - this.stats.totalCompressedBytes / this.stats.totalOriginalBytes;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Get statistics
|
|
3173
|
+
*/
|
|
3174
|
+
getStats() {
|
|
3175
|
+
return { ...this.stats };
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Reset statistics
|
|
3179
|
+
*/
|
|
3180
|
+
resetStats() {
|
|
3181
|
+
this.stats = {
|
|
3182
|
+
totalCompressed: 0,
|
|
3183
|
+
totalDecompressed: 0,
|
|
3184
|
+
totalOriginalBytes: 0,
|
|
3185
|
+
totalCompressedBytes: 0,
|
|
3186
|
+
averageCompressionRatio: 0,
|
|
3187
|
+
compressionTimeMs: 0,
|
|
3188
|
+
decompressionTimeMs: 0
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
var compressionEngineInstance = null;
|
|
3193
|
+
function getCompressionEngine() {
|
|
3194
|
+
if (!compressionEngineInstance) {
|
|
3195
|
+
compressionEngineInstance = new CompressionEngine();
|
|
3196
|
+
}
|
|
3197
|
+
return compressionEngineInstance;
|
|
3198
|
+
}
|
|
3199
|
+
function resetCompressionEngine() {
|
|
3200
|
+
compressionEngineInstance = null;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/compression/DeltaSyncOptimizer.ts
|
|
3204
|
+
var logger4 = getLogger();
|
|
3205
|
+
var DeltaSyncOptimizer = class {
|
|
3206
|
+
operationHistory = /* @__PURE__ */ new Map();
|
|
3207
|
+
stats = {
|
|
3208
|
+
totalOperations: 0,
|
|
3209
|
+
totalFull: 0,
|
|
3210
|
+
totalDelta: 0,
|
|
3211
|
+
totalOriginalSize: 0,
|
|
3212
|
+
totalDeltaSize: 0,
|
|
3213
|
+
averageReductionPercent: 0,
|
|
3214
|
+
lastSyncTime: 0,
|
|
3215
|
+
fullOperationThreshold: 1e3
|
|
3216
|
+
// Force full if delta > 1KB
|
|
3217
|
+
};
|
|
3218
|
+
constructor(fullOperationThreshold = 1e3) {
|
|
3219
|
+
this.stats.fullOperationThreshold = fullOperationThreshold;
|
|
3220
|
+
logger4.debug("[DeltaSyncOptimizer] Initialized", {
|
|
3221
|
+
threshold: fullOperationThreshold
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Compute delta for single operation
|
|
3226
|
+
*/
|
|
3227
|
+
computeDelta(operation) {
|
|
3228
|
+
const operationJson = JSON.stringify(operation);
|
|
3229
|
+
const originalSize = new TextEncoder().encode(operationJson).byteLength;
|
|
3230
|
+
const previous = this.operationHistory.get(operation.id);
|
|
3231
|
+
if (!previous) {
|
|
3232
|
+
const delta = {
|
|
3233
|
+
id: `delta-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
3234
|
+
type: "full",
|
|
3235
|
+
operationId: operation.id,
|
|
3236
|
+
operationType: operation.type,
|
|
3237
|
+
sessionId: operation.sessionId,
|
|
3238
|
+
timestamp: Date.now(),
|
|
3239
|
+
fullData: operation.data,
|
|
3240
|
+
priority: operation.priority
|
|
3241
|
+
};
|
|
3242
|
+
this.stats.totalOperations++;
|
|
3243
|
+
this.stats.totalFull++;
|
|
3244
|
+
this.stats.totalOriginalSize += originalSize;
|
|
3245
|
+
const deltaSize2 = new TextEncoder().encode(
|
|
3246
|
+
JSON.stringify(delta)
|
|
3247
|
+
).byteLength;
|
|
3248
|
+
this.stats.totalDeltaSize += deltaSize2;
|
|
3249
|
+
this.operationHistory.set(operation.id, operation);
|
|
3250
|
+
return delta;
|
|
3251
|
+
}
|
|
3252
|
+
const changes = {};
|
|
3253
|
+
const changeMask = [];
|
|
3254
|
+
let hasMeaningfulChanges = false;
|
|
3255
|
+
for (const [key, value] of Object.entries(operation.data)) {
|
|
3256
|
+
const oldValue = previous.data[key];
|
|
3257
|
+
if (!this.deepEqual(value, oldValue)) {
|
|
3258
|
+
changes[key] = value;
|
|
3259
|
+
changeMask.push(key);
|
|
3260
|
+
hasMeaningfulChanges = true;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
for (const key of Object.keys(previous.data)) {
|
|
3264
|
+
if (!(key in operation.data)) {
|
|
3265
|
+
changes[key] = null;
|
|
3266
|
+
changeMask.push(`${key}:deleted`);
|
|
3267
|
+
hasMeaningfulChanges = true;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
const deltaData = {
|
|
3271
|
+
id: `delta-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
3272
|
+
type: "delta",
|
|
3273
|
+
operationId: operation.id,
|
|
3274
|
+
operationType: operation.type,
|
|
3275
|
+
sessionId: operation.sessionId,
|
|
3276
|
+
timestamp: Date.now(),
|
|
3277
|
+
changes: hasMeaningfulChanges ? changes : void 0,
|
|
3278
|
+
changeMask: hasMeaningfulChanges ? changeMask : void 0,
|
|
3279
|
+
priority: operation.priority
|
|
3280
|
+
};
|
|
3281
|
+
const deltaSize = new TextEncoder().encode(
|
|
3282
|
+
JSON.stringify(deltaData)
|
|
3283
|
+
).byteLength;
|
|
3284
|
+
const finalDelta = deltaSize > this.stats.fullOperationThreshold ? {
|
|
3285
|
+
...deltaData,
|
|
3286
|
+
type: "full",
|
|
3287
|
+
fullData: operation.data,
|
|
3288
|
+
changes: void 0,
|
|
3289
|
+
changeMask: void 0
|
|
3290
|
+
} : deltaData;
|
|
3291
|
+
this.stats.totalOperations++;
|
|
3292
|
+
if (finalDelta.type === "full") {
|
|
3293
|
+
this.stats.totalFull++;
|
|
3294
|
+
} else {
|
|
3295
|
+
this.stats.totalDelta++;
|
|
3296
|
+
}
|
|
3297
|
+
this.stats.totalOriginalSize += originalSize;
|
|
3298
|
+
this.stats.totalDeltaSize += deltaSize;
|
|
3299
|
+
this.operationHistory.set(operation.id, operation);
|
|
3300
|
+
return finalDelta;
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Compute deltas for batch of operations
|
|
3304
|
+
*/
|
|
3305
|
+
computeBatchDeltas(operations) {
|
|
3306
|
+
const deltas = operations.map((op) => this.computeDelta(op));
|
|
3307
|
+
const totalOriginalSize = operations.reduce(
|
|
3308
|
+
(sum, op) => sum + new TextEncoder().encode(JSON.stringify(op)).byteLength,
|
|
3309
|
+
0
|
|
3310
|
+
);
|
|
3311
|
+
const totalDeltaSize = deltas.reduce(
|
|
3312
|
+
(sum, delta) => sum + new TextEncoder().encode(JSON.stringify(delta)).byteLength,
|
|
3313
|
+
0
|
|
3314
|
+
);
|
|
3315
|
+
const reductionPercent = totalOriginalSize > 0 ? Math.round(
|
|
3316
|
+
(totalOriginalSize - totalDeltaSize) / totalOriginalSize * 100
|
|
3317
|
+
) : 0;
|
|
3318
|
+
const batch = {
|
|
3319
|
+
batchId: `batch-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
3320
|
+
operations: deltas,
|
|
3321
|
+
timestamp: Date.now(),
|
|
3322
|
+
totalOriginalSize,
|
|
3323
|
+
totalDeltaSize,
|
|
3324
|
+
reductionPercent
|
|
3325
|
+
};
|
|
3326
|
+
logger4.debug("[DeltaSyncOptimizer] Batch computed", {
|
|
3327
|
+
operations: operations.length,
|
|
3328
|
+
reduction: reductionPercent,
|
|
3329
|
+
size: totalDeltaSize
|
|
3330
|
+
});
|
|
3331
|
+
return batch;
|
|
3332
|
+
}
|
|
3333
|
+
/**
|
|
3334
|
+
* Decompress delta operation back to full operation
|
|
3335
|
+
*/
|
|
3336
|
+
decompressDelta(delta) {
|
|
3337
|
+
if (delta.type === "full") {
|
|
3338
|
+
return {
|
|
3339
|
+
id: delta.operationId,
|
|
3340
|
+
type: delta.operationType,
|
|
3341
|
+
sessionId: delta.sessionId,
|
|
3342
|
+
data: delta.fullData || {},
|
|
3343
|
+
status: "pending",
|
|
3344
|
+
createdAt: delta.timestamp
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
const previous = this.operationHistory.get(delta.operationId);
|
|
3348
|
+
if (!previous) {
|
|
3349
|
+
logger4.warn("[DeltaSyncOptimizer] Cannot decompress - no history", {
|
|
3350
|
+
operationId: delta.operationId
|
|
3351
|
+
});
|
|
3352
|
+
return {
|
|
3353
|
+
id: delta.operationId,
|
|
3354
|
+
type: delta.operationType,
|
|
3355
|
+
sessionId: delta.sessionId,
|
|
3356
|
+
data: delta.changes || {},
|
|
3357
|
+
status: "pending",
|
|
3358
|
+
createdAt: delta.timestamp
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
const reconstructed = {
|
|
3362
|
+
...previous,
|
|
3363
|
+
data: {
|
|
3364
|
+
...previous.data,
|
|
3365
|
+
...delta.changes || {}
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
if (delta.changes) {
|
|
3369
|
+
for (const [key, value] of Object.entries(delta.changes)) {
|
|
3370
|
+
if (value === null) {
|
|
3371
|
+
delete reconstructed.data[key];
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
return reconstructed;
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Update history after successful sync
|
|
3379
|
+
*/
|
|
3380
|
+
updateHistory(operations) {
|
|
3381
|
+
for (const op of operations) {
|
|
3382
|
+
this.operationHistory.set(op.id, op);
|
|
3383
|
+
}
|
|
3384
|
+
logger4.debug("[DeltaSyncOptimizer] History updated", {
|
|
3385
|
+
count: operations.length,
|
|
3386
|
+
totalHistorySize: this.operationHistory.size
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Clear history for specific operations
|
|
3391
|
+
*/
|
|
3392
|
+
clearHistory(operationIds) {
|
|
3393
|
+
for (const id of operationIds) {
|
|
3394
|
+
this.operationHistory.delete(id);
|
|
3395
|
+
}
|
|
3396
|
+
logger4.debug("[DeltaSyncOptimizer] History cleared", {
|
|
3397
|
+
cleared: operationIds.length,
|
|
3398
|
+
remaining: this.operationHistory.size
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Get current performance statistics
|
|
3403
|
+
*/
|
|
3404
|
+
getStats() {
|
|
3405
|
+
if (this.stats.totalOperations > 0) {
|
|
3406
|
+
this.stats.averageReductionPercent = Math.round(
|
|
3407
|
+
(this.stats.totalOriginalSize - this.stats.totalDeltaSize) / this.stats.totalOriginalSize * 100
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
return { ...this.stats };
|
|
3411
|
+
}
|
|
3412
|
+
/**
|
|
3413
|
+
* Reset statistics
|
|
3414
|
+
*/
|
|
3415
|
+
resetStats() {
|
|
3416
|
+
this.stats = {
|
|
3417
|
+
totalOperations: 0,
|
|
3418
|
+
totalFull: 0,
|
|
3419
|
+
totalDelta: 0,
|
|
3420
|
+
totalOriginalSize: 0,
|
|
3421
|
+
totalDeltaSize: 0,
|
|
3422
|
+
averageReductionPercent: 0,
|
|
3423
|
+
lastSyncTime: 0,
|
|
3424
|
+
fullOperationThreshold: this.stats.fullOperationThreshold
|
|
3425
|
+
};
|
|
3426
|
+
logger4.debug("[DeltaSyncOptimizer] Stats reset");
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* Set the full operation threshold
|
|
3430
|
+
*/
|
|
3431
|
+
setFullOperationThreshold(bytes) {
|
|
3432
|
+
this.stats.fullOperationThreshold = bytes;
|
|
3433
|
+
logger4.debug("[DeltaSyncOptimizer] Threshold updated", { bytes });
|
|
3434
|
+
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Get history size for memory monitoring
|
|
3437
|
+
*/
|
|
3438
|
+
getHistorySize() {
|
|
3439
|
+
return this.operationHistory.size;
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Get memory footprint estimate
|
|
3443
|
+
*/
|
|
3444
|
+
getMemoryEstimate() {
|
|
3445
|
+
let totalBytes = 0;
|
|
3446
|
+
for (const op of this.operationHistory.values()) {
|
|
3447
|
+
totalBytes += new TextEncoder().encode(JSON.stringify(op)).byteLength;
|
|
3448
|
+
}
|
|
3449
|
+
return totalBytes;
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Deep equality check for nested objects
|
|
3453
|
+
*/
|
|
3454
|
+
deepEqual(a, b) {
|
|
3455
|
+
if (a === b) return true;
|
|
3456
|
+
if (a == null || b == null) return false;
|
|
3457
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
3458
|
+
const aObj = a;
|
|
3459
|
+
const bObj = b;
|
|
3460
|
+
const aKeys = Object.keys(aObj);
|
|
3461
|
+
const bKeys = Object.keys(bObj);
|
|
3462
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
3463
|
+
for (const key of aKeys) {
|
|
3464
|
+
if (!this.deepEqual(aObj[key], bObj[key])) {
|
|
3465
|
+
return false;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return true;
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
var deltaSyncInstance = null;
|
|
3472
|
+
function getDeltaSyncOptimizer(threshold) {
|
|
3473
|
+
if (!deltaSyncInstance) {
|
|
3474
|
+
deltaSyncInstance = new DeltaSyncOptimizer(threshold);
|
|
3475
|
+
}
|
|
3476
|
+
return deltaSyncInstance;
|
|
3477
|
+
}
|
|
3478
|
+
function resetDeltaSyncOptimizer() {
|
|
3479
|
+
deltaSyncInstance = null;
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
// src/optimization/PrefetchingEngine.ts
|
|
3483
|
+
var logger5 = getLogger();
|
|
3484
|
+
var PrefetchingEngine = class {
|
|
3485
|
+
operationHistory = [];
|
|
3486
|
+
patterns = /* @__PURE__ */ new Map();
|
|
3487
|
+
prefetchCache = /* @__PURE__ */ new Map();
|
|
3488
|
+
maxHistoryEntries = 1e3;
|
|
3489
|
+
maxCachePerType = 5;
|
|
3490
|
+
prefetchTTL = 5 * 60 * 1e3;
|
|
3491
|
+
// 5 minutes
|
|
3492
|
+
predictionThreshold = 0.3;
|
|
3493
|
+
stats = {
|
|
3494
|
+
totalPrefetched: 0,
|
|
3495
|
+
totalHits: 0,
|
|
3496
|
+
totalMisses: 0,
|
|
3497
|
+
totalOverwrites: 0,
|
|
3498
|
+
hitRatio: 0,
|
|
3499
|
+
bandwidthSaved: 0,
|
|
3500
|
+
patternsDetected: 0,
|
|
3501
|
+
predictionAccuracy: 0
|
|
3502
|
+
};
|
|
3503
|
+
lastPredictionTime = 0;
|
|
3504
|
+
predictionInterval = 30 * 1e3;
|
|
3505
|
+
constructor() {
|
|
3506
|
+
logger5.debug("[PrefetchingEngine] Initialized", {
|
|
3507
|
+
ttl: this.prefetchTTL,
|
|
3508
|
+
threshold: this.predictionThreshold
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Record operation for pattern analysis
|
|
3513
|
+
*/
|
|
3514
|
+
recordOperation(operationType, size) {
|
|
3515
|
+
const now = Date.now();
|
|
3516
|
+
this.operationHistory.push({
|
|
3517
|
+
type: operationType,
|
|
3518
|
+
timestamp: now,
|
|
3519
|
+
size
|
|
3520
|
+
});
|
|
3521
|
+
if (this.operationHistory.length > this.maxHistoryEntries) {
|
|
3522
|
+
this.operationHistory.shift();
|
|
3523
|
+
}
|
|
3524
|
+
if (Math.random() < 0.1) {
|
|
3525
|
+
this.cleanExpiredPrefetches();
|
|
3526
|
+
}
|
|
3527
|
+
logger5.debug("[PrefetchingEngine] Operation recorded", {
|
|
3528
|
+
type: operationType,
|
|
3529
|
+
size,
|
|
3530
|
+
historySize: this.operationHistory.length
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Analyze patterns in operation history
|
|
3535
|
+
*/
|
|
3536
|
+
analyzePatterns() {
|
|
3537
|
+
if (this.operationHistory.length < 5) {
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
3541
|
+
for (let length = 2; length <= 3; length++) {
|
|
3542
|
+
for (let i = 0; i < this.operationHistory.length - length; i++) {
|
|
3543
|
+
const sequence = this.operationHistory.slice(i, i + length).map((op) => op.type);
|
|
3544
|
+
const key = sequence.join(" \u2192 ");
|
|
3545
|
+
if (!patterns.has(key)) {
|
|
3546
|
+
patterns.set(key, {
|
|
3547
|
+
sequence,
|
|
3548
|
+
frequency: 0,
|
|
3549
|
+
probability: 0,
|
|
3550
|
+
lastOccurred: 0,
|
|
3551
|
+
avgIntervalMs: 0
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
const pattern = patterns.get(key);
|
|
3555
|
+
pattern.frequency++;
|
|
3556
|
+
pattern.lastOccurred = Date.now();
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
const totalSequences = this.operationHistory.length;
|
|
3560
|
+
for (const [key, pattern] of patterns.entries()) {
|
|
3561
|
+
pattern.probability = Math.min(1, pattern.frequency / totalSequences);
|
|
3562
|
+
}
|
|
3563
|
+
this.patterns = patterns;
|
|
3564
|
+
this.stats.patternsDetected = patterns.size;
|
|
3565
|
+
logger5.debug("[PrefetchingEngine] Patterns analyzed", {
|
|
3566
|
+
patternsFound: patterns.size
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Predict next operations
|
|
3571
|
+
*/
|
|
3572
|
+
predictNextOperations(recentOperations) {
|
|
3573
|
+
const now = Date.now();
|
|
3574
|
+
if (now - this.lastPredictionTime > this.predictionInterval) {
|
|
3575
|
+
this.analyzePatterns();
|
|
3576
|
+
this.lastPredictionTime = now;
|
|
3577
|
+
}
|
|
3578
|
+
if (this.patterns.size === 0) {
|
|
3579
|
+
return [];
|
|
3580
|
+
}
|
|
3581
|
+
const predictions = [];
|
|
3582
|
+
const recentTypeSequence = recentOperations.slice(-3).map((op) => op.type).join(" \u2192 ");
|
|
3583
|
+
for (const [key, pattern] of this.patterns.entries()) {
|
|
3584
|
+
if (key.includes(recentTypeSequence)) {
|
|
3585
|
+
const nextType = pattern.sequence[pattern.sequence.length - 1];
|
|
3586
|
+
const prediction = {
|
|
3587
|
+
operationType: nextType,
|
|
3588
|
+
probability: pattern.probability,
|
|
3589
|
+
reason: `Detected pattern: ${key}`,
|
|
3590
|
+
shouldPrefetch: pattern.probability > this.predictionThreshold,
|
|
3591
|
+
estimatedTimeMs: pattern.avgIntervalMs
|
|
3592
|
+
};
|
|
3593
|
+
predictions.push(prediction);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
const deduped = Array.from(
|
|
3597
|
+
new Map(predictions.map((p) => [p.operationType, p])).values()
|
|
3598
|
+
).sort((a, b) => b.probability - a.probability);
|
|
3599
|
+
logger5.debug("[PrefetchingEngine] Predictions", {
|
|
3600
|
+
predictions: deduped.slice(0, 3).map((p) => ({
|
|
3601
|
+
type: p.operationType,
|
|
3602
|
+
probability: (p.probability * 100).toFixed(1) + "%"
|
|
3603
|
+
}))
|
|
3604
|
+
});
|
|
3605
|
+
return deduped;
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* Add prefetched batch
|
|
3609
|
+
*/
|
|
3610
|
+
addPrefetchedBatch(operationType, compressed, originalSize) {
|
|
3611
|
+
if (!this.prefetchCache.has(operationType)) {
|
|
3612
|
+
this.prefetchCache.set(operationType, []);
|
|
3613
|
+
}
|
|
3614
|
+
const cache = this.prefetchCache.get(operationType);
|
|
3615
|
+
if (cache.length >= this.maxCachePerType) {
|
|
3616
|
+
const oldest = cache.shift();
|
|
3617
|
+
if (oldest.hitCount === 0) {
|
|
3618
|
+
this.stats.totalMisses++;
|
|
3619
|
+
} else {
|
|
3620
|
+
this.stats.totalOverwrites++;
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
const batch = {
|
|
3624
|
+
id: `prefetch-${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
3625
|
+
operationType,
|
|
3626
|
+
compressed,
|
|
3627
|
+
compressedSize: compressed.length,
|
|
3628
|
+
originalSize,
|
|
3629
|
+
compressionRatio: 1 - compressed.length / originalSize,
|
|
3630
|
+
compressed_at: Date.now(),
|
|
3631
|
+
created_at: Date.now(),
|
|
3632
|
+
ttl: this.prefetchTTL,
|
|
3633
|
+
expiresAt: Date.now() + this.prefetchTTL,
|
|
3634
|
+
hitCount: 0,
|
|
3635
|
+
missCount: 0
|
|
3636
|
+
};
|
|
3637
|
+
cache.push(batch);
|
|
3638
|
+
this.stats.totalPrefetched++;
|
|
3639
|
+
this.stats.bandwidthSaved += originalSize - compressed.length;
|
|
3640
|
+
logger5.debug("[PrefetchingEngine] Prefetched batch added", {
|
|
3641
|
+
type: operationType,
|
|
3642
|
+
id: batch.id,
|
|
3643
|
+
ratio: (batch.compressionRatio * 100).toFixed(1) + "%"
|
|
3644
|
+
});
|
|
3645
|
+
return batch;
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Try to use prefetched batch
|
|
3649
|
+
*/
|
|
3650
|
+
getPrefetchedBatch(operationType) {
|
|
3651
|
+
const cache = this.prefetchCache.get(operationType);
|
|
3652
|
+
if (!cache || cache.length === 0) {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3655
|
+
const now = Date.now();
|
|
3656
|
+
for (let i = 0; i < cache.length; i++) {
|
|
3657
|
+
const batch = cache[i];
|
|
3658
|
+
if (batch.expiresAt > now) {
|
|
3659
|
+
batch.hitCount++;
|
|
3660
|
+
this.stats.totalHits++;
|
|
3661
|
+
this.updatePredictionAccuracy(true);
|
|
3662
|
+
logger5.debug("[PrefetchingEngine] Prefetch hit", {
|
|
3663
|
+
type: operationType,
|
|
3664
|
+
id: batch.id
|
|
3665
|
+
});
|
|
3666
|
+
return batch;
|
|
3667
|
+
} else {
|
|
3668
|
+
cache.splice(i, 1);
|
|
3669
|
+
i--;
|
|
3670
|
+
batch.missCount++;
|
|
3671
|
+
this.stats.totalMisses++;
|
|
3672
|
+
this.updatePredictionAccuracy(false);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
return null;
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Update prediction accuracy metric
|
|
3679
|
+
*/
|
|
3680
|
+
updatePredictionAccuracy(hit) {
|
|
3681
|
+
const total = this.stats.totalHits + this.stats.totalMisses;
|
|
3682
|
+
if (total === 0) return;
|
|
3683
|
+
this.stats.predictionAccuracy = this.stats.totalHits / total;
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Clean expired prefetches
|
|
3687
|
+
*/
|
|
3688
|
+
cleanExpiredPrefetches() {
|
|
3689
|
+
const now = Date.now();
|
|
3690
|
+
let cleanedCount = 0;
|
|
3691
|
+
for (const [type, cache] of this.prefetchCache.entries()) {
|
|
3692
|
+
for (let i = cache.length - 1; i >= 0; i--) {
|
|
3693
|
+
if (cache[i].expiresAt < now) {
|
|
3694
|
+
const batch = cache.splice(i, 1)[0];
|
|
3695
|
+
if (batch.hitCount === 0) {
|
|
3696
|
+
this.stats.totalMisses++;
|
|
3697
|
+
}
|
|
3698
|
+
cleanedCount++;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
if (cache.length === 0) {
|
|
3702
|
+
this.prefetchCache.delete(type);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
if (cleanedCount > 0) {
|
|
3706
|
+
logger5.debug("[PrefetchingEngine] Cleaned expired prefetches", {
|
|
3707
|
+
count: cleanedCount
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Get statistics
|
|
3713
|
+
*/
|
|
3714
|
+
getStats() {
|
|
3715
|
+
const total = this.stats.totalHits + this.stats.totalMisses;
|
|
3716
|
+
this.stats.hitRatio = total > 0 ? this.stats.totalHits / total : 0;
|
|
3717
|
+
return { ...this.stats };
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Clear all caches
|
|
3721
|
+
*/
|
|
3722
|
+
clear() {
|
|
3723
|
+
this.operationHistory = [];
|
|
3724
|
+
this.patterns.clear();
|
|
3725
|
+
this.prefetchCache.clear();
|
|
3726
|
+
this.stats = {
|
|
3727
|
+
totalPrefetched: 0,
|
|
3728
|
+
totalHits: 0,
|
|
3729
|
+
totalMisses: 0,
|
|
3730
|
+
totalOverwrites: 0,
|
|
3731
|
+
hitRatio: 0,
|
|
3732
|
+
bandwidthSaved: 0,
|
|
3733
|
+
patternsDetected: 0,
|
|
3734
|
+
predictionAccuracy: 0
|
|
3735
|
+
};
|
|
3736
|
+
logger5.debug("[PrefetchingEngine] Cleared all caches");
|
|
3737
|
+
}
|
|
3738
|
+
};
|
|
3739
|
+
var prefetchingEngineInstance = null;
|
|
3740
|
+
function getPrefetchingEngine() {
|
|
3741
|
+
if (!prefetchingEngineInstance) {
|
|
3742
|
+
prefetchingEngineInstance = new PrefetchingEngine();
|
|
3743
|
+
}
|
|
3744
|
+
return prefetchingEngineInstance;
|
|
3745
|
+
}
|
|
3746
|
+
function resetPrefetchingEngine() {
|
|
3747
|
+
prefetchingEngineInstance = null;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
// src/optimization/BatchTimingOptimizer.ts
|
|
3751
|
+
var logger6 = getLogger();
|
|
3752
|
+
var BatchTimingOptimizer = class {
|
|
3753
|
+
networkHistory = [];
|
|
3754
|
+
activityHistory = [];
|
|
3755
|
+
stats = {
|
|
3756
|
+
totalBatches: 0,
|
|
3757
|
+
immediateDeliveries: 0,
|
|
3758
|
+
deferredBatches: 0,
|
|
3759
|
+
averageWaitTimeMs: 0,
|
|
3760
|
+
averageDeliveryTimeMs: 0,
|
|
3761
|
+
networkWindowsUsed: 0,
|
|
3762
|
+
congestionAvoided: 0,
|
|
3763
|
+
userFocusedOptimizations: 0
|
|
3764
|
+
};
|
|
3765
|
+
lastActivityTime = Date.now();
|
|
3766
|
+
isUserActive = true;
|
|
3767
|
+
congestionDetectionWindow = 60 * 1e3;
|
|
3768
|
+
optimalBatchSize = 50 * 1024;
|
|
3769
|
+
constructor() {
|
|
3770
|
+
logger6.debug("[BatchTimingOptimizer] Initialized", {
|
|
3771
|
+
congestionWindow: this.congestionDetectionWindow,
|
|
3772
|
+
optimalBatchSize: this.optimalBatchSize
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
/**
|
|
3776
|
+
* Record network measurement
|
|
3777
|
+
*/
|
|
3778
|
+
recordNetworkMeasurement(latencyMs, bandwidthMbps) {
|
|
3779
|
+
const quality = this.assessNetworkQuality(latencyMs, bandwidthMbps);
|
|
3780
|
+
this.networkHistory.push({
|
|
3781
|
+
latencyMs,
|
|
3782
|
+
bandwidthMbps,
|
|
3783
|
+
timestamp: Date.now(),
|
|
3784
|
+
quality
|
|
3785
|
+
});
|
|
3786
|
+
if (this.networkHistory.length > 100) {
|
|
3787
|
+
this.networkHistory.shift();
|
|
3788
|
+
}
|
|
3789
|
+
this.stats.networkWindowsUsed++;
|
|
3790
|
+
logger6.debug("[BatchTimingOptimizer] Network measured", {
|
|
3791
|
+
latency: latencyMs + "ms",
|
|
3792
|
+
bandwidth: bandwidthMbps.toFixed(1) + " Mbps",
|
|
3793
|
+
quality
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Assess network quality
|
|
3798
|
+
*/
|
|
3799
|
+
assessNetworkQuality(latencyMs, bandwidthMbps) {
|
|
3800
|
+
if (latencyMs < 20 && bandwidthMbps > 10) return "excellent";
|
|
3801
|
+
if (latencyMs < 50 && bandwidthMbps > 5) return "good";
|
|
3802
|
+
if (latencyMs < 100 && bandwidthMbps > 2) return "fair";
|
|
3803
|
+
return "poor";
|
|
3804
|
+
}
|
|
3805
|
+
/**
|
|
3806
|
+
* Detect congestion in network
|
|
3807
|
+
*/
|
|
3808
|
+
detectCongestion() {
|
|
3809
|
+
const recentMeasurements = this.networkHistory.filter(
|
|
3810
|
+
(m) => Date.now() - m.timestamp < this.congestionDetectionWindow
|
|
3811
|
+
);
|
|
3812
|
+
if (recentMeasurements.length < 3) {
|
|
3813
|
+
return 0;
|
|
3814
|
+
}
|
|
3815
|
+
const poorCount = recentMeasurements.filter(
|
|
3816
|
+
(m) => m.quality === "poor"
|
|
3817
|
+
).length;
|
|
3818
|
+
return poorCount / recentMeasurements.length;
|
|
3819
|
+
}
|
|
3820
|
+
/**
|
|
3821
|
+
* Find next optimal network window
|
|
3822
|
+
*/
|
|
3823
|
+
findOptimalWindow() {
|
|
3824
|
+
const now = Date.now();
|
|
3825
|
+
const recentMeasurements = this.networkHistory.slice(-20);
|
|
3826
|
+
if (recentMeasurements.length === 0) {
|
|
3827
|
+
return {
|
|
3828
|
+
startTime: now,
|
|
3829
|
+
endTime: now + 1e3,
|
|
3830
|
+
expectedDurationMs: 1e3,
|
|
3831
|
+
latencyMs: 50,
|
|
3832
|
+
bandwidthMbps: 5,
|
|
3833
|
+
quality: "good",
|
|
3834
|
+
isStable: true,
|
|
3835
|
+
congestionLevel: 0,
|
|
3836
|
+
recommendedBatchSize: this.optimalBatchSize
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
const avgLatency = recentMeasurements.reduce((sum, m) => sum + m.latencyMs, 0) / recentMeasurements.length;
|
|
3840
|
+
const avgBandwidth = recentMeasurements.reduce((sum, m) => sum + m.bandwidthMbps, 0) / recentMeasurements.length;
|
|
3841
|
+
const latencyVariance = Math.sqrt(
|
|
3842
|
+
recentMeasurements.reduce(
|
|
3843
|
+
(sum, m) => sum + Math.pow(m.latencyMs - avgLatency, 2),
|
|
3844
|
+
0
|
|
3845
|
+
) / recentMeasurements.length
|
|
3846
|
+
) / avgLatency;
|
|
3847
|
+
const isStable = latencyVariance < 0.2;
|
|
3848
|
+
const congestionLevel = this.detectCongestion();
|
|
3849
|
+
const quality = this.assessNetworkQuality(avgLatency, avgBandwidth);
|
|
3850
|
+
const recommendedBatchSize = Math.max(
|
|
3851
|
+
10 * 1024,
|
|
3852
|
+
Math.min(500 * 1024, avgBandwidth * 1024 * 100 / 8)
|
|
3853
|
+
);
|
|
3854
|
+
return {
|
|
3855
|
+
startTime: now,
|
|
3856
|
+
endTime: now + (isStable ? 30 * 1e3 : 10 * 1e3),
|
|
3857
|
+
expectedDurationMs: isStable ? 30 * 1e3 : 10 * 1e3,
|
|
3858
|
+
latencyMs: avgLatency,
|
|
3859
|
+
bandwidthMbps: avgBandwidth,
|
|
3860
|
+
quality,
|
|
3861
|
+
isStable,
|
|
3862
|
+
congestionLevel,
|
|
3863
|
+
recommendedBatchSize
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Get scheduling decision for a batch
|
|
3868
|
+
*/
|
|
3869
|
+
getSchedulingDecision(batchSize, batchPriority = "normal", isUserTriggered = false) {
|
|
3870
|
+
const now = Date.now();
|
|
3871
|
+
const currentWindow = this.findOptimalWindow();
|
|
3872
|
+
const congestionLevel = this.detectCongestion();
|
|
3873
|
+
let shouldSendNow = false;
|
|
3874
|
+
let recommendedDelay = 0;
|
|
3875
|
+
let reason = "";
|
|
3876
|
+
let priority = batchPriority;
|
|
3877
|
+
if (priority === "critical") {
|
|
3878
|
+
shouldSendNow = true;
|
|
3879
|
+
reason = "Critical operation (bypass optimization)";
|
|
3880
|
+
} else if (isUserTriggered && this.isUserActive) {
|
|
3881
|
+
shouldSendNow = true;
|
|
3882
|
+
reason = "User-triggered operation";
|
|
3883
|
+
priority = "high";
|
|
3884
|
+
} else if (currentWindow.quality === "excellent" || currentWindow.quality === "good") {
|
|
3885
|
+
if (congestionLevel < 0.3) {
|
|
3886
|
+
shouldSendNow = true;
|
|
3887
|
+
reason = "Good network conditions";
|
|
3888
|
+
} else {
|
|
3889
|
+
shouldSendNow = true;
|
|
3890
|
+
reason = "Good network despite some congestion";
|
|
3891
|
+
recommendedDelay = 1e3 + Math.random() * 2e3;
|
|
3892
|
+
}
|
|
3893
|
+
} else if (currentWindow.quality === "fair") {
|
|
3894
|
+
if (priority === "high") {
|
|
3895
|
+
shouldSendNow = true;
|
|
3896
|
+
reason = "High priority despite fair network";
|
|
3897
|
+
} else {
|
|
3898
|
+
shouldSendNow = false;
|
|
3899
|
+
reason = "Fair network: waiting for better window";
|
|
3900
|
+
recommendedDelay = 30 * 1e3 + Math.random() * 30 * 1e3;
|
|
3901
|
+
}
|
|
3902
|
+
} else {
|
|
3903
|
+
shouldSendNow = false;
|
|
3904
|
+
reason = "Poor network conditions: deferring";
|
|
3905
|
+
if (priority === "high") {
|
|
3906
|
+
recommendedDelay = 60 * 1e3 + Math.random() * 30 * 1e3;
|
|
3907
|
+
} else {
|
|
3908
|
+
recommendedDelay = 120 * 1e3 + Math.random() * 60 * 1e3;
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
const estimatedDeliveryMs = batchSize / (currentWindow.bandwidthMbps * 1024 * 1024 / 8) * 1e3 + currentWindow.latencyMs + recommendedDelay;
|
|
3912
|
+
const decision = {
|
|
3913
|
+
shouldSendNow,
|
|
3914
|
+
nextOptimalWindowMs: now + recommendedDelay,
|
|
3915
|
+
recommendedDelay,
|
|
3916
|
+
reason,
|
|
3917
|
+
priority,
|
|
3918
|
+
estimatedDeliveryMs
|
|
3919
|
+
};
|
|
3920
|
+
logger6.debug("[BatchTimingOptimizer] Scheduling decision", {
|
|
3921
|
+
size: (batchSize / 1024).toFixed(1) + " KB",
|
|
3922
|
+
shouldSendNow,
|
|
3923
|
+
delay: recommendedDelay + "ms",
|
|
3924
|
+
reason
|
|
3925
|
+
});
|
|
3926
|
+
return decision;
|
|
3927
|
+
}
|
|
3928
|
+
/**
|
|
3929
|
+
* Apply scheduling and update stats
|
|
3930
|
+
*/
|
|
3931
|
+
applyScheduling(batchSize, sendNow, actualDelay) {
|
|
3932
|
+
this.stats.totalBatches++;
|
|
3933
|
+
if (sendNow) {
|
|
3934
|
+
this.stats.immediateDeliveries++;
|
|
3935
|
+
} else {
|
|
3936
|
+
this.stats.deferredBatches++;
|
|
3937
|
+
}
|
|
3938
|
+
const totalWait = this.stats.averageWaitTimeMs * (this.stats.totalBatches - 1) + actualDelay;
|
|
3939
|
+
this.stats.averageWaitTimeMs = totalWait / this.stats.totalBatches;
|
|
3940
|
+
if (this.detectCongestion() > 0.3 && !sendNow) {
|
|
3941
|
+
this.stats.congestionAvoided++;
|
|
3942
|
+
}
|
|
3943
|
+
if (this.isUserActive) {
|
|
3944
|
+
this.stats.userFocusedOptimizations++;
|
|
3945
|
+
}
|
|
3946
|
+
this.stats.networkWindowsUsed++;
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Get optimal batch size recommendation
|
|
3950
|
+
*/
|
|
3951
|
+
getOptimalBatchSize() {
|
|
3952
|
+
const window = this.findOptimalWindow();
|
|
3953
|
+
return window.recommendedBatchSize;
|
|
3954
|
+
}
|
|
3955
|
+
/**
|
|
3956
|
+
* Get current network window
|
|
3957
|
+
*/
|
|
3958
|
+
getCurrentNetworkWindow() {
|
|
3959
|
+
return this.findOptimalWindow();
|
|
3960
|
+
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Set user activity state
|
|
3963
|
+
*/
|
|
3964
|
+
setUserActive(active) {
|
|
3965
|
+
this.isUserActive = active;
|
|
3966
|
+
if (active) {
|
|
3967
|
+
this.lastActivityTime = Date.now();
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
/**
|
|
3971
|
+
* Get statistics
|
|
3972
|
+
*/
|
|
3973
|
+
getStats() {
|
|
3974
|
+
return { ...this.stats };
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Clear history
|
|
3978
|
+
*/
|
|
3979
|
+
clear() {
|
|
3980
|
+
this.networkHistory = [];
|
|
3981
|
+
this.activityHistory = [];
|
|
3982
|
+
this.stats = {
|
|
3983
|
+
totalBatches: 0,
|
|
3984
|
+
immediateDeliveries: 0,
|
|
3985
|
+
deferredBatches: 0,
|
|
3986
|
+
averageWaitTimeMs: 0,
|
|
3987
|
+
averageDeliveryTimeMs: 0,
|
|
3988
|
+
networkWindowsUsed: 0,
|
|
3989
|
+
congestionAvoided: 0,
|
|
3990
|
+
userFocusedOptimizations: 0
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
};
|
|
3994
|
+
var batchTimingOptimizerInstance = null;
|
|
3995
|
+
function getBatchTimingOptimizer() {
|
|
3996
|
+
if (!batchTimingOptimizerInstance) {
|
|
3997
|
+
batchTimingOptimizerInstance = new BatchTimingOptimizer();
|
|
3998
|
+
}
|
|
3999
|
+
return batchTimingOptimizerInstance;
|
|
4000
|
+
}
|
|
4001
|
+
function resetBatchTimingOptimizer() {
|
|
4002
|
+
batchTimingOptimizerInstance = null;
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
// src/optimization/AdaptiveCompressionOptimizer.ts
|
|
4006
|
+
var logger7 = getLogger();
|
|
4007
|
+
var AdaptiveCompressionOptimizer = class {
|
|
4008
|
+
currentLevel = 6;
|
|
4009
|
+
networkProfile = {
|
|
4010
|
+
estimatedSpeedKbps: 5e3,
|
|
4011
|
+
latencyMs: 50,
|
|
4012
|
+
isOnline: true,
|
|
4013
|
+
isWifi: false,
|
|
4014
|
+
isFast: true,
|
|
4015
|
+
isSlow: false,
|
|
4016
|
+
isEmpty: false
|
|
4017
|
+
};
|
|
4018
|
+
deviceProfile = {
|
|
4019
|
+
cpuCores: 4,
|
|
4020
|
+
cpuUtilization: 0.3,
|
|
4021
|
+
memoryAvailableMB: 512,
|
|
4022
|
+
memoryTotalMB: 1024,
|
|
4023
|
+
isConstrained: false,
|
|
4024
|
+
isPremium: false,
|
|
4025
|
+
supportsWebWorkers: true,
|
|
4026
|
+
supportsWebAssembly: true
|
|
4027
|
+
};
|
|
4028
|
+
compressionHistory = [];
|
|
4029
|
+
stats = {
|
|
4030
|
+
currentLevel: 6,
|
|
4031
|
+
averageCompressionMs: 10,
|
|
4032
|
+
averageRatio: 0.85,
|
|
4033
|
+
levelsUsed: /* @__PURE__ */ new Set([6]),
|
|
4034
|
+
adjustmentCount: 0,
|
|
4035
|
+
totalBatches: 0,
|
|
4036
|
+
networkCondition: "normal"
|
|
4037
|
+
};
|
|
4038
|
+
constructor() {
|
|
4039
|
+
logger7.debug("[AdaptiveCompressionOptimizer] Initialized", {
|
|
4040
|
+
level: this.currentLevel
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Update network conditions
|
|
4045
|
+
*/
|
|
4046
|
+
updateNetworkConditions(speedKbps, latencyMs, isOnline) {
|
|
4047
|
+
this.networkProfile.estimatedSpeedKbps = speedKbps;
|
|
4048
|
+
if (latencyMs !== void 0) {
|
|
4049
|
+
this.networkProfile.latencyMs = latencyMs;
|
|
4050
|
+
}
|
|
4051
|
+
if (isOnline !== void 0) {
|
|
4052
|
+
this.networkProfile.isOnline = isOnline;
|
|
4053
|
+
}
|
|
4054
|
+
this.networkProfile.isFast = speedKbps > 5e3;
|
|
4055
|
+
this.networkProfile.isSlow = speedKbps < 1e3;
|
|
4056
|
+
this.networkProfile.isEmpty = speedKbps < 100;
|
|
4057
|
+
if (isOnline === false) {
|
|
4058
|
+
this.stats.networkCondition = "offline";
|
|
4059
|
+
} else if (this.networkProfile.isSlow) {
|
|
4060
|
+
this.stats.networkCondition = "slow";
|
|
4061
|
+
} else if (this.networkProfile.isFast) {
|
|
4062
|
+
this.stats.networkCondition = "fast";
|
|
4063
|
+
} else {
|
|
4064
|
+
this.stats.networkCondition = "normal";
|
|
4065
|
+
}
|
|
4066
|
+
logger7.debug("[AdaptiveCompressionOptimizer] Network updated", {
|
|
4067
|
+
speedKbps,
|
|
4068
|
+
condition: this.stats.networkCondition
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Update device resource usage
|
|
4073
|
+
*/
|
|
4074
|
+
updateDeviceResources(cpuUtilization, memoryAvailableMB) {
|
|
4075
|
+
this.deviceProfile.cpuUtilization = Math.max(0, Math.min(1, cpuUtilization));
|
|
4076
|
+
this.deviceProfile.memoryAvailableMB = memoryAvailableMB;
|
|
4077
|
+
this.deviceProfile.isConstrained = memoryAvailableMB < 512;
|
|
4078
|
+
this.deviceProfile.isPremium = memoryAvailableMB > 2048;
|
|
4079
|
+
logger7.debug("[AdaptiveCompressionOptimizer] Device resources updated", {
|
|
4080
|
+
cpuUtilization: (cpuUtilization * 100).toFixed(1) + "%",
|
|
4081
|
+
memoryAvailableMB
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4084
|
+
/**
|
|
4085
|
+
* Record compression performance
|
|
4086
|
+
*/
|
|
4087
|
+
recordCompressionPerformance(level, compressionMs, ratio) {
|
|
4088
|
+
this.compressionHistory.push({
|
|
4089
|
+
level,
|
|
4090
|
+
ratio,
|
|
4091
|
+
timeMs: compressionMs,
|
|
4092
|
+
timestamp: Date.now()
|
|
4093
|
+
});
|
|
4094
|
+
if (this.compressionHistory.length > 100) {
|
|
4095
|
+
this.compressionHistory.shift();
|
|
4096
|
+
}
|
|
4097
|
+
this.stats.totalBatches++;
|
|
4098
|
+
this.stats.averageCompressionMs = this.compressionHistory.reduce((sum, h) => sum + h.timeMs, 0) / this.compressionHistory.length;
|
|
4099
|
+
this.stats.averageRatio = this.compressionHistory.reduce((sum, h) => sum + h.ratio, 0) / this.compressionHistory.length;
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Get compression recommendation based on conditions
|
|
4103
|
+
*/
|
|
4104
|
+
getRecommendedLevel() {
|
|
4105
|
+
const networkFactor = this.calculateNetworkFactor();
|
|
4106
|
+
const deviceFactor = this.calculateDeviceFactor();
|
|
4107
|
+
const combinedFactor = (networkFactor + deviceFactor) / 2;
|
|
4108
|
+
const recommendedLevel = Math.max(
|
|
4109
|
+
1,
|
|
4110
|
+
Math.min(9, Math.round(combinedFactor * 9))
|
|
4111
|
+
);
|
|
4112
|
+
const estimatedCompressionMs = this.estimateCompressionTime(recommendedLevel);
|
|
4113
|
+
const estimatedRatio = this.estimateCompressionRatio(recommendedLevel);
|
|
4114
|
+
let reason = "";
|
|
4115
|
+
if (networkFactor < 0.3 && deviceFactor < 0.3) {
|
|
4116
|
+
reason = "Slow network + constrained device: using level 1-2 (fast)";
|
|
4117
|
+
} else if (networkFactor > 0.7 && deviceFactor > 0.7) {
|
|
4118
|
+
reason = "Fast network + premium device: using level 8-9 (best compression)";
|
|
4119
|
+
} else if (networkFactor > 0.7) {
|
|
4120
|
+
reason = "Fast network: prioritizing compression ratio";
|
|
4121
|
+
} else if (deviceFactor < 0.3) {
|
|
4122
|
+
reason = "Constrained device: prioritizing speed";
|
|
4123
|
+
} else {
|
|
4124
|
+
reason = "Normal conditions: balanced compression level";
|
|
4125
|
+
}
|
|
4126
|
+
const recommendation = {
|
|
4127
|
+
recommendedLevel,
|
|
4128
|
+
reason,
|
|
4129
|
+
confidence: this.compressionHistory.length > 10 ? 0.9 : 0.5,
|
|
4130
|
+
estimatedCompressionMs,
|
|
4131
|
+
estimatedRatio,
|
|
4132
|
+
networkFactor,
|
|
4133
|
+
deviceFactor
|
|
4134
|
+
};
|
|
4135
|
+
logger7.debug("[AdaptiveCompressionOptimizer] Recommendation", recommendation);
|
|
4136
|
+
return recommendation;
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Calculate network factor (0-1)
|
|
4140
|
+
*/
|
|
4141
|
+
calculateNetworkFactor() {
|
|
4142
|
+
if (!this.networkProfile.isOnline) return 0;
|
|
4143
|
+
const speedMbps = this.networkProfile.estimatedSpeedKbps / 1e3;
|
|
4144
|
+
if (speedMbps < 0.1) return 0;
|
|
4145
|
+
if (speedMbps < 1) return 0.1 + speedMbps / 1 * 0.2;
|
|
4146
|
+
if (speedMbps < 5) return 0.3 + (speedMbps - 1) / 4 * 0.3;
|
|
4147
|
+
if (speedMbps < 20) return 0.6 + (speedMbps - 5) / 15 * 0.3;
|
|
4148
|
+
return Math.min(1, 0.9 + (speedMbps - 20) / 200);
|
|
4149
|
+
}
|
|
4150
|
+
/**
|
|
4151
|
+
* Calculate device factor (0-1)
|
|
4152
|
+
*/
|
|
4153
|
+
calculateDeviceFactor() {
|
|
4154
|
+
let factor = 0.5;
|
|
4155
|
+
if (this.deviceProfile.isPremium) {
|
|
4156
|
+
factor = 0.8;
|
|
4157
|
+
} else if (this.deviceProfile.isConstrained) {
|
|
4158
|
+
factor = 0.2;
|
|
4159
|
+
}
|
|
4160
|
+
if (this.deviceProfile.cpuUtilization > 0.8) {
|
|
4161
|
+
factor *= 0.7;
|
|
4162
|
+
} else if (this.deviceProfile.cpuUtilization < 0.2) {
|
|
4163
|
+
factor *= 1.1;
|
|
4164
|
+
}
|
|
4165
|
+
if (this.deviceProfile.supportsWebAssembly) {
|
|
4166
|
+
factor = Math.min(1, factor + 0.1);
|
|
4167
|
+
}
|
|
4168
|
+
return Math.max(0, Math.min(1, factor));
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Estimate compression time for a level (in ms)
|
|
4172
|
+
*/
|
|
4173
|
+
estimateCompressionTime(level) {
|
|
4174
|
+
return Math.max(1, level * 2.5);
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Estimate compression ratio for a level
|
|
4178
|
+
*/
|
|
4179
|
+
estimateCompressionRatio(level) {
|
|
4180
|
+
return 0.6 + level / 9 * 0.3;
|
|
4181
|
+
}
|
|
4182
|
+
/**
|
|
4183
|
+
* Apply recommendation and get new level
|
|
4184
|
+
*/
|
|
4185
|
+
applyRecommendation() {
|
|
4186
|
+
const recommendation = this.getRecommendedLevel();
|
|
4187
|
+
const oldLevel = this.currentLevel;
|
|
4188
|
+
const shouldChange = recommendation.confidence > 0.7 || Math.abs(recommendation.recommendedLevel - oldLevel) > 2;
|
|
4189
|
+
if (shouldChange) {
|
|
4190
|
+
this.currentLevel = recommendation.recommendedLevel;
|
|
4191
|
+
this.stats.levelsUsed.add(this.currentLevel);
|
|
4192
|
+
if (oldLevel !== this.currentLevel) {
|
|
4193
|
+
this.stats.adjustmentCount++;
|
|
4194
|
+
logger7.debug("[AdaptiveCompressionOptimizer] Level adjusted", {
|
|
4195
|
+
from: oldLevel,
|
|
4196
|
+
to: this.currentLevel,
|
|
4197
|
+
reason: recommendation.reason
|
|
4198
|
+
});
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
this.stats.currentLevel = this.currentLevel;
|
|
4202
|
+
return this.currentLevel;
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
4205
|
+
* Get current level
|
|
4206
|
+
*/
|
|
4207
|
+
getCurrentLevel() {
|
|
4208
|
+
return this.currentLevel;
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* Get statistics
|
|
4212
|
+
*/
|
|
4213
|
+
getStats() {
|
|
4214
|
+
return { ...this.stats };
|
|
4215
|
+
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Get detailed analysis
|
|
4218
|
+
*/
|
|
4219
|
+
getDetailedAnalysis() {
|
|
4220
|
+
return {
|
|
4221
|
+
stats: this.stats,
|
|
4222
|
+
network: this.networkProfile,
|
|
4223
|
+
device: this.deviceProfile,
|
|
4224
|
+
recommendation: this.getRecommendedLevel(),
|
|
4225
|
+
history: this.compressionHistory.slice(-20)
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
};
|
|
4229
|
+
var adaptiveOptimizerInstance = null;
|
|
4230
|
+
function getAdaptiveCompressionOptimizer() {
|
|
4231
|
+
if (!adaptiveOptimizerInstance) {
|
|
4232
|
+
adaptiveOptimizerInstance = new AdaptiveCompressionOptimizer();
|
|
4233
|
+
}
|
|
4234
|
+
return adaptiveOptimizerInstance;
|
|
4235
|
+
}
|
|
4236
|
+
function resetAdaptiveCompressionOptimizer() {
|
|
4237
|
+
adaptiveOptimizerInstance = null;
|
|
4238
|
+
}
|
|
4239
|
+
var logger8 = getLogger();
|
|
4240
|
+
var AgentPresenceManager = class extends eventemitter3.EventEmitter {
|
|
4241
|
+
presences = /* @__PURE__ */ new Map();
|
|
4242
|
+
sessionId;
|
|
4243
|
+
heartbeatInterval = null;
|
|
4244
|
+
heartbeatTimeout = 3e4;
|
|
4245
|
+
inactivityThreshold = 6e4;
|
|
4246
|
+
constructor(sessionId) {
|
|
4247
|
+
super();
|
|
4248
|
+
this.sessionId = sessionId;
|
|
4249
|
+
this.startHeartbeatCheck();
|
|
4250
|
+
logger8.debug("[AgentPresenceManager] Initialized", { sessionId });
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* Add or update agent presence
|
|
4254
|
+
*/
|
|
4255
|
+
updatePresence(agentId, presence) {
|
|
4256
|
+
const existing = this.presences.get(agentId);
|
|
4257
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4258
|
+
const updated = {
|
|
4259
|
+
...existing,
|
|
4260
|
+
...presence,
|
|
4261
|
+
agentId,
|
|
4262
|
+
joinedAt: existing?.joinedAt ?? now,
|
|
4263
|
+
lastSeen: now
|
|
4264
|
+
};
|
|
4265
|
+
this.presences.set(agentId, updated);
|
|
4266
|
+
this.emit("presence_updated", {
|
|
4267
|
+
agentId,
|
|
4268
|
+
presence: updated
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
4271
|
+
/**
|
|
4272
|
+
* Agent joined
|
|
4273
|
+
*/
|
|
4274
|
+
agentJoined(agentId, name, role = "user", metadata) {
|
|
4275
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4276
|
+
const presence = {
|
|
4277
|
+
agentId,
|
|
4278
|
+
name,
|
|
4279
|
+
role,
|
|
4280
|
+
status: "online",
|
|
4281
|
+
joinedAt: now,
|
|
4282
|
+
lastSeen: now,
|
|
4283
|
+
metadata
|
|
4284
|
+
};
|
|
4285
|
+
this.presences.set(agentId, presence);
|
|
4286
|
+
this.emit("agent_joined", { agentId, presence });
|
|
4287
|
+
logger8.debug("[AgentPresenceManager] Agent joined", { agentId, name, role });
|
|
4288
|
+
}
|
|
4289
|
+
/**
|
|
4290
|
+
* Agent left
|
|
4291
|
+
*/
|
|
4292
|
+
agentLeft(agentId) {
|
|
4293
|
+
const presence = this.presences.get(agentId);
|
|
4294
|
+
if (presence) {
|
|
4295
|
+
presence.status = "offline";
|
|
4296
|
+
presence.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4297
|
+
this.presences.set(agentId, presence);
|
|
4298
|
+
this.emit("agent_left", { agentId, presence });
|
|
4299
|
+
logger8.debug("[AgentPresenceManager] Agent left", { agentId });
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
/**
|
|
4303
|
+
* Update cursor position
|
|
4304
|
+
*/
|
|
4305
|
+
updateCursor(agentId, x, y, path) {
|
|
4306
|
+
const presence = this.presences.get(agentId);
|
|
4307
|
+
if (presence) {
|
|
4308
|
+
presence.cursorPosition = { x, y, path };
|
|
4309
|
+
presence.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4310
|
+
this.presences.set(agentId, presence);
|
|
4311
|
+
this.emit("cursor_updated", {
|
|
4312
|
+
agentId,
|
|
4313
|
+
cursorPosition: presence.cursorPosition
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
/**
|
|
4318
|
+
* Update active section
|
|
4319
|
+
*/
|
|
4320
|
+
updateActiveSection(agentId, section) {
|
|
4321
|
+
const presence = this.presences.get(agentId);
|
|
4322
|
+
if (presence) {
|
|
4323
|
+
presence.activeSection = section;
|
|
4324
|
+
presence.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4325
|
+
this.presences.set(agentId, presence);
|
|
4326
|
+
this.emit("section_updated", {
|
|
4327
|
+
agentId,
|
|
4328
|
+
activeSection: section
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* Update status
|
|
4334
|
+
*/
|
|
4335
|
+
updateStatus(agentId, status) {
|
|
4336
|
+
const presence = this.presences.get(agentId);
|
|
4337
|
+
if (presence) {
|
|
4338
|
+
presence.status = status;
|
|
4339
|
+
presence.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4340
|
+
this.presences.set(agentId, presence);
|
|
4341
|
+
this.emit("status_updated", { agentId, status });
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
/**
|
|
4345
|
+
* Heartbeat from agent (keeps them online)
|
|
4346
|
+
*/
|
|
4347
|
+
heartbeat(agentId) {
|
|
4348
|
+
const presence = this.presences.get(agentId);
|
|
4349
|
+
if (presence) {
|
|
4350
|
+
if (presence.status === "reconnecting") {
|
|
4351
|
+
presence.status = "online";
|
|
4352
|
+
this.emit("status_updated", { agentId, status: "online" });
|
|
4353
|
+
}
|
|
4354
|
+
presence.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
4355
|
+
this.presences.set(agentId, presence);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
/**
|
|
4359
|
+
* Get presence for agent
|
|
4360
|
+
*/
|
|
4361
|
+
getPresence(agentId) {
|
|
4362
|
+
return this.presences.get(agentId);
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Get all online agents
|
|
4366
|
+
*/
|
|
4367
|
+
getOnlineAgents() {
|
|
4368
|
+
return Array.from(this.presences.values()).filter(
|
|
4369
|
+
(p) => p.status === "online"
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* Get all agents
|
|
4374
|
+
*/
|
|
4375
|
+
getAllAgents() {
|
|
4376
|
+
return Array.from(this.presences.values());
|
|
4377
|
+
}
|
|
4378
|
+
/**
|
|
4379
|
+
* Get all presences
|
|
4380
|
+
*/
|
|
4381
|
+
getAllPresences() {
|
|
4382
|
+
return Array.from(this.presences.values());
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* Get agent count
|
|
4386
|
+
*/
|
|
4387
|
+
getAgentCount() {
|
|
4388
|
+
const counts = {
|
|
4389
|
+
online: 0,
|
|
4390
|
+
away: 0,
|
|
4391
|
+
offline: 0,
|
|
4392
|
+
reconnecting: 0
|
|
4393
|
+
};
|
|
4394
|
+
this.presences.forEach((p) => {
|
|
4395
|
+
counts[p.status]++;
|
|
4396
|
+
});
|
|
4397
|
+
return counts;
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4400
|
+
* Get statistics
|
|
4401
|
+
*/
|
|
4402
|
+
getStats() {
|
|
4403
|
+
return {
|
|
4404
|
+
totalAgents: this.presences.size,
|
|
4405
|
+
onlineAgents: Array.from(this.presences.values()).filter(
|
|
4406
|
+
(p) => p.status === "online"
|
|
4407
|
+
).length,
|
|
4408
|
+
offlineAgents: Array.from(this.presences.values()).filter(
|
|
4409
|
+
(p) => p.status === "offline"
|
|
4410
|
+
).length,
|
|
4411
|
+
awayAgents: Array.from(this.presences.values()).filter(
|
|
4412
|
+
(p) => p.status === "away"
|
|
4413
|
+
).length,
|
|
4414
|
+
reconnectingAgents: Array.from(this.presences.values()).filter(
|
|
4415
|
+
(p) => p.status === "reconnecting"
|
|
4416
|
+
).length
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4419
|
+
/**
|
|
4420
|
+
* Clear expired presences
|
|
4421
|
+
*/
|
|
4422
|
+
clearExpiredPresences(maxAgeMs) {
|
|
4423
|
+
const now = Date.now();
|
|
4424
|
+
const toRemove = [];
|
|
4425
|
+
this.presences.forEach((presence, agentId) => {
|
|
4426
|
+
const lastSeenTime = new Date(presence.lastSeen).getTime();
|
|
4427
|
+
const ageMs = now - lastSeenTime;
|
|
4428
|
+
if (ageMs > maxAgeMs && presence.status === "offline") {
|
|
4429
|
+
toRemove.push(agentId);
|
|
4430
|
+
}
|
|
4431
|
+
});
|
|
4432
|
+
toRemove.forEach((agentId) => {
|
|
4433
|
+
this.presences.delete(agentId);
|
|
4434
|
+
});
|
|
4435
|
+
if (toRemove.length > 0) {
|
|
4436
|
+
logger8.debug("[AgentPresenceManager] Cleared expired presences", {
|
|
4437
|
+
count: toRemove.length
|
|
4438
|
+
});
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Get agents by role
|
|
4443
|
+
*/
|
|
4444
|
+
getByRole(role) {
|
|
4445
|
+
return Array.from(this.presences.values()).filter((p) => p.role === role);
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Get agents in active section
|
|
4449
|
+
*/
|
|
4450
|
+
getInSection(section) {
|
|
4451
|
+
return Array.from(this.presences.values()).filter(
|
|
4452
|
+
(p) => p.activeSection === section && p.status === "online"
|
|
4453
|
+
);
|
|
4454
|
+
}
|
|
4455
|
+
/**
|
|
4456
|
+
* Get presence timeline
|
|
4457
|
+
*/
|
|
4458
|
+
getPresenceStats() {
|
|
4459
|
+
const stats = {
|
|
4460
|
+
total: this.presences.size,
|
|
4461
|
+
online: 0,
|
|
4462
|
+
away: 0,
|
|
4463
|
+
offline: 0,
|
|
4464
|
+
reconnecting: 0,
|
|
4465
|
+
byRole: {}
|
|
4466
|
+
};
|
|
4467
|
+
this.presences.forEach((p) => {
|
|
4468
|
+
stats[p.status]++;
|
|
4469
|
+
stats.byRole[p.role] = (stats.byRole[p.role] ?? 0) + 1;
|
|
4470
|
+
});
|
|
4471
|
+
return stats;
|
|
4472
|
+
}
|
|
4473
|
+
/**
|
|
4474
|
+
* Start heartbeat check (mark inactive agents as away)
|
|
4475
|
+
*/
|
|
4476
|
+
startHeartbeatCheck() {
|
|
4477
|
+
this.heartbeatInterval = setInterval(() => {
|
|
4478
|
+
const now = Date.now();
|
|
4479
|
+
this.presences.forEach((presence) => {
|
|
4480
|
+
const lastSeenTime = new Date(presence.lastSeen).getTime();
|
|
4481
|
+
const timeSinceLastSeen = now - lastSeenTime;
|
|
4482
|
+
if (timeSinceLastSeen > this.inactivityThreshold && presence.status === "online") {
|
|
4483
|
+
presence.status = "away";
|
|
4484
|
+
this.emit("status_updated", {
|
|
4485
|
+
agentId: presence.agentId,
|
|
4486
|
+
status: "away"
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
4489
|
+
if (timeSinceLastSeen > this.heartbeatTimeout && presence.status !== "offline") {
|
|
4490
|
+
presence.status = "reconnecting";
|
|
4491
|
+
this.emit("status_updated", {
|
|
4492
|
+
agentId: presence.agentId,
|
|
4493
|
+
status: "reconnecting"
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
}, 1e4);
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* Stop heartbeat monitoring
|
|
4501
|
+
*/
|
|
4502
|
+
stopHeartbeatMonitoring() {
|
|
4503
|
+
if (this.heartbeatInterval) {
|
|
4504
|
+
clearInterval(this.heartbeatInterval);
|
|
4505
|
+
this.heartbeatInterval = null;
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
/**
|
|
4509
|
+
* Clear all presences
|
|
4510
|
+
*/
|
|
4511
|
+
clear() {
|
|
4512
|
+
this.presences.clear();
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Destroy the manager
|
|
4516
|
+
*/
|
|
4517
|
+
destroy() {
|
|
4518
|
+
this.stopHeartbeatMonitoring();
|
|
4519
|
+
this.presences.clear();
|
|
4520
|
+
this.removeAllListeners();
|
|
4521
|
+
logger8.debug("[AgentPresenceManager] Destroyed", { sessionId: this.sessionId });
|
|
4522
|
+
}
|
|
4523
|
+
};
|
|
4524
|
+
var instances = /* @__PURE__ */ new Map();
|
|
4525
|
+
function getAgentPresenceManager(sessionId) {
|
|
4526
|
+
if (!instances.has(sessionId)) {
|
|
4527
|
+
instances.set(sessionId, new AgentPresenceManager(sessionId));
|
|
4528
|
+
}
|
|
4529
|
+
return instances.get(sessionId);
|
|
4530
|
+
}
|
|
4531
|
+
function clearAgentPresenceManager(sessionId) {
|
|
4532
|
+
const instance = instances.get(sessionId);
|
|
4533
|
+
if (instance) {
|
|
4534
|
+
instance.destroy();
|
|
4535
|
+
instances.delete(sessionId);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
// src/crypto/types.ts
|
|
4540
|
+
var AEON_CAPABILITIES = {
|
|
4541
|
+
// Basic sync operations
|
|
4542
|
+
SYNC_READ: "aeon:sync:read",
|
|
4543
|
+
SYNC_WRITE: "aeon:sync:write",
|
|
4544
|
+
SYNC_ADMIN: "aeon:sync:admin",
|
|
4545
|
+
// Node operations
|
|
4546
|
+
NODE_REGISTER: "aeon:node:register",
|
|
4547
|
+
NODE_HEARTBEAT: "aeon:node:heartbeat",
|
|
4548
|
+
// Replication operations
|
|
4549
|
+
REPLICATE_READ: "aeon:replicate:read",
|
|
4550
|
+
REPLICATE_WRITE: "aeon:replicate:write",
|
|
4551
|
+
// State operations
|
|
4552
|
+
STATE_READ: "aeon:state:read",
|
|
4553
|
+
STATE_WRITE: "aeon:state:write",
|
|
4554
|
+
STATE_RECONCILE: "aeon:state:reconcile"
|
|
4555
|
+
};
|
|
4556
|
+
var DEFAULT_CRYPTO_CONFIG = {
|
|
4557
|
+
defaultEncryptionMode: "none",
|
|
4558
|
+
requireSignatures: false,
|
|
4559
|
+
requireCapabilities: false,
|
|
4560
|
+
allowedSignatureAlgorithms: ["ES256", "Ed25519"],
|
|
4561
|
+
allowedEncryptionAlgorithms: ["ECIES-P256", "AES-256-GCM"],
|
|
4562
|
+
sessionKeyExpiration: 24 * 60 * 60 * 1e3
|
|
4563
|
+
// 24 hours
|
|
4564
|
+
};
|
|
4565
|
+
|
|
4566
|
+
// src/crypto/CryptoProvider.ts
|
|
4567
|
+
var NullCryptoProvider = class {
|
|
4568
|
+
notConfiguredError() {
|
|
4569
|
+
return new Error("Crypto provider not configured");
|
|
4570
|
+
}
|
|
4571
|
+
async generateIdentity() {
|
|
4572
|
+
throw this.notConfiguredError();
|
|
4573
|
+
}
|
|
4574
|
+
getLocalDID() {
|
|
4575
|
+
return null;
|
|
4576
|
+
}
|
|
4577
|
+
async exportPublicIdentity() {
|
|
4578
|
+
return null;
|
|
4579
|
+
}
|
|
4580
|
+
async registerRemoteNode() {
|
|
4581
|
+
}
|
|
4582
|
+
async getRemotePublicKey() {
|
|
4583
|
+
return null;
|
|
4584
|
+
}
|
|
4585
|
+
async sign() {
|
|
4586
|
+
throw this.notConfiguredError();
|
|
4587
|
+
}
|
|
4588
|
+
async signData(_data) {
|
|
4589
|
+
throw this.notConfiguredError();
|
|
4590
|
+
}
|
|
4591
|
+
async verify() {
|
|
4592
|
+
return true;
|
|
4593
|
+
}
|
|
4594
|
+
async verifySignedData() {
|
|
4595
|
+
return true;
|
|
4596
|
+
}
|
|
4597
|
+
async encrypt() {
|
|
4598
|
+
throw this.notConfiguredError();
|
|
4599
|
+
}
|
|
4600
|
+
async decrypt() {
|
|
4601
|
+
throw this.notConfiguredError();
|
|
4602
|
+
}
|
|
4603
|
+
async getSessionKey() {
|
|
4604
|
+
throw this.notConfiguredError();
|
|
4605
|
+
}
|
|
4606
|
+
async encryptWithSessionKey() {
|
|
4607
|
+
throw this.notConfiguredError();
|
|
4608
|
+
}
|
|
4609
|
+
async decryptWithSessionKey() {
|
|
4610
|
+
throw this.notConfiguredError();
|
|
4611
|
+
}
|
|
4612
|
+
async createUCAN() {
|
|
4613
|
+
throw this.notConfiguredError();
|
|
4614
|
+
}
|
|
4615
|
+
async verifyUCAN() {
|
|
4616
|
+
return { authorized: true };
|
|
4617
|
+
}
|
|
4618
|
+
async delegateCapabilities() {
|
|
4619
|
+
throw this.notConfiguredError();
|
|
4620
|
+
}
|
|
4621
|
+
async hash() {
|
|
4622
|
+
throw this.notConfiguredError();
|
|
4623
|
+
}
|
|
4624
|
+
randomBytes(length) {
|
|
4625
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
4626
|
+
}
|
|
4627
|
+
isInitialized() {
|
|
4628
|
+
return false;
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4631
|
+
|
|
4632
|
+
exports.AEON_CAPABILITIES = AEON_CAPABILITIES;
|
|
4633
|
+
exports.AdaptiveCompressionOptimizer = AdaptiveCompressionOptimizer;
|
|
4634
|
+
exports.AgentPresenceManager = AgentPresenceManager;
|
|
4635
|
+
exports.BatchTimingOptimizer = BatchTimingOptimizer;
|
|
4636
|
+
exports.CompressionEngine = CompressionEngine;
|
|
4637
|
+
exports.DEFAULT_CRYPTO_CONFIG = DEFAULT_CRYPTO_CONFIG;
|
|
4638
|
+
exports.DataTransformer = DataTransformer;
|
|
4639
|
+
exports.DeltaSyncOptimizer = DeltaSyncOptimizer;
|
|
4640
|
+
exports.MigrationEngine = MigrationEngine;
|
|
4641
|
+
exports.MigrationTracker = MigrationTracker;
|
|
4642
|
+
exports.NullCryptoProvider = NullCryptoProvider;
|
|
4643
|
+
exports.OfflineOperationQueue = OfflineOperationQueue;
|
|
4644
|
+
exports.PrefetchingEngine = PrefetchingEngine;
|
|
4645
|
+
exports.ReplicationManager = ReplicationManager;
|
|
4646
|
+
exports.SchemaVersionManager = SchemaVersionManager;
|
|
4647
|
+
exports.StateReconciler = StateReconciler;
|
|
4648
|
+
exports.SyncCoordinator = SyncCoordinator;
|
|
4649
|
+
exports.SyncProtocol = SyncProtocol;
|
|
4650
|
+
exports.clearAgentPresenceManager = clearAgentPresenceManager;
|
|
4651
|
+
exports.createNamespacedLogger = createNamespacedLogger;
|
|
4652
|
+
exports.disableLogging = disableLogging;
|
|
4653
|
+
exports.getAdaptiveCompressionOptimizer = getAdaptiveCompressionOptimizer;
|
|
4654
|
+
exports.getAgentPresenceManager = getAgentPresenceManager;
|
|
4655
|
+
exports.getBatchTimingOptimizer = getBatchTimingOptimizer;
|
|
4656
|
+
exports.getCompressionEngine = getCompressionEngine;
|
|
4657
|
+
exports.getDeltaSyncOptimizer = getDeltaSyncOptimizer;
|
|
4658
|
+
exports.getLogger = getLogger;
|
|
4659
|
+
exports.getOfflineOperationQueue = getOfflineOperationQueue;
|
|
4660
|
+
exports.getPrefetchingEngine = getPrefetchingEngine;
|
|
4661
|
+
exports.logger = logger;
|
|
4662
|
+
exports.resetAdaptiveCompressionOptimizer = resetAdaptiveCompressionOptimizer;
|
|
4663
|
+
exports.resetBatchTimingOptimizer = resetBatchTimingOptimizer;
|
|
4664
|
+
exports.resetCompressionEngine = resetCompressionEngine;
|
|
4665
|
+
exports.resetDeltaSyncOptimizer = resetDeltaSyncOptimizer;
|
|
4666
|
+
exports.resetLogger = resetLogger;
|
|
4667
|
+
exports.resetOfflineOperationQueue = resetOfflineOperationQueue;
|
|
4668
|
+
exports.resetPrefetchingEngine = resetPrefetchingEngine;
|
|
4669
|
+
exports.setLogger = setLogger;
|
|
4670
|
+
//# sourceMappingURL=index.cjs.map
|
|
4671
|
+
//# sourceMappingURL=index.cjs.map
|