@aleonnet/healthcare-scheduler 0.1.10 → 0.1.11

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.
@@ -5,6 +5,10 @@ const expandPlan_1 = require("../planning/expandPlan");
5
5
  const windowPlanner_1 = require("../planning/windowPlanner");
6
6
  const timestamp_1 = require("../utils/timestamp");
7
7
  class WindowScheduler {
8
+ runSerial(task) {
9
+ WindowScheduler.opQueue = WindowScheduler.opQueue.then(task, task);
10
+ return WindowScheduler.opQueue;
11
+ }
8
12
  constructor(plansRepo, occurrencesRepo, notificationDriver, eventBus) {
9
13
  this.plansRepo = plansRepo;
10
14
  this.occurrencesRepo = occurrencesRepo;
@@ -31,224 +35,237 @@ class WindowScheduler {
31
35
  return undefined;
32
36
  }
33
37
  async fillWindow(options = {}) {
34
- const { days = 14 } = options;
35
- try {
36
- console.log(`📅 Filling ${days}-day window...`);
37
- const now = new Date();
38
- const plans = await this.plansRepo.findActive(now);
39
- if (!plans.length) {
40
- console.log('📅 No active plans found');
41
- return;
42
- }
43
- console.log(`📅 Found ${plans.length} active plans`);
44
- for (const plan of plans) {
45
- await this.occurrencesRepo.deleteByPlanIdAfter(plan.id, now.toISOString());
46
- }
47
- const allOccurrences = [];
48
- for (const plan of plans) {
49
- try {
50
- const expanded = (0, expandPlan_1.expandPlan)(plan.schedule_rule, now);
51
- const windowed = (0, windowPlanner_1.selectWindow)(expanded, { windowDays: days, maxPending: 1000 });
52
- const dbOccurrences = windowed.map(occ => ({
53
- id: `${plan.id}:${occ.scheduledAt}`,
54
- plan_id: plan.id,
55
- scheduled_at: occ.scheduledAt,
56
- kind: (plan.item_type || 'MED'),
57
- qty: occ.qty || 1
58
- }));
59
- allOccurrences.push(...dbOccurrences);
60
- console.log(`📅 Plan ${plan.id}: ${dbOccurrences.length} occurrences`);
38
+ return await this.runSerial(async () => {
39
+ const { days = 14 } = options;
40
+ try {
41
+ console.log(`📅 Filling ${days}-day window...`);
42
+ const now = new Date();
43
+ const plans = await this.plansRepo.findActive(now);
44
+ if (!plans.length) {
45
+ console.log('📅 No active plans found');
46
+ return;
61
47
  }
62
- catch (error) {
63
- console.error(`❌ Error expanding plan ${plan.id}:`, error);
48
+ console.log(`📅 Found ${plans.length} active plans`);
49
+ for (const plan of plans) {
50
+ await this.occurrencesRepo.deleteByPlanIdAfter(plan.id, now.toISOString());
64
51
  }
52
+ const allOccurrences = [];
53
+ for (const plan of plans) {
54
+ try {
55
+ const expanded = (0, expandPlan_1.expandPlan)(plan.schedule_rule, now);
56
+ const windowed = (0, windowPlanner_1.selectWindow)(expanded, { windowDays: days, maxPending: 1000 });
57
+ const dbOccurrences = windowed.map(occ => ({
58
+ id: `${plan.id}:${occ.scheduledAt}`,
59
+ plan_id: plan.id,
60
+ scheduled_at: occ.scheduledAt,
61
+ kind: (plan.item_type || 'MED'),
62
+ qty: occ.qty || 1
63
+ }));
64
+ allOccurrences.push(...dbOccurrences);
65
+ console.log(`📅 Plan ${plan.id}: ${dbOccurrences.length} occurrences`);
66
+ }
67
+ catch (error) {
68
+ console.error(`❌ Error expanding plan ${plan.id}:`, error);
69
+ }
70
+ }
71
+ if (allOccurrences.length) {
72
+ await this.occurrencesRepo.insertMany(allOccurrences);
73
+ console.log(`📅 ${allOccurrences.length} occurrences synced`);
74
+ }
75
+ this.eventBus.emit('window:filled', { count: allOccurrences.length });
65
76
  }
66
- if (allOccurrences.length) {
67
- await this.occurrencesRepo.insertMany(allOccurrences);
68
- console.log(`📅 ${allOccurrences.length} occurrences synced`);
77
+ catch (error) {
78
+ console.error('❌ Error filling window:', error);
79
+ throw error;
69
80
  }
70
- this.eventBus.emit('window:filled', { count: allOccurrences.length });
71
- }
72
- catch (error) {
73
- console.error('❌ Error filling window:', error);
74
- throw error;
75
- }
81
+ });
76
82
  }
77
83
  async reconcileWithOS(options = {}) {
78
- const { horizonDays = 2, includePastDay = false } = options;
79
- try {
80
- const pendingOccs = await this.occurrencesRepo.getUpcomingWindow({
81
- days: Math.max(1, horizonDays),
82
- limit: 50,
83
- pastDays: includePastDay ? 1 : 0
84
- });
85
- const pending = pendingOccs.filter(occ => occ.status === 'PENDING');
86
- const scheduledNotifs = await this.notificationDriver.listScheduled();
87
- console.log(`🔄 Reconciling (horizon=${horizonDays}d, cap=45, group=${this.groupSameTime ? 'on' : 'off'}): ${pending.length} pending, ${scheduledNotifs.length} scheduled in OS`);
88
- const groups = new Map();
89
- if (this.groupSameTime) {
90
- for (const occ of pending) {
91
- if (occ.kind !== 'MED')
92
- continue;
93
- const d = (0, timestamp_1.parseUTC)(occ.scheduled_at);
94
- const key = `${occ.kind}:${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}T${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
95
- if (!groups.has(key))
96
- groups.set(key, []);
97
- groups.get(key).push(occ);
98
- }
99
- }
100
- const notifsByOccId = new Map();
101
- const notifsByGroupKey = new Map();
102
- for (const notif of scheduledNotifs) {
103
- const occId = notif.notification?.data?.occurrenceId;
104
- if (occId) {
105
- notifsByOccId.set(occId, notif.notification.id);
106
- }
107
- const gk = notif.notification?.data?.groupKey;
108
- if (gk) {
109
- let count = 0;
110
- try {
111
- const occIdsRaw = notif.notification?.data?.occurrenceIds;
112
- const occIds = Array.isArray(occIdsRaw) ? occIdsRaw : (typeof occIdsRaw === 'string' ? (JSON.parse(occIdsRaw || '[]') || []) : []);
113
- count = Array.isArray(occIds) ? occIds.length : 0;
84
+ return await this.runSerial(async () => {
85
+ const { horizonDays = 2, includePastDay = false } = options;
86
+ try {
87
+ const pendingOccs = await this.occurrencesRepo.getUpcomingWindow({
88
+ days: Math.max(1, horizonDays),
89
+ limit: 50,
90
+ pastDays: includePastDay ? 1 : 0
91
+ });
92
+ const pending = pendingOccs.filter(occ => occ.status === 'PENDING');
93
+ const scheduledNotifs = await this.notificationDriver.listScheduled();
94
+ console.log(`🔄 Reconciling (horizon=${horizonDays}d, cap=45, group=${this.groupSameTime ? 'on' : 'off'}): ${pending.length} pending, ${scheduledNotifs.length} scheduled in OS`);
95
+ const groups = new Map();
96
+ if (this.groupSameTime) {
97
+ for (const occ of pending) {
98
+ if (occ.kind !== 'MED')
99
+ continue;
100
+ const d = (0, timestamp_1.parseUTC)(occ.scheduled_at);
101
+ const key = `${occ.kind}:${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}T${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
102
+ if (!groups.has(key))
103
+ groups.set(key, []);
104
+ groups.get(key).push(occ);
114
105
  }
115
- catch { }
116
- notifsByGroupKey.set(gk, { id: notif.notification?.id || '', count });
117
106
  }
118
- }
119
- let scheduled = 0;
120
- let cancelled = 0;
121
- const now = new Date();
122
- if (this.groupSameTime && groups.size > 0) {
123
- for (const [key, occs] of groups) {
124
- if (!occs || occs.length === 0)
125
- continue;
126
- const occ = occs.sort((a, b) => (0, timestamp_1.parseUTC)(a.scheduled_at).getTime() - (0, timestamp_1.parseUTC)(b.scheduled_at).getTime())[0];
127
- const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
128
- const SAFETY_BUFFER_MS = 2000;
129
- if (scheduledTime.getTime() <= now.getTime() + SAFETY_BUFFER_MS)
130
- continue;
131
- if (occs.length < 2) {
132
- continue;
107
+ const notifsByOccId = new Map();
108
+ const notifsByGroupKey = new Map();
109
+ for (const notif of scheduledNotifs) {
110
+ const occId = notif.notification?.data?.occurrenceId;
111
+ if (occId) {
112
+ notifsByOccId.set(occId, notif.notification.id);
133
113
  }
134
- try {
135
- const hh = scheduledTime.getHours().toString().padStart(2, '0');
136
- const mm = scheduledTime.getMinutes().toString().padStart(2, '0');
137
- const groupKey = key;
138
- const legacyDailyKey = `${occ.kind}:HH:${hh}:${mm}`;
139
- const names = occs.map((o) => o.item_name).filter(Boolean);
140
- const namesShort = names.slice(0, 2).join(', ') + (names.length > 2 ? ` +${names.length - 2}` : '');
141
- const title = `💊 ${occs.length} medicamentos agora`;
142
- const body = namesShort ? `${hh}:${mm} • ${namesShort}` : `${hh}:${mm}`;
143
- const existing = notifsByGroupKey.get(groupKey);
144
- if (existing && Number(existing.count || 0) === occs.length) {
145
- continue;
146
- }
114
+ const gk = notif.notification?.data?.groupKey;
115
+ if (gk) {
116
+ let count = 0;
147
117
  try {
148
- await this.notificationDriver.cancelTriggerByGroup(groupKey);
118
+ const occIdsRaw = notif.notification?.data?.occurrenceIds;
119
+ const occIds = Array.isArray(occIdsRaw) ? occIdsRaw : (typeof occIdsRaw === 'string' ? (JSON.parse(occIdsRaw || '[]') || []) : []);
120
+ count = Array.isArray(occIds) ? occIds.length : 0;
149
121
  }
150
122
  catch { }
151
- try {
152
- await this.notificationDriver.cancelTriggerByGroup(legacyDailyKey);
123
+ notifsByGroupKey.set(gk, { id: notif.notification?.id || '', count });
124
+ }
125
+ }
126
+ let scheduled = 0;
127
+ let cancelled = 0;
128
+ const now = new Date();
129
+ if (this.groupSameTime && groups.size > 0) {
130
+ for (const [key, occs] of groups) {
131
+ if (!occs || occs.length === 0)
132
+ continue;
133
+ const occ = occs.sort((a, b) => (0, timestamp_1.parseUTC)(a.scheduled_at).getTime() - (0, timestamp_1.parseUTC)(b.scheduled_at).getTime())[0];
134
+ const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
135
+ const SAFETY_BUFFER_MS = 2000;
136
+ if (scheduledTime.getTime() <= now.getTime() + SAFETY_BUFFER_MS)
137
+ continue;
138
+ if (occs.length < 2) {
139
+ continue;
153
140
  }
154
- catch { }
155
- for (const o of occs) {
141
+ try {
142
+ const hh = scheduledTime.getHours().toString().padStart(2, '0');
143
+ const mm = scheduledTime.getMinutes().toString().padStart(2, '0');
144
+ const groupKey = key;
145
+ const legacyDailyKey = `${occ.kind}:HH:${hh}:${mm}`;
146
+ const names = occs.map((o) => o.item_name).filter(Boolean);
147
+ const namesShort = names.slice(0, 2).join(', ') + (names.length > 2 ? ` +${names.length - 2}` : '');
148
+ const title = `💊 ${occs.length} medicamentos agora`;
149
+ const body = namesShort ? `${hh}:${mm} • ${namesShort}` : `${hh}:${mm}`;
150
+ const existing = notifsByGroupKey.get(groupKey);
151
+ if (existing && Number(existing.count || 0) === occs.length) {
152
+ continue;
153
+ }
156
154
  try {
157
- await this.notificationDriver.cancelTriggerByOccurrence(o.id);
155
+ await this.notificationDriver.cancelTriggerByGroup(groupKey);
158
156
  }
159
157
  catch { }
160
- }
161
- await this.notificationDriver.scheduleGroupTrigger({
162
- groupKey,
163
- occurrenceIds: occs.map((o) => o.id),
164
- itemType: occ.kind,
165
- itemId: occ.item_id || occ.plan_id,
166
- title,
167
- body,
168
- scheduledAt: scheduledTime,
169
- snoozeMin: this.snoozeMinutes
170
- });
171
- scheduled++;
172
- }
173
- catch (error) {
174
- console.error(`❌ Error scheduling group ${key}:`, error);
175
- }
176
- }
177
- const groupedOccIds = new Set();
178
- for (const occs of groups.values()) {
179
- if (occs && occs.length >= 2) {
180
- for (const o of occs)
181
- groupedOccIds.add(o.id);
182
- }
183
- }
184
- const remainder = pending.filter((o) => o.kind !== 'MED' || !groupedOccIds.has(o.id));
185
- for (const occ of remainder.slice(0, 45)) {
186
- const occId = occ.id;
187
- const isScheduled = notifsByOccId.has(occId);
188
- const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
189
- if (scheduledTime.getTime() <= now.getTime()) {
190
- continue;
191
- }
192
- if (!isScheduled) {
193
- try {
194
- const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
195
- ? await this.getReminderOffset(occ.plan_id)
196
- : undefined;
197
- await this.notificationDriver.scheduleTrigger({
198
- occurrenceId: occId,
158
+ try {
159
+ await this.notificationDriver.cancelTriggerByGroup(legacyDailyKey);
160
+ }
161
+ catch { }
162
+ for (const o of occs) {
163
+ try {
164
+ await this.notificationDriver.cancelTriggerByOccurrence(o.id);
165
+ }
166
+ catch { }
167
+ }
168
+ await this.notificationDriver.scheduleGroupTrigger({
169
+ groupKey,
170
+ occurrenceIds: occs.map((o) => o.id),
199
171
  itemType: occ.kind,
200
172
  itemId: occ.item_id || occ.plan_id,
201
- title: this.formatNotificationTitle(occ),
202
- body: this.formatNotificationBody(occ),
173
+ title,
174
+ body,
203
175
  scheduledAt: scheduledTime,
204
- snoozeMin: this.snoozeMinutes,
205
- reminderOffset
176
+ snoozeMin: this.snoozeMinutes
206
177
  });
207
178
  scheduled++;
208
179
  }
209
180
  catch (error) {
210
- console.error(`❌ Error scheduling ${occId}:`, error);
181
+ console.error(`❌ Error scheduling group ${key}:`, error);
211
182
  }
212
183
  }
213
- }
214
- }
215
- else {
216
- for (const occ of pending.slice(0, 45)) {
217
- const occId = occ.id;
218
- const isScheduled = notifsByOccId.has(occId);
219
- const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
220
- if (scheduledTime.getTime() <= now.getTime())
221
- continue;
222
- if (!isScheduled) {
223
- try {
224
- const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
225
- ? await this.getReminderOffset(occ.plan_id)
226
- : undefined;
227
- await this.notificationDriver.scheduleTrigger({
228
- occurrenceId: occId,
229
- itemType: occ.kind,
230
- itemId: occ.item_id || occ.plan_id,
231
- title: this.formatNotificationTitle(occ),
232
- body: this.formatNotificationBody(occ),
233
- scheduledAt: scheduledTime,
234
- snoozeMin: this.snoozeMinutes,
235
- reminderOffset
236
- });
237
- scheduled++;
184
+ const groupedOccIds = new Set();
185
+ for (const occs of groups.values()) {
186
+ if (occs && occs.length >= 2) {
187
+ for (const o of occs)
188
+ groupedOccIds.add(o.id);
238
189
  }
239
- catch (error) {
240
- console.error(`❌ Error scheduling ${occId}:`, error);
190
+ }
191
+ const remainder = pending.filter((o) => o.kind !== 'MED' || !groupedOccIds.has(o.id));
192
+ for (const occ of remainder.slice(0, 45)) {
193
+ const occId = occ.id;
194
+ const isScheduled = notifsByOccId.has(occId);
195
+ const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
196
+ if (scheduledTime.getTime() <= now.getTime()) {
197
+ continue;
198
+ }
199
+ if (!isScheduled) {
200
+ try {
201
+ const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
202
+ ? await this.getReminderOffset(occ.plan_id)
203
+ : undefined;
204
+ await this.notificationDriver.scheduleTrigger({
205
+ occurrenceId: occId,
206
+ itemType: occ.kind,
207
+ itemId: occ.item_id || occ.plan_id,
208
+ title: this.formatNotificationTitle(occ),
209
+ body: this.formatNotificationBody(occ),
210
+ scheduledAt: scheduledTime,
211
+ snoozeMin: this.snoozeMinutes,
212
+ reminderOffset
213
+ });
214
+ scheduled++;
215
+ }
216
+ catch (error) {
217
+ console.error(`❌ Error scheduling ${occId}:`, error);
218
+ }
241
219
  }
242
220
  }
243
221
  }
244
- }
245
- try {
246
- if (this.groupSameTime) {
247
- const validGroupKeys = new Set();
248
- for (const key of groups.keys())
249
- validGroupKeys.add(key);
250
- for (const [gk] of notifsByGroupKey) {
251
- if (!validGroupKeys.has(gk)) {
222
+ else {
223
+ for (const occ of pending.slice(0, 45)) {
224
+ const occId = occ.id;
225
+ const isScheduled = notifsByOccId.has(occId);
226
+ const scheduledTime = (0, timestamp_1.parseUTC)(occ.scheduled_at);
227
+ if (scheduledTime.getTime() <= now.getTime())
228
+ continue;
229
+ if (!isScheduled) {
230
+ try {
231
+ const reminderOffset = (occ.kind === 'APPT' || occ.kind === 'TEST')
232
+ ? await this.getReminderOffset(occ.plan_id)
233
+ : undefined;
234
+ await this.notificationDriver.scheduleTrigger({
235
+ occurrenceId: occId,
236
+ itemType: occ.kind,
237
+ itemId: occ.item_id || occ.plan_id,
238
+ title: this.formatNotificationTitle(occ),
239
+ body: this.formatNotificationBody(occ),
240
+ scheduledAt: scheduledTime,
241
+ snoozeMin: this.snoozeMinutes,
242
+ reminderOffset
243
+ });
244
+ scheduled++;
245
+ }
246
+ catch (error) {
247
+ console.error(`❌ Error scheduling ${occId}:`, error);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ try {
253
+ if (this.groupSameTime) {
254
+ const validGroupKeys = new Set();
255
+ for (const key of groups.keys())
256
+ validGroupKeys.add(key);
257
+ for (const [gk] of notifsByGroupKey) {
258
+ if (!validGroupKeys.has(gk)) {
259
+ try {
260
+ await this.notificationDriver.cancelTriggerByGroup(gk);
261
+ cancelled++;
262
+ }
263
+ catch { }
264
+ }
265
+ }
266
+ }
267
+ else {
268
+ for (const [gk] of notifsByGroupKey) {
252
269
  try {
253
270
  await this.notificationDriver.cancelTriggerByGroup(gk);
254
271
  cancelled++;
@@ -257,94 +274,85 @@ class WindowScheduler {
257
274
  }
258
275
  }
259
276
  }
260
- else {
261
- for (const [gk] of notifsByGroupKey) {
277
+ catch { }
278
+ try {
279
+ const snoozedToCheck = pendingOccs.filter(o => String(o.last_action || '').toUpperCase() === 'SNOOZED');
280
+ for (const occ of snoozedToCheck) {
281
+ const lastAt = occ.last_action_at ? new Date(occ.last_action_at) : null;
282
+ let snoozeMin = 0;
262
283
  try {
263
- await this.notificationDriver.cancelTriggerByGroup(gk);
264
- cancelled++;
284
+ const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
285
+ snoozeMin = Number(payload?.snoozeMin || 0);
265
286
  }
266
287
  catch { }
288
+ if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
289
+ continue;
290
+ const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
291
+ if (snoozeUntil.getTime() <= now.getTime())
292
+ continue;
293
+ const isScheduled = notifsByOccId.has(occ.id);
294
+ if (!isScheduled) {
295
+ try {
296
+ await this.notificationDriver.scheduleTrigger({
297
+ occurrenceId: occ.id,
298
+ itemType: occ.kind,
299
+ itemId: occ.item_id || occ.plan_id,
300
+ title: this.formatNotificationTitle(occ),
301
+ body: this.formatNotificationBody(occ),
302
+ scheduledAt: snoozeUntil,
303
+ snoozeMin: this.snoozeMinutes
304
+ });
305
+ scheduled++;
306
+ console.log('🔁 Snooze re-scheduled by reconcile for', occ.id);
307
+ }
308
+ catch (e) {
309
+ console.error('❌ Error re-scheduling snooze', occ.id, e);
310
+ }
311
+ }
267
312
  }
268
313
  }
269
- }
270
- catch { }
271
- try {
272
- const snoozedToCheck = pendingOccs.filter(o => String(o.last_action || '').toUpperCase() === 'SNOOZED');
273
- for (const occ of snoozedToCheck) {
274
- const lastAt = occ.last_action_at ? new Date(occ.last_action_at) : null;
275
- let snoozeMin = 0;
276
- try {
277
- const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
278
- snoozeMin = Number(payload?.snoozeMin || 0);
279
- }
280
- catch { }
281
- if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
282
- continue;
283
- const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
284
- if (snoozeUntil.getTime() <= now.getTime())
285
- continue;
286
- const isScheduled = notifsByOccId.has(occ.id);
287
- if (!isScheduled) {
314
+ catch { }
315
+ const validOccIds = new Set(pending.map(occ => occ.id));
316
+ try {
317
+ const snoozedValid = pendingOccs.filter(o => String(o.last_action || '').toUpperCase() === 'SNOOZED');
318
+ for (const occ of snoozedValid) {
319
+ const lastAt = occ.last_action_at ? new Date(occ.last_action_at) : null;
320
+ let snoozeMin = 0;
288
321
  try {
289
- await this.notificationDriver.scheduleTrigger({
290
- occurrenceId: occ.id,
291
- itemType: occ.kind,
292
- itemId: occ.item_id || occ.plan_id,
293
- title: this.formatNotificationTitle(occ),
294
- body: this.formatNotificationBody(occ),
295
- scheduledAt: snoozeUntil,
296
- snoozeMin: this.snoozeMinutes
297
- });
298
- scheduled++;
299
- console.log('🔁 Snooze re-scheduled by reconcile for', occ.id);
322
+ const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
323
+ snoozeMin = Number(payload?.snoozeMin || 0);
300
324
  }
301
- catch (e) {
302
- console.error('❌ Error re-scheduling snooze', occ.id, e);
325
+ catch { }
326
+ if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
327
+ continue;
328
+ const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
329
+ if (snoozeUntil.getTime() > now.getTime()) {
330
+ validOccIds.add(occ.id);
303
331
  }
304
332
  }
305
333
  }
306
- }
307
- catch { }
308
- const validOccIds = new Set(pending.map(occ => occ.id));
309
- try {
310
- const snoozedValid = pendingOccs.filter(o => String(o.last_action || '').toUpperCase() === 'SNOOZED');
311
- for (const occ of snoozedValid) {
312
- const lastAt = occ.last_action_at ? new Date(occ.last_action_at) : null;
313
- let snoozeMin = 0;
314
- try {
315
- const payload = occ.last_action_payload ? JSON.parse(occ.last_action_payload) : null;
316
- snoozeMin = Number(payload?.snoozeMin || 0);
317
- }
318
- catch { }
319
- if (!lastAt || !snoozeMin || Number.isNaN(snoozeMin))
320
- continue;
321
- const snoozeUntil = new Date(lastAt.getTime() + snoozeMin * 60 * 1000);
322
- if (snoozeUntil.getTime() > now.getTime()) {
323
- validOccIds.add(occ.id);
334
+ catch { }
335
+ for (const [occId] of notifsByOccId) {
336
+ if (!validOccIds.has(occId)) {
337
+ try {
338
+ await this.notificationDriver.cancelTriggerByOccurrence(occId);
339
+ cancelled++;
340
+ }
341
+ catch (error) {
342
+ console.error(`❌ Error canceling ${occId}:`, error);
343
+ }
324
344
  }
325
345
  }
326
- }
327
- catch { }
328
- for (const [occId] of notifsByOccId) {
329
- if (!validOccIds.has(occId)) {
330
- try {
331
- await this.notificationDriver.cancelTriggerByOccurrence(occId);
332
- cancelled++;
333
- }
334
- catch (error) {
335
- console.error(`❌ Error canceling ${occId}:`, error);
336
- }
346
+ if (scheduled > 0 || cancelled > 0) {
347
+ console.log(`🔄 Reconciliation complete (horizon=${horizonDays}d, cap=45): ${scheduled} scheduled, ${cancelled} canceled`);
337
348
  }
349
+ this.eventBus.emit('notifications:reconciled', { scheduled, cancelled });
338
350
  }
339
- if (scheduled > 0 || cancelled > 0) {
340
- console.log(`🔄 Reconciliation complete (horizon=${horizonDays}d, cap=45): ${scheduled} scheduled, ${cancelled} canceled`);
351
+ catch (error) {
352
+ console.error('❌ Error in reconciliation:', error);
353
+ throw error;
341
354
  }
342
- this.eventBus.emit('notifications:reconciled', { scheduled, cancelled });
343
- }
344
- catch (error) {
345
- console.error('❌ Error in reconciliation:', error);
346
- throw error;
347
- }
355
+ });
348
356
  }
349
357
  async updateWindow(options = {}) {
350
358
  await this.fillWindow(options);
@@ -394,4 +402,5 @@ class WindowScheduler {
394
402
  }
395
403
  }
396
404
  exports.WindowScheduler = WindowScheduler;
405
+ WindowScheduler.opQueue = Promise.resolve();
397
406
  //# sourceMappingURL=WindowScheduler.js.map