@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.
@@ -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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"}
@@ -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":"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"}
@@ -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