@defai.digital/cross-cutting 13.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +214 -0
- package/dist/dead-letter-queue.d.ts +76 -0
- package/dist/dead-letter-queue.d.ts.map +1 -0
- package/dist/dead-letter-queue.js +228 -0
- package/dist/dead-letter-queue.js.map +1 -0
- package/dist/idempotency-manager.d.ts +66 -0
- package/dist/idempotency-manager.d.ts.map +1 -0
- package/dist/idempotency-manager.js +220 -0
- package/dist/idempotency-manager.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/retention-manager.d.ts +89 -0
- package/dist/retention-manager.d.ts.map +1 -0
- package/dist/retention-manager.js +308 -0
- package/dist/retention-manager.js.map +1 -0
- package/dist/saga-manager.d.ts +57 -0
- package/dist/saga-manager.d.ts.map +1 -0
- package/dist/saga-manager.js +202 -0
- package/dist/saga-manager.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency Manager Implementation
|
|
3
|
+
*
|
|
4
|
+
* Prevents duplicate request processing using idempotency keys.
|
|
5
|
+
* Ensures operations are safely retryable without side effects.
|
|
6
|
+
*
|
|
7
|
+
* Invariants:
|
|
8
|
+
* - INV-ID-001: Same idempotency key returns cached response
|
|
9
|
+
* - INV-ID-002: Different request with same key is rejected (conflict)
|
|
10
|
+
* - INV-ID-003: Cache entries expire after TTL
|
|
11
|
+
*/
|
|
12
|
+
import { createDefaultIdempotencyConfig, calculateExpiration, isEntryExpired, IdempotencyErrorCodes, } from '@defai.digital/contracts';
|
|
13
|
+
/**
|
|
14
|
+
* Creates an in-memory idempotency storage
|
|
15
|
+
*/
|
|
16
|
+
export function createInMemoryIdempotencyStorage() {
|
|
17
|
+
const entries = new Map();
|
|
18
|
+
return {
|
|
19
|
+
async get(key) {
|
|
20
|
+
const entry = entries.get(key);
|
|
21
|
+
if (!entry)
|
|
22
|
+
return null;
|
|
23
|
+
// Check if expired
|
|
24
|
+
if (isEntryExpired(entry)) {
|
|
25
|
+
entries.delete(key);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return entry;
|
|
29
|
+
},
|
|
30
|
+
async set(entry) {
|
|
31
|
+
entries.set(entry.key, entry);
|
|
32
|
+
},
|
|
33
|
+
async delete(key) {
|
|
34
|
+
return entries.delete(key);
|
|
35
|
+
},
|
|
36
|
+
async deleteExpired() {
|
|
37
|
+
let deleted = 0;
|
|
38
|
+
for (const [key, entry] of entries) {
|
|
39
|
+
if (isEntryExpired(entry)) {
|
|
40
|
+
entries.delete(key);
|
|
41
|
+
deleted++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return deleted;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates an idempotency manager
|
|
50
|
+
*/
|
|
51
|
+
export function createIdempotencyManager(storage, config = {}) {
|
|
52
|
+
const cfg = { ...createDefaultIdempotencyConfig(), ...config };
|
|
53
|
+
return {
|
|
54
|
+
getConfig() {
|
|
55
|
+
return { ...cfg };
|
|
56
|
+
},
|
|
57
|
+
async check(key, requestHash) {
|
|
58
|
+
if (!cfg.enabled) {
|
|
59
|
+
return { status: 'new' };
|
|
60
|
+
}
|
|
61
|
+
const entry = await storage.get(key);
|
|
62
|
+
if (!entry) {
|
|
63
|
+
return { status: 'new' };
|
|
64
|
+
}
|
|
65
|
+
// INV-ID-001: Same key returns cached response
|
|
66
|
+
if (entry.status === 'completed') {
|
|
67
|
+
// INV-ID-002: Different request with same key is rejected
|
|
68
|
+
if (entry.requestHash !== requestHash) {
|
|
69
|
+
return {
|
|
70
|
+
status: 'conflict',
|
|
71
|
+
error: 'Request hash mismatch for idempotency key',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
status: 'cached',
|
|
76
|
+
response: entry.response,
|
|
77
|
+
cachedAt: entry.createdAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Request is being processed
|
|
81
|
+
if (entry.status === 'processing') {
|
|
82
|
+
const lockExpiry = new Date(entry.createdAt).getTime() + cfg.lockTimeoutMs;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (now < lockExpiry) {
|
|
85
|
+
return {
|
|
86
|
+
status: 'processing',
|
|
87
|
+
lockExpiresInMs: lockExpiry - now,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Lock expired, allow new processing
|
|
91
|
+
return { status: 'new' };
|
|
92
|
+
}
|
|
93
|
+
// Failed - allow retry with same hash
|
|
94
|
+
if (entry.status === 'failed') {
|
|
95
|
+
if (entry.requestHash !== requestHash) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'conflict',
|
|
98
|
+
error: 'Request hash mismatch for idempotency key',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { status: 'new' };
|
|
102
|
+
}
|
|
103
|
+
return { status: 'new' };
|
|
104
|
+
},
|
|
105
|
+
async startProcessing(key, requestHash) {
|
|
106
|
+
if (!cfg.enabled) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const existing = await storage.get(key);
|
|
110
|
+
// Check if already processing (and not expired)
|
|
111
|
+
if (existing?.status === 'processing') {
|
|
112
|
+
const lockExpiry = new Date(existing.createdAt).getTime() + cfg.lockTimeoutMs;
|
|
113
|
+
if (Date.now() < lockExpiry) {
|
|
114
|
+
return false; // Still locked
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Check if already completed
|
|
118
|
+
if (existing?.status === 'completed') {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// INV-ID-003: Set expiration
|
|
122
|
+
const expiresAt = calculateExpiration(cfg.ttlSeconds);
|
|
123
|
+
const entry = {
|
|
124
|
+
key,
|
|
125
|
+
requestHash,
|
|
126
|
+
response: null,
|
|
127
|
+
createdAt: new Date().toISOString(),
|
|
128
|
+
expiresAt,
|
|
129
|
+
status: 'processing',
|
|
130
|
+
};
|
|
131
|
+
await storage.set(entry);
|
|
132
|
+
return true;
|
|
133
|
+
},
|
|
134
|
+
async completeProcessing(key, response) {
|
|
135
|
+
const existing = await storage.get(key);
|
|
136
|
+
if (!existing) {
|
|
137
|
+
// Entry was deleted or expired during processing.
|
|
138
|
+
// Don't create a new entry with empty requestHash as this would
|
|
139
|
+
// violate INV-ID-002 (hash mismatch detection). Future requests
|
|
140
|
+
// with this key will simply reprocess, which is safe.
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const entry = {
|
|
144
|
+
...existing,
|
|
145
|
+
response,
|
|
146
|
+
status: 'completed',
|
|
147
|
+
updatedAt: new Date().toISOString(),
|
|
148
|
+
};
|
|
149
|
+
await storage.set(entry);
|
|
150
|
+
},
|
|
151
|
+
async failProcessing(key, error) {
|
|
152
|
+
const existing = await storage.get(key);
|
|
153
|
+
if (!existing) {
|
|
154
|
+
return; // Allow silent failure
|
|
155
|
+
}
|
|
156
|
+
const entry = {
|
|
157
|
+
...existing,
|
|
158
|
+
status: 'failed',
|
|
159
|
+
error,
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
|
+
};
|
|
162
|
+
await storage.set(entry);
|
|
163
|
+
},
|
|
164
|
+
async getEntry(key) {
|
|
165
|
+
return storage.get(key);
|
|
166
|
+
},
|
|
167
|
+
async cleanup() {
|
|
168
|
+
return storage.deleteExpired();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Wrapper for idempotent operations
|
|
174
|
+
*/
|
|
175
|
+
export async function withIdempotency(manager, key, requestHash, operation) {
|
|
176
|
+
// Check for duplicate
|
|
177
|
+
const check = await manager.check(key, requestHash);
|
|
178
|
+
if (check.status === 'cached' && check.response !== undefined) {
|
|
179
|
+
return check.response;
|
|
180
|
+
}
|
|
181
|
+
if (check.status === 'processing') {
|
|
182
|
+
throw new IdempotencyError(IdempotencyErrorCodes.LOCK_TIMEOUT, 'Operation already in progress');
|
|
183
|
+
}
|
|
184
|
+
if (check.status === 'conflict') {
|
|
185
|
+
throw new IdempotencyError(IdempotencyErrorCodes.KEY_CONFLICT, check.error ?? 'Request hash mismatch');
|
|
186
|
+
}
|
|
187
|
+
// Start processing
|
|
188
|
+
const started = await manager.startProcessing(key, requestHash);
|
|
189
|
+
if (!started) {
|
|
190
|
+
throw new IdempotencyError(IdempotencyErrorCodes.KEY_CONFLICT, 'Could not acquire lock for idempotent operation');
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const result = await operation();
|
|
194
|
+
await manager.completeProcessing(key, result);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
199
|
+
await manager.failProcessing(key, errorMessage);
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Idempotency error
|
|
205
|
+
*/
|
|
206
|
+
export class IdempotencyError extends Error {
|
|
207
|
+
code;
|
|
208
|
+
constructor(code, message) {
|
|
209
|
+
super(message ?? `Idempotency error: ${code}`);
|
|
210
|
+
this.code = code;
|
|
211
|
+
this.name = 'IdempotencyError';
|
|
212
|
+
}
|
|
213
|
+
static keyConflict(key) {
|
|
214
|
+
return new IdempotencyError(IdempotencyErrorCodes.KEY_CONFLICT, `Idempotency key conflict: ${key}`);
|
|
215
|
+
}
|
|
216
|
+
static lockTimeout(key) {
|
|
217
|
+
return new IdempotencyError(IdempotencyErrorCodes.LOCK_TIMEOUT, `Lock timeout for idempotency key: ${key}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=idempotency-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency-manager.js","sourceRoot":"","sources":["../src/idempotency-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @defai.digital/cross-cutting
|
|
3
|
+
*
|
|
4
|
+
* Cross-Cutting Concerns Domain
|
|
5
|
+
*
|
|
6
|
+
* Provides Dead Letter Queue, Saga pattern, and Idempotency
|
|
7
|
+
* for robust, fault-tolerant system operations.
|
|
8
|
+
*/
|
|
9
|
+
export { createDeadLetterQueue, createInMemoryDeadLetterStorage, DeadLetterQueueError, processRetry, type DeadLetterQueue, type DeadLetterStorage, } from './dead-letter-queue.js';
|
|
10
|
+
export { createSagaManager, defineSaga, SagaError, type SagaManager, type SagaStepExecutor, type CompensationExecutor, } from './saga-manager.js';
|
|
11
|
+
export { createIdempotencyManager, createInMemoryIdempotencyStorage, withIdempotency, IdempotencyError, type IdempotencyManager, type IdempotencyStorage, } from './idempotency-manager.js';
|
|
12
|
+
export { createRetentionManager, createInMemoryRetentionStore, createInMemoryArchiver, RetentionError, type RetentionManager, type RetentionStore, type RetentionEntry, type DataArchiver, } from './retention-manager.js';
|
|
13
|
+
export type { IdempotencyCheckResult, DLQStats, SagaResult, RetentionPolicy, RetentionRunResult, RetentionSummary, } from '@defai.digital/contracts';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EACL,qBAAqB,EACrB,+BAA+B,EAC/B,oBAAoB,EACpB,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,SAAS,EACT,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,eAAe,EACf,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,sBAAsB,EACtB,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,wBAAwB,CAAC;AAGhC,YAAY,EACV,sBAAsB,EACtB,QAAQ,EACR,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @defai.digital/cross-cutting
|
|
3
|
+
*
|
|
4
|
+
* Cross-Cutting Concerns Domain
|
|
5
|
+
*
|
|
6
|
+
* Provides Dead Letter Queue, Saga pattern, and Idempotency
|
|
7
|
+
* for robust, fault-tolerant system operations.
|
|
8
|
+
*/
|
|
9
|
+
// Dead Letter Queue
|
|
10
|
+
export { createDeadLetterQueue, createInMemoryDeadLetterStorage, DeadLetterQueueError, processRetry, } from './dead-letter-queue.js';
|
|
11
|
+
// Saga Manager
|
|
12
|
+
export { createSagaManager, defineSaga, SagaError, } from './saga-manager.js';
|
|
13
|
+
// Idempotency Manager
|
|
14
|
+
export { createIdempotencyManager, createInMemoryIdempotencyStorage, withIdempotency, IdempotencyError, } from './idempotency-manager.js';
|
|
15
|
+
// Retention Manager
|
|
16
|
+
export { createRetentionManager, createInMemoryRetentionStore, createInMemoryArchiver, RetentionError, } from './retention-manager.js';
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oBAAoB;AACpB,OAAO,EACL,qBAAqB,EACrB,+BAA+B,EAC/B,oBAAoB,EACpB,YAAY,GAGb,MAAM,wBAAwB,CAAC;AAEhC,eAAe;AACf,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,SAAS,GAIV,MAAM,mBAAmB,CAAC;AAE3B,sBAAsB;AACtB,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,eAAe,EACf,gBAAgB,GAGjB,MAAM,0BAA0B,CAAC;AAElC,oBAAoB;AACpB,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,sBAAsB,EACtB,cAAc,GAKf,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention Manager Implementation
|
|
3
|
+
*
|
|
4
|
+
* Manages data retention policies for automatic cleanup of old data.
|
|
5
|
+
* Supports archiving before deletion and conditional retention.
|
|
6
|
+
*
|
|
7
|
+
* Invariants:
|
|
8
|
+
* - INV-RT-001: Retention policies applied per data type
|
|
9
|
+
* - INV-RT-002: Archive before delete when configured
|
|
10
|
+
* - INV-RT-003: Conditions respected during cleanup
|
|
11
|
+
*/
|
|
12
|
+
import { type RetentionPolicy, type RetentionRunResult, type RetentionSummary, type RetentionDataType, type RetentionConditions, type ArchiveEntry } from '@defai.digital/contracts';
|
|
13
|
+
/**
|
|
14
|
+
* Retention store interface for data access
|
|
15
|
+
*/
|
|
16
|
+
export interface RetentionStore {
|
|
17
|
+
/** Find entries older than cutoff date */
|
|
18
|
+
findExpired(cutoffDate: string, conditions?: RetentionConditions): Promise<RetentionEntry[]>;
|
|
19
|
+
/** Delete entry by ID */
|
|
20
|
+
delete(id: string): Promise<void>;
|
|
21
|
+
/** Get entry size in bytes */
|
|
22
|
+
getSize(id: string): Promise<number>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Entry returned by retention store
|
|
26
|
+
*/
|
|
27
|
+
export interface RetentionEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
status?: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
lastAccessedAt?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Data archiver interface
|
|
37
|
+
*/
|
|
38
|
+
export interface DataArchiver {
|
|
39
|
+
/** Archive an entry */
|
|
40
|
+
archive(dataType: RetentionDataType, entry: RetentionEntry, format: 'json' | 'csv' | 'parquet', path?: string): Promise<ArchiveEntry>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Retention manager interface
|
|
44
|
+
*/
|
|
45
|
+
export interface RetentionManager {
|
|
46
|
+
/** Get all policies */
|
|
47
|
+
getPolicies(): RetentionPolicy[];
|
|
48
|
+
/** Get policy by ID */
|
|
49
|
+
getPolicy(policyId: string): RetentionPolicy | null;
|
|
50
|
+
/** Add policy */
|
|
51
|
+
addPolicy(policy: RetentionPolicy): void;
|
|
52
|
+
/** Remove policy */
|
|
53
|
+
removePolicy(policyId: string): boolean;
|
|
54
|
+
/** Update policy */
|
|
55
|
+
updatePolicy(policyId: string, updates: Partial<RetentionPolicy>): boolean;
|
|
56
|
+
/** Run a specific policy */
|
|
57
|
+
runPolicy(policyId: string): Promise<RetentionRunResult>;
|
|
58
|
+
/** Run all enabled policies */
|
|
59
|
+
runAllPolicies(): Promise<RetentionRunResult[]>;
|
|
60
|
+
/** Get summary of all retention operations */
|
|
61
|
+
getSummary(): RetentionSummary;
|
|
62
|
+
/** Register store for a data type */
|
|
63
|
+
registerStore(dataType: RetentionDataType, store: RetentionStore): void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a retention manager
|
|
67
|
+
*/
|
|
68
|
+
export declare function createRetentionManager(archiver?: DataArchiver): RetentionManager;
|
|
69
|
+
/**
|
|
70
|
+
* Creates an in-memory retention store (for testing)
|
|
71
|
+
*/
|
|
72
|
+
export declare function createInMemoryRetentionStore(entries: Map<string, RetentionEntry>): RetentionStore;
|
|
73
|
+
/**
|
|
74
|
+
* Creates an in-memory archiver (for testing)
|
|
75
|
+
*/
|
|
76
|
+
export declare function createInMemoryArchiver(): DataArchiver & {
|
|
77
|
+
getArchives(): ArchiveEntry[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Retention error
|
|
81
|
+
*/
|
|
82
|
+
export declare class RetentionError extends Error {
|
|
83
|
+
readonly code: string;
|
|
84
|
+
constructor(code: string, message?: string);
|
|
85
|
+
static policyNotFound(policyId: string): RetentionError;
|
|
86
|
+
static archiveFailed(entryId: string, error: string): RetentionError;
|
|
87
|
+
static deleteFailed(entryId: string, error: string): RetentionError;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=retention-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention-manager.d.ts","sourceRoot":"","sources":["../src/retention-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAGlB,MAAM,0BAA0B,CAAC;AAElC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,mBAAmB,GAC/B,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAE7B,yBAAyB;IACzB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC,8BAA8B;IAC9B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uBAAuB;IACvB,OAAO,CACL,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,EAClC,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,WAAW,IAAI,eAAe,EAAE,CAAC;IAEjC,uBAAuB;IACvB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAAC;IAEpD,iBAAiB;IACjB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAEzC,oBAAoB;IACpB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAExC,oBAAoB;IACpB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC;IAE3E,4BAA4B;IAC5B,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEzD,+BAA+B;IAC/B,cAAc,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAEhD,8CAA8C;IAC9C,UAAU,IAAI,gBAAgB,CAAC;IAE/B,qCAAqC;IACrC,aAAa,CAAC,QAAQ,EAAE,iBAAiB,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CACzE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,CAAC,EAAE,YAAY,GACtB,gBAAgB,CA4RlB;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GACnC,cAAc,CA6BhB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,YAAY,GAAG;IACvD,WAAW,IAAI,YAAY,EAAE,CAAC;CAC/B,CA0BA;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;aAErB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,MAAM;IAMlB,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc;IAOvD,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc;IAOpE,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc;CAMpE"}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention Manager Implementation
|
|
3
|
+
*
|
|
4
|
+
* Manages data retention policies for automatic cleanup of old data.
|
|
5
|
+
* Supports archiving before deletion and conditional retention.
|
|
6
|
+
*
|
|
7
|
+
* Invariants:
|
|
8
|
+
* - INV-RT-001: Retention policies applied per data type
|
|
9
|
+
* - INV-RT-002: Archive before delete when configured
|
|
10
|
+
* - INV-RT-003: Conditions respected during cleanup
|
|
11
|
+
*/
|
|
12
|
+
import { calculateRetentionCutoff, RetentionErrorCodes, } from '@defai.digital/contracts';
|
|
13
|
+
/**
|
|
14
|
+
* Creates a retention manager
|
|
15
|
+
*/
|
|
16
|
+
export function createRetentionManager(archiver) {
|
|
17
|
+
const policies = new Map();
|
|
18
|
+
const stores = new Map();
|
|
19
|
+
const lastRuns = new Map();
|
|
20
|
+
let totalStorageReclaimed = 0;
|
|
21
|
+
let totalEntriesDeleted = 0;
|
|
22
|
+
/**
|
|
23
|
+
* Check if entry matches conditions (INV-RT-003)
|
|
24
|
+
*/
|
|
25
|
+
function matchesConditions(entry, conditions) {
|
|
26
|
+
if (!conditions)
|
|
27
|
+
return true;
|
|
28
|
+
// Check status condition
|
|
29
|
+
if (conditions.status && conditions.status.length > 0) {
|
|
30
|
+
if (!entry.status || !conditions.status.includes(entry.status)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Check exclude tags
|
|
35
|
+
if (conditions.excludeTags && conditions.excludeTags.length > 0) {
|
|
36
|
+
if (entry.tags?.some((tag) => conditions.excludeTags?.includes(tag))) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Check include tags
|
|
41
|
+
if (conditions.includeTags && conditions.includeTags.length > 0) {
|
|
42
|
+
if (!entry.tags?.some((tag) => conditions.includeTags?.includes(tag))) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Check minimum age
|
|
47
|
+
if (conditions.minAgeHours !== undefined && conditions.minAgeHours > 0) {
|
|
48
|
+
const entryAge = Date.now() - new Date(entry.createdAt).getTime();
|
|
49
|
+
const minAgeMs = conditions.minAgeHours * 60 * 60 * 1000;
|
|
50
|
+
if (entryAge < minAgeMs) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Check last accessed time
|
|
55
|
+
if (conditions.keepIfAccessedWithinHours !== undefined &&
|
|
56
|
+
conditions.keepIfAccessedWithinHours > 0 &&
|
|
57
|
+
entry.lastAccessedAt) {
|
|
58
|
+
const lastAccess = new Date(entry.lastAccessedAt).getTime();
|
|
59
|
+
const keepThreshold = Date.now() - conditions.keepIfAccessedWithinHours * 60 * 60 * 1000;
|
|
60
|
+
if (lastAccess > keepThreshold) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
getPolicies() {
|
|
68
|
+
return Array.from(policies.values());
|
|
69
|
+
},
|
|
70
|
+
getPolicy(policyId) {
|
|
71
|
+
return policies.get(policyId) ?? null;
|
|
72
|
+
},
|
|
73
|
+
addPolicy(policy) {
|
|
74
|
+
// Validate archive path if archiving enabled (INV-RT-002)
|
|
75
|
+
if (policy.archiveBeforeDelete && !policy.archivePath) {
|
|
76
|
+
throw new RetentionError(RetentionErrorCodes.ARCHIVE_PATH_REQUIRED, 'archivePath required when archiveBeforeDelete is true');
|
|
77
|
+
}
|
|
78
|
+
policies.set(policy.policyId, policy);
|
|
79
|
+
},
|
|
80
|
+
removePolicy(policyId) {
|
|
81
|
+
return policies.delete(policyId);
|
|
82
|
+
},
|
|
83
|
+
updatePolicy(policyId, updates) {
|
|
84
|
+
const existing = policies.get(policyId);
|
|
85
|
+
if (!existing)
|
|
86
|
+
return false;
|
|
87
|
+
const updated = { ...existing, ...updates };
|
|
88
|
+
// Validate archive path if archiving enabled
|
|
89
|
+
if (updated.archiveBeforeDelete && !updated.archivePath) {
|
|
90
|
+
throw new RetentionError(RetentionErrorCodes.ARCHIVE_PATH_REQUIRED, 'archivePath required when archiveBeforeDelete is true');
|
|
91
|
+
}
|
|
92
|
+
policies.set(policyId, updated);
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
// INV-RT-001: Retention policies applied per data type
|
|
96
|
+
async runPolicy(policyId) {
|
|
97
|
+
const policy = policies.get(policyId);
|
|
98
|
+
if (!policy) {
|
|
99
|
+
throw new RetentionError(RetentionErrorCodes.POLICY_NOT_FOUND, `Policy not found: ${policyId}`);
|
|
100
|
+
}
|
|
101
|
+
const store = stores.get(policy.dataType);
|
|
102
|
+
if (!store) {
|
|
103
|
+
throw new RetentionError(RetentionErrorCodes.DELETE_FAILED, `No store registered for data type: ${policy.dataType}`);
|
|
104
|
+
}
|
|
105
|
+
const runId = crypto.randomUUID();
|
|
106
|
+
const startedAt = new Date().toISOString();
|
|
107
|
+
const errors = [];
|
|
108
|
+
let entriesProcessed = 0;
|
|
109
|
+
let entriesDeleted = 0;
|
|
110
|
+
let entriesArchived = 0;
|
|
111
|
+
let entriesSkipped = 0;
|
|
112
|
+
let storageReclaimedBytes = 0;
|
|
113
|
+
try {
|
|
114
|
+
// Calculate cutoff date
|
|
115
|
+
const cutoffDate = calculateRetentionCutoff(policy.retentionDays);
|
|
116
|
+
// Find expired entries
|
|
117
|
+
const entries = await store.findExpired(cutoffDate.toISOString(), policy.conditions);
|
|
118
|
+
entriesProcessed = entries.length;
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
try {
|
|
121
|
+
// Check conditions (INV-RT-003)
|
|
122
|
+
if (!matchesConditions(entry, policy.conditions)) {
|
|
123
|
+
entriesSkipped++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Get entry size for metrics
|
|
127
|
+
let entrySize = 0;
|
|
128
|
+
try {
|
|
129
|
+
entrySize = await store.getSize(entry.id);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Size tracking is optional
|
|
133
|
+
}
|
|
134
|
+
// Archive before delete if configured (INV-RT-002)
|
|
135
|
+
if (policy.archiveBeforeDelete && archiver) {
|
|
136
|
+
try {
|
|
137
|
+
await archiver.archive(policy.dataType, entry, policy.archiveFormat, policy.archivePath);
|
|
138
|
+
entriesArchived++;
|
|
139
|
+
}
|
|
140
|
+
catch (archiveError) {
|
|
141
|
+
const msg = archiveError instanceof Error
|
|
142
|
+
? archiveError.message
|
|
143
|
+
: 'Unknown archive error';
|
|
144
|
+
errors.push(`Archive failed for ${entry.id}: ${msg}`);
|
|
145
|
+
// Skip deletion if archive fails
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Delete entry
|
|
150
|
+
await store.delete(entry.id);
|
|
151
|
+
entriesDeleted++;
|
|
152
|
+
storageReclaimedBytes += entrySize;
|
|
153
|
+
}
|
|
154
|
+
catch (entryError) {
|
|
155
|
+
const msg = entryError instanceof Error
|
|
156
|
+
? entryError.message
|
|
157
|
+
: 'Unknown error';
|
|
158
|
+
errors.push(`Failed to process ${entry.id}: ${msg}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
164
|
+
errors.push(`Policy execution failed: ${msg}`);
|
|
165
|
+
}
|
|
166
|
+
const result = {
|
|
167
|
+
runId,
|
|
168
|
+
policyId,
|
|
169
|
+
startedAt,
|
|
170
|
+
completedAt: new Date().toISOString(),
|
|
171
|
+
entriesProcessed,
|
|
172
|
+
entriesDeleted,
|
|
173
|
+
entriesArchived,
|
|
174
|
+
entriesSkipped,
|
|
175
|
+
errors,
|
|
176
|
+
success: errors.length === 0,
|
|
177
|
+
storageReclaimedBytes,
|
|
178
|
+
};
|
|
179
|
+
// Update tracking
|
|
180
|
+
lastRuns.set(policyId, result);
|
|
181
|
+
totalStorageReclaimed += storageReclaimedBytes;
|
|
182
|
+
totalEntriesDeleted += entriesDeleted;
|
|
183
|
+
return result;
|
|
184
|
+
},
|
|
185
|
+
async runAllPolicies() {
|
|
186
|
+
const results = [];
|
|
187
|
+
// Get enabled policies sorted by priority (higher first)
|
|
188
|
+
const enabledPolicies = Array.from(policies.values())
|
|
189
|
+
.filter((p) => p.enabled)
|
|
190
|
+
.sort((a, b) => b.priority - a.priority);
|
|
191
|
+
for (const policy of enabledPolicies) {
|
|
192
|
+
try {
|
|
193
|
+
const result = await this.runPolicy(policy.policyId);
|
|
194
|
+
results.push(result);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// Create error result for failed policy
|
|
198
|
+
const result = {
|
|
199
|
+
runId: crypto.randomUUID(),
|
|
200
|
+
policyId: policy.policyId,
|
|
201
|
+
startedAt: new Date().toISOString(),
|
|
202
|
+
completedAt: new Date().toISOString(),
|
|
203
|
+
entriesProcessed: 0,
|
|
204
|
+
entriesDeleted: 0,
|
|
205
|
+
entriesArchived: 0,
|
|
206
|
+
entriesSkipped: 0,
|
|
207
|
+
errors: [
|
|
208
|
+
error instanceof Error ? error.message : 'Unknown error',
|
|
209
|
+
],
|
|
210
|
+
success: false,
|
|
211
|
+
};
|
|
212
|
+
results.push(result);
|
|
213
|
+
lastRuns.set(policy.policyId, result);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
},
|
|
218
|
+
getSummary() {
|
|
219
|
+
const allPolicies = Array.from(policies.values());
|
|
220
|
+
const enabledCount = allPolicies.filter((p) => p.enabled).length;
|
|
221
|
+
const lastRunsRecord = {};
|
|
222
|
+
for (const [policyId, result] of lastRuns) {
|
|
223
|
+
lastRunsRecord[policyId] = result;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
totalPolicies: allPolicies.length,
|
|
227
|
+
enabledPolicies: enabledCount,
|
|
228
|
+
lastRuns: lastRunsRecord,
|
|
229
|
+
totalStorageReclaimedBytes: totalStorageReclaimed,
|
|
230
|
+
totalEntriesDeleted,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
registerStore(dataType, store) {
|
|
234
|
+
stores.set(dataType, store);
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Creates an in-memory retention store (for testing)
|
|
240
|
+
*/
|
|
241
|
+
export function createInMemoryRetentionStore(entries) {
|
|
242
|
+
return {
|
|
243
|
+
async findExpired(cutoffDate, _conditions) {
|
|
244
|
+
const cutoff = new Date(cutoffDate).getTime();
|
|
245
|
+
const result = [];
|
|
246
|
+
for (const entry of entries.values()) {
|
|
247
|
+
const entryTime = new Date(entry.createdAt).getTime();
|
|
248
|
+
if (entryTime < cutoff) {
|
|
249
|
+
result.push(entry);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
},
|
|
254
|
+
async delete(id) {
|
|
255
|
+
entries.delete(id);
|
|
256
|
+
},
|
|
257
|
+
async getSize(id) {
|
|
258
|
+
const entry = entries.get(id);
|
|
259
|
+
if (!entry)
|
|
260
|
+
return 0;
|
|
261
|
+
return JSON.stringify(entry).length;
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Creates an in-memory archiver (for testing)
|
|
267
|
+
*/
|
|
268
|
+
export function createInMemoryArchiver() {
|
|
269
|
+
const archives = [];
|
|
270
|
+
return {
|
|
271
|
+
async archive(dataType, entry, format, path) {
|
|
272
|
+
const archiveEntry = {
|
|
273
|
+
originalId: entry.id,
|
|
274
|
+
dataType,
|
|
275
|
+
archivedAt: new Date().toISOString(),
|
|
276
|
+
policyId: 'archive',
|
|
277
|
+
archivePath: path ?? `/archives/${dataType}/${entry.id}.${format}`,
|
|
278
|
+
sizeBytes: JSON.stringify(entry).length,
|
|
279
|
+
};
|
|
280
|
+
archives.push(archiveEntry);
|
|
281
|
+
return archiveEntry;
|
|
282
|
+
},
|
|
283
|
+
getArchives() {
|
|
284
|
+
return [...archives];
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Retention error
|
|
290
|
+
*/
|
|
291
|
+
export class RetentionError extends Error {
|
|
292
|
+
code;
|
|
293
|
+
constructor(code, message) {
|
|
294
|
+
super(message ?? `Retention error: ${code}`);
|
|
295
|
+
this.code = code;
|
|
296
|
+
this.name = 'RetentionError';
|
|
297
|
+
}
|
|
298
|
+
static policyNotFound(policyId) {
|
|
299
|
+
return new RetentionError(RetentionErrorCodes.POLICY_NOT_FOUND, `Policy not found: ${policyId}`);
|
|
300
|
+
}
|
|
301
|
+
static archiveFailed(entryId, error) {
|
|
302
|
+
return new RetentionError(RetentionErrorCodes.ARCHIVE_FAILED, `Archive failed for ${entryId}: ${error}`);
|
|
303
|
+
}
|
|
304
|
+
static deleteFailed(entryId, error) {
|
|
305
|
+
return new RetentionError(RetentionErrorCodes.DELETE_FAILED, `Delete failed for ${entryId}: ${error}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=retention-manager.js.map
|