@affectively/dash 5.4.0 → 5.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -189
- package/dist/automerge_wasm_bg-4hg5vg2g.wasm +0 -0
- package/dist/engine/sqlite.d.ts +30 -0
- package/dist/engine/vec_extension.d.ts +2 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +53895 -0
- package/dist/middleware/errorHandler.d.ts +60 -0
- package/dist/{src/sync → sync}/AeonDurableSync.d.ts +8 -9
- package/dist/sync/AeonDurableSync.js +1984 -0
- package/dist/{src/sync → sync}/AutomergeProvider.d.ts +8 -8
- package/dist/sync/AutomergeProvider.js +4421 -0
- package/dist/sync/HybridProvider.d.ts +124 -0
- package/dist/sync/HybridProvider.js +8328 -0
- package/dist/sync/connection/WebRTCConnection.d.ts +23 -0
- package/dist/sync/connection/WebRTCConnection.js +59 -0
- package/dist/sync/index.d.ts +13 -0
- package/dist/sync/index.js +12773 -0
- package/dist/sync/provider/YjsSqliteProvider.d.ts +17 -0
- package/dist/sync/provider/YjsSqliteProvider.js +54 -0
- package/dist/sync/types.d.ts +74 -0
- package/dist/sync/webtransport/WebTransportProvider.d.ts +16 -0
- package/dist/sync/webtransport/WebTransportProvider.js +55 -0
- package/package.json +62 -70
- package/dist/src/api/firebase/auth/index.d.ts +0 -137
- package/dist/src/api/firebase/auth/index.js +0 -352
- package/dist/src/api/firebase/auth/providers.d.ts +0 -254
- package/dist/src/api/firebase/auth/providers.js +0 -518
- package/dist/src/api/firebase/database/index.d.ts +0 -108
- package/dist/src/api/firebase/database/index.js +0 -368
- package/dist/src/api/firebase/errors.d.ts +0 -15
- package/dist/src/api/firebase/errors.js +0 -215
- package/dist/src/api/firebase/firestore/data-types.d.ts +0 -116
- package/dist/src/api/firebase/firestore/data-types.js +0 -280
- package/dist/src/api/firebase/firestore/index.d.ts +0 -7
- package/dist/src/api/firebase/firestore/index.js +0 -13
- package/dist/src/api/firebase/firestore/listeners.d.ts +0 -20
- package/dist/src/api/firebase/firestore/listeners.js +0 -50
- package/dist/src/api/firebase/firestore/operations.d.ts +0 -123
- package/dist/src/api/firebase/firestore/operations.js +0 -490
- package/dist/src/api/firebase/firestore/query.d.ts +0 -118
- package/dist/src/api/firebase/firestore/query.js +0 -418
- package/dist/src/api/firebase/index.d.ts +0 -11
- package/dist/src/api/firebase/index.js +0 -17
- package/dist/src/api/firebase/storage/index.d.ts +0 -100
- package/dist/src/api/firebase/storage/index.js +0 -286
- package/dist/src/api/firebase/types.d.ts +0 -341
- package/dist/src/api/firebase/types.js +0 -4
- package/dist/src/auth/manager.d.ts +0 -182
- package/dist/src/auth/manager.js +0 -598
- package/dist/src/engine/ai.js +0 -76
- package/dist/src/engine/sqlite.d.ts +0 -353
- package/dist/src/engine/sqlite.js +0 -1328
- package/dist/src/engine/vec_extension.d.ts +0 -5
- package/dist/src/engine/vec_extension.js +0 -10
- package/dist/src/index.d.ts +0 -21
- package/dist/src/index.js +0 -26
- package/dist/src/mcp/server.js +0 -87
- package/dist/src/reactivity/signal.js +0 -31
- package/dist/src/schema/lens.d.ts +0 -29
- package/dist/src/schema/lens.js +0 -122
- package/dist/src/sync/AeonDurableSync.js +0 -67
- package/dist/src/sync/AutomergeProvider.js +0 -153
- package/dist/src/sync/aeon/config.d.ts +0 -21
- package/dist/src/sync/aeon/config.js +0 -14
- package/dist/src/sync/aeon/delta-adapter.d.ts +0 -62
- package/dist/src/sync/aeon/delta-adapter.js +0 -98
- package/dist/src/sync/aeon/index.d.ts +0 -18
- package/dist/src/sync/aeon/index.js +0 -19
- package/dist/src/sync/aeon/offline-adapter.d.ts +0 -110
- package/dist/src/sync/aeon/offline-adapter.js +0 -227
- package/dist/src/sync/aeon/presence-adapter.d.ts +0 -114
- package/dist/src/sync/aeon/presence-adapter.js +0 -157
- package/dist/src/sync/aeon/schema-adapter.d.ts +0 -95
- package/dist/src/sync/aeon/schema-adapter.js +0 -163
- package/dist/src/sync/backup.d.ts +0 -12
- package/dist/src/sync/backup.js +0 -44
- package/dist/src/sync/connection.d.ts +0 -20
- package/dist/src/sync/connection.js +0 -50
- package/dist/src/sync/d1-provider.d.ts +0 -103
- package/dist/src/sync/d1-provider.js +0 -418
- package/dist/src/sync/hybrid-provider.d.ts +0 -307
- package/dist/src/sync/hybrid-provider.js +0 -1353
- package/dist/src/sync/provider.d.ts +0 -11
- package/dist/src/sync/provider.js +0 -67
- package/dist/src/sync/types.d.ts +0 -32
- package/dist/src/sync/types.js +0 -4
- package/dist/src/sync/verify.d.ts +0 -1
- package/dist/src/sync/verify.js +0 -23
- package/dist/tsconfig.tsbuildinfo +0 -1
- /package/dist/{src/engine → engine}/ai.d.ts +0 -0
- /package/dist/{src/mcp → mcp}/server.d.ts +0 -0
- /package/dist/{src/reactivity → reactivity}/signal.d.ts +0 -0
|
@@ -0,0 +1,1984 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
7
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
8
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
9
|
+
for (let key of __getOwnPropNames(mod))
|
|
10
|
+
if (!__hasOwnProp.call(to, key))
|
|
11
|
+
__defProp(to, key, {
|
|
12
|
+
get: () => mod[key],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
18
|
+
var __export = (target, all) => {
|
|
19
|
+
for (var name in all)
|
|
20
|
+
__defProp(target, name, {
|
|
21
|
+
get: all[name],
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
set: (newValue) => all[name] = () => newValue
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
28
|
+
|
|
29
|
+
// ../../node_modules/.bun/eventemitter3@5.0.4/node_modules/eventemitter3/index.js
|
|
30
|
+
var require_eventemitter3 = __commonJS((exports, module) => {
|
|
31
|
+
var has = Object.prototype.hasOwnProperty;
|
|
32
|
+
var prefix = "~";
|
|
33
|
+
function Events() {}
|
|
34
|
+
if (Object.create) {
|
|
35
|
+
Events.prototype = Object.create(null);
|
|
36
|
+
if (!new Events().__proto__)
|
|
37
|
+
prefix = false;
|
|
38
|
+
}
|
|
39
|
+
function EE(fn, context, once) {
|
|
40
|
+
this.fn = fn;
|
|
41
|
+
this.context = context;
|
|
42
|
+
this.once = once || false;
|
|
43
|
+
}
|
|
44
|
+
function addListener(emitter, event, fn, context, once) {
|
|
45
|
+
if (typeof fn !== "function") {
|
|
46
|
+
throw new TypeError("The listener must be a function");
|
|
47
|
+
}
|
|
48
|
+
var listener = new EE(fn, context || emitter, once), evt = prefix ? prefix + event : event;
|
|
49
|
+
if (!emitter._events[evt])
|
|
50
|
+
emitter._events[evt] = listener, emitter._eventsCount++;
|
|
51
|
+
else if (!emitter._events[evt].fn)
|
|
52
|
+
emitter._events[evt].push(listener);
|
|
53
|
+
else
|
|
54
|
+
emitter._events[evt] = [emitter._events[evt], listener];
|
|
55
|
+
return emitter;
|
|
56
|
+
}
|
|
57
|
+
function clearEvent(emitter, evt) {
|
|
58
|
+
if (--emitter._eventsCount === 0)
|
|
59
|
+
emitter._events = new Events;
|
|
60
|
+
else
|
|
61
|
+
delete emitter._events[evt];
|
|
62
|
+
}
|
|
63
|
+
function EventEmitter() {
|
|
64
|
+
this._events = new Events;
|
|
65
|
+
this._eventsCount = 0;
|
|
66
|
+
}
|
|
67
|
+
EventEmitter.prototype.eventNames = function eventNames() {
|
|
68
|
+
var names = [], events, name;
|
|
69
|
+
if (this._eventsCount === 0)
|
|
70
|
+
return names;
|
|
71
|
+
for (name in events = this._events) {
|
|
72
|
+
if (has.call(events, name))
|
|
73
|
+
names.push(prefix ? name.slice(1) : name);
|
|
74
|
+
}
|
|
75
|
+
if (Object.getOwnPropertySymbols) {
|
|
76
|
+
return names.concat(Object.getOwnPropertySymbols(events));
|
|
77
|
+
}
|
|
78
|
+
return names;
|
|
79
|
+
};
|
|
80
|
+
EventEmitter.prototype.listeners = function listeners(event) {
|
|
81
|
+
var evt = prefix ? prefix + event : event, handlers = this._events[evt];
|
|
82
|
+
if (!handlers)
|
|
83
|
+
return [];
|
|
84
|
+
if (handlers.fn)
|
|
85
|
+
return [handlers.fn];
|
|
86
|
+
for (var i = 0, l = handlers.length, ee = new Array(l);i < l; i++) {
|
|
87
|
+
ee[i] = handlers[i].fn;
|
|
88
|
+
}
|
|
89
|
+
return ee;
|
|
90
|
+
};
|
|
91
|
+
EventEmitter.prototype.listenerCount = function listenerCount(event) {
|
|
92
|
+
var evt = prefix ? prefix + event : event, listeners = this._events[evt];
|
|
93
|
+
if (!listeners)
|
|
94
|
+
return 0;
|
|
95
|
+
if (listeners.fn)
|
|
96
|
+
return 1;
|
|
97
|
+
return listeners.length;
|
|
98
|
+
};
|
|
99
|
+
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
|
|
100
|
+
var evt = prefix ? prefix + event : event;
|
|
101
|
+
if (!this._events[evt])
|
|
102
|
+
return false;
|
|
103
|
+
var listeners = this._events[evt], len = arguments.length, args, i;
|
|
104
|
+
if (listeners.fn) {
|
|
105
|
+
if (listeners.once)
|
|
106
|
+
this.removeListener(event, listeners.fn, undefined, true);
|
|
107
|
+
switch (len) {
|
|
108
|
+
case 1:
|
|
109
|
+
return listeners.fn.call(listeners.context), true;
|
|
110
|
+
case 2:
|
|
111
|
+
return listeners.fn.call(listeners.context, a1), true;
|
|
112
|
+
case 3:
|
|
113
|
+
return listeners.fn.call(listeners.context, a1, a2), true;
|
|
114
|
+
case 4:
|
|
115
|
+
return listeners.fn.call(listeners.context, a1, a2, a3), true;
|
|
116
|
+
case 5:
|
|
117
|
+
return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
|
|
118
|
+
case 6:
|
|
119
|
+
return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
|
|
120
|
+
}
|
|
121
|
+
for (i = 1, args = new Array(len - 1);i < len; i++) {
|
|
122
|
+
args[i - 1] = arguments[i];
|
|
123
|
+
}
|
|
124
|
+
listeners.fn.apply(listeners.context, args);
|
|
125
|
+
} else {
|
|
126
|
+
var length = listeners.length, j;
|
|
127
|
+
for (i = 0;i < length; i++) {
|
|
128
|
+
if (listeners[i].once)
|
|
129
|
+
this.removeListener(event, listeners[i].fn, undefined, true);
|
|
130
|
+
switch (len) {
|
|
131
|
+
case 1:
|
|
132
|
+
listeners[i].fn.call(listeners[i].context);
|
|
133
|
+
break;
|
|
134
|
+
case 2:
|
|
135
|
+
listeners[i].fn.call(listeners[i].context, a1);
|
|
136
|
+
break;
|
|
137
|
+
case 3:
|
|
138
|
+
listeners[i].fn.call(listeners[i].context, a1, a2);
|
|
139
|
+
break;
|
|
140
|
+
case 4:
|
|
141
|
+
listeners[i].fn.call(listeners[i].context, a1, a2, a3);
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
if (!args)
|
|
145
|
+
for (j = 1, args = new Array(len - 1);j < len; j++) {
|
|
146
|
+
args[j - 1] = arguments[j];
|
|
147
|
+
}
|
|
148
|
+
listeners[i].fn.apply(listeners[i].context, args);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
EventEmitter.prototype.on = function on(event, fn, context) {
|
|
155
|
+
return addListener(this, event, fn, context, false);
|
|
156
|
+
};
|
|
157
|
+
EventEmitter.prototype.once = function once(event, fn, context) {
|
|
158
|
+
return addListener(this, event, fn, context, true);
|
|
159
|
+
};
|
|
160
|
+
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
|
|
161
|
+
var evt = prefix ? prefix + event : event;
|
|
162
|
+
if (!this._events[evt])
|
|
163
|
+
return this;
|
|
164
|
+
if (!fn) {
|
|
165
|
+
clearEvent(this, evt);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
var listeners = this._events[evt];
|
|
169
|
+
if (listeners.fn) {
|
|
170
|
+
if (listeners.fn === fn && (!once || listeners.once) && (!context || listeners.context === context)) {
|
|
171
|
+
clearEvent(this, evt);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
for (var i = 0, events = [], length = listeners.length;i < length; i++) {
|
|
175
|
+
if (listeners[i].fn !== fn || once && !listeners[i].once || context && listeners[i].context !== context) {
|
|
176
|
+
events.push(listeners[i]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (events.length)
|
|
180
|
+
this._events[evt] = events.length === 1 ? events[0] : events;
|
|
181
|
+
else
|
|
182
|
+
clearEvent(this, evt);
|
|
183
|
+
}
|
|
184
|
+
return this;
|
|
185
|
+
};
|
|
186
|
+
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
|
|
187
|
+
var evt;
|
|
188
|
+
if (event) {
|
|
189
|
+
evt = prefix ? prefix + event : event;
|
|
190
|
+
if (this._events[evt])
|
|
191
|
+
clearEvent(this, evt);
|
|
192
|
+
} else {
|
|
193
|
+
this._events = new Events;
|
|
194
|
+
this._eventsCount = 0;
|
|
195
|
+
}
|
|
196
|
+
return this;
|
|
197
|
+
};
|
|
198
|
+
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
|
199
|
+
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
|
|
200
|
+
EventEmitter.prefixed = prefix;
|
|
201
|
+
EventEmitter.EventEmitter = EventEmitter;
|
|
202
|
+
if (typeof module !== "undefined") {
|
|
203
|
+
module.exports = EventEmitter;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ../../node_modules/.bun/eventemitter3@5.0.4/node_modules/eventemitter3/index.mjs
|
|
208
|
+
var import__ = __toESM(require_eventemitter3(), 1);
|
|
209
|
+
|
|
210
|
+
// ../aeon/dist/offline/index.js
|
|
211
|
+
var consoleLogger = {
|
|
212
|
+
debug: (...args) => {
|
|
213
|
+
console.debug("[AEON:DEBUG]", ...args);
|
|
214
|
+
},
|
|
215
|
+
info: (...args) => {
|
|
216
|
+
console.info("[AEON:INFO]", ...args);
|
|
217
|
+
},
|
|
218
|
+
warn: (...args) => {
|
|
219
|
+
console.warn("[AEON:WARN]", ...args);
|
|
220
|
+
},
|
|
221
|
+
error: (...args) => {
|
|
222
|
+
console.error("[AEON:ERROR]", ...args);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var currentLogger = consoleLogger;
|
|
226
|
+
function getLogger() {
|
|
227
|
+
return currentLogger;
|
|
228
|
+
}
|
|
229
|
+
var logger = getLogger();
|
|
230
|
+
var OfflineOperationQueue = class _OfflineOperationQueue extends import__.default {
|
|
231
|
+
static DEFAULT_PERSIST_KEY = "aeon:offline-queue:v1";
|
|
232
|
+
queue = /* @__PURE__ */ new Map;
|
|
233
|
+
syncingIds = /* @__PURE__ */ new Set;
|
|
234
|
+
maxQueueSize = 1000;
|
|
235
|
+
defaultMaxRetries = 3;
|
|
236
|
+
persistence = null;
|
|
237
|
+
persistTimer = null;
|
|
238
|
+
persistInFlight = false;
|
|
239
|
+
persistPending = false;
|
|
240
|
+
constructor(maxQueueSizeOrOptions = 1000, defaultMaxRetries = 3) {
|
|
241
|
+
super();
|
|
242
|
+
if (typeof maxQueueSizeOrOptions === "number") {
|
|
243
|
+
this.maxQueueSize = maxQueueSizeOrOptions;
|
|
244
|
+
this.defaultMaxRetries = defaultMaxRetries;
|
|
245
|
+
} else {
|
|
246
|
+
this.maxQueueSize = maxQueueSizeOrOptions.maxQueueSize ?? 1000;
|
|
247
|
+
this.defaultMaxRetries = maxQueueSizeOrOptions.defaultMaxRetries ?? 3;
|
|
248
|
+
if (maxQueueSizeOrOptions.persistence) {
|
|
249
|
+
this.persistence = {
|
|
250
|
+
...maxQueueSizeOrOptions.persistence,
|
|
251
|
+
key: maxQueueSizeOrOptions.persistence.key ?? _OfflineOperationQueue.DEFAULT_PERSIST_KEY,
|
|
252
|
+
autoPersist: maxQueueSizeOrOptions.persistence.autoPersist ?? true,
|
|
253
|
+
autoLoad: maxQueueSizeOrOptions.persistence.autoLoad ?? false,
|
|
254
|
+
persistDebounceMs: maxQueueSizeOrOptions.persistence.persistDebounceMs ?? 25
|
|
255
|
+
};
|
|
256
|
+
if (this.persistence.autoLoad) {
|
|
257
|
+
this.loadFromPersistence().catch((error) => {
|
|
258
|
+
logger.error("[OfflineOperationQueue] Failed to load persistence", {
|
|
259
|
+
key: this.persistence?.key,
|
|
260
|
+
error: error instanceof Error ? error.message : String(error)
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
logger.debug("[OfflineOperationQueue] Initialized", {
|
|
267
|
+
maxQueueSize: this.maxQueueSize,
|
|
268
|
+
defaultMaxRetries: this.defaultMaxRetries,
|
|
269
|
+
persistenceEnabled: this.persistence !== null
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
enqueue(type, data, sessionId, priority = "normal", maxRetries) {
|
|
273
|
+
if (this.queue.size >= this.maxQueueSize) {
|
|
274
|
+
const oldest = this.findOldestLowPriority();
|
|
275
|
+
if (oldest) {
|
|
276
|
+
this.queue.delete(oldest.id);
|
|
277
|
+
logger.warn("[OfflineOperationQueue] Queue full, removed oldest", {
|
|
278
|
+
removedId: oldest.id
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const operation = {
|
|
283
|
+
id: `op-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
284
|
+
type,
|
|
285
|
+
data,
|
|
286
|
+
sessionId,
|
|
287
|
+
priority,
|
|
288
|
+
createdAt: Date.now(),
|
|
289
|
+
retryCount: 0,
|
|
290
|
+
maxRetries: maxRetries ?? this.defaultMaxRetries,
|
|
291
|
+
status: "pending"
|
|
292
|
+
};
|
|
293
|
+
this.queue.set(operation.id, operation);
|
|
294
|
+
this.emit("operation-added", operation);
|
|
295
|
+
this.schedulePersist();
|
|
296
|
+
logger.debug("[OfflineOperationQueue] Operation enqueued", {
|
|
297
|
+
id: operation.id,
|
|
298
|
+
type,
|
|
299
|
+
priority,
|
|
300
|
+
queueSize: this.queue.size
|
|
301
|
+
});
|
|
302
|
+
return operation;
|
|
303
|
+
}
|
|
304
|
+
getNextBatch(batchSize = 10) {
|
|
305
|
+
const pending = Array.from(this.queue.values()).filter((op) => op.status === "pending" && !this.syncingIds.has(op.id)).sort((a, b) => {
|
|
306
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
307
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
308
|
+
if (priorityDiff !== 0)
|
|
309
|
+
return priorityDiff;
|
|
310
|
+
return a.createdAt - b.createdAt;
|
|
311
|
+
});
|
|
312
|
+
return pending.slice(0, batchSize);
|
|
313
|
+
}
|
|
314
|
+
markSyncing(operationIds) {
|
|
315
|
+
let changed = false;
|
|
316
|
+
for (const id of operationIds) {
|
|
317
|
+
const op = this.queue.get(id);
|
|
318
|
+
if (op) {
|
|
319
|
+
op.status = "syncing";
|
|
320
|
+
this.syncingIds.add(id);
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (changed) {
|
|
325
|
+
this.schedulePersist();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
markSynced(operationId) {
|
|
329
|
+
const op = this.queue.get(operationId);
|
|
330
|
+
if (op) {
|
|
331
|
+
op.status = "synced";
|
|
332
|
+
this.syncingIds.delete(operationId);
|
|
333
|
+
this.emit("operation-synced", op);
|
|
334
|
+
this.schedulePersist();
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
this.queue.delete(operationId);
|
|
337
|
+
this.schedulePersist();
|
|
338
|
+
if (this.getPendingCount() === 0) {
|
|
339
|
+
this.emit("queue-empty");
|
|
340
|
+
}
|
|
341
|
+
}, 1000);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
markFailed(operationId, error) {
|
|
345
|
+
const op = this.queue.get(operationId);
|
|
346
|
+
if (op) {
|
|
347
|
+
op.retryCount++;
|
|
348
|
+
op.lastError = error.message;
|
|
349
|
+
this.syncingIds.delete(operationId);
|
|
350
|
+
if (op.retryCount >= op.maxRetries) {
|
|
351
|
+
op.status = "failed";
|
|
352
|
+
this.emit("operation-failed", op, error);
|
|
353
|
+
logger.error("[OfflineOperationQueue] Operation permanently failed", {
|
|
354
|
+
id: operationId,
|
|
355
|
+
retries: op.retryCount,
|
|
356
|
+
error: error.message
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
op.status = "pending";
|
|
360
|
+
logger.warn("[OfflineOperationQueue] Operation failed, will retry", {
|
|
361
|
+
id: operationId,
|
|
362
|
+
retryCount: op.retryCount,
|
|
363
|
+
maxRetries: op.maxRetries
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
this.schedulePersist();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
getOperation(operationId) {
|
|
370
|
+
return this.queue.get(operationId);
|
|
371
|
+
}
|
|
372
|
+
getPendingOperations() {
|
|
373
|
+
return Array.from(this.queue.values()).filter((op) => op.status === "pending");
|
|
374
|
+
}
|
|
375
|
+
getPendingCount() {
|
|
376
|
+
return Array.from(this.queue.values()).filter((op) => op.status === "pending").length;
|
|
377
|
+
}
|
|
378
|
+
getStats() {
|
|
379
|
+
const operations = Array.from(this.queue.values());
|
|
380
|
+
const pending = operations.filter((op) => op.status === "pending").length;
|
|
381
|
+
const syncing = operations.filter((op) => op.status === "syncing").length;
|
|
382
|
+
const failed = operations.filter((op) => op.status === "failed").length;
|
|
383
|
+
const synced = operations.filter((op) => op.status === "synced").length;
|
|
384
|
+
const pendingOps = operations.filter((op) => op.status === "pending");
|
|
385
|
+
const oldestPendingMs = pendingOps.length > 0 ? Date.now() - Math.min(...pendingOps.map((op) => op.createdAt)) : 0;
|
|
386
|
+
const averageRetries = operations.length > 0 ? operations.reduce((sum, op) => sum + op.retryCount, 0) / operations.length : 0;
|
|
387
|
+
return {
|
|
388
|
+
pending,
|
|
389
|
+
syncing,
|
|
390
|
+
failed,
|
|
391
|
+
synced,
|
|
392
|
+
totalOperations: operations.length,
|
|
393
|
+
oldestPendingMs,
|
|
394
|
+
averageRetries
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
clear() {
|
|
398
|
+
this.queue.clear();
|
|
399
|
+
this.syncingIds.clear();
|
|
400
|
+
this.schedulePersist();
|
|
401
|
+
logger.debug("[OfflineOperationQueue] Queue cleared");
|
|
402
|
+
}
|
|
403
|
+
clearFailed() {
|
|
404
|
+
let changed = false;
|
|
405
|
+
for (const [id, op] of this.queue.entries()) {
|
|
406
|
+
if (op.status === "failed") {
|
|
407
|
+
this.queue.delete(id);
|
|
408
|
+
changed = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (changed) {
|
|
412
|
+
this.schedulePersist();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
retryFailed() {
|
|
416
|
+
let changed = false;
|
|
417
|
+
for (const op of this.queue.values()) {
|
|
418
|
+
if (op.status === "failed") {
|
|
419
|
+
op.status = "pending";
|
|
420
|
+
op.retryCount = 0;
|
|
421
|
+
changed = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (changed) {
|
|
425
|
+
this.schedulePersist();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
findOldestLowPriority() {
|
|
429
|
+
const lowPriority = Array.from(this.queue.values()).filter((op) => op.priority === "low" && op.status === "pending").sort((a, b) => a.createdAt - b.createdAt);
|
|
430
|
+
return lowPriority[0] ?? null;
|
|
431
|
+
}
|
|
432
|
+
export() {
|
|
433
|
+
return Array.from(this.queue.values());
|
|
434
|
+
}
|
|
435
|
+
import(operations) {
|
|
436
|
+
this.queue.clear();
|
|
437
|
+
this.syncingIds.clear();
|
|
438
|
+
for (const op of operations) {
|
|
439
|
+
if (this.isValidOfflineOperation(op)) {
|
|
440
|
+
this.queue.set(op.id, {
|
|
441
|
+
...op,
|
|
442
|
+
status: op.status === "syncing" ? "pending" : op.status
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.schedulePersist();
|
|
447
|
+
logger.debug("[OfflineOperationQueue] Imported operations", {
|
|
448
|
+
count: this.queue.size
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
async saveToPersistence() {
|
|
452
|
+
if (!this.persistence) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const envelope = {
|
|
456
|
+
version: 1,
|
|
457
|
+
updatedAt: Date.now(),
|
|
458
|
+
data: this.export()
|
|
459
|
+
};
|
|
460
|
+
const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
|
|
461
|
+
const raw = serialize(envelope);
|
|
462
|
+
await this.persistence.adapter.setItem(this.persistence.key, raw);
|
|
463
|
+
}
|
|
464
|
+
async loadFromPersistence() {
|
|
465
|
+
if (!this.persistence) {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
const raw = await this.persistence.adapter.getItem(this.persistence.key);
|
|
469
|
+
if (!raw) {
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
472
|
+
const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
|
|
473
|
+
const envelope = deserialize(raw);
|
|
474
|
+
if (envelope.version !== 1 || !Array.isArray(envelope.data)) {
|
|
475
|
+
throw new Error("Invalid offline queue persistence payload");
|
|
476
|
+
}
|
|
477
|
+
this.queue.clear();
|
|
478
|
+
this.syncingIds.clear();
|
|
479
|
+
let imported = 0;
|
|
480
|
+
for (const operation of envelope.data) {
|
|
481
|
+
if (this.isValidOfflineOperation(operation)) {
|
|
482
|
+
this.queue.set(operation.id, {
|
|
483
|
+
...operation,
|
|
484
|
+
status: operation.status === "syncing" ? "pending" : operation.status
|
|
485
|
+
});
|
|
486
|
+
imported++;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
logger.debug("[OfflineOperationQueue] Loaded from persistence", {
|
|
490
|
+
key: this.persistence.key,
|
|
491
|
+
imported
|
|
492
|
+
});
|
|
493
|
+
return imported;
|
|
494
|
+
}
|
|
495
|
+
async clearPersistence() {
|
|
496
|
+
if (!this.persistence) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await this.persistence.adapter.removeItem(this.persistence.key);
|
|
500
|
+
}
|
|
501
|
+
schedulePersist() {
|
|
502
|
+
if (!this.persistence || this.persistence.autoPersist === false) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (this.persistTimer) {
|
|
506
|
+
clearTimeout(this.persistTimer);
|
|
507
|
+
}
|
|
508
|
+
this.persistTimer = setTimeout(() => {
|
|
509
|
+
this.persistSafely();
|
|
510
|
+
}, this.persistence.persistDebounceMs ?? 25);
|
|
511
|
+
}
|
|
512
|
+
async persistSafely() {
|
|
513
|
+
if (!this.persistence) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (this.persistInFlight) {
|
|
517
|
+
this.persistPending = true;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
this.persistInFlight = true;
|
|
521
|
+
try {
|
|
522
|
+
await this.saveToPersistence();
|
|
523
|
+
} catch (error) {
|
|
524
|
+
logger.error("[OfflineOperationQueue] Persistence write failed", {
|
|
525
|
+
key: this.persistence.key,
|
|
526
|
+
error: error instanceof Error ? error.message : String(error)
|
|
527
|
+
});
|
|
528
|
+
} finally {
|
|
529
|
+
this.persistInFlight = false;
|
|
530
|
+
const shouldRunAgain = this.persistPending;
|
|
531
|
+
this.persistPending = false;
|
|
532
|
+
if (shouldRunAgain) {
|
|
533
|
+
this.persistSafely();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
isValidOfflineOperation(value) {
|
|
538
|
+
if (typeof value !== "object" || value === null) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
const candidate = value;
|
|
542
|
+
const validType = candidate.type === "create" || candidate.type === "update" || candidate.type === "delete" || candidate.type === "sync" || candidate.type === "batch";
|
|
543
|
+
const validPriority = candidate.priority === "high" || candidate.priority === "normal" || candidate.priority === "low";
|
|
544
|
+
const validStatus = candidate.status === "pending" || candidate.status === "syncing" || candidate.status === "failed" || candidate.status === "synced";
|
|
545
|
+
return typeof candidate.id === "string" && validType && typeof candidate.data === "object" && candidate.data !== null && !Array.isArray(candidate.data) && typeof candidate.sessionId === "string" && validPriority && typeof candidate.createdAt === "number" && typeof candidate.retryCount === "number" && typeof candidate.maxRetries === "number" && validStatus;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// ../aeon/dist/distributed/index.js
|
|
550
|
+
var consoleLogger2 = {
|
|
551
|
+
debug: (...args) => {
|
|
552
|
+
console.debug("[AEON:DEBUG]", ...args);
|
|
553
|
+
},
|
|
554
|
+
info: (...args) => {
|
|
555
|
+
console.info("[AEON:INFO]", ...args);
|
|
556
|
+
},
|
|
557
|
+
warn: (...args) => {
|
|
558
|
+
console.warn("[AEON:WARN]", ...args);
|
|
559
|
+
},
|
|
560
|
+
error: (...args) => {
|
|
561
|
+
console.error("[AEON:ERROR]", ...args);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
var currentLogger2 = consoleLogger2;
|
|
565
|
+
function getLogger2() {
|
|
566
|
+
return currentLogger2;
|
|
567
|
+
}
|
|
568
|
+
var logger2 = {
|
|
569
|
+
debug: (...args) => getLogger2().debug(...args),
|
|
570
|
+
info: (...args) => getLogger2().info(...args),
|
|
571
|
+
warn: (...args) => getLogger2().warn(...args),
|
|
572
|
+
error: (...args) => getLogger2().error(...args)
|
|
573
|
+
};
|
|
574
|
+
var ReplicationManager = class _ReplicationManager {
|
|
575
|
+
static DEFAULT_PERSIST_KEY = "aeon:replication-state:v1";
|
|
576
|
+
replicas = /* @__PURE__ */ new Map;
|
|
577
|
+
policies = /* @__PURE__ */ new Map;
|
|
578
|
+
replicationEvents = [];
|
|
579
|
+
syncStatus = /* @__PURE__ */ new Map;
|
|
580
|
+
cryptoProvider = null;
|
|
581
|
+
replicasByDID = /* @__PURE__ */ new Map;
|
|
582
|
+
persistence = null;
|
|
583
|
+
persistTimer = null;
|
|
584
|
+
persistInFlight = false;
|
|
585
|
+
persistPending = false;
|
|
586
|
+
constructor(options) {
|
|
587
|
+
if (options?.persistence) {
|
|
588
|
+
this.persistence = {
|
|
589
|
+
...options.persistence,
|
|
590
|
+
key: options.persistence.key ?? _ReplicationManager.DEFAULT_PERSIST_KEY,
|
|
591
|
+
autoPersist: options.persistence.autoPersist ?? true,
|
|
592
|
+
autoLoad: options.persistence.autoLoad ?? false,
|
|
593
|
+
persistDebounceMs: options.persistence.persistDebounceMs ?? 25
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (this.persistence?.autoLoad) {
|
|
597
|
+
this.loadFromPersistence().catch((error) => {
|
|
598
|
+
logger2.error("[ReplicationManager] Failed to load persistence", {
|
|
599
|
+
key: this.persistence?.key,
|
|
600
|
+
error: error instanceof Error ? error.message : String(error)
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
configureCrypto(provider) {
|
|
606
|
+
this.cryptoProvider = provider;
|
|
607
|
+
logger2.debug("[ReplicationManager] Crypto configured", {
|
|
608
|
+
initialized: provider.isInitialized()
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
isCryptoEnabled() {
|
|
612
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
613
|
+
}
|
|
614
|
+
async registerAuthenticatedReplica(replica, encrypted = false) {
|
|
615
|
+
const authenticatedReplica = {
|
|
616
|
+
...replica,
|
|
617
|
+
encrypted
|
|
618
|
+
};
|
|
619
|
+
this.replicas.set(replica.id, authenticatedReplica);
|
|
620
|
+
this.replicasByDID.set(replica.did, replica.id);
|
|
621
|
+
if (!this.syncStatus.has(replica.nodeId)) {
|
|
622
|
+
this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
|
|
623
|
+
}
|
|
624
|
+
if (this.cryptoProvider && replica.publicSigningKey) {
|
|
625
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
626
|
+
id: replica.nodeId,
|
|
627
|
+
did: replica.did,
|
|
628
|
+
publicSigningKey: replica.publicSigningKey,
|
|
629
|
+
publicEncryptionKey: replica.publicEncryptionKey
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const event = {
|
|
633
|
+
type: "replica-added",
|
|
634
|
+
replicaId: replica.id,
|
|
635
|
+
nodeId: replica.nodeId,
|
|
636
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
637
|
+
details: { did: replica.did, encrypted, authenticated: true }
|
|
638
|
+
};
|
|
639
|
+
this.replicationEvents.push(event);
|
|
640
|
+
this.schedulePersist();
|
|
641
|
+
logger2.debug("[ReplicationManager] Authenticated replica registered", {
|
|
642
|
+
replicaId: replica.id,
|
|
643
|
+
did: replica.did,
|
|
644
|
+
encrypted
|
|
645
|
+
});
|
|
646
|
+
return authenticatedReplica;
|
|
647
|
+
}
|
|
648
|
+
getReplicaByDID(did) {
|
|
649
|
+
const replicaId = this.replicasByDID.get(did);
|
|
650
|
+
if (!replicaId)
|
|
651
|
+
return;
|
|
652
|
+
return this.replicas.get(replicaId);
|
|
653
|
+
}
|
|
654
|
+
getEncryptedReplicas() {
|
|
655
|
+
return Array.from(this.replicas.values()).filter((r) => r.encrypted);
|
|
656
|
+
}
|
|
657
|
+
async encryptForReplica(data, targetReplicaDID) {
|
|
658
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
659
|
+
throw new Error("Crypto provider not initialized");
|
|
660
|
+
}
|
|
661
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
662
|
+
const encrypted = await this.cryptoProvider.encrypt(dataBytes, targetReplicaDID);
|
|
663
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
664
|
+
return {
|
|
665
|
+
ct: encrypted.ct,
|
|
666
|
+
iv: encrypted.iv,
|
|
667
|
+
tag: encrypted.tag,
|
|
668
|
+
epk: encrypted.epk,
|
|
669
|
+
senderDID: localDID || undefined,
|
|
670
|
+
targetDID: targetReplicaDID,
|
|
671
|
+
encryptedAt: encrypted.encryptedAt
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async decryptReplicationData(encrypted) {
|
|
675
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
676
|
+
throw new Error("Crypto provider not initialized");
|
|
677
|
+
}
|
|
678
|
+
const decrypted = await this.cryptoProvider.decrypt({
|
|
679
|
+
alg: "ECIES-P256",
|
|
680
|
+
ct: encrypted.ct,
|
|
681
|
+
iv: encrypted.iv,
|
|
682
|
+
tag: encrypted.tag,
|
|
683
|
+
epk: encrypted.epk
|
|
684
|
+
}, encrypted.senderDID);
|
|
685
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
686
|
+
}
|
|
687
|
+
createEncryptedPolicy(name, replicationFactor, consistencyLevel, encryptionMode, options) {
|
|
688
|
+
const policy = {
|
|
689
|
+
id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
690
|
+
name,
|
|
691
|
+
replicationFactor,
|
|
692
|
+
consistencyLevel,
|
|
693
|
+
syncInterval: options?.syncInterval || 1000,
|
|
694
|
+
maxReplicationLag: options?.maxReplicationLag || 1e4,
|
|
695
|
+
encryptionMode,
|
|
696
|
+
requiredCapabilities: options?.requiredCapabilities
|
|
697
|
+
};
|
|
698
|
+
this.policies.set(policy.id, policy);
|
|
699
|
+
logger2.debug("[ReplicationManager] Encrypted policy created", {
|
|
700
|
+
policyId: policy.id,
|
|
701
|
+
name,
|
|
702
|
+
replicationFactor,
|
|
703
|
+
encryptionMode
|
|
704
|
+
});
|
|
705
|
+
return policy;
|
|
706
|
+
}
|
|
707
|
+
async verifyReplicaCapabilities(replicaDID, token, policyId) {
|
|
708
|
+
if (!this.cryptoProvider) {
|
|
709
|
+
return { authorized: true };
|
|
710
|
+
}
|
|
711
|
+
const policy = policyId ? this.policies.get(policyId) : undefined;
|
|
712
|
+
const result = await this.cryptoProvider.verifyUCAN(token, {
|
|
713
|
+
requiredCapabilities: policy?.requiredCapabilities?.map((cap) => ({
|
|
714
|
+
can: cap,
|
|
715
|
+
with: "*"
|
|
716
|
+
}))
|
|
717
|
+
});
|
|
718
|
+
if (!result.authorized) {
|
|
719
|
+
logger2.warn("[ReplicationManager] Replica capability verification failed", {
|
|
720
|
+
replicaDID,
|
|
721
|
+
error: result.error
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return result;
|
|
725
|
+
}
|
|
726
|
+
registerReplica(replica) {
|
|
727
|
+
this.replicas.set(replica.id, replica);
|
|
728
|
+
if (!this.syncStatus.has(replica.nodeId)) {
|
|
729
|
+
this.syncStatus.set(replica.nodeId, { synced: 0, failed: 0 });
|
|
730
|
+
}
|
|
731
|
+
const event = {
|
|
732
|
+
type: "replica-added",
|
|
733
|
+
replicaId: replica.id,
|
|
734
|
+
nodeId: replica.nodeId,
|
|
735
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
736
|
+
};
|
|
737
|
+
this.replicationEvents.push(event);
|
|
738
|
+
this.schedulePersist();
|
|
739
|
+
logger2.debug("[ReplicationManager] Replica registered", {
|
|
740
|
+
replicaId: replica.id,
|
|
741
|
+
nodeId: replica.nodeId,
|
|
742
|
+
status: replica.status
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
removeReplica(replicaId) {
|
|
746
|
+
const replica = this.replicas.get(replicaId);
|
|
747
|
+
if (!replica) {
|
|
748
|
+
throw new Error(`Replica ${replicaId} not found`);
|
|
749
|
+
}
|
|
750
|
+
this.replicas.delete(replicaId);
|
|
751
|
+
const event = {
|
|
752
|
+
type: "replica-removed",
|
|
753
|
+
replicaId,
|
|
754
|
+
nodeId: replica.nodeId,
|
|
755
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
756
|
+
};
|
|
757
|
+
this.replicationEvents.push(event);
|
|
758
|
+
this.schedulePersist();
|
|
759
|
+
logger2.debug("[ReplicationManager] Replica removed", { replicaId });
|
|
760
|
+
}
|
|
761
|
+
createPolicy(name, replicationFactor, consistencyLevel, syncInterval = 1000, maxReplicationLag = 1e4) {
|
|
762
|
+
const policy = {
|
|
763
|
+
id: `policy-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
764
|
+
name,
|
|
765
|
+
replicationFactor,
|
|
766
|
+
consistencyLevel,
|
|
767
|
+
syncInterval,
|
|
768
|
+
maxReplicationLag
|
|
769
|
+
};
|
|
770
|
+
this.policies.set(policy.id, policy);
|
|
771
|
+
this.schedulePersist();
|
|
772
|
+
logger2.debug("[ReplicationManager] Policy created", {
|
|
773
|
+
policyId: policy.id,
|
|
774
|
+
name,
|
|
775
|
+
replicationFactor,
|
|
776
|
+
consistencyLevel
|
|
777
|
+
});
|
|
778
|
+
return policy;
|
|
779
|
+
}
|
|
780
|
+
updateReplicaStatus(replicaId, status, lagBytes = 0, lagMillis = 0) {
|
|
781
|
+
const replica = this.replicas.get(replicaId);
|
|
782
|
+
if (!replica) {
|
|
783
|
+
throw new Error(`Replica ${replicaId} not found`);
|
|
784
|
+
}
|
|
785
|
+
replica.status = status;
|
|
786
|
+
replica.lagBytes = lagBytes;
|
|
787
|
+
replica.lagMillis = lagMillis;
|
|
788
|
+
replica.lastSyncTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
789
|
+
const event = {
|
|
790
|
+
type: status === "syncing" ? "replica-synced" : "sync-failed",
|
|
791
|
+
replicaId,
|
|
792
|
+
nodeId: replica.nodeId,
|
|
793
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
794
|
+
details: { status, lagBytes, lagMillis }
|
|
795
|
+
};
|
|
796
|
+
this.replicationEvents.push(event);
|
|
797
|
+
const syncStatus = this.syncStatus.get(replica.nodeId);
|
|
798
|
+
if (syncStatus) {
|
|
799
|
+
if (status === "syncing" || status === "secondary") {
|
|
800
|
+
syncStatus.synced++;
|
|
801
|
+
} else if (status === "failed") {
|
|
802
|
+
syncStatus.failed++;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
logger2.debug("[ReplicationManager] Replica status updated", {
|
|
806
|
+
replicaId,
|
|
807
|
+
status,
|
|
808
|
+
lagBytes,
|
|
809
|
+
lagMillis
|
|
810
|
+
});
|
|
811
|
+
this.schedulePersist();
|
|
812
|
+
}
|
|
813
|
+
getReplicasForNode(nodeId) {
|
|
814
|
+
return Array.from(this.replicas.values()).filter((r) => r.nodeId === nodeId);
|
|
815
|
+
}
|
|
816
|
+
getHealthyReplicas() {
|
|
817
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "secondary" || r.status === "primary");
|
|
818
|
+
}
|
|
819
|
+
getSyncingReplicas() {
|
|
820
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "syncing");
|
|
821
|
+
}
|
|
822
|
+
getFailedReplicas() {
|
|
823
|
+
return Array.from(this.replicas.values()).filter((r) => r.status === "failed");
|
|
824
|
+
}
|
|
825
|
+
checkReplicationHealth(policyId) {
|
|
826
|
+
const policy = this.policies.get(policyId);
|
|
827
|
+
if (!policy) {
|
|
828
|
+
throw new Error(`Policy ${policyId} not found`);
|
|
829
|
+
}
|
|
830
|
+
const healthy = this.getHealthyReplicas();
|
|
831
|
+
const maxLag = Math.max(0, ...healthy.map((r) => r.lagMillis));
|
|
832
|
+
return {
|
|
833
|
+
healthy: healthy.length >= policy.replicationFactor && maxLag <= policy.maxReplicationLag,
|
|
834
|
+
replicasInPolicy: policy.replicationFactor,
|
|
835
|
+
healthyReplicas: healthy.length,
|
|
836
|
+
replicationLag: maxLag
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
getConsistencyLevel(policyId) {
|
|
840
|
+
const policy = this.policies.get(policyId);
|
|
841
|
+
if (!policy) {
|
|
842
|
+
return "eventual";
|
|
843
|
+
}
|
|
844
|
+
return policy.consistencyLevel;
|
|
845
|
+
}
|
|
846
|
+
getReplica(replicaId) {
|
|
847
|
+
return this.replicas.get(replicaId);
|
|
848
|
+
}
|
|
849
|
+
getAllReplicas() {
|
|
850
|
+
return Array.from(this.replicas.values());
|
|
851
|
+
}
|
|
852
|
+
getPolicy(policyId) {
|
|
853
|
+
return this.policies.get(policyId);
|
|
854
|
+
}
|
|
855
|
+
getAllPolicies() {
|
|
856
|
+
return Array.from(this.policies.values());
|
|
857
|
+
}
|
|
858
|
+
getStatistics() {
|
|
859
|
+
const healthy = this.getHealthyReplicas().length;
|
|
860
|
+
const syncing = this.getSyncingReplicas().length;
|
|
861
|
+
const failed = this.getFailedReplicas().length;
|
|
862
|
+
const total = this.replicas.size;
|
|
863
|
+
const replicationLags = Array.from(this.replicas.values()).map((r) => r.lagMillis);
|
|
864
|
+
const avgLag = replicationLags.length > 0 ? replicationLags.reduce((a, b) => a + b) / replicationLags.length : 0;
|
|
865
|
+
const maxLag = replicationLags.length > 0 ? Math.max(...replicationLags) : 0;
|
|
866
|
+
return {
|
|
867
|
+
totalReplicas: total,
|
|
868
|
+
healthyReplicas: healthy,
|
|
869
|
+
syncingReplicas: syncing,
|
|
870
|
+
failedReplicas: failed,
|
|
871
|
+
healthiness: total > 0 ? healthy / total * 100 : 0,
|
|
872
|
+
averageReplicationLagMs: avgLag,
|
|
873
|
+
maxReplicationLagMs: maxLag,
|
|
874
|
+
totalPolicies: this.policies.size
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
getReplicationEvents(limit) {
|
|
878
|
+
const events = [...this.replicationEvents];
|
|
879
|
+
if (limit) {
|
|
880
|
+
return events.slice(-limit);
|
|
881
|
+
}
|
|
882
|
+
return events;
|
|
883
|
+
}
|
|
884
|
+
getSyncStatus(nodeId) {
|
|
885
|
+
return this.syncStatus.get(nodeId) || { synced: 0, failed: 0 };
|
|
886
|
+
}
|
|
887
|
+
getReplicationLagDistribution() {
|
|
888
|
+
const distribution = {
|
|
889
|
+
"0-100ms": 0,
|
|
890
|
+
"100-500ms": 0,
|
|
891
|
+
"500-1000ms": 0,
|
|
892
|
+
"1000+ms": 0
|
|
893
|
+
};
|
|
894
|
+
for (const replica of this.replicas.values()) {
|
|
895
|
+
if (replica.lagMillis <= 100) {
|
|
896
|
+
distribution["0-100ms"]++;
|
|
897
|
+
} else if (replica.lagMillis <= 500) {
|
|
898
|
+
distribution["100-500ms"]++;
|
|
899
|
+
} else if (replica.lagMillis <= 1000) {
|
|
900
|
+
distribution["500-1000ms"]++;
|
|
901
|
+
} else {
|
|
902
|
+
distribution["1000+ms"]++;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return distribution;
|
|
906
|
+
}
|
|
907
|
+
canSatisfyConsistency(policyId, _requiredAcks) {
|
|
908
|
+
const policy = this.policies.get(policyId);
|
|
909
|
+
if (!policy) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
const healthyCount = this.getHealthyReplicas().length;
|
|
913
|
+
switch (policy.consistencyLevel) {
|
|
914
|
+
case "eventual":
|
|
915
|
+
return true;
|
|
916
|
+
case "read-after-write":
|
|
917
|
+
return healthyCount >= 1;
|
|
918
|
+
case "strong":
|
|
919
|
+
return healthyCount >= policy.replicationFactor;
|
|
920
|
+
default:
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async saveToPersistence() {
|
|
925
|
+
if (!this.persistence) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const data = {
|
|
929
|
+
replicas: this.getAllReplicas(),
|
|
930
|
+
policies: this.getAllPolicies(),
|
|
931
|
+
syncStatus: Array.from(this.syncStatus.entries()).map(([nodeId, state]) => ({
|
|
932
|
+
nodeId,
|
|
933
|
+
synced: state.synced,
|
|
934
|
+
failed: state.failed
|
|
935
|
+
}))
|
|
936
|
+
};
|
|
937
|
+
const envelope = {
|
|
938
|
+
version: 1,
|
|
939
|
+
updatedAt: Date.now(),
|
|
940
|
+
data
|
|
941
|
+
};
|
|
942
|
+
const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
|
|
943
|
+
await this.persistence.adapter.setItem(this.persistence.key, serialize(envelope));
|
|
944
|
+
}
|
|
945
|
+
async loadFromPersistence() {
|
|
946
|
+
if (!this.persistence) {
|
|
947
|
+
return { replicas: 0, policies: 0, syncStatus: 0 };
|
|
948
|
+
}
|
|
949
|
+
const raw = await this.persistence.adapter.getItem(this.persistence.key);
|
|
950
|
+
if (!raw) {
|
|
951
|
+
return { replicas: 0, policies: 0, syncStatus: 0 };
|
|
952
|
+
}
|
|
953
|
+
const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
|
|
954
|
+
const envelope = deserialize(raw);
|
|
955
|
+
if (envelope.version !== 1 || !envelope.data) {
|
|
956
|
+
throw new Error("Invalid replication persistence payload");
|
|
957
|
+
}
|
|
958
|
+
if (!Array.isArray(envelope.data.replicas) || !Array.isArray(envelope.data.policies) || !Array.isArray(envelope.data.syncStatus)) {
|
|
959
|
+
throw new Error("Invalid replication persistence structure");
|
|
960
|
+
}
|
|
961
|
+
this.replicas.clear();
|
|
962
|
+
this.policies.clear();
|
|
963
|
+
this.syncStatus.clear();
|
|
964
|
+
this.replicasByDID.clear();
|
|
965
|
+
let importedReplicas = 0;
|
|
966
|
+
for (const replica of envelope.data.replicas) {
|
|
967
|
+
if (this.isValidReplica(replica)) {
|
|
968
|
+
this.replicas.set(replica.id, replica);
|
|
969
|
+
if (replica.did) {
|
|
970
|
+
this.replicasByDID.set(replica.did, replica.id);
|
|
971
|
+
}
|
|
972
|
+
importedReplicas++;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
let importedPolicies = 0;
|
|
976
|
+
for (const policy of envelope.data.policies) {
|
|
977
|
+
if (this.isValidPolicy(policy)) {
|
|
978
|
+
this.policies.set(policy.id, policy);
|
|
979
|
+
importedPolicies++;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
let importedSyncStatus = 0;
|
|
983
|
+
for (const status of envelope.data.syncStatus) {
|
|
984
|
+
if (typeof status.nodeId === "string" && typeof status.synced === "number" && typeof status.failed === "number") {
|
|
985
|
+
this.syncStatus.set(status.nodeId, {
|
|
986
|
+
synced: status.synced,
|
|
987
|
+
failed: status.failed
|
|
988
|
+
});
|
|
989
|
+
importedSyncStatus++;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
logger2.debug("[ReplicationManager] Loaded from persistence", {
|
|
993
|
+
key: this.persistence.key,
|
|
994
|
+
replicas: importedReplicas,
|
|
995
|
+
policies: importedPolicies,
|
|
996
|
+
syncStatus: importedSyncStatus
|
|
997
|
+
});
|
|
998
|
+
return {
|
|
999
|
+
replicas: importedReplicas,
|
|
1000
|
+
policies: importedPolicies,
|
|
1001
|
+
syncStatus: importedSyncStatus
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
async clearPersistence() {
|
|
1005
|
+
if (!this.persistence) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
await this.persistence.adapter.removeItem(this.persistence.key);
|
|
1009
|
+
}
|
|
1010
|
+
schedulePersist() {
|
|
1011
|
+
if (!this.persistence || this.persistence.autoPersist === false) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (this.persistTimer) {
|
|
1015
|
+
clearTimeout(this.persistTimer);
|
|
1016
|
+
}
|
|
1017
|
+
this.persistTimer = setTimeout(() => {
|
|
1018
|
+
this.persistSafely();
|
|
1019
|
+
}, this.persistence.persistDebounceMs ?? 25);
|
|
1020
|
+
}
|
|
1021
|
+
async persistSafely() {
|
|
1022
|
+
if (!this.persistence) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (this.persistInFlight) {
|
|
1026
|
+
this.persistPending = true;
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
this.persistInFlight = true;
|
|
1030
|
+
try {
|
|
1031
|
+
await this.saveToPersistence();
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
logger2.error("[ReplicationManager] Persistence write failed", {
|
|
1034
|
+
key: this.persistence.key,
|
|
1035
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1036
|
+
});
|
|
1037
|
+
} finally {
|
|
1038
|
+
this.persistInFlight = false;
|
|
1039
|
+
const shouldRunAgain = this.persistPending;
|
|
1040
|
+
this.persistPending = false;
|
|
1041
|
+
if (shouldRunAgain) {
|
|
1042
|
+
this.persistSafely();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
isValidReplica(value) {
|
|
1047
|
+
if (typeof value !== "object" || value === null) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
const candidate = value;
|
|
1051
|
+
const validStatus = candidate.status === "primary" || candidate.status === "secondary" || candidate.status === "syncing" || candidate.status === "failed";
|
|
1052
|
+
return typeof candidate.id === "string" && typeof candidate.nodeId === "string" && validStatus && typeof candidate.lastSyncTime === "string" && typeof candidate.lagBytes === "number" && typeof candidate.lagMillis === "number";
|
|
1053
|
+
}
|
|
1054
|
+
isValidPolicy(value) {
|
|
1055
|
+
if (typeof value !== "object" || value === null) {
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
const candidate = value;
|
|
1059
|
+
const validConsistency = candidate.consistencyLevel === "eventual" || candidate.consistencyLevel === "read-after-write" || candidate.consistencyLevel === "strong";
|
|
1060
|
+
return typeof candidate.id === "string" && typeof candidate.name === "string" && typeof candidate.replicationFactor === "number" && validConsistency && typeof candidate.syncInterval === "number" && typeof candidate.maxReplicationLag === "number";
|
|
1061
|
+
}
|
|
1062
|
+
clear() {
|
|
1063
|
+
this.replicas.clear();
|
|
1064
|
+
this.policies.clear();
|
|
1065
|
+
this.replicationEvents = [];
|
|
1066
|
+
this.syncStatus.clear();
|
|
1067
|
+
this.replicasByDID.clear();
|
|
1068
|
+
this.cryptoProvider = null;
|
|
1069
|
+
this.schedulePersist();
|
|
1070
|
+
}
|
|
1071
|
+
getCryptoProvider() {
|
|
1072
|
+
return this.cryptoProvider;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
var SyncProtocol = class _SyncProtocol {
|
|
1076
|
+
static DEFAULT_PERSIST_KEY = "aeon:sync-protocol:v1";
|
|
1077
|
+
version = "1.0.0";
|
|
1078
|
+
messageQueue = [];
|
|
1079
|
+
messageMap = /* @__PURE__ */ new Map;
|
|
1080
|
+
handshakes = /* @__PURE__ */ new Map;
|
|
1081
|
+
protocolErrors = [];
|
|
1082
|
+
messageCounter = 0;
|
|
1083
|
+
cryptoProvider = null;
|
|
1084
|
+
cryptoConfig = null;
|
|
1085
|
+
persistence = null;
|
|
1086
|
+
persistTimer = null;
|
|
1087
|
+
persistInFlight = false;
|
|
1088
|
+
persistPending = false;
|
|
1089
|
+
constructor(options) {
|
|
1090
|
+
if (options?.persistence) {
|
|
1091
|
+
this.persistence = {
|
|
1092
|
+
...options.persistence,
|
|
1093
|
+
key: options.persistence.key ?? _SyncProtocol.DEFAULT_PERSIST_KEY,
|
|
1094
|
+
autoPersist: options.persistence.autoPersist ?? true,
|
|
1095
|
+
autoLoad: options.persistence.autoLoad ?? false,
|
|
1096
|
+
persistDebounceMs: options.persistence.persistDebounceMs ?? 25
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
if (this.persistence?.autoLoad) {
|
|
1100
|
+
this.loadFromPersistence().catch((error) => {
|
|
1101
|
+
logger2.error("[SyncProtocol] Failed to load persistence", {
|
|
1102
|
+
key: this.persistence?.key,
|
|
1103
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
configureCrypto(provider, config) {
|
|
1109
|
+
this.cryptoProvider = provider;
|
|
1110
|
+
this.cryptoConfig = {
|
|
1111
|
+
encryptionMode: config?.encryptionMode ?? "none",
|
|
1112
|
+
requireSignatures: config?.requireSignatures ?? false,
|
|
1113
|
+
requireCapabilities: config?.requireCapabilities ?? false,
|
|
1114
|
+
requiredCapabilities: config?.requiredCapabilities
|
|
1115
|
+
};
|
|
1116
|
+
logger2.debug("[SyncProtocol] Crypto configured", {
|
|
1117
|
+
encryptionMode: this.cryptoConfig.encryptionMode,
|
|
1118
|
+
requireSignatures: this.cryptoConfig.requireSignatures,
|
|
1119
|
+
requireCapabilities: this.cryptoConfig.requireCapabilities
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
isCryptoEnabled() {
|
|
1123
|
+
return this.cryptoProvider !== null && this.cryptoProvider.isInitialized();
|
|
1124
|
+
}
|
|
1125
|
+
getCryptoConfig() {
|
|
1126
|
+
return this.cryptoConfig ? { ...this.cryptoConfig } : null;
|
|
1127
|
+
}
|
|
1128
|
+
getVersion() {
|
|
1129
|
+
return this.version;
|
|
1130
|
+
}
|
|
1131
|
+
async createAuthenticatedHandshake(capabilities, targetDID) {
|
|
1132
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1133
|
+
throw new Error("Crypto provider not initialized");
|
|
1134
|
+
}
|
|
1135
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1136
|
+
if (!localDID) {
|
|
1137
|
+
throw new Error("Local DID not available");
|
|
1138
|
+
}
|
|
1139
|
+
const publicInfo = await this.cryptoProvider.exportPublicIdentity();
|
|
1140
|
+
if (!publicInfo) {
|
|
1141
|
+
throw new Error("Cannot export public identity");
|
|
1142
|
+
}
|
|
1143
|
+
let ucan;
|
|
1144
|
+
if (targetDID && this.cryptoConfig?.requireCapabilities) {
|
|
1145
|
+
const caps = this.cryptoConfig.requiredCapabilities || [
|
|
1146
|
+
{ can: "aeon:sync:read", with: "*" },
|
|
1147
|
+
{ can: "aeon:sync:write", with: "*" }
|
|
1148
|
+
];
|
|
1149
|
+
ucan = await this.cryptoProvider.createUCAN(targetDID, caps);
|
|
1150
|
+
}
|
|
1151
|
+
const handshakePayload = {
|
|
1152
|
+
protocolVersion: this.version,
|
|
1153
|
+
nodeId: localDID,
|
|
1154
|
+
capabilities,
|
|
1155
|
+
state: "initiating",
|
|
1156
|
+
did: localDID,
|
|
1157
|
+
publicSigningKey: publicInfo.publicSigningKey,
|
|
1158
|
+
publicEncryptionKey: publicInfo.publicEncryptionKey,
|
|
1159
|
+
ucan
|
|
1160
|
+
};
|
|
1161
|
+
const message = {
|
|
1162
|
+
type: "handshake",
|
|
1163
|
+
version: this.version,
|
|
1164
|
+
sender: localDID,
|
|
1165
|
+
receiver: targetDID || "",
|
|
1166
|
+
messageId: this.generateMessageId(),
|
|
1167
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1168
|
+
payload: handshakePayload
|
|
1169
|
+
};
|
|
1170
|
+
if (this.cryptoConfig?.requireSignatures) {
|
|
1171
|
+
const signed = await this.cryptoProvider.signData(handshakePayload);
|
|
1172
|
+
message.auth = {
|
|
1173
|
+
senderDID: localDID,
|
|
1174
|
+
receiverDID: targetDID,
|
|
1175
|
+
signature: signed.signature
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
this.messageMap.set(message.messageId, message);
|
|
1179
|
+
this.messageQueue.push(message);
|
|
1180
|
+
this.schedulePersist();
|
|
1181
|
+
logger2.debug("[SyncProtocol] Authenticated handshake created", {
|
|
1182
|
+
messageId: message.messageId,
|
|
1183
|
+
did: localDID,
|
|
1184
|
+
capabilities: capabilities.length,
|
|
1185
|
+
hasUCAN: !!ucan
|
|
1186
|
+
});
|
|
1187
|
+
return message;
|
|
1188
|
+
}
|
|
1189
|
+
async verifyAuthenticatedHandshake(message) {
|
|
1190
|
+
if (message.type !== "handshake") {
|
|
1191
|
+
return { valid: false, error: "Message is not a handshake" };
|
|
1192
|
+
}
|
|
1193
|
+
const handshake = message.payload;
|
|
1194
|
+
if (!this.cryptoProvider || !this.cryptoConfig) {
|
|
1195
|
+
this.handshakes.set(message.sender, handshake);
|
|
1196
|
+
this.schedulePersist();
|
|
1197
|
+
return { valid: true, handshake };
|
|
1198
|
+
}
|
|
1199
|
+
if (handshake.did && handshake.publicSigningKey) {
|
|
1200
|
+
await this.cryptoProvider.registerRemoteNode({
|
|
1201
|
+
id: handshake.nodeId,
|
|
1202
|
+
did: handshake.did,
|
|
1203
|
+
publicSigningKey: handshake.publicSigningKey,
|
|
1204
|
+
publicEncryptionKey: handshake.publicEncryptionKey
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
if (this.cryptoConfig.requireSignatures && message.auth?.signature) {
|
|
1208
|
+
const signed = {
|
|
1209
|
+
payload: handshake,
|
|
1210
|
+
signature: message.auth.signature,
|
|
1211
|
+
signer: message.auth.senderDID || message.sender,
|
|
1212
|
+
algorithm: "ES256",
|
|
1213
|
+
signedAt: Date.now()
|
|
1214
|
+
};
|
|
1215
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
1216
|
+
if (!isValid) {
|
|
1217
|
+
logger2.warn("[SyncProtocol] Handshake signature verification failed", {
|
|
1218
|
+
messageId: message.messageId,
|
|
1219
|
+
sender: message.sender
|
|
1220
|
+
});
|
|
1221
|
+
return { valid: false, error: "Invalid signature" };
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (this.cryptoConfig.requireCapabilities && handshake.ucan) {
|
|
1225
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1226
|
+
const result = await this.cryptoProvider.verifyUCAN(handshake.ucan, {
|
|
1227
|
+
expectedAudience: localDID || undefined,
|
|
1228
|
+
requiredCapabilities: this.cryptoConfig.requiredCapabilities
|
|
1229
|
+
});
|
|
1230
|
+
if (!result.authorized) {
|
|
1231
|
+
logger2.warn("[SyncProtocol] Handshake UCAN verification failed", {
|
|
1232
|
+
messageId: message.messageId,
|
|
1233
|
+
error: result.error
|
|
1234
|
+
});
|
|
1235
|
+
return { valid: false, error: result.error || "Unauthorized" };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
this.handshakes.set(message.sender, handshake);
|
|
1239
|
+
this.schedulePersist();
|
|
1240
|
+
logger2.debug("[SyncProtocol] Authenticated handshake verified", {
|
|
1241
|
+
messageId: message.messageId,
|
|
1242
|
+
did: handshake.did
|
|
1243
|
+
});
|
|
1244
|
+
return { valid: true, handshake };
|
|
1245
|
+
}
|
|
1246
|
+
async signMessage(message, payload, encrypt = false) {
|
|
1247
|
+
if (!this.cryptoProvider || !this.cryptoProvider.isInitialized()) {
|
|
1248
|
+
throw new Error("Crypto provider not initialized");
|
|
1249
|
+
}
|
|
1250
|
+
const localDID = this.cryptoProvider.getLocalDID();
|
|
1251
|
+
const signed = await this.cryptoProvider.signData(payload);
|
|
1252
|
+
message.auth = {
|
|
1253
|
+
senderDID: localDID || undefined,
|
|
1254
|
+
receiverDID: message.receiver || undefined,
|
|
1255
|
+
signature: signed.signature,
|
|
1256
|
+
encrypted: false
|
|
1257
|
+
};
|
|
1258
|
+
if (encrypt && message.receiver && this.cryptoConfig?.encryptionMode !== "none") {
|
|
1259
|
+
const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
1260
|
+
const encrypted = await this.cryptoProvider.encrypt(payloadBytes, message.receiver);
|
|
1261
|
+
message.payload = encrypted;
|
|
1262
|
+
message.auth.encrypted = true;
|
|
1263
|
+
logger2.debug("[SyncProtocol] Message encrypted", {
|
|
1264
|
+
messageId: message.messageId,
|
|
1265
|
+
recipient: message.receiver
|
|
1266
|
+
});
|
|
1267
|
+
} else {
|
|
1268
|
+
message.payload = payload;
|
|
1269
|
+
}
|
|
1270
|
+
return message;
|
|
1271
|
+
}
|
|
1272
|
+
async verifyMessage(message) {
|
|
1273
|
+
if (!this.cryptoProvider || !message.auth) {
|
|
1274
|
+
return { valid: true, payload: message.payload };
|
|
1275
|
+
}
|
|
1276
|
+
let payload = message.payload;
|
|
1277
|
+
if (message.auth.encrypted && message.payload) {
|
|
1278
|
+
try {
|
|
1279
|
+
const encrypted = message.payload;
|
|
1280
|
+
const decrypted = await this.cryptoProvider.decrypt(encrypted, message.auth.senderDID);
|
|
1281
|
+
payload = JSON.parse(new TextDecoder().decode(decrypted));
|
|
1282
|
+
logger2.debug("[SyncProtocol] Message decrypted", {
|
|
1283
|
+
messageId: message.messageId
|
|
1284
|
+
});
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
return {
|
|
1287
|
+
valid: false,
|
|
1288
|
+
error: `Decryption failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (message.auth.signature && message.auth.senderDID) {
|
|
1293
|
+
const signed = {
|
|
1294
|
+
payload,
|
|
1295
|
+
signature: message.auth.signature,
|
|
1296
|
+
signer: message.auth.senderDID,
|
|
1297
|
+
algorithm: "ES256",
|
|
1298
|
+
signedAt: Date.now()
|
|
1299
|
+
};
|
|
1300
|
+
const isValid = await this.cryptoProvider.verifySignedData(signed);
|
|
1301
|
+
if (!isValid) {
|
|
1302
|
+
return { valid: false, error: "Invalid signature" };
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return { valid: true, payload };
|
|
1306
|
+
}
|
|
1307
|
+
createHandshakeMessage(nodeId, capabilities) {
|
|
1308
|
+
const message = {
|
|
1309
|
+
type: "handshake",
|
|
1310
|
+
version: this.version,
|
|
1311
|
+
sender: nodeId,
|
|
1312
|
+
receiver: "",
|
|
1313
|
+
messageId: this.generateMessageId(),
|
|
1314
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1315
|
+
payload: {
|
|
1316
|
+
protocolVersion: this.version,
|
|
1317
|
+
nodeId,
|
|
1318
|
+
capabilities,
|
|
1319
|
+
state: "initiating"
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
this.messageMap.set(message.messageId, message);
|
|
1323
|
+
this.messageQueue.push(message);
|
|
1324
|
+
this.schedulePersist();
|
|
1325
|
+
logger2.debug("[SyncProtocol] Handshake message created", {
|
|
1326
|
+
messageId: message.messageId,
|
|
1327
|
+
nodeId,
|
|
1328
|
+
capabilities: capabilities.length
|
|
1329
|
+
});
|
|
1330
|
+
return message;
|
|
1331
|
+
}
|
|
1332
|
+
createSyncRequestMessage(sender, receiver, sessionId, fromVersion, toVersion, filter) {
|
|
1333
|
+
const message = {
|
|
1334
|
+
type: "sync-request",
|
|
1335
|
+
version: this.version,
|
|
1336
|
+
sender,
|
|
1337
|
+
receiver,
|
|
1338
|
+
messageId: this.generateMessageId(),
|
|
1339
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1340
|
+
payload: {
|
|
1341
|
+
sessionId,
|
|
1342
|
+
fromVersion,
|
|
1343
|
+
toVersion,
|
|
1344
|
+
filter
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
this.messageMap.set(message.messageId, message);
|
|
1348
|
+
this.messageQueue.push(message);
|
|
1349
|
+
this.schedulePersist();
|
|
1350
|
+
logger2.debug("[SyncProtocol] Sync request created", {
|
|
1351
|
+
messageId: message.messageId,
|
|
1352
|
+
sessionId,
|
|
1353
|
+
fromVersion,
|
|
1354
|
+
toVersion
|
|
1355
|
+
});
|
|
1356
|
+
return message;
|
|
1357
|
+
}
|
|
1358
|
+
createSyncResponseMessage(sender, receiver, sessionId, fromVersion, toVersion, data, hasMore = false, offset = 0) {
|
|
1359
|
+
const message = {
|
|
1360
|
+
type: "sync-response",
|
|
1361
|
+
version: this.version,
|
|
1362
|
+
sender,
|
|
1363
|
+
receiver,
|
|
1364
|
+
messageId: this.generateMessageId(),
|
|
1365
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1366
|
+
payload: {
|
|
1367
|
+
sessionId,
|
|
1368
|
+
fromVersion,
|
|
1369
|
+
toVersion,
|
|
1370
|
+
data,
|
|
1371
|
+
hasMore,
|
|
1372
|
+
offset
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
this.messageMap.set(message.messageId, message);
|
|
1376
|
+
this.messageQueue.push(message);
|
|
1377
|
+
this.schedulePersist();
|
|
1378
|
+
logger2.debug("[SyncProtocol] Sync response created", {
|
|
1379
|
+
messageId: message.messageId,
|
|
1380
|
+
sessionId,
|
|
1381
|
+
itemCount: data.length,
|
|
1382
|
+
hasMore
|
|
1383
|
+
});
|
|
1384
|
+
return message;
|
|
1385
|
+
}
|
|
1386
|
+
createAckMessage(sender, receiver, messageId) {
|
|
1387
|
+
const message = {
|
|
1388
|
+
type: "ack",
|
|
1389
|
+
version: this.version,
|
|
1390
|
+
sender,
|
|
1391
|
+
receiver,
|
|
1392
|
+
messageId: this.generateMessageId(),
|
|
1393
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1394
|
+
payload: { acknowledgedMessageId: messageId }
|
|
1395
|
+
};
|
|
1396
|
+
this.messageMap.set(message.messageId, message);
|
|
1397
|
+
this.messageQueue.push(message);
|
|
1398
|
+
this.schedulePersist();
|
|
1399
|
+
return message;
|
|
1400
|
+
}
|
|
1401
|
+
createErrorMessage(sender, receiver, error, relatedMessageId) {
|
|
1402
|
+
const message = {
|
|
1403
|
+
type: "error",
|
|
1404
|
+
version: this.version,
|
|
1405
|
+
sender,
|
|
1406
|
+
receiver,
|
|
1407
|
+
messageId: this.generateMessageId(),
|
|
1408
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1409
|
+
payload: {
|
|
1410
|
+
error,
|
|
1411
|
+
relatedMessageId
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
this.messageMap.set(message.messageId, message);
|
|
1415
|
+
this.messageQueue.push(message);
|
|
1416
|
+
this.protocolErrors.push({
|
|
1417
|
+
error,
|
|
1418
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1419
|
+
});
|
|
1420
|
+
this.schedulePersist();
|
|
1421
|
+
logger2.error("[SyncProtocol] Error message created", {
|
|
1422
|
+
messageId: message.messageId,
|
|
1423
|
+
errorCode: error.code,
|
|
1424
|
+
recoverable: error.recoverable
|
|
1425
|
+
});
|
|
1426
|
+
return message;
|
|
1427
|
+
}
|
|
1428
|
+
validateMessage(message) {
|
|
1429
|
+
const errors = [];
|
|
1430
|
+
if (!message.type) {
|
|
1431
|
+
errors.push("Message type is required");
|
|
1432
|
+
}
|
|
1433
|
+
if (!message.sender) {
|
|
1434
|
+
errors.push("Sender is required");
|
|
1435
|
+
}
|
|
1436
|
+
if (!message.messageId) {
|
|
1437
|
+
errors.push("Message ID is required");
|
|
1438
|
+
}
|
|
1439
|
+
if (!message.timestamp) {
|
|
1440
|
+
errors.push("Timestamp is required");
|
|
1441
|
+
}
|
|
1442
|
+
const timestampValue = new Date(message.timestamp);
|
|
1443
|
+
if (Number.isNaN(timestampValue.getTime())) {
|
|
1444
|
+
errors.push("Invalid timestamp format");
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
valid: errors.length === 0,
|
|
1448
|
+
errors
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
serializeMessage(message) {
|
|
1452
|
+
try {
|
|
1453
|
+
return JSON.stringify(message);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
logger2.error("[SyncProtocol] Message serialization failed", {
|
|
1456
|
+
messageId: message.messageId,
|
|
1457
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1458
|
+
});
|
|
1459
|
+
throw new Error(`Failed to serialize message: ${error instanceof Error ? error.message : String(error)}`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
deserializeMessage(data) {
|
|
1463
|
+
try {
|
|
1464
|
+
const message = JSON.parse(data);
|
|
1465
|
+
const validation = this.validateMessage(message);
|
|
1466
|
+
if (!validation.valid) {
|
|
1467
|
+
throw new Error(`Invalid message: ${validation.errors.join(", ")}`);
|
|
1468
|
+
}
|
|
1469
|
+
return message;
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
logger2.error("[SyncProtocol] Message deserialization failed", {
|
|
1472
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1473
|
+
});
|
|
1474
|
+
throw new Error(`Failed to deserialize message: ${error instanceof Error ? error.message : String(error)}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
processHandshake(message) {
|
|
1478
|
+
if (message.type !== "handshake") {
|
|
1479
|
+
throw new Error("Message is not a handshake");
|
|
1480
|
+
}
|
|
1481
|
+
const handshake = message.payload;
|
|
1482
|
+
const nodeId = message.sender;
|
|
1483
|
+
this.handshakes.set(nodeId, handshake);
|
|
1484
|
+
this.schedulePersist();
|
|
1485
|
+
logger2.debug("[SyncProtocol] Handshake processed", {
|
|
1486
|
+
nodeId,
|
|
1487
|
+
protocolVersion: handshake.protocolVersion,
|
|
1488
|
+
capabilities: handshake.capabilities.length
|
|
1489
|
+
});
|
|
1490
|
+
return handshake;
|
|
1491
|
+
}
|
|
1492
|
+
getMessage(messageId) {
|
|
1493
|
+
return this.messageMap.get(messageId);
|
|
1494
|
+
}
|
|
1495
|
+
getAllMessages() {
|
|
1496
|
+
return [...this.messageQueue];
|
|
1497
|
+
}
|
|
1498
|
+
getMessagesByType(type) {
|
|
1499
|
+
return this.messageQueue.filter((m) => m.type === type);
|
|
1500
|
+
}
|
|
1501
|
+
getMessagesFromSender(sender) {
|
|
1502
|
+
return this.messageQueue.filter((m) => m.sender === sender);
|
|
1503
|
+
}
|
|
1504
|
+
getPendingMessages(receiver) {
|
|
1505
|
+
return this.messageQueue.filter((m) => m.receiver === receiver);
|
|
1506
|
+
}
|
|
1507
|
+
getHandshakes() {
|
|
1508
|
+
return new Map(this.handshakes);
|
|
1509
|
+
}
|
|
1510
|
+
getStatistics() {
|
|
1511
|
+
const messagesByType = {};
|
|
1512
|
+
for (const message of this.messageQueue) {
|
|
1513
|
+
messagesByType[message.type] = (messagesByType[message.type] || 0) + 1;
|
|
1514
|
+
}
|
|
1515
|
+
const errorCount = this.protocolErrors.length;
|
|
1516
|
+
const recoverableErrors = this.protocolErrors.filter((e) => e.error.recoverable).length;
|
|
1517
|
+
return {
|
|
1518
|
+
totalMessages: this.messageQueue.length,
|
|
1519
|
+
messagesByType,
|
|
1520
|
+
totalHandshakes: this.handshakes.size,
|
|
1521
|
+
totalErrors: errorCount,
|
|
1522
|
+
recoverableErrors,
|
|
1523
|
+
unrecoverableErrors: errorCount - recoverableErrors
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
getErrors() {
|
|
1527
|
+
return [...this.protocolErrors];
|
|
1528
|
+
}
|
|
1529
|
+
async saveToPersistence() {
|
|
1530
|
+
if (!this.persistence) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const data = {
|
|
1534
|
+
protocolVersion: this.version,
|
|
1535
|
+
messageCounter: this.messageCounter,
|
|
1536
|
+
messageQueue: this.getAllMessages(),
|
|
1537
|
+
handshakes: Array.from(this.handshakes.entries()).map(([nodeId, handshake]) => ({
|
|
1538
|
+
nodeId,
|
|
1539
|
+
handshake
|
|
1540
|
+
})),
|
|
1541
|
+
protocolErrors: this.getErrors()
|
|
1542
|
+
};
|
|
1543
|
+
const envelope = {
|
|
1544
|
+
version: 1,
|
|
1545
|
+
updatedAt: Date.now(),
|
|
1546
|
+
data
|
|
1547
|
+
};
|
|
1548
|
+
const serialize = this.persistence.serializer ?? ((value) => JSON.stringify(value));
|
|
1549
|
+
await this.persistence.adapter.setItem(this.persistence.key, serialize(envelope));
|
|
1550
|
+
}
|
|
1551
|
+
async loadFromPersistence() {
|
|
1552
|
+
if (!this.persistence) {
|
|
1553
|
+
return { messages: 0, handshakes: 0, errors: 0 };
|
|
1554
|
+
}
|
|
1555
|
+
const raw = await this.persistence.adapter.getItem(this.persistence.key);
|
|
1556
|
+
if (!raw) {
|
|
1557
|
+
return { messages: 0, handshakes: 0, errors: 0 };
|
|
1558
|
+
}
|
|
1559
|
+
const deserialize = this.persistence.deserializer ?? ((value) => JSON.parse(value));
|
|
1560
|
+
const envelope = deserialize(raw);
|
|
1561
|
+
if (envelope.version !== 1 || !envelope.data) {
|
|
1562
|
+
throw new Error("Invalid sync protocol persistence payload");
|
|
1563
|
+
}
|
|
1564
|
+
if (!Array.isArray(envelope.data.messageQueue) || !Array.isArray(envelope.data.handshakes) || !Array.isArray(envelope.data.protocolErrors)) {
|
|
1565
|
+
throw new Error("Invalid sync protocol persistence structure");
|
|
1566
|
+
}
|
|
1567
|
+
const nextMessages = [];
|
|
1568
|
+
for (const message of envelope.data.messageQueue) {
|
|
1569
|
+
const validation = this.validateMessage(message);
|
|
1570
|
+
if (!validation.valid) {
|
|
1571
|
+
throw new Error(`Invalid persisted message ${message?.messageId ?? "unknown"}: ${validation.errors.join(", ")}`);
|
|
1572
|
+
}
|
|
1573
|
+
nextMessages.push(message);
|
|
1574
|
+
}
|
|
1575
|
+
const nextHandshakes = /* @__PURE__ */ new Map;
|
|
1576
|
+
for (const entry of envelope.data.handshakes) {
|
|
1577
|
+
if (typeof entry.nodeId !== "string" || !this.isValidHandshake(entry.handshake)) {
|
|
1578
|
+
throw new Error("Invalid persisted handshake payload");
|
|
1579
|
+
}
|
|
1580
|
+
nextHandshakes.set(entry.nodeId, entry.handshake);
|
|
1581
|
+
}
|
|
1582
|
+
const nextErrors = [];
|
|
1583
|
+
for (const entry of envelope.data.protocolErrors) {
|
|
1584
|
+
if (!this.isValidProtocolErrorEntry(entry)) {
|
|
1585
|
+
throw new Error("Invalid persisted protocol error payload");
|
|
1586
|
+
}
|
|
1587
|
+
nextErrors.push(entry);
|
|
1588
|
+
}
|
|
1589
|
+
this.messageQueue = nextMessages;
|
|
1590
|
+
this.messageMap = new Map(nextMessages.map((m) => [m.messageId, m]));
|
|
1591
|
+
this.handshakes = nextHandshakes;
|
|
1592
|
+
this.protocolErrors = nextErrors;
|
|
1593
|
+
this.messageCounter = Math.max(envelope.data.messageCounter || 0, this.messageQueue.length);
|
|
1594
|
+
logger2.debug("[SyncProtocol] Loaded from persistence", {
|
|
1595
|
+
key: this.persistence.key,
|
|
1596
|
+
messages: this.messageQueue.length,
|
|
1597
|
+
handshakes: this.handshakes.size,
|
|
1598
|
+
errors: this.protocolErrors.length
|
|
1599
|
+
});
|
|
1600
|
+
return {
|
|
1601
|
+
messages: this.messageQueue.length,
|
|
1602
|
+
handshakes: this.handshakes.size,
|
|
1603
|
+
errors: this.protocolErrors.length
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
async clearPersistence() {
|
|
1607
|
+
if (!this.persistence) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
await this.persistence.adapter.removeItem(this.persistence.key);
|
|
1611
|
+
}
|
|
1612
|
+
schedulePersist() {
|
|
1613
|
+
if (!this.persistence || this.persistence.autoPersist === false) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (this.persistTimer) {
|
|
1617
|
+
clearTimeout(this.persistTimer);
|
|
1618
|
+
}
|
|
1619
|
+
this.persistTimer = setTimeout(() => {
|
|
1620
|
+
this.persistSafely();
|
|
1621
|
+
}, this.persistence.persistDebounceMs ?? 25);
|
|
1622
|
+
}
|
|
1623
|
+
async persistSafely() {
|
|
1624
|
+
if (!this.persistence) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (this.persistInFlight) {
|
|
1628
|
+
this.persistPending = true;
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
this.persistInFlight = true;
|
|
1632
|
+
try {
|
|
1633
|
+
await this.saveToPersistence();
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
logger2.error("[SyncProtocol] Persistence write failed", {
|
|
1636
|
+
key: this.persistence.key,
|
|
1637
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1638
|
+
});
|
|
1639
|
+
} finally {
|
|
1640
|
+
this.persistInFlight = false;
|
|
1641
|
+
const shouldRunAgain = this.persistPending;
|
|
1642
|
+
this.persistPending = false;
|
|
1643
|
+
if (shouldRunAgain) {
|
|
1644
|
+
this.persistSafely();
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
isValidHandshake(value) {
|
|
1649
|
+
if (typeof value !== "object" || value === null) {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
const handshake = value;
|
|
1653
|
+
const validState = handshake.state === "initiating" || handshake.state === "responding" || handshake.state === "completed";
|
|
1654
|
+
return typeof handshake.protocolVersion === "string" && typeof handshake.nodeId === "string" && Array.isArray(handshake.capabilities) && handshake.capabilities.every((cap) => typeof cap === "string") && validState;
|
|
1655
|
+
}
|
|
1656
|
+
isValidProtocolErrorEntry(entry) {
|
|
1657
|
+
if (typeof entry !== "object" || entry === null) {
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
const candidate = entry;
|
|
1661
|
+
return typeof candidate.timestamp === "string" && typeof candidate.error?.code === "string" && typeof candidate.error.message === "string" && typeof candidate.error.recoverable === "boolean";
|
|
1662
|
+
}
|
|
1663
|
+
generateMessageId() {
|
|
1664
|
+
this.messageCounter++;
|
|
1665
|
+
return `msg-${Date.now()}-${this.messageCounter}`;
|
|
1666
|
+
}
|
|
1667
|
+
clear() {
|
|
1668
|
+
this.messageQueue = [];
|
|
1669
|
+
this.messageMap.clear();
|
|
1670
|
+
this.handshakes.clear();
|
|
1671
|
+
this.protocolErrors = [];
|
|
1672
|
+
this.messageCounter = 0;
|
|
1673
|
+
this.cryptoProvider = null;
|
|
1674
|
+
this.cryptoConfig = null;
|
|
1675
|
+
this.schedulePersist();
|
|
1676
|
+
}
|
|
1677
|
+
getCryptoProvider() {
|
|
1678
|
+
return this.cryptoProvider;
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
// ../aeon/dist/persistence/index.js
|
|
1683
|
+
var DEFAULT_RULE = {
|
|
1684
|
+
urgency: "deferred",
|
|
1685
|
+
debounce: 50,
|
|
1686
|
+
maxBufferSize: 5000,
|
|
1687
|
+
readThrough: true
|
|
1688
|
+
};
|
|
1689
|
+
var DashStorageAdapter = class {
|
|
1690
|
+
backend;
|
|
1691
|
+
syncClient;
|
|
1692
|
+
rules;
|
|
1693
|
+
hooks;
|
|
1694
|
+
pendingChanges = /* @__PURE__ */ new Map;
|
|
1695
|
+
syncTimer = null;
|
|
1696
|
+
syncInFlight = false;
|
|
1697
|
+
syncPending = false;
|
|
1698
|
+
constructor(backend, options = {}) {
|
|
1699
|
+
this.backend = backend;
|
|
1700
|
+
this.syncClient = options.syncClient ?? null;
|
|
1701
|
+
this.hooks = options.hooks ?? {};
|
|
1702
|
+
const defaultRule = {
|
|
1703
|
+
...DEFAULT_RULE,
|
|
1704
|
+
...options.rules?.default ?? {}
|
|
1705
|
+
};
|
|
1706
|
+
if (options.syncDebounceMs !== undefined)
|
|
1707
|
+
defaultRule.debounce = options.syncDebounceMs;
|
|
1708
|
+
if (options.maxPendingChanges !== undefined)
|
|
1709
|
+
defaultRule.maxBufferSize = options.maxPendingChanges;
|
|
1710
|
+
if (options.onSyncError && !this.hooks.onSyncError)
|
|
1711
|
+
this.hooks.onSyncError = options.onSyncError;
|
|
1712
|
+
this.rules = {
|
|
1713
|
+
default: defaultRule,
|
|
1714
|
+
prefixes: options.rules?.prefixes ?? {}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
async getItem(key) {
|
|
1718
|
+
const rule = this.getRuleForKey(key);
|
|
1719
|
+
if (rule.readThrough !== false) {
|
|
1720
|
+
const pending = this.pendingChanges.get(key);
|
|
1721
|
+
if (pending) {
|
|
1722
|
+
return pending.operation === "delete" ? null : pending.value ?? null;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return await this.backend.get(key);
|
|
1726
|
+
}
|
|
1727
|
+
async setItem(key, value) {
|
|
1728
|
+
await this.backend.set(key, value);
|
|
1729
|
+
this.trackChange({
|
|
1730
|
+
key,
|
|
1731
|
+
operation: "set",
|
|
1732
|
+
value,
|
|
1733
|
+
timestamp: Date.now()
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
async removeItem(key) {
|
|
1737
|
+
await this.backend.delete(key);
|
|
1738
|
+
this.trackChange({
|
|
1739
|
+
key,
|
|
1740
|
+
operation: "delete",
|
|
1741
|
+
timestamp: Date.now()
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
getPendingSyncCount() {
|
|
1745
|
+
return this.pendingChanges.size;
|
|
1746
|
+
}
|
|
1747
|
+
async flushSync() {
|
|
1748
|
+
if (!this.syncClient || this.pendingChanges.size === 0) {
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (this.syncTimer) {
|
|
1752
|
+
clearTimeout(this.syncTimer);
|
|
1753
|
+
this.syncTimer = null;
|
|
1754
|
+
}
|
|
1755
|
+
await this.performSync();
|
|
1756
|
+
}
|
|
1757
|
+
trackChange(change) {
|
|
1758
|
+
this.pendingChanges.set(change.key, change);
|
|
1759
|
+
const rule = this.getRuleForKey(change.key);
|
|
1760
|
+
if (rule.urgency === "realtime") {
|
|
1761
|
+
this.performSync();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const maxSize = rule.maxBufferSize ?? 5000;
|
|
1765
|
+
if (this.pendingChanges.size >= maxSize) {
|
|
1766
|
+
this.hooks.onBufferOverflow?.(this.getPrefixMatch(change.key) || "default", this.pendingChanges.size, maxSize);
|
|
1767
|
+
this.performSync();
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
this.scheduleSync(rule);
|
|
1771
|
+
}
|
|
1772
|
+
getRuleForKey(key) {
|
|
1773
|
+
const prefix = this.getPrefixMatch(key);
|
|
1774
|
+
return (prefix ? this.rules.prefixes?.[prefix] : this.rules.default) ?? this.rules.default;
|
|
1775
|
+
}
|
|
1776
|
+
getPrefixMatch(key) {
|
|
1777
|
+
if (!this.rules.prefixes)
|
|
1778
|
+
return null;
|
|
1779
|
+
const prefixes = Object.keys(this.rules.prefixes).sort((a, b) => b.length - a.length);
|
|
1780
|
+
return prefixes.find((p) => key.startsWith(p)) ?? null;
|
|
1781
|
+
}
|
|
1782
|
+
scheduleSync(rule) {
|
|
1783
|
+
if (!this.syncClient || this.syncTimer) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
const debounceMs = typeof rule.debounce === "string" ? this.parseInterval(rule.debounce) : rule.debounce ?? 50;
|
|
1787
|
+
this.syncTimer = setTimeout(() => {
|
|
1788
|
+
this.syncTimer = null;
|
|
1789
|
+
this.performSync();
|
|
1790
|
+
}, debounceMs);
|
|
1791
|
+
}
|
|
1792
|
+
async performSync() {
|
|
1793
|
+
if (!this.syncClient) {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (this.syncInFlight) {
|
|
1797
|
+
this.syncPending = true;
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const changes = Array.from(this.pendingChanges.values()).sort((a, b) => a.timestamp - b.timestamp);
|
|
1801
|
+
if (changes.length === 0) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
this.pendingChanges.clear();
|
|
1805
|
+
this.syncInFlight = true;
|
|
1806
|
+
try {
|
|
1807
|
+
await this.syncClient.syncChanges(changes);
|
|
1808
|
+
this.hooks.onSync?.(changes);
|
|
1809
|
+
this.hooks.onFlush?.(changes.length);
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
for (const change of changes) {
|
|
1812
|
+
const current = this.pendingChanges.get(change.key);
|
|
1813
|
+
if (!current || change.timestamp > current.timestamp) {
|
|
1814
|
+
this.pendingChanges.set(change.key, change);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (this.hooks.onSyncError) {
|
|
1818
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
1819
|
+
this.hooks.onSyncError(normalizedError, changes);
|
|
1820
|
+
}
|
|
1821
|
+
} finally {
|
|
1822
|
+
this.syncInFlight = false;
|
|
1823
|
+
const rerun = this.syncPending || this.pendingChanges.size > 0;
|
|
1824
|
+
this.syncPending = false;
|
|
1825
|
+
if (rerun) {
|
|
1826
|
+
this.scheduleSync(this.rules.default);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
parseInterval(input) {
|
|
1831
|
+
const match = input.match(/^(\d+)(ms|s|m|h|d)$/);
|
|
1832
|
+
if (!match)
|
|
1833
|
+
return 50;
|
|
1834
|
+
const value = parseInt(match[1], 10);
|
|
1835
|
+
const unit = match[2];
|
|
1836
|
+
switch (unit) {
|
|
1837
|
+
case "ms":
|
|
1838
|
+
return value;
|
|
1839
|
+
case "s":
|
|
1840
|
+
return value * 1000;
|
|
1841
|
+
case "m":
|
|
1842
|
+
return value * 60 * 1000;
|
|
1843
|
+
case "h":
|
|
1844
|
+
return value * 60 * 60 * 1000;
|
|
1845
|
+
case "d":
|
|
1846
|
+
return value * 24 * 60 * 60 * 1000;
|
|
1847
|
+
default:
|
|
1848
|
+
return 50;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
var InMemoryStorageAdapter = class {
|
|
1853
|
+
store = /* @__PURE__ */ new Map;
|
|
1854
|
+
getItem(key) {
|
|
1855
|
+
return this.store.get(key) ?? null;
|
|
1856
|
+
}
|
|
1857
|
+
setItem(key, value) {
|
|
1858
|
+
this.store.set(key, value);
|
|
1859
|
+
}
|
|
1860
|
+
removeItem(key) {
|
|
1861
|
+
this.store.delete(key);
|
|
1862
|
+
}
|
|
1863
|
+
async flushSync() {}
|
|
1864
|
+
clear() {
|
|
1865
|
+
this.store.clear();
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
// src/sync/AeonDurableSync.ts
|
|
1870
|
+
function createBrowserStorageBackend() {
|
|
1871
|
+
return {
|
|
1872
|
+
get(key) {
|
|
1873
|
+
if (typeof localStorage === "undefined") {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
try {
|
|
1877
|
+
return localStorage.getItem(key);
|
|
1878
|
+
} catch {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
},
|
|
1882
|
+
set(key, value) {
|
|
1883
|
+
if (typeof localStorage === "undefined") {
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
localStorage.setItem(key, value);
|
|
1888
|
+
} catch {}
|
|
1889
|
+
},
|
|
1890
|
+
delete(key) {
|
|
1891
|
+
if (typeof localStorage === "undefined") {
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
try {
|
|
1895
|
+
localStorage.removeItem(key);
|
|
1896
|
+
} catch {}
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
class AeonDurableSyncRuntime {
|
|
1902
|
+
adapter;
|
|
1903
|
+
queue;
|
|
1904
|
+
protocol;
|
|
1905
|
+
replication;
|
|
1906
|
+
constructor(config) {
|
|
1907
|
+
const prefix = config.persistenceKeyPrefix ?? `dash:aeon:${config.roomName}`;
|
|
1908
|
+
this.adapter = config.enabled === false ? new InMemoryStorageAdapter : new DashStorageAdapter(config.storageBackend ?? createBrowserStorageBackend(), config.storageOptions ?? {});
|
|
1909
|
+
this.queue = new OfflineOperationQueue({
|
|
1910
|
+
persistence: {
|
|
1911
|
+
adapter: this.adapter,
|
|
1912
|
+
key: `${prefix}:offline`,
|
|
1913
|
+
autoPersist: true,
|
|
1914
|
+
autoLoad: true
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
this.protocol = new SyncProtocol({
|
|
1918
|
+
persistence: {
|
|
1919
|
+
adapter: this.adapter,
|
|
1920
|
+
key: `${prefix}:protocol`,
|
|
1921
|
+
autoPersist: true,
|
|
1922
|
+
autoLoad: true
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
this.replication = new ReplicationManager({
|
|
1926
|
+
persistence: {
|
|
1927
|
+
adapter: this.adapter,
|
|
1928
|
+
key: `${prefix}:replication`,
|
|
1929
|
+
autoPersist: true,
|
|
1930
|
+
autoLoad: true
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
async initialize() {
|
|
1935
|
+
try {
|
|
1936
|
+
await Promise.all([
|
|
1937
|
+
this.queue.loadFromPersistence(),
|
|
1938
|
+
this.protocol.loadFromPersistence(),
|
|
1939
|
+
this.replication.loadFromPersistence()
|
|
1940
|
+
]);
|
|
1941
|
+
} catch {}
|
|
1942
|
+
}
|
|
1943
|
+
async flushSync() {
|
|
1944
|
+
if ("flushSync" in this.adapter) {
|
|
1945
|
+
await this.adapter.flushSync();
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
recordTransportSelection(roomName, transport, relay) {
|
|
1949
|
+
if (!transport) {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
this.queue.enqueue("update", {
|
|
1954
|
+
roomName,
|
|
1955
|
+
event: "transport-selected",
|
|
1956
|
+
transport,
|
|
1957
|
+
relay,
|
|
1958
|
+
ts: Date.now()
|
|
1959
|
+
}, roomName, "normal");
|
|
1960
|
+
this.protocol.createSyncRequestMessage(roomName, relay ?? "unknown-relay", `transport-${Date.now()}`, "0.0.0", "1.1.0", {
|
|
1961
|
+
transport,
|
|
1962
|
+
relay
|
|
1963
|
+
});
|
|
1964
|
+
} catch {}
|
|
1965
|
+
}
|
|
1966
|
+
recordDiscovery(roomName, relays) {
|
|
1967
|
+
const sanitizedRelays = relays.filter((relay) => typeof relay === "string" && relay.length > 0).slice(0, 32);
|
|
1968
|
+
try {
|
|
1969
|
+
this.queue.enqueue("sync", {
|
|
1970
|
+
roomName,
|
|
1971
|
+
event: "relay-discovery",
|
|
1972
|
+
count: sanitizedRelays.length,
|
|
1973
|
+
relays: sanitizedRelays,
|
|
1974
|
+
ts: Date.now()
|
|
1975
|
+
}, roomName, "low");
|
|
1976
|
+
} catch {}
|
|
1977
|
+
}
|
|
1978
|
+
getPendingOperations() {
|
|
1979
|
+
return this.queue.getPendingCount();
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
export {
|
|
1983
|
+
AeonDurableSyncRuntime
|
|
1984
|
+
};
|