@openmdm/core 0.2.0 → 0.3.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/src/queue.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * OpenMDM Message Queue Manager
3
+ *
4
+ * Provides persistent message queue management for the MDM system.
5
+ * Ensures reliable message delivery with retry and expiration handling.
6
+ */
7
+
8
+ import type {
9
+ MessageQueueManager,
10
+ QueuedMessage,
11
+ EnqueueMessageInput,
12
+ QueueStats,
13
+ DatabaseAdapter,
14
+ } from './types';
15
+
16
+ /**
17
+ * Default maximum attempts for message delivery
18
+ */
19
+ const DEFAULT_MAX_ATTEMPTS = 3;
20
+
21
+ /**
22
+ * Default TTL in seconds (24 hours)
23
+ */
24
+ const DEFAULT_TTL_SECONDS = 86400;
25
+
26
+ /**
27
+ * Create a MessageQueueManager instance
28
+ */
29
+ export function createMessageQueueManager(db: DatabaseAdapter): MessageQueueManager {
30
+ return {
31
+ async enqueue(message: EnqueueMessageInput): Promise<QueuedMessage> {
32
+ if (!db.enqueueMessage) {
33
+ throw new Error('Database adapter does not support message queue');
34
+ }
35
+
36
+ // Set defaults
37
+ const enrichedMessage: EnqueueMessageInput = {
38
+ ...message,
39
+ priority: message.priority ?? 'normal',
40
+ maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
41
+ ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS,
42
+ };
43
+
44
+ return db.enqueueMessage(enrichedMessage);
45
+ },
46
+
47
+ async enqueueBatch(messages: EnqueueMessageInput[]): Promise<QueuedMessage[]> {
48
+ if (!db.enqueueMessage) {
49
+ throw new Error('Database adapter does not support message queue');
50
+ }
51
+
52
+ const results: QueuedMessage[] = [];
53
+
54
+ for (const message of messages) {
55
+ const enrichedMessage: EnqueueMessageInput = {
56
+ ...message,
57
+ priority: message.priority ?? 'normal',
58
+ maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
59
+ ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS,
60
+ };
61
+
62
+ const queued = await db.enqueueMessage(enrichedMessage);
63
+ results.push(queued);
64
+ }
65
+
66
+ return results;
67
+ },
68
+
69
+ async dequeue(deviceId: string, limit: number = 10): Promise<QueuedMessage[]> {
70
+ if (!db.dequeueMessages) {
71
+ throw new Error('Database adapter does not support message queue');
72
+ }
73
+ return db.dequeueMessages(deviceId, limit);
74
+ },
75
+
76
+ async acknowledge(messageId: string): Promise<void> {
77
+ if (!db.acknowledgeMessage) {
78
+ throw new Error('Database adapter does not support message queue');
79
+ }
80
+ await db.acknowledgeMessage(messageId);
81
+ },
82
+
83
+ async fail(messageId: string, error: string): Promise<void> {
84
+ if (!db.failMessage) {
85
+ throw new Error('Database adapter does not support message queue');
86
+ }
87
+ await db.failMessage(messageId, error);
88
+ },
89
+
90
+ async retryFailed(maxAttempts: number = DEFAULT_MAX_ATTEMPTS): Promise<number> {
91
+ if (!db.retryFailedMessages) {
92
+ throw new Error('Database adapter does not support message queue');
93
+ }
94
+ return db.retryFailedMessages(maxAttempts);
95
+ },
96
+
97
+ async purgeExpired(): Promise<number> {
98
+ if (!db.purgeExpiredMessages) {
99
+ throw new Error('Database adapter does not support message queue');
100
+ }
101
+ return db.purgeExpiredMessages();
102
+ },
103
+
104
+ async getStats(tenantId?: string): Promise<QueueStats> {
105
+ if (!db.getQueueStats) {
106
+ throw new Error('Database adapter does not support message queue');
107
+ }
108
+ return db.getQueueStats(tenantId);
109
+ },
110
+
111
+ async peek(deviceId: string, limit: number = 10): Promise<QueuedMessage[]> {
112
+ if (!db.peekMessages) {
113
+ throw new Error('Database adapter does not support message queue');
114
+ }
115
+ return db.peekMessages(deviceId, limit);
116
+ },
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Message priority weights for sorting
122
+ */
123
+ export const PRIORITY_WEIGHTS = {
124
+ high: 3,
125
+ normal: 2,
126
+ low: 1,
127
+ } as const;
128
+
129
+ /**
130
+ * Compare messages by priority (higher priority first)
131
+ */
132
+ export function compareByPriority(a: QueuedMessage, b: QueuedMessage): number {
133
+ return PRIORITY_WEIGHTS[b.priority] - PRIORITY_WEIGHTS[a.priority];
134
+ }
135
+
136
+ /**
137
+ * Check if a message has expired
138
+ */
139
+ export function isMessageExpired(message: QueuedMessage): boolean {
140
+ if (!message.expiresAt) return false;
141
+ return new Date(message.expiresAt) < new Date();
142
+ }
143
+
144
+ /**
145
+ * Check if a message can be retried
146
+ */
147
+ export function canRetryMessage(message: QueuedMessage): boolean {
148
+ return message.status === 'failed' && message.attempts < message.maxAttempts;
149
+ }
150
+
151
+ /**
152
+ * Calculate exponential backoff delay for retries
153
+ */
154
+ export function calculateBackoffDelay(
155
+ attempts: number,
156
+ baseDelayMs: number = 1000,
157
+ maxDelayMs: number = 300000 // 5 minutes
158
+ ): number {
159
+ const delay = baseDelayMs * Math.pow(2, attempts - 1);
160
+ return Math.min(delay, maxDelayMs);
161
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * OpenMDM Schedule Manager
3
+ *
4
+ * Provides scheduled task management for the MDM system.
5
+ * Enables scheduling of recurring operations, maintenance windows, and one-time tasks.
6
+ */
7
+
8
+ import type {
9
+ ScheduleManager,
10
+ ScheduledTask,
11
+ ScheduledTaskFilter,
12
+ ScheduledTaskListResult,
13
+ CreateScheduledTaskInput,
14
+ UpdateScheduledTaskInput,
15
+ TaskSchedule,
16
+ TaskExecution,
17
+ DatabaseAdapter,
18
+ } from './types';
19
+
20
+ /**
21
+ * Parse cron expression and calculate next run time
22
+ * Supports: minute hour dayOfMonth month dayOfWeek
23
+ */
24
+ function parseCronNextRun(cron: string, from: Date = new Date()): Date | null {
25
+ try {
26
+ const parts = cron.trim().split(/\s+/);
27
+ if (parts.length !== 5) return null;
28
+
29
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
30
+
31
+ // Simple cron parsing - handles basic patterns
32
+ const now = new Date(from);
33
+ const next = new Date(now);
34
+
35
+ // Start from the next minute
36
+ next.setSeconds(0);
37
+ next.setMilliseconds(0);
38
+ next.setMinutes(next.getMinutes() + 1);
39
+
40
+ // Try to find next valid time (up to 1 year)
41
+ const maxIterations = 365 * 24 * 60; // 1 year in minutes
42
+ for (let i = 0; i < maxIterations; i++) {
43
+ const matches =
44
+ matchesCronField(minute, next.getMinutes()) &&
45
+ matchesCronField(hour, next.getHours()) &&
46
+ matchesCronField(dayOfMonth, next.getDate()) &&
47
+ matchesCronField(month, next.getMonth() + 1) &&
48
+ matchesCronField(dayOfWeek, next.getDay());
49
+
50
+ if (matches) {
51
+ return next;
52
+ }
53
+
54
+ next.setMinutes(next.getMinutes() + 1);
55
+ }
56
+
57
+ return null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Check if a value matches a cron field pattern
65
+ */
66
+ function matchesCronField(pattern: string, value: number): boolean {
67
+ if (pattern === '*') return true;
68
+
69
+ // Handle step values: */5, */15, etc.
70
+ if (pattern.startsWith('*/')) {
71
+ const step = parseInt(pattern.slice(2), 10);
72
+ return value % step === 0;
73
+ }
74
+
75
+ // Handle ranges: 1-5
76
+ if (pattern.includes('-')) {
77
+ const [start, end] = pattern.split('-').map((n) => parseInt(n, 10));
78
+ return value >= start && value <= end;
79
+ }
80
+
81
+ // Handle lists: 1,3,5
82
+ if (pattern.includes(',')) {
83
+ const values = pattern.split(',').map((n) => parseInt(n, 10));
84
+ return values.includes(value);
85
+ }
86
+
87
+ // Simple number
88
+ return parseInt(pattern, 10) === value;
89
+ }
90
+
91
+ /**
92
+ * Check if current time is within a maintenance window
93
+ */
94
+ function isInMaintenanceWindow(
95
+ window: TaskSchedule['window'],
96
+ now: Date = new Date()
97
+ ): boolean {
98
+ if (!window) return false;
99
+
100
+ const dayOfWeek = now.getDay();
101
+ if (!window.daysOfWeek.includes(dayOfWeek)) return false;
102
+
103
+ // Parse times
104
+ const [startHour, startMin] = window.startTime.split(':').map(Number);
105
+ const [endHour, endMin] = window.endTime.split(':').map(Number);
106
+
107
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
108
+ const startMinutes = startHour * 60 + startMin;
109
+ const endMinutes = endHour * 60 + endMin;
110
+
111
+ // Handle overnight windows
112
+ if (endMinutes < startMinutes) {
113
+ return currentMinutes >= startMinutes || currentMinutes < endMinutes;
114
+ }
115
+
116
+ return currentMinutes >= startMinutes && currentMinutes < endMinutes;
117
+ }
118
+
119
+ /**
120
+ * Calculate next run time for a maintenance window
121
+ */
122
+ function calculateNextWindowRun(
123
+ window: TaskSchedule['window'],
124
+ from: Date = new Date()
125
+ ): Date | null {
126
+ if (!window) return null;
127
+
128
+ const [startHour, startMin] = window.startTime.split(':').map(Number);
129
+
130
+ // Try each day for the next 7 days
131
+ for (let dayOffset = 0; dayOffset <= 7; dayOffset++) {
132
+ const candidate = new Date(from);
133
+ candidate.setDate(candidate.getDate() + dayOffset);
134
+ candidate.setHours(startHour, startMin, 0, 0);
135
+
136
+ // Skip if in the past
137
+ if (candidate <= from) continue;
138
+
139
+ // Check if day matches
140
+ if (window.daysOfWeek.includes(candidate.getDay())) {
141
+ return candidate;
142
+ }
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Create a ScheduleManager instance
150
+ */
151
+ export function createScheduleManager(db: DatabaseAdapter): ScheduleManager {
152
+ /**
153
+ * Calculate the next run time for a schedule
154
+ */
155
+ function calculateNextRun(schedule: TaskSchedule): Date | null {
156
+ const now = new Date();
157
+
158
+ switch (schedule.type) {
159
+ case 'once':
160
+ // For one-time tasks, return the scheduled time if it's in the future
161
+ if (schedule.executeAt && new Date(schedule.executeAt) > now) {
162
+ return new Date(schedule.executeAt);
163
+ }
164
+ return null;
165
+
166
+ case 'recurring':
167
+ // Parse cron expression
168
+ if (schedule.cron) {
169
+ return parseCronNextRun(schedule.cron, now);
170
+ }
171
+ return null;
172
+
173
+ case 'window':
174
+ // Calculate next maintenance window start
175
+ if (schedule.window) {
176
+ return calculateNextWindowRun(schedule.window, now);
177
+ }
178
+ return null;
179
+
180
+ default:
181
+ return null;
182
+ }
183
+ }
184
+
185
+ return {
186
+ async get(id: string): Promise<ScheduledTask | null> {
187
+ if (!db.findScheduledTask) {
188
+ throw new Error('Database adapter does not support task scheduling');
189
+ }
190
+ return db.findScheduledTask(id);
191
+ },
192
+
193
+ async list(filter?: ScheduledTaskFilter): Promise<ScheduledTaskListResult> {
194
+ if (!db.listScheduledTasks) {
195
+ throw new Error('Database adapter does not support task scheduling');
196
+ }
197
+ return db.listScheduledTasks(filter);
198
+ },
199
+
200
+ async create(data: CreateScheduledTaskInput): Promise<ScheduledTask> {
201
+ if (!db.createScheduledTask) {
202
+ throw new Error('Database adapter does not support task scheduling');
203
+ }
204
+
205
+ // Calculate initial next run time
206
+ const nextRunAt = calculateNextRun(data.schedule);
207
+
208
+ // Create task with calculated next run
209
+ const task = await db.createScheduledTask({
210
+ ...data,
211
+ // Note: nextRunAt is set by the database adapter based on schedule
212
+ });
213
+
214
+ // Update next run time if needed
215
+ if (nextRunAt && db.updateScheduledTask) {
216
+ return db.updateScheduledTask(task.id, {
217
+ ...data,
218
+ });
219
+ }
220
+
221
+ return task;
222
+ },
223
+
224
+ async update(id: string, data: UpdateScheduledTaskInput): Promise<ScheduledTask> {
225
+ if (!db.updateScheduledTask || !db.findScheduledTask) {
226
+ throw new Error('Database adapter does not support task scheduling');
227
+ }
228
+
229
+ const existing = await db.findScheduledTask(id);
230
+ if (!existing) {
231
+ throw new Error(`Scheduled task not found: ${id}`);
232
+ }
233
+
234
+ return db.updateScheduledTask(id, data);
235
+ },
236
+
237
+ async delete(id: string): Promise<void> {
238
+ if (!db.deleteScheduledTask) {
239
+ throw new Error('Database adapter does not support task scheduling');
240
+ }
241
+ await db.deleteScheduledTask(id);
242
+ },
243
+
244
+ async pause(id: string): Promise<ScheduledTask> {
245
+ if (!db.updateScheduledTask || !db.findScheduledTask) {
246
+ throw new Error('Database adapter does not support task scheduling');
247
+ }
248
+
249
+ const task = await db.findScheduledTask(id);
250
+ if (!task) {
251
+ throw new Error(`Scheduled task not found: ${id}`);
252
+ }
253
+
254
+ if (task.status === 'paused') {
255
+ return task;
256
+ }
257
+
258
+ return db.updateScheduledTask(id, { status: 'paused' });
259
+ },
260
+
261
+ async resume(id: string): Promise<ScheduledTask> {
262
+ if (!db.updateScheduledTask || !db.findScheduledTask) {
263
+ throw new Error('Database adapter does not support task scheduling');
264
+ }
265
+
266
+ const task = await db.findScheduledTask(id);
267
+ if (!task) {
268
+ throw new Error(`Scheduled task not found: ${id}`);
269
+ }
270
+
271
+ if (task.status !== 'paused') {
272
+ return task;
273
+ }
274
+
275
+ // Recalculate next run time
276
+ const nextRunAt = calculateNextRun(task.schedule);
277
+
278
+ return db.updateScheduledTask(id, { status: 'active' });
279
+ },
280
+
281
+ async runNow(id: string): Promise<TaskExecution> {
282
+ if (
283
+ !db.findScheduledTask ||
284
+ !db.createTaskExecution ||
285
+ !db.updateScheduledTask
286
+ ) {
287
+ throw new Error('Database adapter does not support task scheduling');
288
+ }
289
+
290
+ const task = await db.findScheduledTask(id);
291
+ if (!task) {
292
+ throw new Error(`Scheduled task not found: ${id}`);
293
+ }
294
+
295
+ // Create execution record
296
+ const execution = await db.createTaskExecution({ taskId: id });
297
+
298
+ // Update task last run time
299
+ await db.updateScheduledTask(id, {});
300
+
301
+ return execution;
302
+ },
303
+
304
+ async getUpcoming(hours: number): Promise<ScheduledTask[]> {
305
+ if (!db.getUpcomingTasks) {
306
+ throw new Error('Database adapter does not support task scheduling');
307
+ }
308
+ return db.getUpcomingTasks(hours);
309
+ },
310
+
311
+ async getExecutions(taskId: string, limit: number = 10): Promise<TaskExecution[]> {
312
+ if (!db.listTaskExecutions) {
313
+ throw new Error('Database adapter does not support task scheduling');
314
+ }
315
+ return db.listTaskExecutions(taskId, limit);
316
+ },
317
+
318
+ calculateNextRun,
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Export utility functions
324
+ */
325
+ export { parseCronNextRun, isInMaintenanceWindow, calculateNextWindowRun };