@weave_protocol/domere 1.0.18 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestration/index.d.ts +125 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +258 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/registry.d.ts +156 -0
- package/dist/orchestration/registry.d.ts.map +1 -0
- package/dist/orchestration/registry.js +411 -0
- package/dist/orchestration/registry.js.map +1 -0
- package/dist/orchestration/scheduler.d.ts +192 -0
- package/dist/orchestration/scheduler.d.ts.map +1 -0
- package/dist/orchestration/scheduler.js +544 -0
- package/dist/orchestration/scheduler.js.map +1 -0
- package/dist/orchestration/state.d.ts +258 -0
- package/dist/orchestration/state.d.ts.map +1 -0
- package/dist/orchestration/state.js +665 -0
- package/dist/orchestration/state.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/orchestration/index.ts +341 -0
- package/src/orchestration/registry.ts +568 -0
- package/src/orchestration/scheduler.ts +748 -0
- package/src/orchestration/state.ts +894 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dōmere - State Manager
|
|
3
|
+
*
|
|
4
|
+
* Distributed state management with locking, branching, and conflict resolution
|
|
5
|
+
* for multi-agent AI orchestration systems.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export type ConflictResolution = 'last-write-wins' | 'first-write-wins' | 'merge' | 'manual';
|
|
15
|
+
export type LockType = 'exclusive' | 'shared';
|
|
16
|
+
|
|
17
|
+
export interface StateEntry {
|
|
18
|
+
key: string;
|
|
19
|
+
value: any;
|
|
20
|
+
version: number;
|
|
21
|
+
hash: string;
|
|
22
|
+
created_at: Date;
|
|
23
|
+
updated_at: Date;
|
|
24
|
+
updated_by: string;
|
|
25
|
+
branch: string;
|
|
26
|
+
metadata: Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Lock {
|
|
30
|
+
id: string;
|
|
31
|
+
key: string;
|
|
32
|
+
type: LockType;
|
|
33
|
+
holder: string;
|
|
34
|
+
acquired_at: Date;
|
|
35
|
+
expires_at: Date;
|
|
36
|
+
renewed_count: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LockRequest {
|
|
40
|
+
key: string;
|
|
41
|
+
holder: string;
|
|
42
|
+
type?: LockType;
|
|
43
|
+
duration_ms?: number;
|
|
44
|
+
wait_ms?: number; // How long to wait if locked
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LockResult {
|
|
48
|
+
acquired: boolean;
|
|
49
|
+
lock?: Lock;
|
|
50
|
+
reason?: string;
|
|
51
|
+
current_holder?: string;
|
|
52
|
+
retry_after_ms?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Branch {
|
|
56
|
+
name: string;
|
|
57
|
+
parent: string;
|
|
58
|
+
created_at: Date;
|
|
59
|
+
created_by: string;
|
|
60
|
+
head_version: number;
|
|
61
|
+
merged: boolean;
|
|
62
|
+
merged_at?: Date;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface MergeResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
conflicts: Conflict[];
|
|
68
|
+
merged_keys: string[];
|
|
69
|
+
source_branch: string;
|
|
70
|
+
target_branch: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Conflict {
|
|
74
|
+
key: string;
|
|
75
|
+
source_value: any;
|
|
76
|
+
target_value: any;
|
|
77
|
+
source_version: number;
|
|
78
|
+
target_version: number;
|
|
79
|
+
base_value?: any;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface StateChange {
|
|
83
|
+
type: 'set' | 'delete' | 'merge';
|
|
84
|
+
key: string;
|
|
85
|
+
old_value?: any;
|
|
86
|
+
new_value?: any;
|
|
87
|
+
version: number;
|
|
88
|
+
timestamp: Date;
|
|
89
|
+
agent_id: string;
|
|
90
|
+
branch: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface StateSnapshot {
|
|
94
|
+
id: string;
|
|
95
|
+
branch: string;
|
|
96
|
+
timestamp: Date;
|
|
97
|
+
entries: Map<string, StateEntry>;
|
|
98
|
+
version: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// State Manager
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
export class StateManager {
|
|
106
|
+
private state: Map<string, Map<string, StateEntry>> = new Map(); // branch -> key -> entry
|
|
107
|
+
private locks: Map<string, Lock> = new Map(); // key -> lock
|
|
108
|
+
private branches: Map<string, Branch> = new Map();
|
|
109
|
+
private snapshots: Map<string, StateSnapshot> = new Map();
|
|
110
|
+
private changeLog: StateChange[] = [];
|
|
111
|
+
private changeCallbacks: ((change: StateChange) => void)[] = [];
|
|
112
|
+
|
|
113
|
+
private conflictResolution: ConflictResolution;
|
|
114
|
+
private defaultLockDuration: number;
|
|
115
|
+
|
|
116
|
+
constructor(options?: {
|
|
117
|
+
conflict_resolution?: ConflictResolution;
|
|
118
|
+
default_lock_duration_ms?: number;
|
|
119
|
+
}) {
|
|
120
|
+
this.conflictResolution = options?.conflict_resolution || 'last-write-wins';
|
|
121
|
+
this.defaultLockDuration = options?.default_lock_duration_ms || 30000;
|
|
122
|
+
|
|
123
|
+
// Initialize main branch
|
|
124
|
+
this.branches.set('main', {
|
|
125
|
+
name: 'main',
|
|
126
|
+
parent: '',
|
|
127
|
+
created_at: new Date(),
|
|
128
|
+
created_by: 'system',
|
|
129
|
+
head_version: 0,
|
|
130
|
+
merged: false,
|
|
131
|
+
});
|
|
132
|
+
this.state.set('main', new Map());
|
|
133
|
+
|
|
134
|
+
// Start lock cleanup timer
|
|
135
|
+
setInterval(() => this.cleanupExpiredLocks(), 5000);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ===========================================================================
|
|
139
|
+
// Basic State Operations
|
|
140
|
+
// ===========================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get a value
|
|
144
|
+
*/
|
|
145
|
+
async get(key: string, options?: { branch?: string }): Promise<any | undefined> {
|
|
146
|
+
const branch = options?.branch || 'main';
|
|
147
|
+
const branchState = this.state.get(branch);
|
|
148
|
+
if (!branchState) return undefined;
|
|
149
|
+
|
|
150
|
+
const entry = branchState.get(key);
|
|
151
|
+
return entry?.value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get entry with metadata
|
|
156
|
+
*/
|
|
157
|
+
async getEntry(key: string, options?: { branch?: string }): Promise<StateEntry | undefined> {
|
|
158
|
+
const branch = options?.branch || 'main';
|
|
159
|
+
const branchState = this.state.get(branch);
|
|
160
|
+
if (!branchState) return undefined;
|
|
161
|
+
|
|
162
|
+
return branchState.get(key);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set a value
|
|
167
|
+
*/
|
|
168
|
+
async set(key: string, value: any, options?: {
|
|
169
|
+
branch?: string;
|
|
170
|
+
agent_id?: string;
|
|
171
|
+
metadata?: Record<string, any>;
|
|
172
|
+
require_lock?: boolean;
|
|
173
|
+
}): Promise<StateEntry> {
|
|
174
|
+
const branch = options?.branch || 'main';
|
|
175
|
+
const agentId = options?.agent_id || 'unknown';
|
|
176
|
+
|
|
177
|
+
// Check lock
|
|
178
|
+
if (options?.require_lock) {
|
|
179
|
+
const lock = this.locks.get(key);
|
|
180
|
+
if (!lock || lock.holder !== agentId) {
|
|
181
|
+
throw new Error(`Agent ${agentId} does not hold lock on ${key}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check for existing exclusive lock by another holder
|
|
186
|
+
const existingLock = this.locks.get(key);
|
|
187
|
+
if (existingLock && existingLock.type === 'exclusive' && existingLock.holder !== agentId) {
|
|
188
|
+
throw new Error(`Key ${key} is exclusively locked by ${existingLock.holder}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let branchState = this.state.get(branch);
|
|
192
|
+
if (!branchState) {
|
|
193
|
+
throw new Error(`Branch ${branch} does not exist`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const existing = branchState.get(key);
|
|
197
|
+
const now = new Date();
|
|
198
|
+
const version = existing ? existing.version + 1 : 1;
|
|
199
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
200
|
+
|
|
201
|
+
const entry: StateEntry = {
|
|
202
|
+
key,
|
|
203
|
+
value,
|
|
204
|
+
version,
|
|
205
|
+
hash,
|
|
206
|
+
created_at: existing?.created_at || now,
|
|
207
|
+
updated_at: now,
|
|
208
|
+
updated_by: agentId,
|
|
209
|
+
branch,
|
|
210
|
+
metadata: options?.metadata || existing?.metadata || {},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
branchState.set(key, entry);
|
|
214
|
+
|
|
215
|
+
// Update branch head
|
|
216
|
+
const branchInfo = this.branches.get(branch)!;
|
|
217
|
+
branchInfo.head_version = Math.max(branchInfo.head_version, version);
|
|
218
|
+
|
|
219
|
+
// Log change
|
|
220
|
+
const change: StateChange = {
|
|
221
|
+
type: 'set',
|
|
222
|
+
key,
|
|
223
|
+
old_value: existing?.value,
|
|
224
|
+
new_value: value,
|
|
225
|
+
version,
|
|
226
|
+
timestamp: now,
|
|
227
|
+
agent_id: agentId,
|
|
228
|
+
branch,
|
|
229
|
+
};
|
|
230
|
+
this.changeLog.push(change);
|
|
231
|
+
this.notifyChange(change);
|
|
232
|
+
|
|
233
|
+
return entry;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Delete a value
|
|
238
|
+
*/
|
|
239
|
+
async delete(key: string, options?: {
|
|
240
|
+
branch?: string;
|
|
241
|
+
agent_id?: string;
|
|
242
|
+
}): Promise<boolean> {
|
|
243
|
+
const branch = options?.branch || 'main';
|
|
244
|
+
const agentId = options?.agent_id || 'unknown';
|
|
245
|
+
|
|
246
|
+
const branchState = this.state.get(branch);
|
|
247
|
+
if (!branchState) return false;
|
|
248
|
+
|
|
249
|
+
const existing = branchState.get(key);
|
|
250
|
+
if (!existing) return false;
|
|
251
|
+
|
|
252
|
+
// Check for lock
|
|
253
|
+
const lock = this.locks.get(key);
|
|
254
|
+
if (lock && lock.type === 'exclusive' && lock.holder !== agentId) {
|
|
255
|
+
throw new Error(`Key ${key} is exclusively locked by ${lock.holder}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
branchState.delete(key);
|
|
259
|
+
|
|
260
|
+
// Log change
|
|
261
|
+
const change: StateChange = {
|
|
262
|
+
type: 'delete',
|
|
263
|
+
key,
|
|
264
|
+
old_value: existing.value,
|
|
265
|
+
version: existing.version,
|
|
266
|
+
timestamp: new Date(),
|
|
267
|
+
agent_id: agentId,
|
|
268
|
+
branch,
|
|
269
|
+
};
|
|
270
|
+
this.changeLog.push(change);
|
|
271
|
+
this.notifyChange(change);
|
|
272
|
+
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* List all keys
|
|
278
|
+
*/
|
|
279
|
+
async keys(options?: { branch?: string; prefix?: string }): Promise<string[]> {
|
|
280
|
+
const branch = options?.branch || 'main';
|
|
281
|
+
const branchState = this.state.get(branch);
|
|
282
|
+
if (!branchState) return [];
|
|
283
|
+
|
|
284
|
+
let keys = Array.from(branchState.keys());
|
|
285
|
+
|
|
286
|
+
if (options?.prefix) {
|
|
287
|
+
keys = keys.filter(k => k.startsWith(options.prefix!));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return keys;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if key exists
|
|
295
|
+
*/
|
|
296
|
+
async has(key: string, options?: { branch?: string }): Promise<boolean> {
|
|
297
|
+
const branch = options?.branch || 'main';
|
|
298
|
+
const branchState = this.state.get(branch);
|
|
299
|
+
return branchState?.has(key) || false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ===========================================================================
|
|
303
|
+
// Locking
|
|
304
|
+
// ===========================================================================
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Acquire a lock
|
|
308
|
+
*/
|
|
309
|
+
async acquireLock(request: LockRequest): Promise<LockResult> {
|
|
310
|
+
const { key, holder, type = 'exclusive', duration_ms = this.defaultLockDuration, wait_ms = 0 } = request;
|
|
311
|
+
|
|
312
|
+
const existingLock = this.locks.get(key);
|
|
313
|
+
|
|
314
|
+
// Check if already locked
|
|
315
|
+
if (existingLock) {
|
|
316
|
+
// Check if expired
|
|
317
|
+
if (new Date() > existingLock.expires_at) {
|
|
318
|
+
this.locks.delete(key);
|
|
319
|
+
} else {
|
|
320
|
+
// Locked by someone else
|
|
321
|
+
if (existingLock.holder !== holder) {
|
|
322
|
+
// Can acquire shared lock if existing is shared
|
|
323
|
+
if (type === 'shared' && existingLock.type === 'shared') {
|
|
324
|
+
// Allow multiple shared locks (simplified: just extend)
|
|
325
|
+
} else {
|
|
326
|
+
// Wait or fail
|
|
327
|
+
if (wait_ms > 0) {
|
|
328
|
+
return {
|
|
329
|
+
acquired: false,
|
|
330
|
+
reason: 'Key is locked',
|
|
331
|
+
current_holder: existingLock.holder,
|
|
332
|
+
retry_after_ms: Math.min(wait_ms, existingLock.expires_at.getTime() - Date.now()),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
acquired: false,
|
|
337
|
+
reason: 'Key is locked',
|
|
338
|
+
current_holder: existingLock.holder,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
// Same holder - renew
|
|
343
|
+
existingLock.expires_at = new Date(Date.now() + duration_ms);
|
|
344
|
+
existingLock.renewed_count++;
|
|
345
|
+
return { acquired: true, lock: existingLock };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Create new lock
|
|
351
|
+
const lock: Lock = {
|
|
352
|
+
id: `lock_${crypto.randomUUID()}`,
|
|
353
|
+
key,
|
|
354
|
+
type,
|
|
355
|
+
holder,
|
|
356
|
+
acquired_at: new Date(),
|
|
357
|
+
expires_at: new Date(Date.now() + duration_ms),
|
|
358
|
+
renewed_count: 0,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
this.locks.set(key, lock);
|
|
362
|
+
|
|
363
|
+
return { acquired: true, lock };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Release a lock
|
|
368
|
+
*/
|
|
369
|
+
async releaseLock(key: string, holder: string): Promise<boolean> {
|
|
370
|
+
const lock = this.locks.get(key);
|
|
371
|
+
|
|
372
|
+
if (!lock) return false;
|
|
373
|
+
if (lock.holder !== holder) {
|
|
374
|
+
throw new Error(`Lock on ${key} is held by ${lock.holder}, not ${holder}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.locks.delete(key);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Renew a lock
|
|
383
|
+
*/
|
|
384
|
+
async renewLock(key: string, holder: string, duration_ms?: number): Promise<LockResult> {
|
|
385
|
+
const lock = this.locks.get(key);
|
|
386
|
+
|
|
387
|
+
if (!lock) {
|
|
388
|
+
return { acquired: false, reason: 'Lock not found' };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (lock.holder !== holder) {
|
|
392
|
+
return { acquired: false, reason: 'Lock held by another holder', current_holder: lock.holder };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
lock.expires_at = new Date(Date.now() + (duration_ms || this.defaultLockDuration));
|
|
396
|
+
lock.renewed_count++;
|
|
397
|
+
|
|
398
|
+
return { acquired: true, lock };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Check if key is locked
|
|
403
|
+
*/
|
|
404
|
+
isLocked(key: string): { locked: boolean; holder?: string; expires_at?: Date } {
|
|
405
|
+
const lock = this.locks.get(key);
|
|
406
|
+
|
|
407
|
+
if (!lock) {
|
|
408
|
+
return { locked: false };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (new Date() > lock.expires_at) {
|
|
412
|
+
this.locks.delete(key);
|
|
413
|
+
return { locked: false };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { locked: true, holder: lock.holder, expires_at: lock.expires_at };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get all locks held by an agent
|
|
421
|
+
*/
|
|
422
|
+
getLocksForHolder(holder: string): Lock[] {
|
|
423
|
+
return Array.from(this.locks.values()).filter(l => l.holder === holder);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Release all locks held by an agent
|
|
428
|
+
*/
|
|
429
|
+
async releaseAllLocks(holder: string): Promise<number> {
|
|
430
|
+
let released = 0;
|
|
431
|
+
|
|
432
|
+
for (const [key, lock] of this.locks) {
|
|
433
|
+
if (lock.holder === holder) {
|
|
434
|
+
this.locks.delete(key);
|
|
435
|
+
released++;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return released;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ===========================================================================
|
|
443
|
+
// Branching
|
|
444
|
+
// ===========================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create a branch
|
|
448
|
+
*/
|
|
449
|
+
async createBranch(name: string, options?: {
|
|
450
|
+
parent?: string;
|
|
451
|
+
created_by?: string;
|
|
452
|
+
}): Promise<Branch> {
|
|
453
|
+
if (this.branches.has(name)) {
|
|
454
|
+
throw new Error(`Branch ${name} already exists`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const parent = options?.parent || 'main';
|
|
458
|
+
const parentBranch = this.branches.get(parent);
|
|
459
|
+
if (!parentBranch) {
|
|
460
|
+
throw new Error(`Parent branch ${parent} does not exist`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const parentState = this.state.get(parent)!;
|
|
464
|
+
|
|
465
|
+
// Create branch info
|
|
466
|
+
const branch: Branch = {
|
|
467
|
+
name,
|
|
468
|
+
parent,
|
|
469
|
+
created_at: new Date(),
|
|
470
|
+
created_by: options?.created_by || 'unknown',
|
|
471
|
+
head_version: parentBranch.head_version,
|
|
472
|
+
merged: false,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
this.branches.set(name, branch);
|
|
476
|
+
|
|
477
|
+
// Copy state from parent
|
|
478
|
+
const branchState = new Map<string, StateEntry>();
|
|
479
|
+
for (const [key, entry] of parentState) {
|
|
480
|
+
branchState.set(key, { ...entry, branch: name });
|
|
481
|
+
}
|
|
482
|
+
this.state.set(name, branchState);
|
|
483
|
+
|
|
484
|
+
return branch;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* List branches
|
|
489
|
+
*/
|
|
490
|
+
listBranches(): Branch[] {
|
|
491
|
+
return Array.from(this.branches.values());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get branch info
|
|
496
|
+
*/
|
|
497
|
+
getBranch(name: string): Branch | undefined {
|
|
498
|
+
return this.branches.get(name);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Merge branch into target
|
|
503
|
+
*/
|
|
504
|
+
async merge(source: string, target: string, options?: {
|
|
505
|
+
agent_id?: string;
|
|
506
|
+
resolution?: ConflictResolution;
|
|
507
|
+
}): Promise<MergeResult> {
|
|
508
|
+
const sourceBranch = this.branches.get(source);
|
|
509
|
+
const targetBranch = this.branches.get(target);
|
|
510
|
+
|
|
511
|
+
if (!sourceBranch) throw new Error(`Source branch ${source} does not exist`);
|
|
512
|
+
if (!targetBranch) throw new Error(`Target branch ${target} does not exist`);
|
|
513
|
+
if (sourceBranch.merged) throw new Error(`Branch ${source} already merged`);
|
|
514
|
+
|
|
515
|
+
const sourceState = this.state.get(source)!;
|
|
516
|
+
const targetState = this.state.get(target)!;
|
|
517
|
+
|
|
518
|
+
const conflicts: Conflict[] = [];
|
|
519
|
+
const mergedKeys: string[] = [];
|
|
520
|
+
const resolution = options?.resolution || this.conflictResolution;
|
|
521
|
+
|
|
522
|
+
// Find all keys
|
|
523
|
+
const allKeys = new Set([...sourceState.keys(), ...targetState.keys()]);
|
|
524
|
+
|
|
525
|
+
for (const key of allKeys) {
|
|
526
|
+
const sourceEntry = sourceState.get(key);
|
|
527
|
+
const targetEntry = targetState.get(key);
|
|
528
|
+
|
|
529
|
+
// Key only in source - add to target
|
|
530
|
+
if (sourceEntry && !targetEntry) {
|
|
531
|
+
targetState.set(key, { ...sourceEntry, branch: target });
|
|
532
|
+
mergedKeys.push(key);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Key only in target - keep
|
|
537
|
+
if (!sourceEntry && targetEntry) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Both have key - check for conflict
|
|
542
|
+
if (sourceEntry && targetEntry) {
|
|
543
|
+
if (sourceEntry.hash === targetEntry.hash) {
|
|
544
|
+
// Same value, no conflict
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Conflict!
|
|
549
|
+
const conflict: Conflict = {
|
|
550
|
+
key,
|
|
551
|
+
source_value: sourceEntry.value,
|
|
552
|
+
target_value: targetEntry.value,
|
|
553
|
+
source_version: sourceEntry.version,
|
|
554
|
+
target_version: targetEntry.version,
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// Apply resolution strategy
|
|
558
|
+
if (resolution === 'last-write-wins') {
|
|
559
|
+
if (sourceEntry.updated_at > targetEntry.updated_at) {
|
|
560
|
+
targetState.set(key, { ...sourceEntry, branch: target, version: targetEntry.version + 1 });
|
|
561
|
+
mergedKeys.push(key);
|
|
562
|
+
}
|
|
563
|
+
// else keep target
|
|
564
|
+
} else if (resolution === 'first-write-wins') {
|
|
565
|
+
if (sourceEntry.updated_at < targetEntry.updated_at) {
|
|
566
|
+
targetState.set(key, { ...sourceEntry, branch: target, version: targetEntry.version + 1 });
|
|
567
|
+
mergedKeys.push(key);
|
|
568
|
+
}
|
|
569
|
+
// else keep target
|
|
570
|
+
} else if (resolution === 'merge') {
|
|
571
|
+
// Try to merge objects
|
|
572
|
+
if (typeof sourceEntry.value === 'object' && typeof targetEntry.value === 'object') {
|
|
573
|
+
const merged = { ...targetEntry.value, ...sourceEntry.value };
|
|
574
|
+
targetState.set(key, {
|
|
575
|
+
...targetEntry,
|
|
576
|
+
value: merged,
|
|
577
|
+
version: targetEntry.version + 1,
|
|
578
|
+
updated_at: new Date(),
|
|
579
|
+
hash: crypto.createHash('sha256').update(JSON.stringify(merged)).digest('hex'),
|
|
580
|
+
});
|
|
581
|
+
mergedKeys.push(key);
|
|
582
|
+
} else {
|
|
583
|
+
conflicts.push(conflict);
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
// Manual resolution needed
|
|
587
|
+
conflicts.push(conflict);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Mark source as merged if no conflicts
|
|
593
|
+
if (conflicts.length === 0) {
|
|
594
|
+
sourceBranch.merged = true;
|
|
595
|
+
sourceBranch.merged_at = new Date();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
success: conflicts.length === 0,
|
|
600
|
+
conflicts,
|
|
601
|
+
merged_keys: mergedKeys,
|
|
602
|
+
source_branch: source,
|
|
603
|
+
target_branch: target,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Resolve conflicts manually
|
|
609
|
+
*/
|
|
610
|
+
async resolveConflicts(conflicts: Conflict[], resolutions: Map<string, 'source' | 'target' | any>, options?: {
|
|
611
|
+
source: string;
|
|
612
|
+
target: string;
|
|
613
|
+
agent_id?: string;
|
|
614
|
+
}): Promise<void> {
|
|
615
|
+
if (!options?.source || !options?.target) {
|
|
616
|
+
throw new Error('Source and target branches required');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const sourceState = this.state.get(options.source);
|
|
620
|
+
const targetState = this.state.get(options.target);
|
|
621
|
+
|
|
622
|
+
if (!sourceState || !targetState) {
|
|
623
|
+
throw new Error('Invalid branches');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
for (const conflict of conflicts) {
|
|
627
|
+
const resolution = resolutions.get(conflict.key);
|
|
628
|
+
if (!resolution) continue;
|
|
629
|
+
|
|
630
|
+
const targetEntry = targetState.get(conflict.key);
|
|
631
|
+
const sourceEntry = sourceState.get(conflict.key);
|
|
632
|
+
|
|
633
|
+
if (!targetEntry) continue;
|
|
634
|
+
|
|
635
|
+
let newValue: any;
|
|
636
|
+
if (resolution === 'source' && sourceEntry) {
|
|
637
|
+
newValue = sourceEntry.value;
|
|
638
|
+
} else if (resolution === 'target') {
|
|
639
|
+
continue; // Keep target
|
|
640
|
+
} else {
|
|
641
|
+
newValue = resolution; // Custom value
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
targetState.set(conflict.key, {
|
|
645
|
+
...targetEntry,
|
|
646
|
+
value: newValue,
|
|
647
|
+
version: targetEntry.version + 1,
|
|
648
|
+
updated_at: new Date(),
|
|
649
|
+
updated_by: options.agent_id || 'unknown',
|
|
650
|
+
hash: crypto.createHash('sha256').update(JSON.stringify(newValue)).digest('hex'),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Mark source as merged
|
|
655
|
+
const sourceBranch = this.branches.get(options.source);
|
|
656
|
+
if (sourceBranch) {
|
|
657
|
+
sourceBranch.merged = true;
|
|
658
|
+
sourceBranch.merged_at = new Date();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Delete a branch
|
|
664
|
+
*/
|
|
665
|
+
async deleteBranch(name: string): Promise<boolean> {
|
|
666
|
+
if (name === 'main') {
|
|
667
|
+
throw new Error('Cannot delete main branch');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const branch = this.branches.get(name);
|
|
671
|
+
if (!branch) return false;
|
|
672
|
+
|
|
673
|
+
this.branches.delete(name);
|
|
674
|
+
this.state.delete(name);
|
|
675
|
+
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ===========================================================================
|
|
680
|
+
// Snapshots
|
|
681
|
+
// ===========================================================================
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Create a snapshot
|
|
685
|
+
*/
|
|
686
|
+
async createSnapshot(options?: { branch?: string }): Promise<StateSnapshot> {
|
|
687
|
+
const branch = options?.branch || 'main';
|
|
688
|
+
const branchState = this.state.get(branch);
|
|
689
|
+
const branchInfo = this.branches.get(branch);
|
|
690
|
+
|
|
691
|
+
if (!branchState || !branchInfo) {
|
|
692
|
+
throw new Error(`Branch ${branch} does not exist`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const snapshot: StateSnapshot = {
|
|
696
|
+
id: `snap_${crypto.randomUUID()}`,
|
|
697
|
+
branch,
|
|
698
|
+
timestamp: new Date(),
|
|
699
|
+
entries: new Map(branchState),
|
|
700
|
+
version: branchInfo.head_version,
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
this.snapshots.set(snapshot.id, snapshot);
|
|
704
|
+
|
|
705
|
+
return snapshot;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Restore from snapshot
|
|
710
|
+
*/
|
|
711
|
+
async restoreSnapshot(snapshotId: string): Promise<void> {
|
|
712
|
+
const snapshot = this.snapshots.get(snapshotId);
|
|
713
|
+
if (!snapshot) {
|
|
714
|
+
throw new Error(`Snapshot ${snapshotId} not found`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Replace branch state
|
|
718
|
+
this.state.set(snapshot.branch, new Map(snapshot.entries));
|
|
719
|
+
|
|
720
|
+
// Update branch version
|
|
721
|
+
const branch = this.branches.get(snapshot.branch);
|
|
722
|
+
if (branch) {
|
|
723
|
+
branch.head_version = snapshot.version;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* List snapshots
|
|
729
|
+
*/
|
|
730
|
+
listSnapshots(branch?: string): StateSnapshot[] {
|
|
731
|
+
const snapshots = Array.from(this.snapshots.values());
|
|
732
|
+
|
|
733
|
+
if (branch) {
|
|
734
|
+
return snapshots.filter(s => s.branch === branch);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return snapshots;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Delete a snapshot
|
|
742
|
+
*/
|
|
743
|
+
deleteSnapshot(snapshotId: string): boolean {
|
|
744
|
+
return this.snapshots.delete(snapshotId);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ===========================================================================
|
|
748
|
+
// Change Tracking
|
|
749
|
+
// ===========================================================================
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Get change history
|
|
753
|
+
*/
|
|
754
|
+
getChanges(options?: {
|
|
755
|
+
branch?: string;
|
|
756
|
+
key?: string;
|
|
757
|
+
agent_id?: string;
|
|
758
|
+
since?: Date;
|
|
759
|
+
limit?: number;
|
|
760
|
+
}): StateChange[] {
|
|
761
|
+
let changes = [...this.changeLog];
|
|
762
|
+
|
|
763
|
+
if (options?.branch) {
|
|
764
|
+
changes = changes.filter(c => c.branch === options.branch);
|
|
765
|
+
}
|
|
766
|
+
if (options?.key) {
|
|
767
|
+
changes = changes.filter(c => c.key === options.key);
|
|
768
|
+
}
|
|
769
|
+
if (options?.agent_id) {
|
|
770
|
+
changes = changes.filter(c => c.agent_id === options.agent_id);
|
|
771
|
+
}
|
|
772
|
+
if (options?.since) {
|
|
773
|
+
changes = changes.filter(c => c.timestamp >= options.since!);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Sort by timestamp descending
|
|
777
|
+
changes.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
778
|
+
|
|
779
|
+
if (options?.limit) {
|
|
780
|
+
changes = changes.slice(0, options.limit);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return changes;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Subscribe to changes
|
|
788
|
+
*/
|
|
789
|
+
onChange(callback: (change: StateChange) => void): () => void {
|
|
790
|
+
this.changeCallbacks.push(callback);
|
|
791
|
+
|
|
792
|
+
return () => {
|
|
793
|
+
const index = this.changeCallbacks.indexOf(callback);
|
|
794
|
+
if (index !== -1) this.changeCallbacks.splice(index, 1);
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private notifyChange(change: StateChange): void {
|
|
799
|
+
for (const cb of this.changeCallbacks) {
|
|
800
|
+
try {
|
|
801
|
+
cb(change);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
// Ignore
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ===========================================================================
|
|
809
|
+
// Utilities
|
|
810
|
+
// ===========================================================================
|
|
811
|
+
|
|
812
|
+
private cleanupExpiredLocks(): void {
|
|
813
|
+
const now = new Date();
|
|
814
|
+
|
|
815
|
+
for (const [key, lock] of this.locks) {
|
|
816
|
+
if (now > lock.expires_at) {
|
|
817
|
+
this.locks.delete(key);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Get state statistics
|
|
824
|
+
*/
|
|
825
|
+
getStats(): {
|
|
826
|
+
branches: number;
|
|
827
|
+
total_keys: number;
|
|
828
|
+
active_locks: number;
|
|
829
|
+
snapshots: number;
|
|
830
|
+
changes_logged: number;
|
|
831
|
+
} {
|
|
832
|
+
let totalKeys = 0;
|
|
833
|
+
for (const branchState of this.state.values()) {
|
|
834
|
+
totalKeys += branchState.size;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
branches: this.branches.size,
|
|
839
|
+
total_keys: totalKeys,
|
|
840
|
+
active_locks: this.locks.size,
|
|
841
|
+
snapshots: this.snapshots.size,
|
|
842
|
+
changes_logged: this.changeLog.length,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Export state for backup
|
|
848
|
+
*/
|
|
849
|
+
async exportState(branch?: string): Promise<string> {
|
|
850
|
+
const exportData: any = {
|
|
851
|
+
exported_at: new Date(),
|
|
852
|
+
branches: branch ? [branch] : Array.from(this.branches.keys()),
|
|
853
|
+
data: {},
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
for (const branchName of exportData.branches) {
|
|
857
|
+
const branchState = this.state.get(branchName);
|
|
858
|
+
if (branchState) {
|
|
859
|
+
exportData.data[branchName] = Object.fromEntries(branchState);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return JSON.stringify(exportData, null, 2);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Import state from backup
|
|
868
|
+
*/
|
|
869
|
+
async importState(data: string, options?: { merge?: boolean }): Promise<{ imported_keys: number }> {
|
|
870
|
+
const importData = JSON.parse(data);
|
|
871
|
+
let importedKeys = 0;
|
|
872
|
+
|
|
873
|
+
for (const branchName of Object.keys(importData.data)) {
|
|
874
|
+
if (!this.branches.has(branchName) && branchName !== 'main') {
|
|
875
|
+
await this.createBranch(branchName);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const branchState = this.state.get(branchName)!;
|
|
879
|
+
const entries = importData.data[branchName];
|
|
880
|
+
|
|
881
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
882
|
+
if (options?.merge && branchState.has(key)) {
|
|
883
|
+
continue; // Skip existing
|
|
884
|
+
}
|
|
885
|
+
branchState.set(key, entry as StateEntry);
|
|
886
|
+
importedKeys++;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return { imported_keys: importedKeys };
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export default StateManager;
|