@open-mercato/scheduler 0.8.1-develop.7275.1.f772b944d4 → 0.8.1-develop.7294.1.0ef99fc518

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.
@@ -1,23 +1,11 @@
1
- const MIN_SCHEDULE_INTERVAL_MS = 60 * 1e3;
1
+ import {
2
+ MIN_SCHEDULE_INTERVAL_MS,
3
+ isValidScheduleInterval,
4
+ matchScheduleInterval,
5
+ parseScheduleInterval
6
+ } from "@open-mercato/shared/lib/schedule/interval";
2
7
  function parseInterval(interval) {
3
- const regex = /^(\d+)(s|m|h|d)$/;
4
- const match = interval.match(regex);
5
- if (!match) {
6
- throw new Error(`Invalid interval format: ${interval}. Expected format: <number><unit> (e.g., 15m, 2h, 1d)`);
7
- }
8
- const value = parseInt(match[1], 10);
9
- const unit = match[2];
10
- const multipliers = {
11
- s: 1e3,
12
- // seconds
13
- m: 60 * 1e3,
14
- // minutes
15
- h: 60 * 60 * 1e3,
16
- // hours
17
- d: 24 * 60 * 60 * 1e3
18
- // days
19
- };
20
- return value * multipliers[unit];
8
+ return parseScheduleInterval(interval);
21
9
  }
22
10
  function resolveScheduleIntervalMs(interval) {
23
11
  return Math.max(parseInterval(interval), MIN_SCHEDULE_INTERVAL_MS);
@@ -27,20 +15,15 @@ function calculateNextRunFromInterval(interval, fromDate = /* @__PURE__ */ new D
27
15
  return new Date(fromDate.getTime() + ms);
28
16
  }
29
17
  function validateInterval(interval) {
30
- try {
31
- return parseInterval(interval) >= MIN_SCHEDULE_INTERVAL_MS;
32
- } catch {
33
- return false;
34
- }
18
+ return isValidScheduleInterval(interval);
35
19
  }
36
20
  function intervalToHuman(interval) {
37
21
  try {
38
- const regex = /^(\d+)(s|m|h|d)$/;
39
- const match = interval.match(regex);
40
- if (!match) {
22
+ const parts = matchScheduleInterval(interval);
23
+ if (!parts) {
41
24
  return interval;
42
25
  }
43
- const unit = match[2];
26
+ const unit = parts.unit;
44
27
  const ms = parseInterval(interval);
45
28
  const seconds = ms / 1e3;
46
29
  if (seconds === 0) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/scheduler/lib/intervalParser.ts"],
4
- "sourcesContent": ["/**\n * Parse simple interval strings like '15m', '2h', '1d' into milliseconds\n */\nexport const MIN_SCHEDULE_INTERVAL_MS = 60 * 1000\n\nexport function parseInterval(interval: string): number {\n const regex = /^(\\d+)(s|m|h|d)$/\n const match = interval.match(regex)\n \n if (!match) {\n throw new Error(`Invalid interval format: ${interval}. Expected format: <number><unit> (e.g., 15m, 2h, 1d)`)\n }\n \n const value = parseInt(match[1], 10)\n const unit = match[2]\n \n const multipliers: Record<string, number> = {\n s: 1000, // seconds\n m: 60 * 1000, // minutes\n h: 60 * 60 * 1000, // hours\n d: 24 * 60 * 60 * 1000, // days\n }\n \n return value * multipliers[unit]\n}\n\nexport function resolveScheduleIntervalMs(interval: string): number {\n return Math.max(parseInterval(interval), MIN_SCHEDULE_INTERVAL_MS)\n}\n\n/**\n * Calculate next run time based on interval\n */\nexport function calculateNextRunFromInterval(\n interval: string,\n fromDate: Date = new Date()\n): Date {\n const ms = resolveScheduleIntervalMs(interval)\n return new Date(fromDate.getTime() + ms)\n}\n\n/**\n * Validate an interval string\n */\nexport function validateInterval(interval: string): boolean {\n try {\n return parseInterval(interval) >= MIN_SCHEDULE_INTERVAL_MS\n } catch {\n return false\n }\n}\n\n/**\n * Convert interval to human-readable string\n * Preserves the original unit unless it can be cleanly converted to a larger unit\n * Special case: Zero always displays as \"0 seconds\"\n */\nexport function intervalToHuman(interval: string): string {\n try {\n const regex = /^(\\d+)(s|m|h|d)$/\n const match = interval.match(regex)\n \n if (!match) {\n return interval\n }\n \n const unit = match[2]\n \n const ms = parseInterval(interval)\n const seconds = ms / 1000\n \n // Special case: zero always displays as \"0 seconds\"\n if (seconds === 0) {\n return '0 seconds'\n }\n \n const minutes = seconds / 60\n const hours = minutes / 60\n const days = hours / 24\n \n // Only convert to larger units based on the input unit\n // This preserves the original unit choice (e.g., 60s stays as \"60 seconds\")\n switch (unit) {\n case 's':\n // Seconds never convert up (preserve user's choice)\n return `${seconds} second${seconds !== 1 ? 's' : ''}`\n \n case 'm':\n // Minutes can convert to hours or days\n if (days >= 1 && days === Math.floor(days)) {\n return `${days} day${days !== 1 ? 's' : ''}`\n }\n if (hours >= 1 && hours === Math.floor(hours)) {\n return `${hours} hour${hours !== 1 ? 's' : ''}`\n }\n return `${minutes} minute${minutes !== 1 ? 's' : ''}`\n \n case 'h':\n // Hours can convert to days\n if (days >= 1 && days === Math.floor(days)) {\n return `${days} day${days !== 1 ? 's' : ''}`\n }\n return `${hours} hour${hours !== 1 ? 's' : ''}`\n \n case 'd':\n // Days stay as days\n return `${days} day${days !== 1 ? 's' : ''}`\n \n default:\n return interval\n }\n } catch {\n return interval\n }\n}\n"],
5
- "mappings": "AAGO,MAAM,2BAA2B,KAAK;AAEtC,SAAS,cAAc,UAA0B;AACtD,QAAM,QAAQ;AACd,QAAM,QAAQ,SAAS,MAAM,KAAK;AAElC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,4BAA4B,QAAQ,uDAAuD;AAAA,EAC7G;AAEA,QAAM,QAAQ,SAAS,MAAM,CAAC,GAAG,EAAE;AACnC,QAAM,OAAO,MAAM,CAAC;AAEpB,QAAM,cAAsC;AAAA,IAC1C,GAAG;AAAA;AAAA,IACH,GAAG,KAAK;AAAA;AAAA,IACR,GAAG,KAAK,KAAK;AAAA;AAAA,IACb,GAAG,KAAK,KAAK,KAAK;AAAA;AAAA,EACpB;AAEA,SAAO,QAAQ,YAAY,IAAI;AACjC;AAEO,SAAS,0BAA0B,UAA0B;AAClE,SAAO,KAAK,IAAI,cAAc,QAAQ,GAAG,wBAAwB;AACnE;AAKO,SAAS,6BACd,UACA,WAAiB,oBAAI,KAAK,GACpB;AACN,QAAM,KAAK,0BAA0B,QAAQ;AAC7C,SAAO,IAAI,KAAK,SAAS,QAAQ,IAAI,EAAE;AACzC;AAKO,SAAS,iBAAiB,UAA2B;AAC1D,MAAI;AACF,WAAO,cAAc,QAAQ,KAAK;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,gBAAgB,UAA0B;AACxD,MAAI;AACF,UAAM,QAAQ;AACd,UAAM,QAAQ,SAAS,MAAM,KAAK;AAElC,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,CAAC;AAEpB,UAAM,KAAK,cAAc,QAAQ;AACjC,UAAM,UAAU,KAAK;AAGrB,QAAI,YAAY,GAAG;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,UAAU;AAC1B,UAAM,QAAQ,UAAU;AACxB,UAAM,OAAO,QAAQ;AAIrB,YAAQ,MAAM;AAAA,MACZ,KAAK;AAEH,eAAO,GAAG,OAAO,UAAU,YAAY,IAAI,MAAM,EAAE;AAAA,MAErD,KAAK;AAEH,YAAI,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI,GAAG;AAC1C,iBAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,QAC5C;AACA,YAAI,SAAS,KAAK,UAAU,KAAK,MAAM,KAAK,GAAG;AAC7C,iBAAO,GAAG,KAAK,QAAQ,UAAU,IAAI,MAAM,EAAE;AAAA,QAC/C;AACA,eAAO,GAAG,OAAO,UAAU,YAAY,IAAI,MAAM,EAAE;AAAA,MAErD,KAAK;AAEH,YAAI,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI,GAAG;AAC1C,iBAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,QAC5C;AACA,eAAO,GAAG,KAAK,QAAQ,UAAU,IAAI,MAAM,EAAE;AAAA,MAE/C,KAAK;AAEH,eAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,MAE5C;AACE,eAAO;AAAA,IACX;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import {\n MIN_SCHEDULE_INTERVAL_MS,\n isValidScheduleInterval,\n matchScheduleInterval,\n parseScheduleInterval,\n} from '@open-mercato/shared/lib/schedule/interval'\n\n/**\n * Parse simple interval strings like '15m', '2h', '1d' into milliseconds\n */\nexport { MIN_SCHEDULE_INTERVAL_MS }\n\nexport function parseInterval(interval: string): number {\n return parseScheduleInterval(interval)\n}\n\nexport function resolveScheduleIntervalMs(interval: string): number {\n return Math.max(parseInterval(interval), MIN_SCHEDULE_INTERVAL_MS)\n}\n\n/**\n * Calculate next run time based on interval\n */\nexport function calculateNextRunFromInterval(\n interval: string,\n fromDate: Date = new Date()\n): Date {\n const ms = resolveScheduleIntervalMs(interval)\n return new Date(fromDate.getTime() + ms)\n}\n\n/**\n * Validate an interval string\n */\nexport function validateInterval(interval: string): boolean {\n return isValidScheduleInterval(interval)\n}\n\n/**\n * Convert interval to human-readable string\n * Preserves the original unit unless it can be cleanly converted to a larger unit\n * Special case: Zero always displays as \"0 seconds\"\n */\nexport function intervalToHuman(interval: string): string {\n try {\n const parts = matchScheduleInterval(interval)\n\n if (!parts) {\n return interval\n }\n\n const unit = parts.unit\n\n const ms = parseInterval(interval)\n const seconds = ms / 1000\n \n // Special case: zero always displays as \"0 seconds\"\n if (seconds === 0) {\n return '0 seconds'\n }\n \n const minutes = seconds / 60\n const hours = minutes / 60\n const days = hours / 24\n \n // Only convert to larger units based on the input unit\n // This preserves the original unit choice (e.g., 60s stays as \"60 seconds\")\n switch (unit) {\n case 's':\n // Seconds never convert up (preserve user's choice)\n return `${seconds} second${seconds !== 1 ? 's' : ''}`\n \n case 'm':\n // Minutes can convert to hours or days\n if (days >= 1 && days === Math.floor(days)) {\n return `${days} day${days !== 1 ? 's' : ''}`\n }\n if (hours >= 1 && hours === Math.floor(hours)) {\n return `${hours} hour${hours !== 1 ? 's' : ''}`\n }\n return `${minutes} minute${minutes !== 1 ? 's' : ''}`\n \n case 'h':\n // Hours can convert to days\n if (days >= 1 && days === Math.floor(days)) {\n return `${days} day${days !== 1 ? 's' : ''}`\n }\n return `${hours} hour${hours !== 1 ? 's' : ''}`\n \n case 'd':\n // Days stay as days\n return `${days} day${days !== 1 ? 's' : ''}`\n \n default:\n return interval\n }\n } catch {\n return interval\n }\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOA,SAAS,cAAc,UAA0B;AACtD,SAAO,sBAAsB,QAAQ;AACvC;AAEO,SAAS,0BAA0B,UAA0B;AAClE,SAAO,KAAK,IAAI,cAAc,QAAQ,GAAG,wBAAwB;AACnE;AAKO,SAAS,6BACd,UACA,WAAiB,oBAAI,KAAK,GACpB;AACN,QAAM,KAAK,0BAA0B,QAAQ;AAC7C,SAAO,IAAI,KAAK,SAAS,QAAQ,IAAI,EAAE;AACzC;AAKO,SAAS,iBAAiB,UAA2B;AAC1D,SAAO,wBAAwB,QAAQ;AACzC;AAOO,SAAS,gBAAgB,UAA0B;AACxD,MAAI;AACF,UAAM,QAAQ,sBAAsB,QAAQ;AAE5C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM;AAEnB,UAAM,KAAK,cAAc,QAAQ;AACjC,UAAM,UAAU,KAAK;AAGrB,QAAI,YAAY,GAAG;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,UAAU;AAC1B,UAAM,QAAQ,UAAU;AACxB,UAAM,OAAO,QAAQ;AAIrB,YAAQ,MAAM;AAAA,MACZ,KAAK;AAEH,eAAO,GAAG,OAAO,UAAU,YAAY,IAAI,MAAM,EAAE;AAAA,MAErD,KAAK;AAEH,YAAI,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI,GAAG;AAC1C,iBAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,QAC5C;AACA,YAAI,SAAS,KAAK,UAAU,KAAK,MAAM,KAAK,GAAG;AAC7C,iBAAO,GAAG,KAAK,QAAQ,UAAU,IAAI,MAAM,EAAE;AAAA,QAC/C;AACA,eAAO,GAAG,OAAO,UAAU,YAAY,IAAI,MAAM,EAAE;AAAA,MAErD,KAAK;AAEH,YAAI,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI,GAAG;AAC1C,iBAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,QAC5C;AACA,eAAO,GAAG,KAAK,QAAQ,UAAU,IAAI,MAAM,EAAE;AAAA,MAE/C,KAAK;AAEH,eAAO,GAAG,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,MAE5C;AACE,eAAO;AAAA,IACX;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { ScheduledJob } from "../data/entities.js";
2
2
  import { calculateNextRunForWrite } from "../lib/nextRunCalculator.js";
3
3
  import { enforceTenantActiveScheduleLimit } from "../lib/activeScheduleLimits.js";
4
4
  import { createLogger } from "@open-mercato/shared/lib/logger";
5
+ import { createInvalidScheduleValueError } from "@open-mercato/shared/lib/schedule/invalidScheduleValue";
5
6
  const logger = createLogger("scheduler");
6
7
  class SchedulerService {
7
8
  constructor(em, bullmqService) {
@@ -21,7 +22,11 @@ class SchedulerService {
21
22
  registration.timezone || "UTC"
22
23
  );
23
24
  if (!nextRunAt) {
24
- throw new Error(`Failed to calculate next run time for schedule: ${registration.id}`);
25
+ throw createInvalidScheduleValueError(
26
+ registration.scheduleType,
27
+ registration.scheduleValue,
28
+ `Failed to calculate next run time for schedule: ${registration.id}`
29
+ );
25
30
  }
26
31
  let schedule = await em.findOne(ScheduledJob, { id: registration.id });
27
32
  const nextTenantId = registration.tenantId || null;
@@ -131,7 +136,11 @@ class SchedulerService {
131
136
  changes.timezone ?? schedule.timezone
132
137
  ) : null;
133
138
  if (scheduleChanged && !nextRunAt) {
134
- throw new Error(`Failed to calculate next run time for schedule: ${scheduleId}`);
139
+ throw createInvalidScheduleValueError(
140
+ changes.scheduleType ?? schedule.scheduleType,
141
+ changes.scheduleValue ?? schedule.scheduleValue,
142
+ `Failed to calculate next run time for schedule: ${scheduleId}`
143
+ );
135
144
  }
136
145
  if (changes.name !== void 0) schedule.name = changes.name;
137
146
  if (changes.description !== void 0) schedule.description = changes.description || null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/scheduler/services/schedulerService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { ScheduledJob } from '../data/entities.js'\nimport { calculateNextRunForWrite } from '../lib/nextRunCalculator.js'\nimport { enforceTenantActiveScheduleLimit } from '../lib/activeScheduleLimits.js'\nimport type { BullMQSchedulerService } from './bullmqSchedulerService.js'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler')\n\nexport interface ScheduleRegistration {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: unknown\n requireFeature?: string\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n}\n\nexport class SchedulerService {\n constructor(\n private em: () => EntityManager,\n private bullmqService?: BullMQSchedulerService,\n ) {}\n\n /**\n * Register a new schedule (upsert)\n */\n async register(registration: ScheduleRegistration): Promise<void> {\n const em = this.em().fork()\n \n // Validate scope consistency\n this.validateScope(registration)\n \n // Validate target consistency\n this.validateTarget(registration)\n \n // Calculate next run time\n const nextRunAt = calculateNextRunForWrite(\n registration.scheduleType,\n registration.scheduleValue,\n registration.timezone || 'UTC'\n )\n \n if (!nextRunAt) {\n throw new Error(`Failed to calculate next run time for schedule: ${registration.id}`)\n }\n \n // Check if schedule already exists\n let schedule = await em.findOne(ScheduledJob, { id: registration.id })\n const nextTenantId = registration.tenantId || null\n const nextIsEnabled = registration.isEnabled !== undefined ? registration.isEnabled : (schedule?.isEnabled ?? true)\n const tenantChanged = schedule ? (schedule.tenantId || null) !== nextTenantId : false\n if (nextIsEnabled && (schedule?.isEnabled !== true || tenantChanged)) {\n await enforceTenantActiveScheduleLimit(em, nextTenantId)\n }\n \n if (schedule) {\n // Update existing\n schedule.name = registration.name\n schedule.description = registration.description || null\n schedule.scopeType = registration.scopeType\n schedule.organizationId = registration.organizationId || null\n schedule.tenantId = registration.tenantId || null\n schedule.scheduleType = registration.scheduleType\n schedule.scheduleValue = registration.scheduleValue\n schedule.timezone = registration.timezone || 'UTC'\n schedule.targetType = registration.targetType\n schedule.targetQueue = registration.targetQueue || null\n schedule.targetCommand = registration.targetCommand || null\n schedule.targetPayload = registration.targetPayload as Record<string, unknown> || null\n schedule.requireFeature = registration.requireFeature || null\n schedule.sourceType = registration.sourceType || 'user'\n schedule.sourceModule = registration.sourceModule || null\n schedule.isEnabled = registration.isEnabled !== undefined ? registration.isEnabled : schedule.isEnabled\n schedule.nextRunAt = nextRunAt\n schedule.updatedAt = new Date()\n } else {\n // Create new\n schedule = em.create(ScheduledJob, {\n id: registration.id,\n name: registration.name,\n description: registration.description || null,\n scopeType: registration.scopeType,\n organizationId: registration.organizationId || null,\n tenantId: registration.tenantId || null,\n scheduleType: registration.scheduleType,\n scheduleValue: registration.scheduleValue,\n timezone: registration.timezone || 'UTC',\n targetType: registration.targetType,\n targetQueue: registration.targetQueue || null,\n targetCommand: registration.targetCommand || null,\n targetPayload: registration.targetPayload as Record<string, unknown> || null,\n requireFeature: registration.requireFeature || null,\n sourceType: registration.sourceType || 'module',\n sourceModule: registration.sourceModule || null,\n isEnabled: registration.isEnabled !== undefined ? registration.isEnabled : true,\n nextRunAt,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(schedule)\n }\n \n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService && schedule) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(schedule.id)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync with BullMQ', { scheduleId: schedule.id, err: error })\n // Don't throw - DB is source of truth, BullMQ sync is best-effort\n }\n }\n }\n\n /**\n * Unregister a schedule\n */\n async unregister(scheduleId: string): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (schedule) {\n await em.remove(schedule).flush()\n \n // Unregister from BullMQ if available\n if (this.bullmqService) {\n try {\n await this.bullmqService.unregister(scheduleId)\n } catch (error: unknown) {\n logger.error('Failed to unregister from BullMQ', { scheduleId, err: error })\n }\n }\n }\n }\n\n /**\n * Check if a schedule exists\n */\n async exists(scheduleId: string): Promise<boolean> {\n const em = this.em()\n const count = await em.count(ScheduledJob, { id: scheduleId })\n return count > 0\n }\n\n /**\n * Update an existing schedule\n */\n async update(\n scheduleId: string,\n changes: Partial<Omit<ScheduleRegistration, 'id'>>\n ): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (!schedule) {\n throw new Error(`Schedule not found: ${scheduleId}`)\n }\n\n if (changes.isEnabled === true && schedule.isEnabled !== true) {\n await enforceTenantActiveScheduleLimit(em, schedule.tenantId)\n }\n\n const scheduleChanged = changes.scheduleType !== undefined || changes.scheduleValue !== undefined || changes.timezone !== undefined\n const nextRunAt = scheduleChanged\n ? calculateNextRunForWrite(\n changes.scheduleType ?? schedule.scheduleType,\n changes.scheduleValue ?? schedule.scheduleValue,\n changes.timezone ?? schedule.timezone,\n )\n : null\n if (scheduleChanged && !nextRunAt) {\n throw new Error(`Failed to calculate next run time for schedule: ${scheduleId}`)\n }\n \n // Apply changes\n if (changes.name !== undefined) schedule.name = changes.name\n if (changes.description !== undefined) schedule.description = changes.description || null\n if (changes.scheduleType !== undefined) schedule.scheduleType = changes.scheduleType\n if (changes.scheduleValue !== undefined) schedule.scheduleValue = changes.scheduleValue\n if (changes.timezone !== undefined) schedule.timezone = changes.timezone\n if (changes.targetPayload !== undefined) {\n schedule.targetPayload = changes.targetPayload as Record<string, unknown> || null\n }\n if (changes.requireFeature !== undefined) schedule.requireFeature = changes.requireFeature || null\n if (changes.isEnabled !== undefined) schedule.isEnabled = changes.isEnabled\n \n // Handle target type changes - clear stale values when switching between queue and command\n if (changes.targetType !== undefined) {\n schedule.targetType = changes.targetType\n \n if (changes.targetType === 'queue') {\n // Switching to queue: set new queue and clear command\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n schedule.targetCommand = null\n } else if (changes.targetType === 'command') {\n // Switching to command: set new command and clear queue\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n schedule.targetQueue = null\n }\n } else {\n // targetType not changing, but allow updating individual target fields\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n }\n \n if (nextRunAt) {\n schedule.nextRunAt = nextRunAt\n }\n \n schedule.updatedAt = new Date()\n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(scheduleId)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync update with BullMQ', { scheduleId, err: error })\n }\n }\n }\n\n /**\n * Find schedules by module\n */\n async findByModule(moduleId: string, limit = 100): Promise<ScheduledJob[]> {\n const em = this.em()\n return em.find(ScheduledJob, { sourceModule: moduleId, deletedAt: null }, { limit })\n }\n\n /**\n * Enable a schedule\n */\n async enable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: true })\n }\n\n /**\n * Disable a schedule\n */\n async disable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: false })\n }\n\n private validateScope(registration: ScheduleRegistration): void {\n const { scopeType, organizationId, tenantId } = registration\n \n if (scopeType === 'system') {\n if (organizationId || tenantId) {\n throw new Error('System-scoped schedules cannot have organizationId or tenantId')\n }\n } else if (scopeType === 'organization') {\n if (!organizationId || !tenantId) {\n throw new Error('Organization-scoped schedules must have both organizationId and tenantId')\n }\n } else if (scopeType === 'tenant') {\n if (organizationId || !tenantId) {\n throw new Error('Tenant-scoped schedules must have tenantId and no organizationId')\n }\n }\n }\n\n private validateTarget(registration: ScheduleRegistration): void {\n const { targetType, targetQueue, targetCommand } = registration\n \n if (targetType === 'queue') {\n if (!targetQueue) {\n throw new Error('Queue target must have targetQueue')\n }\n } else if (targetType === 'command') {\n if (!targetCommand) {\n throw new Error('Command target must have targetCommand')\n }\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { ScheduledJob } from '../data/entities.js'\nimport { calculateNextRunForWrite } from '../lib/nextRunCalculator.js'\nimport { enforceTenantActiveScheduleLimit } from '../lib/activeScheduleLimits.js'\nimport type { BullMQSchedulerService } from './bullmqSchedulerService.js'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { createInvalidScheduleValueError } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'\n\nconst logger = createLogger('scheduler')\n\nexport interface ScheduleRegistration {\n id: string\n name: string\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId?: string\n tenantId?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: unknown\n requireFeature?: string\n sourceType?: 'user' | 'module'\n sourceModule?: string\n isEnabled?: boolean\n description?: string\n}\n\nexport class SchedulerService {\n constructor(\n private em: () => EntityManager,\n private bullmqService?: BullMQSchedulerService,\n ) {}\n\n /**\n * Register a new schedule (upsert)\n */\n async register(registration: ScheduleRegistration): Promise<void> {\n const em = this.em().fork()\n \n // Validate scope consistency\n this.validateScope(registration)\n \n // Validate target consistency\n this.validateTarget(registration)\n \n // Calculate next run time\n const nextRunAt = calculateNextRunForWrite(\n registration.scheduleType,\n registration.scheduleValue,\n registration.timezone || 'UTC'\n )\n \n if (!nextRunAt) {\n throw createInvalidScheduleValueError(\n registration.scheduleType,\n registration.scheduleValue,\n `Failed to calculate next run time for schedule: ${registration.id}`,\n )\n }\n \n // Check if schedule already exists\n let schedule = await em.findOne(ScheduledJob, { id: registration.id })\n const nextTenantId = registration.tenantId || null\n const nextIsEnabled = registration.isEnabled !== undefined ? registration.isEnabled : (schedule?.isEnabled ?? true)\n const tenantChanged = schedule ? (schedule.tenantId || null) !== nextTenantId : false\n if (nextIsEnabled && (schedule?.isEnabled !== true || tenantChanged)) {\n await enforceTenantActiveScheduleLimit(em, nextTenantId)\n }\n \n if (schedule) {\n // Update existing\n schedule.name = registration.name\n schedule.description = registration.description || null\n schedule.scopeType = registration.scopeType\n schedule.organizationId = registration.organizationId || null\n schedule.tenantId = registration.tenantId || null\n schedule.scheduleType = registration.scheduleType\n schedule.scheduleValue = registration.scheduleValue\n schedule.timezone = registration.timezone || 'UTC'\n schedule.targetType = registration.targetType\n schedule.targetQueue = registration.targetQueue || null\n schedule.targetCommand = registration.targetCommand || null\n schedule.targetPayload = registration.targetPayload as Record<string, unknown> || null\n schedule.requireFeature = registration.requireFeature || null\n schedule.sourceType = registration.sourceType || 'user'\n schedule.sourceModule = registration.sourceModule || null\n schedule.isEnabled = registration.isEnabled !== undefined ? registration.isEnabled : schedule.isEnabled\n schedule.nextRunAt = nextRunAt\n schedule.updatedAt = new Date()\n } else {\n // Create new\n schedule = em.create(ScheduledJob, {\n id: registration.id,\n name: registration.name,\n description: registration.description || null,\n scopeType: registration.scopeType,\n organizationId: registration.organizationId || null,\n tenantId: registration.tenantId || null,\n scheduleType: registration.scheduleType,\n scheduleValue: registration.scheduleValue,\n timezone: registration.timezone || 'UTC',\n targetType: registration.targetType,\n targetQueue: registration.targetQueue || null,\n targetCommand: registration.targetCommand || null,\n targetPayload: registration.targetPayload as Record<string, unknown> || null,\n requireFeature: registration.requireFeature || null,\n sourceType: registration.sourceType || 'module',\n sourceModule: registration.sourceModule || null,\n isEnabled: registration.isEnabled !== undefined ? registration.isEnabled : true,\n nextRunAt,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(schedule)\n }\n \n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService && schedule) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(schedule.id)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync with BullMQ', { scheduleId: schedule.id, err: error })\n // Don't throw - DB is source of truth, BullMQ sync is best-effort\n }\n }\n }\n\n /**\n * Unregister a schedule\n */\n async unregister(scheduleId: string): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (schedule) {\n await em.remove(schedule).flush()\n \n // Unregister from BullMQ if available\n if (this.bullmqService) {\n try {\n await this.bullmqService.unregister(scheduleId)\n } catch (error: unknown) {\n logger.error('Failed to unregister from BullMQ', { scheduleId, err: error })\n }\n }\n }\n }\n\n /**\n * Check if a schedule exists\n */\n async exists(scheduleId: string): Promise<boolean> {\n const em = this.em()\n const count = await em.count(ScheduledJob, { id: scheduleId })\n return count > 0\n }\n\n /**\n * Update an existing schedule\n */\n async update(\n scheduleId: string,\n changes: Partial<Omit<ScheduleRegistration, 'id'>>\n ): Promise<void> {\n const em = this.em().fork()\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId })\n \n if (!schedule) {\n throw new Error(`Schedule not found: ${scheduleId}`)\n }\n\n if (changes.isEnabled === true && schedule.isEnabled !== true) {\n await enforceTenantActiveScheduleLimit(em, schedule.tenantId)\n }\n\n const scheduleChanged = changes.scheduleType !== undefined || changes.scheduleValue !== undefined || changes.timezone !== undefined\n const nextRunAt = scheduleChanged\n ? calculateNextRunForWrite(\n changes.scheduleType ?? schedule.scheduleType,\n changes.scheduleValue ?? schedule.scheduleValue,\n changes.timezone ?? schedule.timezone,\n )\n : null\n if (scheduleChanged && !nextRunAt) {\n throw createInvalidScheduleValueError(\n changes.scheduleType ?? schedule.scheduleType,\n changes.scheduleValue ?? schedule.scheduleValue,\n `Failed to calculate next run time for schedule: ${scheduleId}`,\n )\n }\n \n // Apply changes\n if (changes.name !== undefined) schedule.name = changes.name\n if (changes.description !== undefined) schedule.description = changes.description || null\n if (changes.scheduleType !== undefined) schedule.scheduleType = changes.scheduleType\n if (changes.scheduleValue !== undefined) schedule.scheduleValue = changes.scheduleValue\n if (changes.timezone !== undefined) schedule.timezone = changes.timezone\n if (changes.targetPayload !== undefined) {\n schedule.targetPayload = changes.targetPayload as Record<string, unknown> || null\n }\n if (changes.requireFeature !== undefined) schedule.requireFeature = changes.requireFeature || null\n if (changes.isEnabled !== undefined) schedule.isEnabled = changes.isEnabled\n \n // Handle target type changes - clear stale values when switching between queue and command\n if (changes.targetType !== undefined) {\n schedule.targetType = changes.targetType\n \n if (changes.targetType === 'queue') {\n // Switching to queue: set new queue and clear command\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n schedule.targetCommand = null\n } else if (changes.targetType === 'command') {\n // Switching to command: set new command and clear queue\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n schedule.targetQueue = null\n }\n } else {\n // targetType not changing, but allow updating individual target fields\n if (changes.targetQueue !== undefined) schedule.targetQueue = changes.targetQueue || null\n if (changes.targetCommand !== undefined) schedule.targetCommand = changes.targetCommand || null\n }\n \n if (nextRunAt) {\n schedule.nextRunAt = nextRunAt\n }\n \n schedule.updatedAt = new Date()\n await em.flush()\n\n // Sync with BullMQ if available\n if (this.bullmqService) {\n try {\n if (schedule.isEnabled) {\n await this.bullmqService.register(schedule)\n } else {\n await this.bullmqService.unregister(scheduleId)\n }\n } catch (error: unknown) {\n logger.error('Failed to sync update with BullMQ', { scheduleId, err: error })\n }\n }\n }\n\n /**\n * Find schedules by module\n */\n async findByModule(moduleId: string, limit = 100): Promise<ScheduledJob[]> {\n const em = this.em()\n return em.find(ScheduledJob, { sourceModule: moduleId, deletedAt: null }, { limit })\n }\n\n /**\n * Enable a schedule\n */\n async enable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: true })\n }\n\n /**\n * Disable a schedule\n */\n async disable(scheduleId: string): Promise<void> {\n await this.update(scheduleId, { isEnabled: false })\n }\n\n private validateScope(registration: ScheduleRegistration): void {\n const { scopeType, organizationId, tenantId } = registration\n \n if (scopeType === 'system') {\n if (organizationId || tenantId) {\n throw new Error('System-scoped schedules cannot have organizationId or tenantId')\n }\n } else if (scopeType === 'organization') {\n if (!organizationId || !tenantId) {\n throw new Error('Organization-scoped schedules must have both organizationId and tenantId')\n }\n } else if (scopeType === 'tenant') {\n if (organizationId || !tenantId) {\n throw new Error('Tenant-scoped schedules must have tenantId and no organizationId')\n }\n }\n }\n\n private validateTarget(registration: ScheduleRegistration): void {\n const { targetType, targetQueue, targetCommand } = registration\n \n if (targetType === 'queue') {\n if (!targetQueue) {\n throw new Error('Queue target must have targetQueue')\n }\n } else if (targetType === 'command') {\n if (!targetCommand) {\n throw new Error('Command target must have targetCommand')\n }\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/scheduler",
3
- "version": "0.8.1-develop.7275.1.f772b944d4",
3
+ "version": "0.8.1-develop.7294.1.0ef99fc518",
4
4
  "license": "MIT",
5
5
  "description": "Database-managed scheduled jobs with admin UI",
6
6
  "type": "module",
@@ -59,13 +59,13 @@
59
59
  "./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json"
60
60
  },
61
61
  "dependencies": {
62
- "@open-mercato/events": "0.8.1-develop.7275.1.f772b944d4",
63
- "@open-mercato/queue": "0.8.1-develop.7275.1.f772b944d4",
62
+ "@open-mercato/events": "0.8.1-develop.7294.1.0ef99fc518",
63
+ "@open-mercato/queue": "0.8.1-develop.7294.1.0ef99fc518",
64
64
  "cron-parser": "^5.6.2"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@mikro-orm/core": "^7.0.14",
68
- "@open-mercato/shared": "0.8.1-develop.7275.1.f772b944d4",
68
+ "@open-mercato/shared": "0.8.1-develop.7294.1.0ef99fc518",
69
69
  "bullmq": "^5.0.0 || ^6.0.0",
70
70
  "ioredis": "^5.0.0 || ^6.0.0",
71
71
  "zod": ">=3.23.0"
@@ -79,7 +79,7 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@open-mercato/shared": "0.8.1-develop.7275.1.f772b944d4",
82
+ "@open-mercato/shared": "0.8.1-develop.7294.1.0ef99fc518",
83
83
  "@types/jest": "^30.0.0",
84
84
  "@types/node": "^26.4.0",
85
85
  "jest": "^30.4.2",
@@ -1,27 +1,17 @@
1
+ import {
2
+ MIN_SCHEDULE_INTERVAL_MS,
3
+ isValidScheduleInterval,
4
+ matchScheduleInterval,
5
+ parseScheduleInterval,
6
+ } from '@open-mercato/shared/lib/schedule/interval'
7
+
1
8
  /**
2
9
  * Parse simple interval strings like '15m', '2h', '1d' into milliseconds
3
10
  */
4
- export const MIN_SCHEDULE_INTERVAL_MS = 60 * 1000
11
+ export { MIN_SCHEDULE_INTERVAL_MS }
5
12
 
6
13
  export function parseInterval(interval: string): number {
7
- const regex = /^(\d+)(s|m|h|d)$/
8
- const match = interval.match(regex)
9
-
10
- if (!match) {
11
- throw new Error(`Invalid interval format: ${interval}. Expected format: <number><unit> (e.g., 15m, 2h, 1d)`)
12
- }
13
-
14
- const value = parseInt(match[1], 10)
15
- const unit = match[2]
16
-
17
- const multipliers: Record<string, number> = {
18
- s: 1000, // seconds
19
- m: 60 * 1000, // minutes
20
- h: 60 * 60 * 1000, // hours
21
- d: 24 * 60 * 60 * 1000, // days
22
- }
23
-
24
- return value * multipliers[unit]
14
+ return parseScheduleInterval(interval)
25
15
  }
26
16
 
27
17
  export function resolveScheduleIntervalMs(interval: string): number {
@@ -43,11 +33,7 @@ export function calculateNextRunFromInterval(
43
33
  * Validate an interval string
44
34
  */
45
35
  export function validateInterval(interval: string): boolean {
46
- try {
47
- return parseInterval(interval) >= MIN_SCHEDULE_INTERVAL_MS
48
- } catch {
49
- return false
50
- }
36
+ return isValidScheduleInterval(interval)
51
37
  }
52
38
 
53
39
  /**
@@ -57,15 +43,14 @@ export function validateInterval(interval: string): boolean {
57
43
  */
58
44
  export function intervalToHuman(interval: string): string {
59
45
  try {
60
- const regex = /^(\d+)(s|m|h|d)$/
61
- const match = interval.match(regex)
62
-
63
- if (!match) {
46
+ const parts = matchScheduleInterval(interval)
47
+
48
+ if (!parts) {
64
49
  return interval
65
50
  }
66
-
67
- const unit = match[2]
68
-
51
+
52
+ const unit = parts.unit
53
+
69
54
  const ms = parseInterval(interval)
70
55
  const seconds = ms / 1000
71
56
 
@@ -4,6 +4,7 @@ import { calculateNextRunForWrite } from '../lib/nextRunCalculator.js'
4
4
  import { enforceTenantActiveScheduleLimit } from '../lib/activeScheduleLimits.js'
5
5
  import type { BullMQSchedulerService } from './bullmqSchedulerService.js'
6
6
  import { createLogger } from '@open-mercato/shared/lib/logger'
7
+ import { createInvalidScheduleValueError } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'
7
8
 
8
9
  const logger = createLogger('scheduler')
9
10
 
@@ -53,7 +54,11 @@ export class SchedulerService {
53
54
  )
54
55
 
55
56
  if (!nextRunAt) {
56
- throw new Error(`Failed to calculate next run time for schedule: ${registration.id}`)
57
+ throw createInvalidScheduleValueError(
58
+ registration.scheduleType,
59
+ registration.scheduleValue,
60
+ `Failed to calculate next run time for schedule: ${registration.id}`,
61
+ )
57
62
  }
58
63
 
59
64
  // Check if schedule already exists
@@ -186,7 +191,11 @@ export class SchedulerService {
186
191
  )
187
192
  : null
188
193
  if (scheduleChanged && !nextRunAt) {
189
- throw new Error(`Failed to calculate next run time for schedule: ${scheduleId}`)
194
+ throw createInvalidScheduleValueError(
195
+ changes.scheduleType ?? schedule.scheduleType,
196
+ changes.scheduleValue ?? schedule.scheduleValue,
197
+ `Failed to calculate next run time for schedule: ${scheduleId}`,
198
+ )
190
199
  }
191
200
 
192
201
  // Apply changes