@dawntech/dispatcher 0.0.1 → 0.2.2
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/README.md +321 -2
- package/dist/{chunk-6IAFH374.mjs → chunk-4LYB64T2.mjs} +1 -1
- package/dist/{chunk-6IAFH374.mjs.map → chunk-4LYB64T2.mjs.map} +1 -1
- package/dist/chunk-B7IDVU6T.mjs +2 -0
- package/dist/chunk-B7IDVU6T.mjs.map +1 -0
- package/dist/{chunk-XLRB2LAD.mjs → chunk-CVPGUSFU.mjs} +2 -2
- package/dist/chunk-JD53PFR4.mjs +2 -0
- package/dist/chunk-JD53PFR4.mjs.map +1 -0
- package/dist/chunk-LKT4N6L5.mjs +2 -0
- package/dist/chunk-LKT4N6L5.mjs.map +1 -0
- package/dist/{chunk-YDWZJWCU.mjs → chunk-VTJMLZE3.mjs} +2 -2
- package/dist/core/Blip.js +1 -1
- package/dist/core/Blip.mjs +1 -1
- package/dist/core/Dispatcher.js +1 -1
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Dispatcher.mjs +1 -1
- package/dist/core/DispatcherDescriptor.mjs +1 -1
- package/dist/core/DispatcherMonitor.js.map +1 -1
- package/dist/core/DispatcherMonitor.mjs +1 -1
- package/dist/core/DispatcherQuery.js +1 -1
- package/dist/core/DispatcherQuery.js.map +1 -1
- package/dist/core/DispatcherQuery.mjs +1 -1
- package/dist/core/DispatcherRepository.d.mts +4 -1
- package/dist/core/DispatcherRepository.js +1 -1
- package/dist/core/DispatcherRepository.js.map +1 -1
- package/dist/core/DispatcherRepository.mjs +1 -1
- package/dist/core/DispatcherStateMachine.js +1 -1
- package/dist/core/DispatcherStateMachine.mjs +1 -1
- package/dist/errors/index.mjs +1 -1
- package/dist/errors/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1 -1
- package/dist/server.mjs.map +1 -1
- package/dist/types/blip.mjs +1 -1
- package/dist/types/index.d.mts +6 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/index.mjs +1 -1
- package/dist/utils/logger.mjs +1 -1
- package/package.json +36 -37
- package/.prettierignore +0 -4
- package/.prettierrc +0 -7
- package/dist/chunk-OR34YAY3.mjs +0 -2
- package/dist/chunk-OR34YAY3.mjs.map +0 -1
- package/dist/chunk-SLZLYBPB.mjs +0 -2
- package/dist/chunk-SLZLYBPB.mjs.map +0 -1
- package/jest.config.js +0 -48
- package/tsconfig.json +0 -22
- package/tsconfig.test.json +0 -10
- package/tsup.config.js +0 -10
- /package/dist/{chunk-XLRB2LAD.mjs.map → chunk-CVPGUSFU.mjs.map} +0 -0
- /package/dist/{chunk-YDWZJWCU.mjs.map → chunk-VTJMLZE3.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/DispatcherMonitor.ts","../../src/types/index.ts","../../src/utils/logger.ts"],"sourcesContent":["import { EventEmitter } from 'events';\nimport { Queue, Worker, Job } from 'bullmq';\nimport { DispatcherRepository } from './DispatcherRepository.js';\nimport { DispatcherMetrics, MessageState, MessageStatus } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\n\nconst logger = getLogger('DispatcherMonitor');\n\nexport interface MonitorRule {\n type: 'failure_rate' | 'queue_size';\n threshold: number;\n window?: number; // Time window in ms for rate calculation (only for failure_rate)\n debounce?: number; // Time to wait before re-alerting in ms\n}\n\nexport interface MonitorOptions {\n interval?: number; // Polling interval in ms (default: 60s)\n rules: MonitorRule[];\n historySize?: number; // Max snapshots to keep (default: 1000)\n}\n\ninterface MetricSnapshot {\n timestamp: number;\n metrics: DispatcherMetrics;\n}\n\nexport type MonitorAlert = {\n type: string;\n message: string;\n level: 'warning' | 'critical';\n details: any;\n timestamp: string;\n};\n\nexport class DispatcherMonitor extends EventEmitter {\n private id: string;\n private repository: DispatcherRepository;\n private options: MonitorOptions;\n\n // BullMQ for scheduling\n private queue: Queue;\n private worker: Worker;\n private queueName: string;\n\n private history: MetricSnapshot[] = [];\n private lastAlerts: Record<string, number> = {};\n private activeAlerts: Set<string> = new Set();\n private isRunning: boolean = false;\n\n constructor(id: string, repository: DispatcherRepository, options: MonitorOptions) {\n super();\n this.id = id;\n this.repository = repository;\n this.options = {\n interval: 60000,\n historySize: 1000,\n ...options,\n };\n\n this.queueName = `monitor-${this.id}`;\n\n // Reuse dispatcher's Redis connection for BullMQ\n // Note: BullMQ will manage duplication for blocking connections if needed\n this.queue = new Queue(this.queueName, {\n // @ts-ignore\n connection: repository.redis,\n defaultJobOptions: {\n removeOnComplete: true,\n removeOnFail: true,\n },\n });\n\n this.worker = new Worker(\n this.queueName,\n async (job: Job) => {\n if (job.name === 'check') {\n await this.check();\n }\n },\n {\n // @ts-ignore\n connection: repository.redis,\n }\n );\n\n this.worker.on('error', (err) => logger.error('[MonitorWorker] Error', err));\n this.worker.on('failed', (job, err) =>\n logger.error(`[MonitorWorker] Job ${job?.id} failed`, err)\n );\n }\n\n async start(): Promise<void> {\n if (this.isRunning) return;\n\n logger.info('[Monitor] Started');\n\n // Clean up any old repeatable jobs\n await this.queue.obliterate({ force: true });\n\n // Add repeatable job or just simple delayed recursion?\n // Using repeatable job is robust.\n await this.queue.add(\n 'check',\n {},\n {\n repeat: {\n every: this.options.interval,\n immediately: true, // Run on start\n },\n }\n );\n\n this.isRunning = true;\n }\n\n async stop(): Promise<void> {\n this.isRunning = false;\n await this.queue.close();\n await this.worker.close();\n logger.info('[Monitor] Stopped');\n }\n\n /**\n * Independent metrics collection directly from Repository\n */\n private async collectMetrics(): Promise<DispatcherMetrics> {\n const repository = this.repository;\n\n const metrics: DispatcherMetrics = {\n total: 0,\n byState: {},\n byStatus: {},\n cumulative: {\n dispatched: 0,\n delivered: 0,\n failed: 0,\n },\n };\n\n // Parallelize fetch? For simplicity/consistency, sequential is safer for now but parallel is faster.\n // Let's optimize slightly with Promise.all for groups.\n\n const states = Object.values(MessageState);\n const statePromises = states.map(async (state) => {\n metrics.byState[state] = await repository.countMessages({ state });\n });\n\n const statuses = Object.values(MessageStatus);\n const statusPromises = statuses.map(async (status) => {\n metrics.byStatus[status] = await repository.countMessages({ status });\n });\n\n const cumulativeKeys = ['dispatched', 'delivered', 'failed'] as const;\n const cumulativePromises = cumulativeKeys.map(async (key) => {\n metrics.cumulative![key] = await repository.getMetric(key);\n });\n\n await Promise.all([...statePromises, ...statusPromises, ...cumulativePromises]);\n\n metrics.total = Object.values(metrics.byState).reduce((a, b) => a + (b || 0), 0);\n\n return metrics;\n }\n\n private async check(): Promise<void> {\n try {\n const metrics = await this.collectMetrics();\n const now = Date.now();\n\n // Record snapshot\n this.history.push({ timestamp: now, metrics });\n this.cleanHistory();\n\n // Check rules\n for (const rule of this.options.rules) {\n await this.evaluateRule(rule, metrics, now);\n }\n } catch (error) {\n logger.error('[Monitor] Error during check', error);\n }\n }\n\n private cleanHistory(): void {\n const maxSize = this.options.historySize!;\n if (this.history.length > maxSize) {\n this.history = this.history.slice(this.history.length - maxSize);\n }\n\n // Also remove snapshots older than the largest window needed\n const maxWindow = Math.max(...this.options.rules.map((r) => r.window || 0));\n const cutoff = Date.now() - maxWindow - 60000; // Buffer\n if (this.history.length > 0 && this.history[0].timestamp < cutoff) {\n const idx = this.history.findIndex((s) => s.timestamp >= cutoff);\n if (idx > 0) {\n this.history = this.history.slice(idx);\n }\n }\n }\n\n private async evaluateRule(\n rule: MonitorRule,\n currentMetrics: DispatcherMetrics,\n now: number\n ): Promise<void> {\n const alertKey = `${rule.type}`;\n let triggered = false;\n let value = 0;\n let details = {};\n\n switch (rule.type) {\n case 'queue_size':\n const finalCount = currentMetrics.byState.FINAL || 0;\n value = currentMetrics.total - finalCount;\n triggered = value > rule.threshold;\n details = { current: value, threshold: rule.threshold };\n break;\n\n case 'failure_rate':\n if (!rule.window) {\n logger.warn('[Monitor] failure_rate rule missing window');\n return;\n }\n\n const previousSnapshot = this.findSnapshotAt(now - rule.window);\n if (!previousSnapshot) {\n return;\n }\n\n const currFailed = currentMetrics.cumulative!.failed;\n const prevFailed = previousSnapshot.metrics.cumulative!.failed;\n const failedDelta = currFailed - prevFailed;\n\n const currDispatched = currentMetrics.cumulative!.dispatched;\n const prevDispatched = previousSnapshot.metrics.cumulative!.dispatched;\n const dispatchedDelta = currDispatched - prevDispatched;\n\n if (dispatchedDelta === 0) {\n value = 0;\n } else {\n value = failedDelta / dispatchedDelta;\n }\n\n triggered = value > rule.threshold;\n details = {\n rate: (value * 100).toFixed(2) + '%',\n threshold: (rule.threshold * 100).toFixed(2) + '%',\n failed: failedDelta,\n dispatched: dispatchedDelta,\n window: rule.window,\n };\n break;\n }\n\n if (triggered) {\n if (!this.activeAlerts.has(alertKey)) {\n this.emitAlert(alertKey, rule, value, details);\n this.activeAlerts.add(alertKey);\n } else {\n if (rule.debounce && !this.isDebounced(alertKey, rule.debounce)) {\n this.emitAlert(alertKey, rule, value, details);\n }\n }\n } else {\n if (this.activeAlerts.has(alertKey)) {\n this.resolveAlert(alertKey, rule);\n this.activeAlerts.delete(alertKey);\n }\n }\n }\n\n private isDebounced(key: string, debounceTime?: number): boolean {\n if (!debounceTime) return false;\n const last = this.lastAlerts[key];\n if (!last) return false;\n return Date.now() - last < debounceTime;\n }\n\n private emitAlert(key: string, rule: MonitorRule, value: number, details: any): void {\n logger.warn(`[Monitor] Alert triggered: ${rule.type}`, details);\n this.lastAlerts[key] = Date.now();\n\n const alert: MonitorAlert = {\n type: rule.type,\n message: `${rule.type} exceeded threshold`,\n level: 'warning',\n details,\n timestamp: new Date().toISOString(),\n };\n\n this.emit('alert', alert);\n }\n\n private resolveAlert(key: string, rule: MonitorRule): void {\n logger.info(`[Monitor] Alert resolved: ${rule.type}`);\n const alert: MonitorAlert = {\n type: rule.type,\n message: `${rule.type} resolved`,\n level: 'warning',\n details: {},\n timestamp: new Date().toISOString(),\n };\n this.emit('resolved', alert);\n }\n\n private findSnapshotAt(timestamp: number): MetricSnapshot | null {\n if (this.history.length === 0) return null;\n for (const snap of this.history) {\n if (snap.timestamp >= timestamp) {\n return snap;\n }\n }\n return this.history[0];\n }\n}\n","import { Vnd } from './blip';\n\nexport type Contact = Omit<Vnd.Iris.Contact, 'identity'>;\n\nexport type MessageData = {\n type: string;\n content: Record<string, any> | string;\n};\n\nexport type MessagePayload = Record<string, any> | Array<any> | string;\n\nexport enum MessageState {\n INIT = 'INIT',\n DISPATCHED = 'DISPATCHED',\n SCHEDULED = 'SCHEDULED',\n QUEUED = 'QUEUED',\n FINAL = 'FINAL',\n}\n\nexport enum MessageStatus {\n INIT = 'INIT',\n PENDING = 'PENDING',\n SENDING = 'SENDING',\n DELIVERED = 'DELIVERED',\n READ = 'READ',\n REPLIED = 'REPLIED',\n FAILED = 'FAILED',\n CANCELED = 'CANCELED',\n}\n\nexport enum DispatchState {\n ACCEPTED = 'accepted',\n DISPATCHED = 'dispatched',\n RECEIVED = 'received',\n CONSUMED = 'consumed',\n FAILED = 'failed',\n}\n\nexport interface Message {\n messageId: string;\n contactId: string;\n descriptorId: string;\n\n payload: MessageData;\n status: MessageStatus;\n state: MessageState;\n\n attempts?: number;\n retries?: number;\n\n options?: MessageOptions; // Persisted options for scheduled/retried messages\n\n expiresAt?: string; // ISO 8601 datetime\n\n scheduledTo?: string; // ISO 8601 datetime\n sentAt?: string; // ISO 8601 datetime\n acceptedAt?: string; // ISO 8601 datetime - When message was accepted by gateway (SENDING status)\n lastDispatchAttemptAt?: string; // ISO 8601 datetime - When last dispatch attempt started\n createdAt: string; // ISO 8601 datetime\n deliveredAt?: string; // ISO 8601 datetime\n readAt?: string; // ISO 8601 datetime\n repliedAt?: string; // ISO 8601 datetime\n failedAt?: string; // ISO 8601 datetime\n\n error?: string;\n}\n\nexport interface Shift {\n days: number; // Bitmask: use Weekdays enum (e.g., Weekdays.MONDAY | Weekdays.FRIDAY) or manual (31=Mon-Fri)\n start: string; // HH:mm format (e.g., \"09:00\")\n end: string; // HH:mm format (e.g., \"18:00\")\n gmt?: string; // Timezone offset (default: \"-3\" for BRT)\n}\n\nexport enum Weekdays {\n MONDAY = 1,\n TUESDAY = 2,\n WEDNESDAY = 4,\n THURSDAY = 8,\n FRIDAY = 16,\n SATURDAY = 32,\n SUNDAY = 64,\n}\n\nexport interface Intent {\n intent: string;\n dueDate?: string;\n event?: string;\n payload?: any | null;\n expired?: {\n event?: string;\n intent?: string;\n };\n}\n\nexport type CallbackEvent =\n | 'dispatch' // Before message send process begins\n | 'sending' // MessageStatus.SENDING - message accepted by gateway\n | 'delivered' // MessageStatus.DELIVERED - message successfully delivered\n | 'failed' // MessageStatus.FAILED - message failed permanently\n | 'read' // MessageStatus.READ - message read by recipient\n | 'replied' // MessageStatus.REPLIED - recipient replied\n | 'scheduled' // MessageState.SCHEDULED - message scheduled\n | 'retry' // Retry attempt scheduled\n | 'evicted' // Message evicted from state machine due to TTL\n | 'canceled'; // Message canceled before send\n\nexport type ConnectionConfig = {\n contract: string; // Contract ID (without https:// prefix)\n key: string; // Bot authorization key\n};\n\nexport interface MessageOptions {\n shifts?: Array<Shift>; // Business hours constraints\n contact?: Contact;\n state?: {\n // Bot state to set after send\n stateId: string;\n botId: string;\n botIds?: Array<string>; // Bot IDs to reset before setting state\n };\n intent?: string | Intent; // Intent to trigger after send\n finalStatus?: 'DELIVERED' | 'READ' | 'REPLIED'; // Default: 'DELIVERED'\n}\n\nexport interface DispatchMessageOptions extends MessageOptions {\n schedule?: string; // ISO 8601 datetime to schedule send\n}\n\nexport interface DispatcherOptions {\n maxRetries?: number; // Default: 0\n retryIntervals?: number[]; // Default: [1000, 5000, 15000] (exponential backoff in ms)\n\n // Storage Duration (Redis TTL)\n retention?: number; // Default: 3600000 ms (1 hour) - Time to keep message in storage after creation/dispatch\n\n // State Constraints (Force transition to FAILED)\n timeouts?: {\n pending?: number; // Default: 10000 ms (10 seconds)\n sending?: number; // Default: 60000 ms (60 seconds)\n };\n\n batchSize?: number; // Default: 100 messages per polling cycle\n pollingIntervals?: {\n scheduled?: number; // Default: 5000 ms\n pending?: number; // Default: 5000 ms\n sending?: number; // Default: 10000 ms\n delivered?: number; // Default: 1800000 ms (30 minutes)\n read?: number; // Default: 3600000 ms (60 minutes)\n queue?: number; // Default: 100 ms\n };\n maxQueueSize?: number; // Maximum number of pending/scheduled messages. Oldest evicted if exceeded.\n rateLimits?: {\n global?: { points: number; duration: number }; // Global rate limit (requests per duration)\n message?: { points: number; duration: number }; // Message sending rate limit (requests per duration)\n };\n}\n\nexport interface QueryFilter {\n contactId?: string;\n descriptorId?: string;\n status?: MessageStatus | MessageStatus[];\n state?: MessageState | MessageState[];\n size?: number;\n skip?: number;\n}\n\nexport interface DispatcherMetrics {\n total: number;\n byState: {\n [key in MessageState]?: number;\n };\n byStatus: {\n [key in MessageStatus]?: number;\n };\n cumulative: {\n dispatched: number;\n delivered: number;\n failed: number;\n };\n}\n\n/**\n * @deprecated Use DispatcherOptions instead\n */\nexport type MessageDispatcherOptions = DispatcherOptions;\n","/**\n * Logger utility using the 'debug' package.\n *\n * This module provides a configurable logger based on the 'debug' package.\n * By default, all logging is disabled. To enable logging, set the DEBUG\n * environment variable.\n *\n * The DEBUG variable can be a single namespace or a comma-separated list of\n * namespaces. Wildcards are supported.\n *\n * For example:\n * - `DEBUG=blip-api` enables all logs for the 'blip-api' scope.\n * - `DEBUG=blip-api:*` enables all sub-scopes under 'blip-api'.\n * - `DEBUG=*` enables all logs.\n *\n * The logger exposes different levels (debug, info, warn, error), which are\n * different namespaces.\n *\n * Example usage:\n * ```\n * import { getLogger } from './logger';\n *\n * const logger = getLogger('my-feature');\n *\n * logger.info('This is an info message');\n * logger.error('This is an error');\n * ```\n *\n * To see the output, run your application with:\n * `DEBUG=my-feature:* node your-app.js`\n */\n\nimport debug from 'debug';\n\n// Define the available log levels\nexport const LogLevels = {\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n};\n\nexport type LogLevel = keyof typeof LogLevels;\n\ntype LogFunction = (...args: any[]) => void;\n\nexport interface Logger {\n debug: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n error: LogFunction;\n}\n\nconst loggers = new Map<string, Logger>();\n\nfunction serializeArg(arg: any): any {\n if (arg === null || arg === undefined) return arg;\n if (typeof arg === 'object') {\n try {\n return JSON.stringify(arg, null, 2);\n } catch (e) {\n return arg;\n }\n }\n return arg;\n}\n\nfunction createLogFunction(debugInstance: debug.Debugger): LogFunction {\n return (...args: any[]) => {\n const serialized = args.map(serializeArg);\n (debugInstance as any)(...serialized);\n };\n}\n\n/**\n * Creates a logger with a specific scope.\n *\n * @param scope - The scope for the logger (e.g., 'blip-api', 'dispatcher').\n * @returns A logger object with methods for each log level.\n */\nexport function getLogger(scope: string): Logger {\n if (!loggers.has(scope)) {\n const debugInstance = debug(`${scope}:${LogLevels.debug}`);\n const infoInstance = debug(`${scope}:${LogLevels.info}`);\n const warnInstance = debug(`${scope}:${LogLevels.warn}`);\n const errorInstance = debug(`${scope}:${LogLevels.error}`);\n\n errorInstance.log = console.error.bind(console);\n\n const logger: Logger = {\n debug: createLogFunction(debugInstance),\n info: createLogFunction(infoInstance),\n warn: createLogFunction(warnInstance),\n error: createLogFunction(errorInstance),\n };\n\n loggers.set(scope, logger);\n }\n return loggers.get(scope)!;\n}\n\n/**\n * Enables logging for a specific scope and level.\n * This is a convenience function to programmatically enable logging,\n * equivalent to setting the DEBUG environment variable.\n *\n * @param scope - The scope to enable (e.g., 'blip-api').\n * @param level - The minimum log level to enable (e.g., 'info').\n */\nexport function enableLogger(namespaces: string): void {\n const currentDebug = debug.disable();\n debug.enable(`${currentDebug},${namespaces}`);\n}\n\n/**\n * Disables all logging.\n */\nexport function disableLogger(): void {\n debug.disable();\n}\n\n/**\n * Get all registered logger scopes.\n *\n * @returns An array of all registered scopes.\n */\nexport function getAllScopes(): string[] {\n return Array.from(loggers.keys());\n}\n"],"mappings":"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","names":["DispatcherMonitor_exports","__export","DispatcherMonitor","__toCommonJS","import_events","import_bullmq","MessageState","MessageStatus","import_debug","LogLevels","loggers","serializeArg","arg","createLogFunction","debugInstance","args","serialized","getLogger","scope","debug","infoInstance","warnInstance","errorInstance","logger","logger","getLogger","DispatcherMonitor","id","repository","options","job","err","metrics","statePromises","MessageState","state","statusPromises","MessageStatus","status","cumulativePromises","key","b","now","rule","error","maxSize","maxWindow","r","cutoff","idx","s","currentMetrics","alertKey","triggered","value","details","finalCount","previousSnapshot","currFailed","prevFailed","failedDelta","currDispatched","prevDispatched","dispatchedDelta","debounceTime","last","alert","timestamp","snap"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/DispatcherMonitor.ts","../../src/types/index.ts","../../src/utils/logger.ts"],"sourcesContent":["import { EventEmitter } from 'events';\nimport { Queue, Worker, Job } from 'bullmq';\nimport { DispatcherRepository } from './DispatcherRepository.js';\nimport { DispatcherMetrics, MessageState, MessageStatus } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\n\nconst logger = getLogger('DispatcherMonitor');\n\nexport interface MonitorRule {\n type: 'failure_rate' | 'queue_size';\n threshold: number;\n window?: number; // Time window in ms for rate calculation (only for failure_rate)\n debounce?: number; // Time to wait before re-alerting in ms\n}\n\nexport interface MonitorOptions {\n interval?: number; // Polling interval in ms (default: 60s)\n rules: MonitorRule[];\n historySize?: number; // Max snapshots to keep (default: 1000)\n}\n\ninterface MetricSnapshot {\n timestamp: number;\n metrics: DispatcherMetrics;\n}\n\nexport type MonitorAlert = {\n type: string;\n message: string;\n level: 'warning' | 'critical';\n details: any;\n timestamp: string;\n};\n\nexport class DispatcherMonitor extends EventEmitter {\n private id: string;\n private repository: DispatcherRepository;\n private options: MonitorOptions;\n\n // BullMQ for scheduling\n private queue: Queue;\n private worker: Worker;\n private queueName: string;\n\n private history: MetricSnapshot[] = [];\n private lastAlerts: Record<string, number> = {};\n private activeAlerts: Set<string> = new Set();\n private isRunning: boolean = false;\n\n constructor(id: string, repository: DispatcherRepository, options: MonitorOptions) {\n super();\n this.id = id;\n this.repository = repository;\n this.options = {\n interval: 60000,\n historySize: 1000,\n ...options,\n };\n\n this.queueName = `monitor-${this.id}`;\n\n // Reuse dispatcher's Redis connection for BullMQ\n // Note: BullMQ will manage duplication for blocking connections if needed\n this.queue = new Queue(this.queueName, {\n // @ts-ignore\n connection: repository.redis,\n defaultJobOptions: {\n removeOnComplete: true,\n removeOnFail: true,\n },\n });\n\n this.worker = new Worker(\n this.queueName,\n async (job: Job) => {\n if (job.name === 'check') {\n await this.check();\n }\n },\n {\n // @ts-ignore\n connection: repository.redis,\n }\n );\n\n this.worker.on('error', (err) => logger.error('[MonitorWorker] Error', err));\n this.worker.on('failed', (job, err) =>\n logger.error(`[MonitorWorker] Job ${job?.id} failed`, err)\n );\n }\n\n async start(): Promise<void> {\n if (this.isRunning) return;\n\n logger.info('[Monitor] Started');\n\n // Clean up any old repeatable jobs\n await this.queue.obliterate({ force: true });\n\n // Add repeatable job or just simple delayed recursion?\n // Using repeatable job is robust.\n await this.queue.add(\n 'check',\n {},\n {\n repeat: {\n every: this.options.interval,\n immediately: true, // Run on start\n },\n }\n );\n\n this.isRunning = true;\n }\n\n async stop(): Promise<void> {\n this.isRunning = false;\n await this.queue.close();\n await this.worker.close();\n logger.info('[Monitor] Stopped');\n }\n\n /**\n * Independent metrics collection directly from Repository\n */\n private async collectMetrics(): Promise<DispatcherMetrics> {\n const repository = this.repository;\n\n const metrics: DispatcherMetrics = {\n total: 0,\n byState: {},\n byStatus: {},\n cumulative: {\n dispatched: 0,\n delivered: 0,\n failed: 0,\n },\n };\n\n // Parallelize fetch? For simplicity/consistency, sequential is safer for now but parallel is faster.\n // Let's optimize slightly with Promise.all for groups.\n\n const states = Object.values(MessageState);\n const statePromises = states.map(async (state) => {\n metrics.byState[state] = await repository.countMessages({ state });\n });\n\n const statuses = Object.values(MessageStatus);\n const statusPromises = statuses.map(async (status) => {\n metrics.byStatus[status] = await repository.countMessages({ status });\n });\n\n const cumulativeKeys = ['dispatched', 'delivered', 'failed'] as const;\n const cumulativePromises = cumulativeKeys.map(async (key) => {\n metrics.cumulative![key] = await repository.getMetric(key);\n });\n\n await Promise.all([...statePromises, ...statusPromises, ...cumulativePromises]);\n\n metrics.total = Object.values(metrics.byState).reduce((a, b) => a + (b || 0), 0);\n\n return metrics;\n }\n\n private async check(): Promise<void> {\n try {\n const metrics = await this.collectMetrics();\n const now = Date.now();\n\n // Record snapshot\n this.history.push({ timestamp: now, metrics });\n this.cleanHistory();\n\n // Check rules\n for (const rule of this.options.rules) {\n await this.evaluateRule(rule, metrics, now);\n }\n } catch (error) {\n logger.error('[Monitor] Error during check', error);\n }\n }\n\n private cleanHistory(): void {\n const maxSize = this.options.historySize!;\n if (this.history.length > maxSize) {\n this.history = this.history.slice(this.history.length - maxSize);\n }\n\n // Also remove snapshots older than the largest window needed\n const maxWindow = Math.max(...this.options.rules.map((r) => r.window || 0));\n const cutoff = Date.now() - maxWindow - 60000; // Buffer\n if (this.history.length > 0 && this.history[0].timestamp < cutoff) {\n const idx = this.history.findIndex((s) => s.timestamp >= cutoff);\n if (idx > 0) {\n this.history = this.history.slice(idx);\n }\n }\n }\n\n private async evaluateRule(\n rule: MonitorRule,\n currentMetrics: DispatcherMetrics,\n now: number\n ): Promise<void> {\n const alertKey = `${rule.type}`;\n let triggered = false;\n let value = 0;\n let details = {};\n\n switch (rule.type) {\n case 'queue_size':\n const finalCount = currentMetrics.byState.FINAL || 0;\n value = currentMetrics.total - finalCount;\n triggered = value > rule.threshold;\n details = { current: value, threshold: rule.threshold };\n break;\n\n case 'failure_rate':\n if (!rule.window) {\n logger.warn('[Monitor] failure_rate rule missing window');\n return;\n }\n\n const previousSnapshot = this.findSnapshotAt(now - rule.window);\n if (!previousSnapshot) {\n return;\n }\n\n const currFailed = currentMetrics.cumulative!.failed;\n const prevFailed = previousSnapshot.metrics.cumulative!.failed;\n const failedDelta = currFailed - prevFailed;\n\n const currDispatched = currentMetrics.cumulative!.dispatched;\n const prevDispatched = previousSnapshot.metrics.cumulative!.dispatched;\n const dispatchedDelta = currDispatched - prevDispatched;\n\n if (dispatchedDelta === 0) {\n value = 0;\n } else {\n value = failedDelta / dispatchedDelta;\n }\n\n triggered = value > rule.threshold;\n details = {\n rate: (value * 100).toFixed(2) + '%',\n threshold: (rule.threshold * 100).toFixed(2) + '%',\n failed: failedDelta,\n dispatched: dispatchedDelta,\n window: rule.window,\n };\n break;\n }\n\n if (triggered) {\n if (!this.activeAlerts.has(alertKey)) {\n this.emitAlert(alertKey, rule, value, details);\n this.activeAlerts.add(alertKey);\n } else {\n if (rule.debounce && !this.isDebounced(alertKey, rule.debounce)) {\n this.emitAlert(alertKey, rule, value, details);\n }\n }\n } else {\n if (this.activeAlerts.has(alertKey)) {\n this.resolveAlert(alertKey, rule);\n this.activeAlerts.delete(alertKey);\n }\n }\n }\n\n private isDebounced(key: string, debounceTime?: number): boolean {\n if (!debounceTime) return false;\n const last = this.lastAlerts[key];\n if (!last) return false;\n return Date.now() - last < debounceTime;\n }\n\n private emitAlert(key: string, rule: MonitorRule, value: number, details: any): void {\n logger.warn(`[Monitor] Alert triggered: ${rule.type}`, details);\n this.lastAlerts[key] = Date.now();\n\n const alert: MonitorAlert = {\n type: rule.type,\n message: `${rule.type} exceeded threshold`,\n level: 'warning',\n details,\n timestamp: new Date().toISOString(),\n };\n\n this.emit('alert', alert);\n }\n\n private resolveAlert(key: string, rule: MonitorRule): void {\n logger.info(`[Monitor] Alert resolved: ${rule.type}`);\n const alert: MonitorAlert = {\n type: rule.type,\n message: `${rule.type} resolved`,\n level: 'warning',\n details: {},\n timestamp: new Date().toISOString(),\n };\n this.emit('resolved', alert);\n }\n\n private findSnapshotAt(timestamp: number): MetricSnapshot | null {\n if (this.history.length === 0) return null;\n for (const snap of this.history) {\n if (snap.timestamp >= timestamp) {\n return snap;\n }\n }\n return this.history[0];\n }\n}\n","import { Vnd } from './blip';\n\nexport type Contact = Omit<Vnd.Iris.Contact, 'identity'>;\n\nexport type MessageData = {\n type: string;\n content: Record<string, any> | string;\n};\n\nexport type MessagePayload = Record<string, any> | Array<any> | string;\n\nexport enum MessageState {\n INIT = 'INIT',\n DISPATCHED = 'DISPATCHED',\n SCHEDULED = 'SCHEDULED',\n QUEUED = 'QUEUED',\n FINAL = 'FINAL',\n}\n\nexport enum MessageStatus {\n INIT = 'INIT',\n PENDING = 'PENDING',\n SENDING = 'SENDING',\n DELIVERED = 'DELIVERED',\n READ = 'READ',\n REPLIED = 'REPLIED',\n FAILED = 'FAILED',\n CANCELED = 'CANCELED',\n}\n\nexport enum DispatchState {\n ACCEPTED = 'accepted',\n DISPATCHED = 'dispatched',\n RECEIVED = 'received',\n CONSUMED = 'consumed',\n FAILED = 'failed',\n}\n\nexport interface Message {\n messageId: string;\n contactId: string;\n descriptorId: string;\n\n payload: MessageData;\n status: MessageStatus;\n state: MessageState;\n\n attempts?: number;\n retries?: number;\n\n options?: MessageOptions; // Persisted options for scheduled/retried messages\n\n expiresAt?: string; // ISO 8601 datetime\n\n scheduledTo?: string; // ISO 8601 datetime\n sentAt?: string; // ISO 8601 datetime\n acceptedAt?: string; // ISO 8601 datetime - When message was accepted by gateway (SENDING status)\n lastDispatchAttemptAt?: string; // ISO 8601 datetime - When last dispatch attempt started\n createdAt: string; // ISO 8601 datetime\n deliveredAt?: string; // ISO 8601 datetime\n readAt?: string; // ISO 8601 datetime\n repliedAt?: string; // ISO 8601 datetime\n failedAt?: string; // ISO 8601 datetime\n\n error?: string;\n}\n\nexport interface Shift {\n days: number; // Bitmask: use Weekdays enum (e.g., Weekdays.MONDAY | Weekdays.FRIDAY) or manual (31=Mon-Fri)\n start: string; // HH:mm format (e.g., \"09:00\")\n end: string; // HH:mm format (e.g., \"18:00\")\n gmt?: string; // Timezone offset (default: \"-3\" for BRT)\n}\n\nexport enum Weekdays {\n MONDAY = 1,\n TUESDAY = 2,\n WEDNESDAY = 4,\n THURSDAY = 8,\n FRIDAY = 16,\n SATURDAY = 32,\n SUNDAY = 64,\n}\n\nexport interface Intent {\n intent: string;\n dueDate?: string;\n event?: string;\n payload?: any | null;\n expired?: {\n event?: string;\n intent?: string;\n };\n}\n\nexport type CallbackEvent =\n | 'dispatch' // Before message send process begins\n | 'sending' // MessageStatus.SENDING - message accepted by gateway\n | 'delivered' // MessageStatus.DELIVERED - message successfully delivered\n | 'failed' // MessageStatus.FAILED - message failed permanently\n | 'read' // MessageStatus.READ - message read by recipient\n | 'replied' // MessageStatus.REPLIED - recipient replied\n | 'scheduled' // MessageState.SCHEDULED - message scheduled\n | 'retry' // Retry attempt scheduled\n | 'evicted' // Message evicted from state machine due to TTL\n | 'canceled'; // Message canceled before send\n\nexport type ConnectionConfig = {\n contract: string; // Contract ID (without https:// prefix)\n key: string; // Bot authorization key\n};\n\nexport interface MessageOptions {\n shifts?: Array<Shift>; // Business hours constraints\n contact?: Contact;\n state?: {\n // Bot state to set after send\n stateId: string;\n botId: string;\n botIds?: Array<string>; // Bot IDs to reset before setting state\n };\n intent?: string | Intent; // Intent to trigger after send\n finalStatus?: 'DELIVERED' | 'READ' | 'REPLIED'; // Default: 'DELIVERED'\n}\n\nexport interface DispatchMessageOptions extends MessageOptions {\n schedule?: string; // ISO 8601 datetime to schedule send\n}\n\nexport interface DispatcherOptions {\n maxRetries?: number; // Default: 0\n retryIntervals?: number[]; // Default: [1000, 5000, 15000] (exponential backoff in ms)\n\n // Storage Duration (Redis TTL)\n retention?: number; // Default: 3600000 ms (1 hour) - Time to keep message in storage after creation/dispatch\n\n // State Constraints (Force transition to FAILED)\n timeouts?: {\n pending?: number; // Default: 10000 ms (10 seconds)\n sending?: number; // Default: 60000 ms (60 seconds)\n };\n\n batchSize?: number; // Default: 100 messages per polling cycle\n pollingIntervals?: {\n scheduled?: number; // Default: 5000 ms\n pending?: number; // Default: 5000 ms\n sending?: number; // Default: 10000 ms\n delivered?: number; // Default: 1800000 ms (30 minutes)\n read?: number; // Default: 3600000 ms (60 minutes)\n queue?: number; // Default: 100 ms\n };\n maxQueueSize?: number; // Maximum number of pending/scheduled messages. Oldest evicted if exceeded.\n rateLimits?: {\n global?: { points: number; duration: number }; // Global rate limit (requests per duration)\n message?: { points: number; duration: number }; // Message sending rate limit (requests per duration)\n };\n}\n\nexport interface QueryFilter {\n contactId?: string;\n descriptorId?: string;\n status?: MessageStatus | MessageStatus[];\n state?: MessageState | MessageState[];\n size?: number;\n skip?: number;\n}\n\nexport interface DispatcherMetrics {\n total: number;\n byState: {\n [key in MessageState]?: number;\n };\n byStatus: {\n [key in MessageStatus]?: number;\n };\n cumulative: {\n dispatched: number;\n delivered: number;\n failed: number;\n };\n}\n\nexport interface DispatcherManifest {\n version: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * @deprecated Use DispatcherOptions instead\n */\nexport type MessageDispatcherOptions = DispatcherOptions;\n","/**\n * Logger utility using the 'debug' package.\n *\n * This module provides a configurable logger based on the 'debug' package.\n * By default, all logging is disabled. To enable logging, set the DEBUG\n * environment variable.\n *\n * The DEBUG variable can be a single namespace or a comma-separated list of\n * namespaces. Wildcards are supported.\n *\n * For example:\n * - `DEBUG=blip-api` enables all logs for the 'blip-api' scope.\n * - `DEBUG=blip-api:*` enables all sub-scopes under 'blip-api'.\n * - `DEBUG=*` enables all logs.\n *\n * The logger exposes different levels (debug, info, warn, error), which are\n * different namespaces.\n *\n * Example usage:\n * ```\n * import { getLogger } from './logger';\n *\n * const logger = getLogger('my-feature');\n *\n * logger.info('This is an info message');\n * logger.error('This is an error');\n * ```\n *\n * To see the output, run your application with:\n * `DEBUG=my-feature:* node your-app.js`\n */\n\nimport debug from 'debug';\n\n// Define the available log levels\nexport const LogLevels = {\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n};\n\nexport type LogLevel = keyof typeof LogLevels;\n\ntype LogFunction = (...args: any[]) => void;\n\nexport interface Logger {\n debug: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n error: LogFunction;\n}\n\nconst loggers = new Map<string, Logger>();\n\nfunction serializeArg(arg: any): any {\n if (arg === null || arg === undefined) return arg;\n if (typeof arg === 'object') {\n try {\n return JSON.stringify(arg, null, 2);\n } catch (e) {\n return arg;\n }\n }\n return arg;\n}\n\nfunction createLogFunction(debugInstance: debug.Debugger): LogFunction {\n return (...args: any[]) => {\n const serialized = args.map(serializeArg);\n (debugInstance as any)(...serialized);\n };\n}\n\n/**\n * Creates a logger with a specific scope.\n *\n * @param scope - The scope for the logger (e.g., 'blip-api', 'dispatcher').\n * @returns A logger object with methods for each log level.\n */\nexport function getLogger(scope: string): Logger {\n if (!loggers.has(scope)) {\n const debugInstance = debug(`${scope}:${LogLevels.debug}`);\n const infoInstance = debug(`${scope}:${LogLevels.info}`);\n const warnInstance = debug(`${scope}:${LogLevels.warn}`);\n const errorInstance = debug(`${scope}:${LogLevels.error}`);\n\n errorInstance.log = console.error.bind(console);\n\n const logger: Logger = {\n debug: createLogFunction(debugInstance),\n info: createLogFunction(infoInstance),\n warn: createLogFunction(warnInstance),\n error: createLogFunction(errorInstance),\n };\n\n loggers.set(scope, logger);\n }\n return loggers.get(scope)!;\n}\n\n/**\n * Enables logging for a specific scope and level.\n * This is a convenience function to programmatically enable logging,\n * equivalent to setting the DEBUG environment variable.\n *\n * @param scope - The scope to enable (e.g., 'blip-api').\n * @param level - The minimum log level to enable (e.g., 'info').\n */\nexport function enableLogger(namespaces: string): void {\n const currentDebug = debug.disable();\n debug.enable(`${currentDebug},${namespaces}`);\n}\n\n/**\n * Disables all logging.\n */\nexport function disableLogger(): void {\n debug.disable();\n}\n\n/**\n * Get all registered logger scopes.\n *\n * @returns An array of all registered scopes.\n */\nexport function getAllScopes(): string[] {\n return Array.from(loggers.keys());\n}\n"],"mappings":"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","names":["DispatcherMonitor_exports","__export","DispatcherMonitor","__toCommonJS","import_events","import_bullmq","MessageState","MessageStatus","import_debug","LogLevels","loggers","serializeArg","arg","createLogFunction","debugInstance","args","serialized","getLogger","scope","debug","infoInstance","warnInstance","errorInstance","logger","logger","getLogger","DispatcherMonitor","id","repository","options","job","err","metrics","statePromises","MessageState","state","statusPromises","MessageStatus","status","cumulativePromises","key","b","now","rule","error","maxSize","maxWindow","r","cutoff","idx","s","currentMetrics","alertKey","triggered","value","details","finalCount","previousSnapshot","currFailed","prevFailed","failedDelta","currDispatched","prevDispatched","dispatchedDelta","debounceTime","last","alert","timestamp","snap"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a}from"../chunk-
|
|
1
|
+
import{a}from"../chunk-VTJMLZE3.mjs";import"../chunk-4LYB64T2.mjs";import"../chunk-OXXLVJVC.mjs";import"../chunk-JD53PFR4.mjs";export{a as DispatcherMonitor};
|
|
2
2
|
//# sourceMappingURL=DispatcherMonitor.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var E=Object.create;var p=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var v=Object.getPrototypeOf,R=Object.prototype.hasOwnProperty;var C=(e,t)=>{for(var s in t)p(e,s,{get:t[s],enumerable:!0})},b=(e,t,s,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of x(t))!R.call(e,r)&&r!==s&&p(e,r,{get:()=>t[r],enumerable:!(i=L(t,r))||i.enumerable});return e};var N=(e,t,s)=>(s=e!=null?E(v(e)):{},b(t||!e||!e.__esModule?p(s,"default",{value:e,enumerable:!0}):s,e)),S=e=>b(p({},"__esModule",{value:!0}),e);var O={};C(O,{DispatcherQuery:()=>D});module.exports=S(O);var l=(o=>(o.INIT="INIT",o.PENDING="PENDING",o.SENDING="SENDING",o.DELIVERED="DELIVERED",o.READ="READ",o.REPLIED="REPLIED",o.FAILED="FAILED",o.CANCELED="CANCELED",o))(l||{});var u=N(require("debug")),d={debug:"debug",info:"info",warn:"warn",error:"error"},I=new Map;function F(e){if(e==null)return e;if(typeof e=="object")try{return JSON.stringify(e,null,2)}catch{return e}return e}function y(e){return(...t)=>{let s=t.map(F);e(...s)}}function h(e){if(!I.has(e)){let t=(0,u.default)(`${e}:${d.debug}`),s=(0,u.default)(`${e}:${d.info}`),i=(0,u.default)(`${e}:${d.warn}`),r=(0,u.default)(`${e}:${d.error}`);r.log=console.error.bind(console);let a={debug:y(t),info:y(s),warn:y(i),error:y(r)};I.set(e,a)}return I.get(e)}var M=h("DispatcherQuery"),D=class{constructor(t){this.repository=t}get client(){return this.repository.redis}async query(t){let s=[],i=this.repository.keyPrefix;if(t.contactId&&s.push(this.repository.getContactKey(t.contactId)),t.descriptorId&&s.push(this.repository.getDescriptorKey(t.descriptorId)),t.status){let n=Array.isArray(t.status)?t.status:[t.status];n.length===1?s.push(this.repository.getStatusKey(n[0])):n.length>1&&s.push(this.repository.getStatusKey(n[0]))}if(t.state){let n=Array.isArray(t.state)?t.state:[t.state];n.length===1&&s.push(this.repository.getStateKey(n[0]))}let r=[];if(s.length>0)r=await this.client.sinter(s);else{let n=Object.values(l).map(c=>this.repository.getStatusKey(c));r=await this.client.sunion(n)}let a=t.skip??0,g=t.size??50,f=r.slice(a,a+g),o=[],m=[];for(let n of f){let c=await this.repository.getMessage(n);if(c){if(t.status&&!(Array.isArray(t.status)?t.status:[t.status]).includes(c.status)||t.state&&!(Array.isArray(t.state)?t.state:[t.state]).includes(c.state))continue;o.push(c)}else m.push(n)}return m.length>0&&this.cleanupIndices(m,t),o}async cleanupIndices(t,s){let i=this.client.pipeline(),r=this.repository.keyPrefix;s.contactId&&i.srem(this.repository.getContactKey(s.contactId),t),s.descriptorId&&i.srem(this.repository.getDescriptorKey(s.descriptorId),t),s.status&&(Array.isArray(s.status)?s.status:[s.status]).forEach(g=>{i.srem(this.repository.getStatusKey(g),t)}),s.state&&(Array.isArray(s.state)?s.state:[s.state]).forEach(g=>{i.srem(this.repository.getStateKey(g),t)}),await i.exec(),M.debug("[cleanupIndices] Removed expired IDs from checked indices",{count:t.length})}};0&&(module.exports={DispatcherQuery});
|
|
2
2
|
//# sourceMappingURL=DispatcherQuery.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/DispatcherQuery.ts","../../src/types/index.ts","../../src/utils/logger.ts"],"sourcesContent":["import { DispatcherRepository } from './DispatcherRepository.js';\nimport { Message, MessageState, MessageStatus, QueryFilter } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport IORedis from 'ioredis';\n\nconst logger = getLogger('DispatcherQuery');\n\nexport class DispatcherQuery {\n private repository: DispatcherRepository;\n\n constructor(repository: DispatcherRepository) {\n this.repository = repository;\n }\n\n private get client(): IORedis {\n return this.repository.redis;\n }\n\n public async query(filter: QueryFilter): Promise<Message[]> {\n const keysToIntersect: string[] = [];\n const prefix = this.repository['keyPrefix'];\n\n // 1. Build Intersection Sets using new INDEX keys\n if (filter.contactId) {\n keysToIntersect.push(this.repository.getContactKey(filter.contactId));\n }\n\n if (filter.descriptorId) {\n keysToIntersect.push(this.repository.getDescriptorKey(filter.descriptorId));\n }\n\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (statuses.length === 1) {\n keysToIntersect.push(this.repository.getStatusKey(statuses[0]));\n } else if (statuses.length > 1) {\n // For MVP, if multiple statuses, we intersect the FIRST one and filter the rest in memory.\n keysToIntersect.push(this.repository.getStatusKey(statuses[0]));\n }\n }\n\n // Now safe to use state index for ALL states (even SCHEDULED/QUEUED) because we index them as Sets too.\n if (filter.state) {\n const states = Array.isArray(filter.state) ? filter.state : [filter.state];\n if (states.length === 1) {\n keysToIntersect.push(this.repository.getStateKey(states[0]));\n }\n }\n\n let messageIds: string[] = [];\n\n if (keysToIntersect.length > 0) {\n // Execute Intersection\n messageIds = await this.client.sinter(keysToIntersect);\n } else {\n // Fallback: No filters provided (e.g. \"All\" selected without other filters)\n // We want to return ALL messages. We can do this by Unioning all Status sets.\n // This allows us to see Delivered/Failed messages even if not filtering by them.\n const allStatusKeys = Object.values(MessageStatus).map((status) =>\n this.repository.getStatusKey(status)\n );\n messageIds = await this.client.sunion(allStatusKeys);\n }\n\n // 2. Handle OR logic for multiple statuses/states if needed\n // (Omitted for MVP, assuming single values or handling in memory for small intersections)\n\n // 3. Paging\n const skip = filter.skip ?? 0;\n const size = filter.size ?? 50;\n const pagedIds = messageIds.slice(skip, skip + size);\n\n // 4. Fetch Messages & Lazy Cleanup\n const messages: Message[] = [];\n const idsToRemove: string[] = [];\n\n for (const id of pagedIds) {\n const message = await this.repository.getMessage(id);\n if (message) {\n // Validate in-memory filters (e.g. multiple statuses, or if SINTER wasn't perfect)\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (!statuses.includes(message.status)) continue;\n }\n if (filter.state) {\n const states = Array.isArray(filter.state) ? filter.state : [filter.state];\n if (!states.includes(message.state)) continue;\n }\n\n messages.push(message);\n } else {\n // Message ID exists in index but not in storage -> Expired\n idsToRemove.push(id);\n }\n }\n\n // Lazy Cleanup\n if (idsToRemove.length > 0) {\n this.cleanupIndices(idsToRemove, filter);\n }\n\n return messages;\n }\n\n private async cleanupIndices(ids: string[], context: QueryFilter): Promise<void> {\n const pipeline = this.client.pipeline();\n const prefix = this.repository['keyPrefix'];\n\n // Basic cleanup: Remove from the keys we just queried.\n if (context.contactId) {\n pipeline.srem(this.repository.getContactKey(context.contactId), ids);\n }\n if (context.descriptorId) {\n pipeline.srem(this.repository.getDescriptorKey(context.descriptorId), ids);\n }\n\n if (context.status) {\n const statuses = Array.isArray(context.status) ? context.status : [context.status];\n statuses.forEach((s) => {\n pipeline.srem(this.repository.getStatusKey(s), ids);\n });\n }\n if (context.state) {\n const states = Array.isArray(context.state) ? context.state : [context.state];\n states.forEach((s) => {\n pipeline.srem(this.repository.getStateKey(s), ids);\n });\n }\n\n await pipeline.exec();\n logger.debug('[cleanupIndices] Removed expired IDs from checked indices', {\n count: ids.length,\n });\n }\n}\n","import { Vnd } from './blip';\n\nexport type Contact = Omit<Vnd.Iris.Contact, 'identity'>;\n\nexport type MessageData = {\n type: string;\n content: Record<string, any> | string;\n};\n\nexport type MessagePayload = Record<string, any> | Array<any> | string;\n\nexport enum MessageState {\n INIT = 'INIT',\n DISPATCHED = 'DISPATCHED',\n SCHEDULED = 'SCHEDULED',\n QUEUED = 'QUEUED',\n FINAL = 'FINAL',\n}\n\nexport enum MessageStatus {\n INIT = 'INIT',\n PENDING = 'PENDING',\n SENDING = 'SENDING',\n DELIVERED = 'DELIVERED',\n READ = 'READ',\n REPLIED = 'REPLIED',\n FAILED = 'FAILED',\n CANCELED = 'CANCELED',\n}\n\nexport enum DispatchState {\n ACCEPTED = 'accepted',\n DISPATCHED = 'dispatched',\n RECEIVED = 'received',\n CONSUMED = 'consumed',\n FAILED = 'failed',\n}\n\nexport interface Message {\n messageId: string;\n contactId: string;\n descriptorId: string;\n\n payload: MessageData;\n status: MessageStatus;\n state: MessageState;\n\n attempts?: number;\n retries?: number;\n\n options?: MessageOptions; // Persisted options for scheduled/retried messages\n\n expiresAt?: string; // ISO 8601 datetime\n\n scheduledTo?: string; // ISO 8601 datetime\n sentAt?: string; // ISO 8601 datetime\n acceptedAt?: string; // ISO 8601 datetime - When message was accepted by gateway (SENDING status)\n lastDispatchAttemptAt?: string; // ISO 8601 datetime - When last dispatch attempt started\n createdAt: string; // ISO 8601 datetime\n deliveredAt?: string; // ISO 8601 datetime\n readAt?: string; // ISO 8601 datetime\n repliedAt?: string; // ISO 8601 datetime\n failedAt?: string; // ISO 8601 datetime\n\n error?: string;\n}\n\nexport interface Shift {\n days: number; // Bitmask: use Weekdays enum (e.g., Weekdays.MONDAY | Weekdays.FRIDAY) or manual (31=Mon-Fri)\n start: string; // HH:mm format (e.g., \"09:00\")\n end: string; // HH:mm format (e.g., \"18:00\")\n gmt?: string; // Timezone offset (default: \"-3\" for BRT)\n}\n\nexport enum Weekdays {\n MONDAY = 1,\n TUESDAY = 2,\n WEDNESDAY = 4,\n THURSDAY = 8,\n FRIDAY = 16,\n SATURDAY = 32,\n SUNDAY = 64,\n}\n\nexport interface Intent {\n intent: string;\n dueDate?: string;\n event?: string;\n payload?: any | null;\n expired?: {\n event?: string;\n intent?: string;\n };\n}\n\nexport type CallbackEvent =\n | 'dispatch' // Before message send process begins\n | 'sending' // MessageStatus.SENDING - message accepted by gateway\n | 'delivered' // MessageStatus.DELIVERED - message successfully delivered\n | 'failed' // MessageStatus.FAILED - message failed permanently\n | 'read' // MessageStatus.READ - message read by recipient\n | 'replied' // MessageStatus.REPLIED - recipient replied\n | 'scheduled' // MessageState.SCHEDULED - message scheduled\n | 'retry' // Retry attempt scheduled\n | 'evicted' // Message evicted from state machine due to TTL\n | 'canceled'; // Message canceled before send\n\nexport type ConnectionConfig = {\n contract: string; // Contract ID (without https:// prefix)\n key: string; // Bot authorization key\n};\n\nexport interface MessageOptions {\n shifts?: Array<Shift>; // Business hours constraints\n contact?: Contact;\n state?: {\n // Bot state to set after send\n stateId: string;\n botId: string;\n botIds?: Array<string>; // Bot IDs to reset before setting state\n };\n intent?: string | Intent; // Intent to trigger after send\n finalStatus?: 'DELIVERED' | 'READ' | 'REPLIED'; // Default: 'DELIVERED'\n}\n\nexport interface DispatchMessageOptions extends MessageOptions {\n schedule?: string; // ISO 8601 datetime to schedule send\n}\n\nexport interface DispatcherOptions {\n maxRetries?: number; // Default: 0\n retryIntervals?: number[]; // Default: [1000, 5000, 15000] (exponential backoff in ms)\n\n // Storage Duration (Redis TTL)\n retention?: number; // Default: 3600000 ms (1 hour) - Time to keep message in storage after creation/dispatch\n\n // State Constraints (Force transition to FAILED)\n timeouts?: {\n pending?: number; // Default: 10000 ms (10 seconds)\n sending?: number; // Default: 60000 ms (60 seconds)\n };\n\n batchSize?: number; // Default: 100 messages per polling cycle\n pollingIntervals?: {\n scheduled?: number; // Default: 5000 ms\n pending?: number; // Default: 5000 ms\n sending?: number; // Default: 10000 ms\n delivered?: number; // Default: 1800000 ms (30 minutes)\n read?: number; // Default: 3600000 ms (60 minutes)\n queue?: number; // Default: 100 ms\n };\n maxQueueSize?: number; // Maximum number of pending/scheduled messages. Oldest evicted if exceeded.\n rateLimits?: {\n global?: { points: number; duration: number }; // Global rate limit (requests per duration)\n message?: { points: number; duration: number }; // Message sending rate limit (requests per duration)\n };\n}\n\nexport interface QueryFilter {\n contactId?: string;\n descriptorId?: string;\n status?: MessageStatus | MessageStatus[];\n state?: MessageState | MessageState[];\n size?: number;\n skip?: number;\n}\n\nexport interface DispatcherMetrics {\n total: number;\n byState: {\n [key in MessageState]?: number;\n };\n byStatus: {\n [key in MessageStatus]?: number;\n };\n cumulative: {\n dispatched: number;\n delivered: number;\n failed: number;\n };\n}\n\n/**\n * @deprecated Use DispatcherOptions instead\n */\nexport type MessageDispatcherOptions = DispatcherOptions;\n","/**\n * Logger utility using the 'debug' package.\n *\n * This module provides a configurable logger based on the 'debug' package.\n * By default, all logging is disabled. To enable logging, set the DEBUG\n * environment variable.\n *\n * The DEBUG variable can be a single namespace or a comma-separated list of\n * namespaces. Wildcards are supported.\n *\n * For example:\n * - `DEBUG=blip-api` enables all logs for the 'blip-api' scope.\n * - `DEBUG=blip-api:*` enables all sub-scopes under 'blip-api'.\n * - `DEBUG=*` enables all logs.\n *\n * The logger exposes different levels (debug, info, warn, error), which are\n * different namespaces.\n *\n * Example usage:\n * ```\n * import { getLogger } from './logger';\n *\n * const logger = getLogger('my-feature');\n *\n * logger.info('This is an info message');\n * logger.error('This is an error');\n * ```\n *\n * To see the output, run your application with:\n * `DEBUG=my-feature:* node your-app.js`\n */\n\nimport debug from 'debug';\n\n// Define the available log levels\nexport const LogLevels = {\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n};\n\nexport type LogLevel = keyof typeof LogLevels;\n\ntype LogFunction = (...args: any[]) => void;\n\nexport interface Logger {\n debug: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n error: LogFunction;\n}\n\nconst loggers = new Map<string, Logger>();\n\nfunction serializeArg(arg: any): any {\n if (arg === null || arg === undefined) return arg;\n if (typeof arg === 'object') {\n try {\n return JSON.stringify(arg, null, 2);\n } catch (e) {\n return arg;\n }\n }\n return arg;\n}\n\nfunction createLogFunction(debugInstance: debug.Debugger): LogFunction {\n return (...args: any[]) => {\n const serialized = args.map(serializeArg);\n (debugInstance as any)(...serialized);\n };\n}\n\n/**\n * Creates a logger with a specific scope.\n *\n * @param scope - The scope for the logger (e.g., 'blip-api', 'dispatcher').\n * @returns A logger object with methods for each log level.\n */\nexport function getLogger(scope: string): Logger {\n if (!loggers.has(scope)) {\n const debugInstance = debug(`${scope}:${LogLevels.debug}`);\n const infoInstance = debug(`${scope}:${LogLevels.info}`);\n const warnInstance = debug(`${scope}:${LogLevels.warn}`);\n const errorInstance = debug(`${scope}:${LogLevels.error}`);\n\n errorInstance.log = console.error.bind(console);\n\n const logger: Logger = {\n debug: createLogFunction(debugInstance),\n info: createLogFunction(infoInstance),\n warn: createLogFunction(warnInstance),\n error: createLogFunction(errorInstance),\n };\n\n loggers.set(scope, logger);\n }\n return loggers.get(scope)!;\n}\n\n/**\n * Enables logging for a specific scope and level.\n * This is a convenience function to programmatically enable logging,\n * equivalent to setting the DEBUG environment variable.\n *\n * @param scope - The scope to enable (e.g., 'blip-api').\n * @param level - The minimum log level to enable (e.g., 'info').\n */\nexport function enableLogger(namespaces: string): void {\n const currentDebug = debug.disable();\n debug.enable(`${currentDebug},${namespaces}`);\n}\n\n/**\n * Disables all logging.\n */\nexport function disableLogger(): void {\n debug.disable();\n}\n\n/**\n * Get all registered logger scopes.\n *\n * @returns An array of all registered scopes.\n */\nexport function getAllScopes(): string[] {\n return Array.from(loggers.keys());\n}\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,qBAAAE,IAAA,eAAAC,EAAAH,GCmBO,IAAKI,OACVA,EAAA,KAAO,OACPA,EAAA,QAAU,UACVA,EAAA,QAAU,UACVA,EAAA,UAAY,YACZA,EAAA,KAAO,OACPA,EAAA,QAAU,UACVA,EAAA,OAAS,SACTA,EAAA,SAAW,WARDA,OAAA,ICaZ,IAAAC,EAAkB,oBAGLC,EAAY,CACvB,MAAO,QACP,KAAM,OACN,KAAM,OACN,MAAO,OACT,EAaMC,EAAU,IAAI,IAEpB,SAASC,EAAaC,EAAe,CACnC,GAAIA,GAAQ,KAA2B,OAAOA,EAC9C,GAAI,OAAOA,GAAQ,SACjB,GAAI,CACF,OAAO,KAAK,UAAUA,EAAK,KAAM,CAAC,CACpC,MAAY,CACV,OAAOA,CACT,CAEF,OAAOA,CACT,CAEA,SAASC,EAAkBC,EAA4C,CACrE,MAAO,IAAIC,IAAgB,CACzB,IAAMC,EAAaD,EAAK,IAAIJ,CAAY,EACvCG,EAAsB,GAAGE,CAAU,CACtC,CACF,CAQO,SAASC,EAAUC,EAAuB,CAC/C,GAAI,CAACR,EAAQ,IAAIQ,CAAK,EAAG,CACvB,IAAMJ,KAAgB,EAAAK,SAAM,GAAGD,CAAK,IAAIT,EAAU,KAAK,EAAE,EACnDW,KAAe,EAAAD,SAAM,GAAGD,CAAK,IAAIT,EAAU,IAAI,EAAE,EACjDY,KAAe,EAAAF,SAAM,GAAGD,CAAK,IAAIT,EAAU,IAAI,EAAE,EACjDa,KAAgB,EAAAH,SAAM,GAAGD,CAAK,IAAIT,EAAU,KAAK,EAAE,EAEzDa,EAAc,IAAM,QAAQ,MAAM,KAAK,OAAO,EAE9C,IAAMC,EAAiB,CACrB,MAAOV,EAAkBC,CAAa,EACtC,KAAMD,EAAkBO,CAAY,EACpC,KAAMP,EAAkBQ,CAAY,EACpC,MAAOR,EAAkBS,CAAa,CACxC,EAEAZ,EAAQ,IAAIQ,EAAOK,CAAM,CAC3B,CACA,OAAOb,EAAQ,IAAIQ,CAAK,CAC1B,CF9FA,IAAMM,EAASC,EAAU,iBAAiB,EAE7BC,EAAN,KAAsB,CAG3B,YAAYC,EAAkC,CAC5C,KAAK,WAAaA,CACpB,CAEA,IAAY,QAAkB,CAC5B,OAAO,KAAK,WAAW,KACzB,CAEA,MAAa,MAAMC,EAAyC,CAC1D,IAAMC,EAA4B,CAAC,EAC7BC,EAAS,KAAK,WAAW,UAW/B,GARIF,EAAO,WACTC,EAAgB,KAAK,KAAK,WAAW,cAAcD,EAAO,SAAS,CAAC,EAGlEA,EAAO,cACTC,EAAgB,KAAK,KAAK,WAAW,iBAAiBD,EAAO,YAAY,CAAC,EAGxEA,EAAO,OAAQ,CACjB,IAAMG,EAAW,MAAM,QAAQH,EAAO,MAAM,EAAIA,EAAO,OAAS,CAACA,EAAO,MAAM,EAC1EG,EAAS,SAAW,EACtBF,EAAgB,KAAK,KAAK,WAAW,aAAaE,EAAS,CAAC,CAAC,CAAC,EACrDA,EAAS,OAAS,GAE3BF,EAAgB,KAAK,KAAK,WAAW,aAAaE,EAAS,CAAC,CAAC,CAAC,CAElE,CAGA,GAAIH,EAAO,MAAO,CAChB,IAAMI,EAAS,MAAM,QAAQJ,EAAO,KAAK,EAAIA,EAAO,MAAQ,CAACA,EAAO,KAAK,EACrEI,EAAO,SAAW,GACpBH,EAAgB,KAAK,KAAK,WAAW,YAAYG,EAAO,CAAC,CAAC,CAAC,CAE/D,CAEA,IAAIC,EAAuB,CAAC,EAE5B,GAAIJ,EAAgB,OAAS,EAE3BI,EAAa,MAAM,KAAK,OAAO,OAAOJ,CAAe,MAChD,CAIL,IAAMK,EAAgB,OAAO,OAAOC,CAAa,EAAE,IAAKC,GACtD,KAAK,WAAW,aAAaA,CAAM,CACrC,EACAH,EAAa,MAAM,KAAK,OAAO,OAAOC,CAAa,CACrD,CAMA,IAAMG,EAAOT,EAAO,MAAQ,EACtBU,EAAOV,EAAO,MAAQ,GACtBW,EAAWN,EAAW,MAAMI,EAAMA,EAAOC,CAAI,EAG7CE,EAAsB,CAAC,EACvBC,EAAwB,CAAC,EAE/B,QAAWC,KAAMH,EAAU,CACzB,IAAMI,EAAU,MAAM,KAAK,WAAW,WAAWD,CAAE,EACnD,GAAIC,EAAS,CAMX,GAJIf,EAAO,QAEL,EADa,MAAM,QAAQA,EAAO,MAAM,EAAIA,EAAO,OAAS,CAACA,EAAO,MAAM,GAChE,SAASe,EAAQ,MAAM,GAEnCf,EAAO,OAEL,EADW,MAAM,QAAQA,EAAO,KAAK,EAAIA,EAAO,MAAQ,CAACA,EAAO,KAAK,GAC7D,SAASe,EAAQ,KAAK,EAAG,SAGvCH,EAAS,KAAKG,CAAO,CACvB,MAEEF,EAAY,KAAKC,CAAE,CAEvB,CAGA,OAAID,EAAY,OAAS,GACvB,KAAK,eAAeA,EAAab,CAAM,EAGlCY,CACT,CAEA,MAAc,eAAeI,EAAeC,EAAqC,CAC/E,IAAMC,EAAW,KAAK,OAAO,SAAS,EAChChB,EAAS,KAAK,WAAW,UAG3Be,EAAQ,WACVC,EAAS,KAAK,KAAK,WAAW,cAAcD,EAAQ,SAAS,EAAGD,CAAG,EAEjEC,EAAQ,cACVC,EAAS,KAAK,KAAK,WAAW,iBAAiBD,EAAQ,YAAY,EAAGD,CAAG,EAGvEC,EAAQ,SACO,MAAM,QAAQA,EAAQ,MAAM,EAAIA,EAAQ,OAAS,CAACA,EAAQ,MAAM,GACxE,QAASE,GAAM,CACtBD,EAAS,KAAK,KAAK,WAAW,aAAaC,CAAC,EAAGH,CAAG,CACpD,CAAC,EAECC,EAAQ,QACK,MAAM,QAAQA,EAAQ,KAAK,EAAIA,EAAQ,MAAQ,CAACA,EAAQ,KAAK,GACrE,QAASE,GAAM,CACpBD,EAAS,KAAK,KAAK,WAAW,YAAYC,CAAC,EAAGH,CAAG,CACnD,CAAC,EAGH,MAAME,EAAS,KAAK,EACpBtB,EAAO,MAAM,4DAA6D,CACxE,MAAOoB,EAAI,MACb,CAAC,CACH,CACF","names":["DispatcherQuery_exports","__export","DispatcherQuery","__toCommonJS","MessageStatus","import_debug","LogLevels","loggers","serializeArg","arg","createLogFunction","debugInstance","args","serialized","getLogger","scope","debug","infoInstance","warnInstance","errorInstance","logger","logger","getLogger","DispatcherQuery","repository","filter","keysToIntersect","prefix","statuses","states","messageIds","allStatusKeys","MessageStatus","status","skip","size","pagedIds","messages","idsToRemove","id","message","ids","context","pipeline","s"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/DispatcherQuery.ts","../../src/types/index.ts","../../src/utils/logger.ts"],"sourcesContent":["import { DispatcherRepository } from './DispatcherRepository.js';\nimport { Message, MessageState, MessageStatus, QueryFilter } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport IORedis from 'ioredis';\n\nconst logger = getLogger('DispatcherQuery');\n\nexport class DispatcherQuery {\n private repository: DispatcherRepository;\n\n constructor(repository: DispatcherRepository) {\n this.repository = repository;\n }\n\n private get client(): IORedis {\n return this.repository.redis;\n }\n\n public async query(filter: QueryFilter): Promise<Message[]> {\n const keysToIntersect: string[] = [];\n const prefix = this.repository['keyPrefix'];\n\n // 1. Build Intersection Sets using new INDEX keys\n if (filter.contactId) {\n keysToIntersect.push(this.repository.getContactKey(filter.contactId));\n }\n\n if (filter.descriptorId) {\n keysToIntersect.push(this.repository.getDescriptorKey(filter.descriptorId));\n }\n\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (statuses.length === 1) {\n keysToIntersect.push(this.repository.getStatusKey(statuses[0]));\n } else if (statuses.length > 1) {\n // For MVP, if multiple statuses, we intersect the FIRST one and filter the rest in memory.\n keysToIntersect.push(this.repository.getStatusKey(statuses[0]));\n }\n }\n\n // Now safe to use state index for ALL states (even SCHEDULED/QUEUED) because we index them as Sets too.\n if (filter.state) {\n const states = Array.isArray(filter.state) ? filter.state : [filter.state];\n if (states.length === 1) {\n keysToIntersect.push(this.repository.getStateKey(states[0]));\n }\n }\n\n let messageIds: string[] = [];\n\n if (keysToIntersect.length > 0) {\n // Execute Intersection\n messageIds = await this.client.sinter(keysToIntersect);\n } else {\n // Fallback: No filters provided (e.g. \"All\" selected without other filters)\n // We want to return ALL messages. We can do this by Unioning all Status sets.\n // This allows us to see Delivered/Failed messages even if not filtering by them.\n const allStatusKeys = Object.values(MessageStatus).map((status) =>\n this.repository.getStatusKey(status)\n );\n messageIds = await this.client.sunion(allStatusKeys);\n }\n\n // 2. Handle OR logic for multiple statuses/states if needed\n // (Omitted for MVP, assuming single values or handling in memory for small intersections)\n\n // 3. Paging\n const skip = filter.skip ?? 0;\n const size = filter.size ?? 50;\n const pagedIds = messageIds.slice(skip, skip + size);\n\n // 4. Fetch Messages & Lazy Cleanup\n const messages: Message[] = [];\n const idsToRemove: string[] = [];\n\n for (const id of pagedIds) {\n const message = await this.repository.getMessage(id);\n if (message) {\n // Validate in-memory filters (e.g. multiple statuses, or if SINTER wasn't perfect)\n if (filter.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];\n if (!statuses.includes(message.status)) continue;\n }\n if (filter.state) {\n const states = Array.isArray(filter.state) ? filter.state : [filter.state];\n if (!states.includes(message.state)) continue;\n }\n\n messages.push(message);\n } else {\n // Message ID exists in index but not in storage -> Expired\n idsToRemove.push(id);\n }\n }\n\n // Lazy Cleanup\n if (idsToRemove.length > 0) {\n this.cleanupIndices(idsToRemove, filter);\n }\n\n return messages;\n }\n\n private async cleanupIndices(ids: string[], context: QueryFilter): Promise<void> {\n const pipeline = this.client.pipeline();\n const prefix = this.repository['keyPrefix'];\n\n // Basic cleanup: Remove from the keys we just queried.\n if (context.contactId) {\n pipeline.srem(this.repository.getContactKey(context.contactId), ids);\n }\n if (context.descriptorId) {\n pipeline.srem(this.repository.getDescriptorKey(context.descriptorId), ids);\n }\n\n if (context.status) {\n const statuses = Array.isArray(context.status) ? context.status : [context.status];\n statuses.forEach((s) => {\n pipeline.srem(this.repository.getStatusKey(s), ids);\n });\n }\n if (context.state) {\n const states = Array.isArray(context.state) ? context.state : [context.state];\n states.forEach((s) => {\n pipeline.srem(this.repository.getStateKey(s), ids);\n });\n }\n\n await pipeline.exec();\n logger.debug('[cleanupIndices] Removed expired IDs from checked indices', {\n count: ids.length,\n });\n }\n}\n","import { Vnd } from './blip';\n\nexport type Contact = Omit<Vnd.Iris.Contact, 'identity'>;\n\nexport type MessageData = {\n type: string;\n content: Record<string, any> | string;\n};\n\nexport type MessagePayload = Record<string, any> | Array<any> | string;\n\nexport enum MessageState {\n INIT = 'INIT',\n DISPATCHED = 'DISPATCHED',\n SCHEDULED = 'SCHEDULED',\n QUEUED = 'QUEUED',\n FINAL = 'FINAL',\n}\n\nexport enum MessageStatus {\n INIT = 'INIT',\n PENDING = 'PENDING',\n SENDING = 'SENDING',\n DELIVERED = 'DELIVERED',\n READ = 'READ',\n REPLIED = 'REPLIED',\n FAILED = 'FAILED',\n CANCELED = 'CANCELED',\n}\n\nexport enum DispatchState {\n ACCEPTED = 'accepted',\n DISPATCHED = 'dispatched',\n RECEIVED = 'received',\n CONSUMED = 'consumed',\n FAILED = 'failed',\n}\n\nexport interface Message {\n messageId: string;\n contactId: string;\n descriptorId: string;\n\n payload: MessageData;\n status: MessageStatus;\n state: MessageState;\n\n attempts?: number;\n retries?: number;\n\n options?: MessageOptions; // Persisted options for scheduled/retried messages\n\n expiresAt?: string; // ISO 8601 datetime\n\n scheduledTo?: string; // ISO 8601 datetime\n sentAt?: string; // ISO 8601 datetime\n acceptedAt?: string; // ISO 8601 datetime - When message was accepted by gateway (SENDING status)\n lastDispatchAttemptAt?: string; // ISO 8601 datetime - When last dispatch attempt started\n createdAt: string; // ISO 8601 datetime\n deliveredAt?: string; // ISO 8601 datetime\n readAt?: string; // ISO 8601 datetime\n repliedAt?: string; // ISO 8601 datetime\n failedAt?: string; // ISO 8601 datetime\n\n error?: string;\n}\n\nexport interface Shift {\n days: number; // Bitmask: use Weekdays enum (e.g., Weekdays.MONDAY | Weekdays.FRIDAY) or manual (31=Mon-Fri)\n start: string; // HH:mm format (e.g., \"09:00\")\n end: string; // HH:mm format (e.g., \"18:00\")\n gmt?: string; // Timezone offset (default: \"-3\" for BRT)\n}\n\nexport enum Weekdays {\n MONDAY = 1,\n TUESDAY = 2,\n WEDNESDAY = 4,\n THURSDAY = 8,\n FRIDAY = 16,\n SATURDAY = 32,\n SUNDAY = 64,\n}\n\nexport interface Intent {\n intent: string;\n dueDate?: string;\n event?: string;\n payload?: any | null;\n expired?: {\n event?: string;\n intent?: string;\n };\n}\n\nexport type CallbackEvent =\n | 'dispatch' // Before message send process begins\n | 'sending' // MessageStatus.SENDING - message accepted by gateway\n | 'delivered' // MessageStatus.DELIVERED - message successfully delivered\n | 'failed' // MessageStatus.FAILED - message failed permanently\n | 'read' // MessageStatus.READ - message read by recipient\n | 'replied' // MessageStatus.REPLIED - recipient replied\n | 'scheduled' // MessageState.SCHEDULED - message scheduled\n | 'retry' // Retry attempt scheduled\n | 'evicted' // Message evicted from state machine due to TTL\n | 'canceled'; // Message canceled before send\n\nexport type ConnectionConfig = {\n contract: string; // Contract ID (without https:// prefix)\n key: string; // Bot authorization key\n};\n\nexport interface MessageOptions {\n shifts?: Array<Shift>; // Business hours constraints\n contact?: Contact;\n state?: {\n // Bot state to set after send\n stateId: string;\n botId: string;\n botIds?: Array<string>; // Bot IDs to reset before setting state\n };\n intent?: string | Intent; // Intent to trigger after send\n finalStatus?: 'DELIVERED' | 'READ' | 'REPLIED'; // Default: 'DELIVERED'\n}\n\nexport interface DispatchMessageOptions extends MessageOptions {\n schedule?: string; // ISO 8601 datetime to schedule send\n}\n\nexport interface DispatcherOptions {\n maxRetries?: number; // Default: 0\n retryIntervals?: number[]; // Default: [1000, 5000, 15000] (exponential backoff in ms)\n\n // Storage Duration (Redis TTL)\n retention?: number; // Default: 3600000 ms (1 hour) - Time to keep message in storage after creation/dispatch\n\n // State Constraints (Force transition to FAILED)\n timeouts?: {\n pending?: number; // Default: 10000 ms (10 seconds)\n sending?: number; // Default: 60000 ms (60 seconds)\n };\n\n batchSize?: number; // Default: 100 messages per polling cycle\n pollingIntervals?: {\n scheduled?: number; // Default: 5000 ms\n pending?: number; // Default: 5000 ms\n sending?: number; // Default: 10000 ms\n delivered?: number; // Default: 1800000 ms (30 minutes)\n read?: number; // Default: 3600000 ms (60 minutes)\n queue?: number; // Default: 100 ms\n };\n maxQueueSize?: number; // Maximum number of pending/scheduled messages. Oldest evicted if exceeded.\n rateLimits?: {\n global?: { points: number; duration: number }; // Global rate limit (requests per duration)\n message?: { points: number; duration: number }; // Message sending rate limit (requests per duration)\n };\n}\n\nexport interface QueryFilter {\n contactId?: string;\n descriptorId?: string;\n status?: MessageStatus | MessageStatus[];\n state?: MessageState | MessageState[];\n size?: number;\n skip?: number;\n}\n\nexport interface DispatcherMetrics {\n total: number;\n byState: {\n [key in MessageState]?: number;\n };\n byStatus: {\n [key in MessageStatus]?: number;\n };\n cumulative: {\n dispatched: number;\n delivered: number;\n failed: number;\n };\n}\n\nexport interface DispatcherManifest {\n version: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * @deprecated Use DispatcherOptions instead\n */\nexport type MessageDispatcherOptions = DispatcherOptions;\n","/**\n * Logger utility using the 'debug' package.\n *\n * This module provides a configurable logger based on the 'debug' package.\n * By default, all logging is disabled. To enable logging, set the DEBUG\n * environment variable.\n *\n * The DEBUG variable can be a single namespace or a comma-separated list of\n * namespaces. Wildcards are supported.\n *\n * For example:\n * - `DEBUG=blip-api` enables all logs for the 'blip-api' scope.\n * - `DEBUG=blip-api:*` enables all sub-scopes under 'blip-api'.\n * - `DEBUG=*` enables all logs.\n *\n * The logger exposes different levels (debug, info, warn, error), which are\n * different namespaces.\n *\n * Example usage:\n * ```\n * import { getLogger } from './logger';\n *\n * const logger = getLogger('my-feature');\n *\n * logger.info('This is an info message');\n * logger.error('This is an error');\n * ```\n *\n * To see the output, run your application with:\n * `DEBUG=my-feature:* node your-app.js`\n */\n\nimport debug from 'debug';\n\n// Define the available log levels\nexport const LogLevels = {\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n};\n\nexport type LogLevel = keyof typeof LogLevels;\n\ntype LogFunction = (...args: any[]) => void;\n\nexport interface Logger {\n debug: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n error: LogFunction;\n}\n\nconst loggers = new Map<string, Logger>();\n\nfunction serializeArg(arg: any): any {\n if (arg === null || arg === undefined) return arg;\n if (typeof arg === 'object') {\n try {\n return JSON.stringify(arg, null, 2);\n } catch (e) {\n return arg;\n }\n }\n return arg;\n}\n\nfunction createLogFunction(debugInstance: debug.Debugger): LogFunction {\n return (...args: any[]) => {\n const serialized = args.map(serializeArg);\n (debugInstance as any)(...serialized);\n };\n}\n\n/**\n * Creates a logger with a specific scope.\n *\n * @param scope - The scope for the logger (e.g., 'blip-api', 'dispatcher').\n * @returns A logger object with methods for each log level.\n */\nexport function getLogger(scope: string): Logger {\n if (!loggers.has(scope)) {\n const debugInstance = debug(`${scope}:${LogLevels.debug}`);\n const infoInstance = debug(`${scope}:${LogLevels.info}`);\n const warnInstance = debug(`${scope}:${LogLevels.warn}`);\n const errorInstance = debug(`${scope}:${LogLevels.error}`);\n\n errorInstance.log = console.error.bind(console);\n\n const logger: Logger = {\n debug: createLogFunction(debugInstance),\n info: createLogFunction(infoInstance),\n warn: createLogFunction(warnInstance),\n error: createLogFunction(errorInstance),\n };\n\n loggers.set(scope, logger);\n }\n return loggers.get(scope)!;\n}\n\n/**\n * Enables logging for a specific scope and level.\n * This is a convenience function to programmatically enable logging,\n * equivalent to setting the DEBUG environment variable.\n *\n * @param scope - The scope to enable (e.g., 'blip-api').\n * @param level - The minimum log level to enable (e.g., 'info').\n */\nexport function enableLogger(namespaces: string): void {\n const currentDebug = debug.disable();\n debug.enable(`${currentDebug},${namespaces}`);\n}\n\n/**\n * Disables all logging.\n */\nexport function disableLogger(): void {\n debug.disable();\n}\n\n/**\n * Get all registered logger scopes.\n *\n * @returns An array of all registered scopes.\n */\nexport function getAllScopes(): string[] {\n return Array.from(loggers.keys());\n}\n"],"mappings":"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","names":["DispatcherQuery_exports","__export","DispatcherQuery","__toCommonJS","MessageStatus","import_debug","LogLevels","loggers","serializeArg","arg","createLogFunction","debugInstance","args","serialized","getLogger","scope","debug","infoInstance","warnInstance","errorInstance","logger","logger","getLogger","DispatcherQuery","repository","filter","keysToIntersect","prefix","statuses","states","messageIds","allStatusKeys","MessageStatus","status","skip","size","pagedIds","messages","idsToRemove","id","message","ids","context","pipeline","s"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a}from"../chunk-
|
|
1
|
+
import{a}from"../chunk-CVPGUSFU.mjs";import"../chunk-4LYB64T2.mjs";import"../chunk-OXXLVJVC.mjs";import"../chunk-JD53PFR4.mjs";export{a as DispatcherQuery};
|
|
2
2
|
//# sourceMappingURL=DispatcherQuery.mjs.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MessageState, MessageStatus, Message } from '../types/index.mjs';
|
|
1
|
+
import { MessageState, MessageStatus, Message, DispatcherManifest } from '../types/index.mjs';
|
|
2
2
|
import IORedis from 'ioredis';
|
|
3
3
|
import '../types/blip.mjs';
|
|
4
4
|
|
|
@@ -11,6 +11,7 @@ declare class DispatcherRepository {
|
|
|
11
11
|
setup(): Promise<void>;
|
|
12
12
|
teardown(): Promise<void>;
|
|
13
13
|
get redis(): IORedis;
|
|
14
|
+
getManifestKey(): string;
|
|
14
15
|
getKey(messageId: string): string;
|
|
15
16
|
getStateKey(state: MessageState): string;
|
|
16
17
|
getStatusKey(status: MessageStatus): string;
|
|
@@ -42,6 +43,8 @@ declare class DispatcherRepository {
|
|
|
42
43
|
id: string;
|
|
43
44
|
count: number;
|
|
44
45
|
}[]>;
|
|
46
|
+
writeManifest(manifest: DispatcherManifest): Promise<void>;
|
|
47
|
+
getManifest(): Promise<DispatcherManifest | null>;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export { DispatcherRepository };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var b=Object.create;var h=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var A=Object.getPrototypeOf,K=Object.prototype.hasOwnProperty;var x=(r,e)=>{for(var t in e)h(r,t,{get:e[t],enumerable:!0})},E=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of S(e))!K.call(r,n)&&n!==t&&h(r,n,{get:()=>e[n],enumerable:!(s=w(e,n))||s.enumerable});return r};var m=(r,e,t)=>(t=r!=null?b(A(r)):{},E(e||!r||!r.__esModule?h(t,"default",{value:r,enumerable:!0}):t,r)),L=r=>E(h({},"__esModule",{value:!0}),r);var P={};x(P,{DispatcherRepository:()=>D});module.exports=L(P);var g=m(require("debug")),p={debug:"debug",info:"info",warn:"warn",error:"error"},y=new Map;function M(r){if(r==null)return r;if(typeof r=="object")try{return JSON.stringify(r,null,2)}catch{return r}return r}function l(r){return(...e)=>{let t=e.map(M);r(...t)}}function f(r){if(!y.has(r)){let e=(0,g.default)(`${r}:${p.debug}`),t=(0,g.default)(`${r}:${p.info}`),s=(0,g.default)(`${r}:${p.warn}`),n=(0,g.default)(`${r}:${p.error}`);n.log=console.error.bind(console);let i={debug:l(e),info:l(t),warn:l(s),error:l(n)};y.set(r,i)}return y.get(r)}var I=m(require("ioredis")),u=f("Repository"),o=class o{constructor(e,t){this.client=new I.default(t,{maxRetriesPerRequest:null}),this.keyPrefix=`dwn-dispatcher:${e}`,this.client.on("error",s=>{u.error("[client] Redis error",s)})}async setup(){if(this.client.status==="ready"){u.debug("[setup] Redis already connected, skipping");return}this.client.status==="wait"&&await this.client.connect(),u.info("[setup] Repository connected",{status:this.client.status})}async teardown(){this.client.status!=="end"&&(await this.client.quit(),u.info("[teardown] Repository disconnected"))}get redis(){return this.client}getManifestKey(){return`${this.keyPrefix}:manifest`}getKey(e){return`${this.keyPrefix}:message:${e}`}getStateKey(e){return`${this.keyPrefix}:index:state:${e.toLowerCase()}`}getStatusKey(e){return`${this.keyPrefix}:index:status:${e.toLowerCase()}`}getContactKey(e){return`${this.keyPrefix}:index:contact:${e}`}getDescriptorKey(e){return`${this.keyPrefix}:index:descriptor:${e}`}getQueueKey(e){return`${this.keyPrefix}:queue:${e.toLowerCase()}`}async upsertMessage(e,t){let s=this.getKey(e.messageId),n=JSON.stringify(e),i=this.client.pipeline();i.set(s,n),e.contactId&&i.sadd(this.getContactKey(e.contactId),e.messageId),e.descriptorId&&i.sadd(this.getDescriptorKey(e.descriptorId),e.messageId);for(let c of o.INDEXED_STATUSES){let d=this.getStatusKey(c);e.status===c?i.sadd(d,e.messageId):i.srem(d,e.messageId)}for(let c of o.INDEXED_STATES){let d=this.getStateKey(c);e.state===c?i.sadd(d,e.messageId):i.srem(d,e.messageId)}let a=Date.now()+(t||36e5*24*2);if(e.state==="SCHEDULED"&&e.scheduledTo&&(a=new Date(e.scheduledTo).getTime()+(t||0)),i.zadd(this.getQueueKey("retention"),a,e.messageId),e.state==="SCHEDULED"&&e.scheduledTo){let c=new Date(e.scheduledTo).getTime();i.zadd(this.getQueueKey("scheduled"),c,e.messageId)}else i.zrem(this.getQueueKey("scheduled"),e.messageId);if(e.state==="QUEUED"){let c=new Date(e.createdAt||Date.now()).getTime();i.zadd(this.getQueueKey("queued"),c,e.messageId)}else i.zrem(this.getQueueKey("queued"),e.messageId);if(e.state==="DISPATCHED"){let c=new Date(e.createdAt||Date.now()).getTime();i.zadd(this.getQueueKey("dispatched"),c,e.messageId)}else i.zrem(this.getQueueKey("dispatched"),e.messageId);await i.exec(),u.debug("[upsertMessage]",{messageId:e.messageId,status:e.status,state:e.state})}async getMessage(e){let t=this.getKey(e),s=await this.client.get(t);return s?JSON.parse(s):null}async getMessages(e){let t=[];if(e.state==="SCHEDULED"){let n=Date.now(),i=e.skip??0,a=e.size??0;a>0?t=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,n,"LIMIT",i,a):t=await this.client.zrangebyscore(this.getQueueKey("scheduled"),0,n)}else if(e.state==="QUEUED")t=await this.client.zrange(this.getQueueKey("queued"),e.skip??0,(e.skip??0)+(e.size?e.size-1:-1));else if(e.state==="DISPATCHED")t=await this.client.zrange(this.getQueueKey("dispatched"),e.skip??0,(e.skip??0)+(e.size?e.size-1:-1));else if(e.status){let n=await this.client.smembers(this.getStatusKey(e.status)),i=e.skip??0,a=e.size;t=a?n.slice(i,i+a):n.slice(i)}else if(e.state)try{let n=await this.client.smembers(this.getStateKey(e.state)),i=e.skip??0,a=e.size;t=a?n.slice(i,i+a):n.slice(i)}catch{return[]}else return u.warn("[getMessages] no filter provided"),[];let s=[];for(let n of t){let i=await this.getMessage(n);if(i){if(e.status&&i.status!==e.status||e.state&&i.state!==e.state)continue;s.push(i)}}return u.debug("[getMessages]",{count:s.length,filter:e}),s}async getQueueSize(){return await this.client.zcard(this.getQueueKey("dispatched"))}async evictOldest(e){if(e<=0)return 0;let t=await this.client.zpopmin(this.getQueueKey("dispatched"),e),s=0;for(let n=0;n<t.length;n+=2){let i=t[n];await this.deleteMessage(i),s++}return s}async getExpiredMessages(e=50){let t=Date.now();return await this.client.zrangebyscore(this.getQueueKey("expiration"),0,t,"LIMIT",0,e)}async getRetentionMessages(e=50){let t=Date.now();return await this.client.zrangebyscore(this.getQueueKey("retention"),0,t,"LIMIT",0,e)}async incrementMetric(e,t=1){let s=`${this.keyPrefix}:metrics:${e}`;return await this.client.incrby(s,t)}async getMetric(e){let t=`${this.keyPrefix}:metrics:${e}`,s=await this.client.get(t);return s?parseInt(s,10):0}async deleteMessage(e){let t=await this.getMessage(e);t&&await this.deleteMessageData(e,t)}async deleteMessageData(e,t){let s=this.client.pipeline();s.del(this.getKey(e)),s.zrem(this.getQueueKey("scheduled"),e),s.zrem(this.getQueueKey("queued"),e),s.zrem(this.getQueueKey("dispatched"),e),s.zrem(this.getQueueKey("expiration"),e),s.zrem(this.getQueueKey("retention"),e);for(let n of o.INDEXED_STATUSES)s.srem(this.getStatusKey(n),e);for(let n of o.INDEXED_STATES)s.srem(this.getStateKey(n),e);t.contactId&&s.srem(this.getContactKey(t.contactId),e),t.descriptorId&&s.srem(this.getDescriptorKey(t.descriptorId),e),await s.exec()}async countMessages(e){if(e.state==="SCHEDULED")return await this.client.zcard(this.getQueueKey("scheduled"));if(e.state==="QUEUED")return await this.client.zcard(this.getQueueKey("queued"));if(e.state==="DISPATCHED")return await this.client.zcard(this.getQueueKey("dispatched"));if(e.status)return await this.client.scard(this.getStatusKey(e.status));if(e.state)try{return await this.client.scard(this.getStateKey(e.state))}catch{return 0}return 0}async getMetrics(e){let t={cumulative:{dispatched:0,delivered:0,failed:0},queues:{queued:0,scheduled:0,dispatched:0},status:{}},s=async a=>{if(e){let c=this.getDescriptorKey(e);return(await this.client.sinter(a,c)).length}return await this.client.scard(a)};for(let a of o.INDEXED_STATUSES){let c=await s(this.getStatusKey(a));t.status[a]=c,a==="DELIVERED"&&(t.cumulative.delivered=c),a==="FAILED"&&(t.cumulative.failed=c)}let n=this.getStateKey("DISPATCHED"),i=await s(n);return t.cumulative.dispatched=i+t.cumulative.delivered+t.cumulative.failed,e?(t.queues.queued=await s(this.getStateKey("QUEUED")),t.queues.scheduled=await s(this.getStateKey("SCHEDULED")),t.queues.dispatched=i):(t.queues.queued=await this.client.zcard(this.getQueueKey("queued")),t.queues.scheduled=await this.client.zcard(this.getQueueKey("scheduled")),t.queues.dispatched=await this.client.zcard(this.getQueueKey("dispatched"))),t}async getDescriptors(){let e=this.getDescriptorKey("*"),t=`${this.keyPrefix}:index:descriptor:`,s=await this.client.keys(e),n=[];for(let i of s){let a=i.slice(t.length);if(a){let c=await this.client.scard(i);n.push({id:a,count:c})}}return n.sort((i,a)=>a.count-i.count),n}async writeManifest(e){let t=this.getManifestKey();await this.client.hset(t,{version:e.version,createdAt:e.createdAt,updatedAt:e.updatedAt}),u.info("[writeManifest] Manifest written",{key:t})}async getManifest(){let e=this.getManifestKey(),t=await this.client.hgetall(e);return!t||Object.keys(t).length===0?null:t}};o.INDEXED_STATUSES=["INIT","PENDING","SENDING","DELIVERED","READ","REPLIED","FAILED","CANCELED"],o.INDEXED_STATES=["INIT","DISPATCHED","SCHEDULED","QUEUED","FINAL"];var D=o;0&&(module.exports={DispatcherRepository});
|
|
2
2
|
//# sourceMappingURL=DispatcherRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/DispatcherRepository.ts","../../src/utils/logger.ts"],"sourcesContent":["import { Message, MessageState, MessageStatus } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport IORedis from 'ioredis';\n\nconst logger = getLogger('Repository');\n\nexport class DispatcherRepository {\n private static readonly INDEXED_STATUSES = [\n MessageStatus.INIT,\n MessageStatus.PENDING,\n MessageStatus.SENDING,\n MessageStatus.DELIVERED,\n MessageStatus.READ,\n MessageStatus.REPLIED,\n MessageStatus.FAILED,\n MessageStatus.CANCELED,\n ];\n\n private static readonly INDEXED_STATES = [\n MessageState.INIT,\n MessageState.DISPATCHED,\n MessageState.SCHEDULED,\n MessageState.QUEUED,\n MessageState.FINAL,\n ];\n\n private client: IORedis;\n private keyPrefix: string;\n\n constructor(dispatcherId: string, redisUrl: string) {\n this.client = new IORedis(redisUrl, { maxRetriesPerRequest: null });\n this.keyPrefix = `dwn:dispatcher:${dispatcherId}`;\n\n this.client.on('error', (err) => {\n logger.error('[client] Redis error', err);\n });\n }\n\n async setup(): Promise<void> {\n if (this.client.status === 'ready') {\n logger.debug('[setup] Redis already connected, skipping');\n return;\n }\n // IORedis connects automatically, but we can wait for ready\n if (this.client.status === 'wait') {\n await this.client.connect();\n }\n logger.info('[setup] Repository connected', { status: this.client.status });\n }\n\n async teardown(): Promise<void> {\n if (this.client.status === 'end') {\n return;\n }\n await this.client.quit();\n logger.info('[teardown] Repository disconnected');\n }\n\n public get redis(): IORedis {\n return this.client;\n }\n\n public getKey(messageId: string): string {\n return `${this.keyPrefix}:message:${messageId}`;\n }\n\n public getStateKey(state: MessageState): string {\n return `${this.keyPrefix}:index:state:${state.toLowerCase()}`;\n }\n\n public getStatusKey(status: MessageStatus): string {\n return `${this.keyPrefix}:index:status:${status.toLowerCase()}`;\n }\n\n public getContactKey(contactId: string): string {\n return `${this.keyPrefix}:index:contact:${contactId}`;\n }\n\n public getDescriptorKey(descriptorId: string): string {\n return `${this.keyPrefix}:index:descriptor:${descriptorId}`;\n }\n\n public getQueueKey(\n name: 'scheduled' | 'queued' | 'dispatched' | 'expiration' | 'retention'\n ): string {\n return `${this.keyPrefix}:queue:${name.toLowerCase()}`;\n }\n\n async upsertMessage(message: Message, ttl?: number): Promise<void> {\n const key = this.getKey(message.messageId);\n const data = JSON.stringify(message);\n\n const pipeline = this.client.pipeline();\n\n pipeline.set(key, data);\n\n // --- INDICES (Sets for Querying) ---\n // Prefix: index:contact, index:descriptor, index:status, index:state\n\n // 1. Contact Index\n if (message.contactId) {\n pipeline.sadd(this.getContactKey(message.contactId), message.messageId);\n }\n // 2. Descriptor Index\n if (message.descriptorId) {\n pipeline.sadd(this.getDescriptorKey(message.descriptorId), message.messageId);\n }\n\n // 3. Status Index (ALL statuses are indexed)\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n const indexKey = this.getStatusKey(status);\n if (message.status === status) {\n pipeline.sadd(indexKey, message.messageId);\n } else {\n pipeline.srem(indexKey, message.messageId);\n }\n }\n\n // 4. State Index (ALL states are indexed)\n for (const state of DispatcherRepository.INDEXED_STATES) {\n const indexKey = this.getStateKey(state);\n if (message.state === state) {\n pipeline.sadd(indexKey, message.messageId);\n } else {\n pipeline.srem(indexKey, message.messageId);\n }\n }\n\n // --- QUEUES (ZSets for Processing) ---\n // Prefix: queue:scheduled, queue:queued, queue:dispatched, queue:retention, queue:expiration\n\n // 0. Retention Queue (Data Cleanup)\n // We ALWAYS enforce retention.\n let retentionTime = Date.now() + (ttl || 3600000 * 24 * 2); // Default to provided TTL or fallbacks (should be provided by Dispatcher)\n\n if (message.state === MessageState.SCHEDULED && message.scheduledTo) {\n // Retention starts after scheduled time\n retentionTime = new Date(message.scheduledTo).getTime() + (ttl || 0);\n }\n pipeline.zadd(this.getQueueKey('retention'), retentionTime, message.messageId);\n\n // 0.5 Expiration Queue (State Timeouts - PENDING/SENDING)\n // If message is in transient state, we might want to track it in queue:expiration for TimeoutMonitor\n // The Dispatcher currently handles TimeoutMonitor by scanning INDEXED PENDING/SENDING.\n // We can keep that logic or move to ZSET.\n // For now, let's strictly follow the plan: queue:retention for cleanup.\n // \"queue:expiration is reserved for state timeouts\" - Implementation of that would be in Dispatcher/StateMachine logic\n // explicitly adding to it, OR we can infer here.\n // Let's stick to existing TimeoutMonitor scanning Indices for now as it works,\n // unless we want to optimize. The user just wanted to separate concerns.\n // I will NOT add implicit queue:expiration logic here to avoid conflict with existing Monitor.\n // But I MUST remove usage of queue:expiration for *retention* purposes if it was there.\n // The previous code used queue:expiration for cleanup. I will rename that to queue:expiration -> queue:retention in effect.\n\n // 1. Scheduled Queue\n if (message.state === MessageState.SCHEDULED && message.scheduledTo) {\n const scheduledTime = new Date(message.scheduledTo).getTime();\n pipeline.zadd(this.getQueueKey('scheduled'), scheduledTime, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('scheduled'), message.messageId);\n }\n\n // 2. Queued Queue (FIFO)\n if (message.state === MessageState.QUEUED) {\n const score = new Date(message.createdAt || Date.now()).getTime();\n pipeline.zadd(this.getQueueKey('queued'), score, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('queued'), message.messageId);\n }\n\n // 3. Dispatched Queue (LRU/Tracking)\n if (message.state === MessageState.DISPATCHED) {\n const score = new Date(message.createdAt || Date.now()).getTime();\n pipeline.zadd(this.getQueueKey('dispatched'), score, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('dispatched'), message.messageId);\n }\n\n await pipeline.exec();\n\n logger.debug('[upsertMessage]', {\n messageId: message.messageId,\n status: message.status,\n state: message.state,\n });\n }\n\n async getMessage(messageId: string): Promise<Message | null> {\n const key = this.getKey(messageId);\n const data = await this.client.get(key);\n\n if (!data) {\n return null;\n }\n\n return JSON.parse(data) as Message;\n }\n\n async getMessages(filter: {\n status?: MessageStatus;\n state?: MessageState;\n size?: number;\n skip?: number;\n }): Promise<Message[]> {\n let messageIds: string[] = [];\n\n // Note: DispatcherQuery is now the preferred way to query with complex filters.\n // This method is kept for backward compatibility and simple state/queue polling.\n\n if (filter.state === MessageState.SCHEDULED) {\n const now = Date.now();\n const skip = filter.skip ?? 0;\n const size = filter.size ?? 0;\n\n // Use the QUEUE key for scheduled checking\n // IORedis: zrangebyscore(key, min, max, 'LIMIT', offset, count)\n if (size > 0) {\n messageIds = await this.client.zrangebyscore(\n this.getQueueKey('scheduled'),\n 0,\n now,\n 'LIMIT',\n skip,\n size\n );\n } else {\n messageIds = await this.client.zrangebyscore(this.getQueueKey('scheduled'), 0, now);\n }\n } else if (filter.state === MessageState.QUEUED) {\n // Use the QUEUE key for FIFO processing\n messageIds = await this.client.zrange(\n this.getQueueKey('queued'),\n filter.skip ?? 0,\n (filter.skip ?? 0) + (filter.size ? filter.size - 1 : -1)\n );\n } else if (filter.state === MessageState.DISPATCHED) {\n // Use the QUEUE key for Tracking\n messageIds = await this.client.zrange(\n this.getQueueKey('dispatched'),\n filter.skip ?? 0,\n (filter.skip ?? 0) + (filter.size ? filter.size - 1 : -1)\n );\n } else if (filter.status) {\n // Use the INDEX key for status\n const allIds = await this.client.smembers(this.getStatusKey(filter.status));\n const skip = filter.skip ?? 0;\n const size = filter.size;\n messageIds = size ? allIds.slice(skip, skip + size) : allIds.slice(skip);\n } else if (filter.state) {\n // Use the INDEX key for generic state querying\n try {\n const allIds = await this.client.smembers(this.getStateKey(filter.state));\n const skip = filter.skip ?? 0;\n const size = filter.size;\n messageIds = size ? allIds.slice(skip, skip + size) : allIds.slice(skip);\n } catch (error) {\n return [];\n }\n } else {\n logger.warn('[getMessages] no filter provided');\n return [];\n }\n\n const messages: Message[] = [];\n for (const messageId of messageIds) {\n const message = await this.getMessage(messageId);\n if (message) {\n if (filter.status && message.status !== filter.status) {\n continue;\n }\n if (filter.state && message.state !== filter.state) {\n continue;\n }\n messages.push(message);\n }\n }\n\n logger.debug('[getMessages]', { count: messages.length, filter });\n return messages;\n }\n\n async getQueueSize(): Promise<number> {\n return await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n async evictOldest(count: number): Promise<number> {\n if (count <= 0) return 0;\n\n // Evict from DISPATCHED ZSet (Oldest first -> min score)\n // IORedis: zpopmin(key, count)\n const popped = await this.client.zpopmin(this.getQueueKey('dispatched'), count);\n // popped is [value1, score1, value2, score2, ...]\n\n let evicted = 0;\n for (let i = 0; i < popped.length; i += 2) {\n const messageId = popped[i];\n await this.deleteMessage(messageId);\n evicted++;\n }\n\n return evicted;\n }\n\n async getExpiredMessages(count: number = 50): Promise<string[]> {\n const now = Date.now();\n return await this.client.zrangebyscore(\n this.getQueueKey('expiration'),\n 0,\n now,\n 'LIMIT',\n 0,\n count\n );\n }\n\n async getRetentionMessages(count: number = 50): Promise<string[]> {\n const now = Date.now();\n return await this.client.zrangebyscore(\n this.getQueueKey('retention'),\n 0,\n now,\n 'LIMIT',\n 0,\n count\n );\n }\n\n async incrementMetric(key: string, value: number = 1): Promise<number> {\n const metricKey = `${this.keyPrefix}:metrics:${key}`;\n return await this.client.incrby(metricKey, value);\n }\n\n async getMetric(key: string): Promise<number> {\n const metricKey = `${this.keyPrefix}:metrics:${key}`;\n const value = await this.client.get(metricKey);\n return value ? parseInt(value, 10) : 0;\n }\n\n async deleteMessage(messageId: string): Promise<void> {\n const msg = await this.getMessage(messageId);\n if (!msg) return;\n await this.deleteMessageData(messageId, msg);\n }\n\n private async deleteMessageData(messageId: string, message: Message): Promise<void> {\n const pipeline = this.client.pipeline();\n pipeline.del(this.getKey(messageId));\n\n // Remove from Queues\n pipeline.zrem(this.getQueueKey('scheduled'), messageId);\n pipeline.zrem(this.getQueueKey('queued'), messageId);\n pipeline.zrem(this.getQueueKey('dispatched'), messageId);\n pipeline.zrem(this.getQueueKey('expiration'), messageId);\n pipeline.zrem(this.getQueueKey('retention'), messageId);\n\n // Remove from Indices\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n pipeline.srem(this.getStatusKey(status), messageId);\n }\n\n for (const state of DispatcherRepository.INDEXED_STATES) {\n pipeline.srem(this.getStateKey(state), messageId);\n }\n\n if (message.contactId) {\n pipeline.srem(this.getContactKey(message.contactId), messageId);\n }\n\n if (message.descriptorId) {\n pipeline.srem(this.getDescriptorKey(message.descriptorId), messageId);\n }\n\n await pipeline.exec();\n }\n\n async countMessages(filter: { status?: MessageStatus; state?: MessageState }): Promise<number> {\n if (filter.state === MessageState.SCHEDULED) {\n return await this.client.zcard(this.getQueueKey('scheduled'));\n }\n\n if (filter.state === MessageState.QUEUED) {\n return await this.client.zcard(this.getQueueKey('queued'));\n }\n\n if (filter.state === MessageState.DISPATCHED) {\n return await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n if (filter.status) {\n return await this.client.scard(this.getStatusKey(filter.status));\n }\n\n if (filter.state) {\n try {\n return await this.client.scard(this.getStateKey(filter.state));\n } catch (error) {\n return 0;\n }\n }\n\n return 0;\n }\n\n async getMetrics(descriptorId?: string): Promise<any> {\n const metrics: any = {\n cumulative: { dispatched: 0, delivered: 0, failed: 0 },\n queues: { queued: 0, scheduled: 0, dispatched: 0 },\n status: {},\n };\n\n // Helper to get count\n const getCount = async (setKey: string) => {\n if (descriptorId) {\n const descriptorKey = this.getDescriptorKey(descriptorId);\n const intersection = await this.client.sinter(setKey, descriptorKey);\n return intersection.length;\n }\n return await this.client.scard(setKey);\n };\n\n // Status Counts\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n const count = await getCount(this.getStatusKey(status));\n metrics.status[status] = count;\n\n if (status === MessageStatus.DELIVERED) metrics.cumulative.delivered = count;\n if (status === MessageStatus.FAILED) metrics.cumulative.failed = count;\n }\n\n // Dispatched Total (approximate)\n const dispatchedKey = this.getStateKey(MessageState.DISPATCHED);\n const dispatchedCount = await getCount(dispatchedKey);\n metrics.cumulative.dispatched =\n dispatchedCount + metrics.cumulative.delivered + metrics.cumulative.failed;\n\n // Queues\n if (descriptorId) {\n // Filtered queues via indices (approximate intersection using state sets)\n metrics.queues.queued = await getCount(this.getStateKey(MessageState.QUEUED));\n metrics.queues.scheduled = await getCount(this.getStateKey(MessageState.SCHEDULED));\n metrics.queues.dispatched = dispatchedCount;\n } else {\n // Direct queue counts\n metrics.queues.queued = await this.client.zcard(this.getQueueKey('queued'));\n metrics.queues.scheduled = await this.client.zcard(this.getQueueKey('scheduled'));\n metrics.queues.dispatched = await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n return metrics;\n }\n\n async getDescriptors(): Promise<{ id: string; count: number }[]> {\n const pattern = this.getDescriptorKey('*');\n const keys = await this.client.keys(pattern);\n\n const descriptors = [];\n for (const key of keys) {\n const id = key.split(':').pop();\n if (id) {\n const count = await this.client.scard(key);\n descriptors.push({ id, count });\n }\n }\n\n descriptors.sort((a, b) => b.count - a.count);\n return descriptors;\n }\n}\n","/**\n * Logger utility using the 'debug' package.\n *\n * This module provides a configurable logger based on the 'debug' package.\n * By default, all logging is disabled. To enable logging, set the DEBUG\n * environment variable.\n *\n * The DEBUG variable can be a single namespace or a comma-separated list of\n * namespaces. Wildcards are supported.\n *\n * For example:\n * - `DEBUG=blip-api` enables all logs for the 'blip-api' scope.\n * - `DEBUG=blip-api:*` enables all sub-scopes under 'blip-api'.\n * - `DEBUG=*` enables all logs.\n *\n * The logger exposes different levels (debug, info, warn, error), which are\n * different namespaces.\n *\n * Example usage:\n * ```\n * import { getLogger } from './logger';\n *\n * const logger = getLogger('my-feature');\n *\n * logger.info('This is an info message');\n * logger.error('This is an error');\n * ```\n *\n * To see the output, run your application with:\n * `DEBUG=my-feature:* node your-app.js`\n */\n\nimport debug from 'debug';\n\n// Define the available log levels\nexport const LogLevels = {\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n};\n\nexport type LogLevel = keyof typeof LogLevels;\n\ntype LogFunction = (...args: any[]) => void;\n\nexport interface Logger {\n debug: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n error: LogFunction;\n}\n\nconst loggers = new Map<string, Logger>();\n\nfunction serializeArg(arg: any): any {\n if (arg === null || arg === undefined) return arg;\n if (typeof arg === 'object') {\n try {\n return JSON.stringify(arg, null, 2);\n } catch (e) {\n return arg;\n }\n }\n return arg;\n}\n\nfunction createLogFunction(debugInstance: debug.Debugger): LogFunction {\n return (...args: any[]) => {\n const serialized = args.map(serializeArg);\n (debugInstance as any)(...serialized);\n };\n}\n\n/**\n * Creates a logger with a specific scope.\n *\n * @param scope - The scope for the logger (e.g., 'blip-api', 'dispatcher').\n * @returns A logger object with methods for each log level.\n */\nexport function getLogger(scope: string): Logger {\n if (!loggers.has(scope)) {\n const debugInstance = debug(`${scope}:${LogLevels.debug}`);\n const infoInstance = debug(`${scope}:${LogLevels.info}`);\n const warnInstance = debug(`${scope}:${LogLevels.warn}`);\n const errorInstance = debug(`${scope}:${LogLevels.error}`);\n\n errorInstance.log = console.error.bind(console);\n\n const logger: Logger = {\n debug: createLogFunction(debugInstance),\n info: createLogFunction(infoInstance),\n warn: createLogFunction(warnInstance),\n error: createLogFunction(errorInstance),\n };\n\n loggers.set(scope, logger);\n }\n return loggers.get(scope)!;\n}\n\n/**\n * Enables logging for a specific scope and level.\n * This is a convenience function to programmatically enable logging,\n * equivalent to setting the DEBUG environment variable.\n *\n * @param scope - The scope to enable (e.g., 'blip-api').\n * @param level - The minimum log level to enable (e.g., 'info').\n */\nexport function enableLogger(namespaces: string): void {\n const currentDebug = debug.disable();\n debug.enable(`${currentDebug},${namespaces}`);\n}\n\n/**\n * Disables all logging.\n */\nexport function disableLogger(): void {\n debug.disable();\n}\n\n/**\n * Get all registered logger scopes.\n *\n * @returns An array of all registered scopes.\n */\nexport function getAllScopes(): string[] {\n return Array.from(loggers.keys());\n}\n"],"mappings":"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","names":["DispatcherRepository_exports","__export","DispatcherRepository","__toCommonJS","import_debug","LogLevels","loggers","serializeArg","arg","createLogFunction","debugInstance","args","serialized","getLogger","scope","debug","infoInstance","warnInstance","errorInstance","logger","import_ioredis","logger","getLogger","_DispatcherRepository","dispatcherId","redisUrl","IORedis","err","messageId","state","status","contactId","descriptorId","name","message","ttl","key","data","pipeline","indexKey","retentionTime","scheduledTime","score","filter","messageIds","now","skip","size","allIds","messages","count","popped","evicted","i","value","metricKey","msg","metrics","getCount","setKey","descriptorKey","dispatchedKey","dispatchedCount","pattern","keys","descriptors","id","a","b","DispatcherRepository"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/DispatcherRepository.ts","../../src/utils/logger.ts"],"sourcesContent":["import { DispatcherManifest, Message, MessageState, MessageStatus } from '../types/index.js';\nimport { getLogger } from '../utils/logger.js';\nimport IORedis from 'ioredis';\n\nconst logger = getLogger('Repository');\n\nexport class DispatcherRepository {\n private static readonly INDEXED_STATUSES = [\n MessageStatus.INIT,\n MessageStatus.PENDING,\n MessageStatus.SENDING,\n MessageStatus.DELIVERED,\n MessageStatus.READ,\n MessageStatus.REPLIED,\n MessageStatus.FAILED,\n MessageStatus.CANCELED,\n ];\n\n private static readonly INDEXED_STATES = [\n MessageState.INIT,\n MessageState.DISPATCHED,\n MessageState.SCHEDULED,\n MessageState.QUEUED,\n MessageState.FINAL,\n ];\n\n private client: IORedis;\n private keyPrefix: string;\n\n constructor(dispatcherId: string, redisUrl: string) {\n this.client = new IORedis(redisUrl, { maxRetriesPerRequest: null });\n this.keyPrefix = `dwn-dispatcher:${dispatcherId}`;\n\n this.client.on('error', (err) => {\n logger.error('[client] Redis error', err);\n });\n }\n\n async setup(): Promise<void> {\n if (this.client.status === 'ready') {\n logger.debug('[setup] Redis already connected, skipping');\n return;\n }\n // IORedis connects automatically, but we can wait for ready\n if (this.client.status === 'wait') {\n await this.client.connect();\n }\n logger.info('[setup] Repository connected', { status: this.client.status });\n }\n\n async teardown(): Promise<void> {\n if (this.client.status === 'end') {\n return;\n }\n await this.client.quit();\n logger.info('[teardown] Repository disconnected');\n }\n\n public get redis(): IORedis {\n return this.client;\n }\n\n public getManifestKey(): string {\n return `${this.keyPrefix}:manifest`;\n }\n\n public getKey(messageId: string): string {\n return `${this.keyPrefix}:message:${messageId}`;\n }\n\n public getStateKey(state: MessageState): string {\n return `${this.keyPrefix}:index:state:${state.toLowerCase()}`;\n }\n\n public getStatusKey(status: MessageStatus): string {\n return `${this.keyPrefix}:index:status:${status.toLowerCase()}`;\n }\n\n public getContactKey(contactId: string): string {\n return `${this.keyPrefix}:index:contact:${contactId}`;\n }\n\n public getDescriptorKey(descriptorId: string): string {\n return `${this.keyPrefix}:index:descriptor:${descriptorId}`;\n }\n\n public getQueueKey(\n name: 'scheduled' | 'queued' | 'dispatched' | 'expiration' | 'retention'\n ): string {\n return `${this.keyPrefix}:queue:${name.toLowerCase()}`;\n }\n\n async upsertMessage(message: Message, ttl?: number): Promise<void> {\n const key = this.getKey(message.messageId);\n const data = JSON.stringify(message);\n\n const pipeline = this.client.pipeline();\n\n pipeline.set(key, data);\n\n // --- INDICES (Sets for Querying) ---\n // Prefix: index:contact, index:descriptor, index:status, index:state\n\n // 1. Contact Index\n if (message.contactId) {\n pipeline.sadd(this.getContactKey(message.contactId), message.messageId);\n }\n // 2. Descriptor Index\n if (message.descriptorId) {\n pipeline.sadd(this.getDescriptorKey(message.descriptorId), message.messageId);\n }\n\n // 3. Status Index (ALL statuses are indexed)\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n const indexKey = this.getStatusKey(status);\n if (message.status === status) {\n pipeline.sadd(indexKey, message.messageId);\n } else {\n pipeline.srem(indexKey, message.messageId);\n }\n }\n\n // 4. State Index (ALL states are indexed)\n for (const state of DispatcherRepository.INDEXED_STATES) {\n const indexKey = this.getStateKey(state);\n if (message.state === state) {\n pipeline.sadd(indexKey, message.messageId);\n } else {\n pipeline.srem(indexKey, message.messageId);\n }\n }\n\n // --- QUEUES (ZSets for Processing) ---\n // Prefix: queue:scheduled, queue:queued, queue:dispatched, queue:retention, queue:expiration\n\n // 0. Retention Queue (Data Cleanup)\n // We ALWAYS enforce retention.\n let retentionTime = Date.now() + (ttl || 3600000 * 24 * 2); // Default to provided TTL or fallbacks (should be provided by Dispatcher)\n\n if (message.state === MessageState.SCHEDULED && message.scheduledTo) {\n // Retention starts after scheduled time\n retentionTime = new Date(message.scheduledTo).getTime() + (ttl || 0);\n }\n pipeline.zadd(this.getQueueKey('retention'), retentionTime, message.messageId);\n\n // 0.5 Expiration Queue (State Timeouts - PENDING/SENDING)\n // If message is in transient state, we might want to track it in queue:expiration for TimeoutMonitor\n // The Dispatcher currently handles TimeoutMonitor by scanning INDEXED PENDING/SENDING.\n // We can keep that logic or move to ZSET.\n // For now, let's strictly follow the plan: queue:retention for cleanup.\n // \"queue:expiration is reserved for state timeouts\" - Implementation of that would be in Dispatcher/StateMachine logic\n // explicitly adding to it, OR we can infer here.\n // Let's stick to existing TimeoutMonitor scanning Indices for now as it works,\n // unless we want to optimize. The user just wanted to separate concerns.\n // I will NOT add implicit queue:expiration logic here to avoid conflict with existing Monitor.\n // But I MUST remove usage of queue:expiration for *retention* purposes if it was there.\n // The previous code used queue:expiration for cleanup. I will rename that to queue:expiration -> queue:retention in effect.\n\n // 1. Scheduled Queue\n if (message.state === MessageState.SCHEDULED && message.scheduledTo) {\n const scheduledTime = new Date(message.scheduledTo).getTime();\n pipeline.zadd(this.getQueueKey('scheduled'), scheduledTime, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('scheduled'), message.messageId);\n }\n\n // 2. Queued Queue (FIFO)\n if (message.state === MessageState.QUEUED) {\n const score = new Date(message.createdAt || Date.now()).getTime();\n pipeline.zadd(this.getQueueKey('queued'), score, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('queued'), message.messageId);\n }\n\n // 3. Dispatched Queue (LRU/Tracking)\n if (message.state === MessageState.DISPATCHED) {\n const score = new Date(message.createdAt || Date.now()).getTime();\n pipeline.zadd(this.getQueueKey('dispatched'), score, message.messageId);\n } else {\n pipeline.zrem(this.getQueueKey('dispatched'), message.messageId);\n }\n\n await pipeline.exec();\n\n logger.debug('[upsertMessage]', {\n messageId: message.messageId,\n status: message.status,\n state: message.state,\n });\n }\n\n async getMessage(messageId: string): Promise<Message | null> {\n const key = this.getKey(messageId);\n const data = await this.client.get(key);\n\n if (!data) {\n return null;\n }\n\n return JSON.parse(data) as Message;\n }\n\n async getMessages(filter: {\n status?: MessageStatus;\n state?: MessageState;\n size?: number;\n skip?: number;\n }): Promise<Message[]> {\n let messageIds: string[] = [];\n\n // Note: DispatcherQuery is now the preferred way to query with complex filters.\n // This method is kept for backward compatibility and simple state/queue polling.\n\n if (filter.state === MessageState.SCHEDULED) {\n const now = Date.now();\n const skip = filter.skip ?? 0;\n const size = filter.size ?? 0;\n\n // Use the QUEUE key for scheduled checking\n // IORedis: zrangebyscore(key, min, max, 'LIMIT', offset, count)\n if (size > 0) {\n messageIds = await this.client.zrangebyscore(\n this.getQueueKey('scheduled'),\n 0,\n now,\n 'LIMIT',\n skip,\n size\n );\n } else {\n messageIds = await this.client.zrangebyscore(this.getQueueKey('scheduled'), 0, now);\n }\n } else if (filter.state === MessageState.QUEUED) {\n // Use the QUEUE key for FIFO processing\n messageIds = await this.client.zrange(\n this.getQueueKey('queued'),\n filter.skip ?? 0,\n (filter.skip ?? 0) + (filter.size ? filter.size - 1 : -1)\n );\n } else if (filter.state === MessageState.DISPATCHED) {\n // Use the QUEUE key for Tracking\n messageIds = await this.client.zrange(\n this.getQueueKey('dispatched'),\n filter.skip ?? 0,\n (filter.skip ?? 0) + (filter.size ? filter.size - 1 : -1)\n );\n } else if (filter.status) {\n // Use the INDEX key for status\n const allIds = await this.client.smembers(this.getStatusKey(filter.status));\n const skip = filter.skip ?? 0;\n const size = filter.size;\n messageIds = size ? allIds.slice(skip, skip + size) : allIds.slice(skip);\n } else if (filter.state) {\n // Use the INDEX key for generic state querying\n try {\n const allIds = await this.client.smembers(this.getStateKey(filter.state));\n const skip = filter.skip ?? 0;\n const size = filter.size;\n messageIds = size ? allIds.slice(skip, skip + size) : allIds.slice(skip);\n } catch (error) {\n return [];\n }\n } else {\n logger.warn('[getMessages] no filter provided');\n return [];\n }\n\n const messages: Message[] = [];\n for (const messageId of messageIds) {\n const message = await this.getMessage(messageId);\n if (message) {\n if (filter.status && message.status !== filter.status) {\n continue;\n }\n if (filter.state && message.state !== filter.state) {\n continue;\n }\n messages.push(message);\n }\n }\n\n logger.debug('[getMessages]', { count: messages.length, filter });\n return messages;\n }\n\n async getQueueSize(): Promise<number> {\n return await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n async evictOldest(count: number): Promise<number> {\n if (count <= 0) return 0;\n\n // Evict from DISPATCHED ZSet (Oldest first -> min score)\n // IORedis: zpopmin(key, count)\n const popped = await this.client.zpopmin(this.getQueueKey('dispatched'), count);\n // popped is [value1, score1, value2, score2, ...]\n\n let evicted = 0;\n for (let i = 0; i < popped.length; i += 2) {\n const messageId = popped[i];\n await this.deleteMessage(messageId);\n evicted++;\n }\n\n return evicted;\n }\n\n async getExpiredMessages(count: number = 50): Promise<string[]> {\n const now = Date.now();\n return await this.client.zrangebyscore(\n this.getQueueKey('expiration'),\n 0,\n now,\n 'LIMIT',\n 0,\n count\n );\n }\n\n async getRetentionMessages(count: number = 50): Promise<string[]> {\n const now = Date.now();\n return await this.client.zrangebyscore(\n this.getQueueKey('retention'),\n 0,\n now,\n 'LIMIT',\n 0,\n count\n );\n }\n\n async incrementMetric(key: string, value: number = 1): Promise<number> {\n const metricKey = `${this.keyPrefix}:metrics:${key}`;\n return await this.client.incrby(metricKey, value);\n }\n\n async getMetric(key: string): Promise<number> {\n const metricKey = `${this.keyPrefix}:metrics:${key}`;\n const value = await this.client.get(metricKey);\n return value ? parseInt(value, 10) : 0;\n }\n\n async deleteMessage(messageId: string): Promise<void> {\n const msg = await this.getMessage(messageId);\n if (!msg) return;\n await this.deleteMessageData(messageId, msg);\n }\n\n private async deleteMessageData(messageId: string, message: Message): Promise<void> {\n const pipeline = this.client.pipeline();\n pipeline.del(this.getKey(messageId));\n\n // Remove from Queues\n pipeline.zrem(this.getQueueKey('scheduled'), messageId);\n pipeline.zrem(this.getQueueKey('queued'), messageId);\n pipeline.zrem(this.getQueueKey('dispatched'), messageId);\n pipeline.zrem(this.getQueueKey('expiration'), messageId);\n pipeline.zrem(this.getQueueKey('retention'), messageId);\n\n // Remove from Indices\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n pipeline.srem(this.getStatusKey(status), messageId);\n }\n\n for (const state of DispatcherRepository.INDEXED_STATES) {\n pipeline.srem(this.getStateKey(state), messageId);\n }\n\n if (message.contactId) {\n pipeline.srem(this.getContactKey(message.contactId), messageId);\n }\n\n if (message.descriptorId) {\n pipeline.srem(this.getDescriptorKey(message.descriptorId), messageId);\n }\n\n await pipeline.exec();\n }\n\n async countMessages(filter: { status?: MessageStatus; state?: MessageState }): Promise<number> {\n if (filter.state === MessageState.SCHEDULED) {\n return await this.client.zcard(this.getQueueKey('scheduled'));\n }\n\n if (filter.state === MessageState.QUEUED) {\n return await this.client.zcard(this.getQueueKey('queued'));\n }\n\n if (filter.state === MessageState.DISPATCHED) {\n return await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n if (filter.status) {\n return await this.client.scard(this.getStatusKey(filter.status));\n }\n\n if (filter.state) {\n try {\n return await this.client.scard(this.getStateKey(filter.state));\n } catch (error) {\n return 0;\n }\n }\n\n return 0;\n }\n\n async getMetrics(descriptorId?: string): Promise<any> {\n const metrics: any = {\n cumulative: { dispatched: 0, delivered: 0, failed: 0 },\n queues: { queued: 0, scheduled: 0, dispatched: 0 },\n status: {},\n };\n\n // Helper to get count\n const getCount = async (setKey: string) => {\n if (descriptorId) {\n const descriptorKey = this.getDescriptorKey(descriptorId);\n const intersection = await this.client.sinter(setKey, descriptorKey);\n return intersection.length;\n }\n return await this.client.scard(setKey);\n };\n\n // Status Counts\n for (const status of DispatcherRepository.INDEXED_STATUSES) {\n const count = await getCount(this.getStatusKey(status));\n metrics.status[status] = count;\n\n if (status === MessageStatus.DELIVERED) metrics.cumulative.delivered = count;\n if (status === MessageStatus.FAILED) metrics.cumulative.failed = count;\n }\n\n // Dispatched Total (approximate)\n const dispatchedKey = this.getStateKey(MessageState.DISPATCHED);\n const dispatchedCount = await getCount(dispatchedKey);\n metrics.cumulative.dispatched =\n dispatchedCount + metrics.cumulative.delivered + metrics.cumulative.failed;\n\n // Queues\n if (descriptorId) {\n // Filtered queues via indices (approximate intersection using state sets)\n metrics.queues.queued = await getCount(this.getStateKey(MessageState.QUEUED));\n metrics.queues.scheduled = await getCount(this.getStateKey(MessageState.SCHEDULED));\n metrics.queues.dispatched = dispatchedCount;\n } else {\n // Direct queue counts\n metrics.queues.queued = await this.client.zcard(this.getQueueKey('queued'));\n metrics.queues.scheduled = await this.client.zcard(this.getQueueKey('scheduled'));\n metrics.queues.dispatched = await this.client.zcard(this.getQueueKey('dispatched'));\n }\n\n return metrics;\n }\n\n async getDescriptors(): Promise<{ id: string; count: number }[]> {\n const pattern = this.getDescriptorKey('*');\n const descriptorPrefix = `${this.keyPrefix}:index:descriptor:`;\n const keys = await this.client.keys(pattern);\n\n const descriptors = [];\n for (const key of keys) {\n const id = key.slice(descriptorPrefix.length);\n if (id) {\n const count = await this.client.scard(key);\n descriptors.push({ id, count });\n }\n }\n\n descriptors.sort((a, b) => b.count - a.count);\n return descriptors;\n }\n\n async writeManifest(manifest: DispatcherManifest): Promise<void> {\n const key = this.getManifestKey();\n await this.client.hset(key, {\n version: manifest.version,\n createdAt: manifest.createdAt,\n updatedAt: manifest.updatedAt,\n });\n logger.info('[writeManifest] Manifest written', { key });\n }\n\n async getManifest(): Promise<DispatcherManifest | null> {\n const key = this.getManifestKey();\n const data = await this.client.hgetall(key);\n if (!data || Object.keys(data).length === 0) {\n return null;\n }\n return data as unknown as DispatcherManifest;\n }\n}\n","/**\n * Logger utility using the 'debug' package.\n *\n * This module provides a configurable logger based on the 'debug' package.\n * By default, all logging is disabled. To enable logging, set the DEBUG\n * environment variable.\n *\n * The DEBUG variable can be a single namespace or a comma-separated list of\n * namespaces. Wildcards are supported.\n *\n * For example:\n * - `DEBUG=blip-api` enables all logs for the 'blip-api' scope.\n * - `DEBUG=blip-api:*` enables all sub-scopes under 'blip-api'.\n * - `DEBUG=*` enables all logs.\n *\n * The logger exposes different levels (debug, info, warn, error), which are\n * different namespaces.\n *\n * Example usage:\n * ```\n * import { getLogger } from './logger';\n *\n * const logger = getLogger('my-feature');\n *\n * logger.info('This is an info message');\n * logger.error('This is an error');\n * ```\n *\n * To see the output, run your application with:\n * `DEBUG=my-feature:* node your-app.js`\n */\n\nimport debug from 'debug';\n\n// Define the available log levels\nexport const LogLevels = {\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n};\n\nexport type LogLevel = keyof typeof LogLevels;\n\ntype LogFunction = (...args: any[]) => void;\n\nexport interface Logger {\n debug: LogFunction;\n info: LogFunction;\n warn: LogFunction;\n error: LogFunction;\n}\n\nconst loggers = new Map<string, Logger>();\n\nfunction serializeArg(arg: any): any {\n if (arg === null || arg === undefined) return arg;\n if (typeof arg === 'object') {\n try {\n return JSON.stringify(arg, null, 2);\n } catch (e) {\n return arg;\n }\n }\n return arg;\n}\n\nfunction createLogFunction(debugInstance: debug.Debugger): LogFunction {\n return (...args: any[]) => {\n const serialized = args.map(serializeArg);\n (debugInstance as any)(...serialized);\n };\n}\n\n/**\n * Creates a logger with a specific scope.\n *\n * @param scope - The scope for the logger (e.g., 'blip-api', 'dispatcher').\n * @returns A logger object with methods for each log level.\n */\nexport function getLogger(scope: string): Logger {\n if (!loggers.has(scope)) {\n const debugInstance = debug(`${scope}:${LogLevels.debug}`);\n const infoInstance = debug(`${scope}:${LogLevels.info}`);\n const warnInstance = debug(`${scope}:${LogLevels.warn}`);\n const errorInstance = debug(`${scope}:${LogLevels.error}`);\n\n errorInstance.log = console.error.bind(console);\n\n const logger: Logger = {\n debug: createLogFunction(debugInstance),\n info: createLogFunction(infoInstance),\n warn: createLogFunction(warnInstance),\n error: createLogFunction(errorInstance),\n };\n\n loggers.set(scope, logger);\n }\n return loggers.get(scope)!;\n}\n\n/**\n * Enables logging for a specific scope and level.\n * This is a convenience function to programmatically enable logging,\n * equivalent to setting the DEBUG environment variable.\n *\n * @param scope - The scope to enable (e.g., 'blip-api').\n * @param level - The minimum log level to enable (e.g., 'info').\n */\nexport function enableLogger(namespaces: string): void {\n const currentDebug = debug.disable();\n debug.enable(`${currentDebug},${namespaces}`);\n}\n\n/**\n * Disables all logging.\n */\nexport function disableLogger(): void {\n debug.disable();\n}\n\n/**\n * Get all registered logger scopes.\n *\n * @returns An array of all registered scopes.\n */\nexport function getAllScopes(): string[] {\n return Array.from(loggers.keys());\n}\n"],"mappings":"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","names":["DispatcherRepository_exports","__export","DispatcherRepository","__toCommonJS","import_debug","LogLevels","loggers","serializeArg","arg","createLogFunction","debugInstance","args","serialized","getLogger","scope","debug","infoInstance","warnInstance","errorInstance","logger","import_ioredis","logger","getLogger","_DispatcherRepository","dispatcherId","redisUrl","IORedis","err","messageId","state","status","contactId","descriptorId","name","message","ttl","key","data","pipeline","indexKey","retentionTime","scheduledTime","score","filter","messageIds","now","skip","size","allIds","messages","count","popped","evicted","i","value","metricKey","msg","metrics","getCount","setKey","descriptorKey","dispatchedKey","dispatchedCount","pattern","descriptorPrefix","keys","descriptors","id","a","b","manifest","DispatcherRepository"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a}from"../chunk-
|
|
1
|
+
import{a}from"../chunk-B7IDVU6T.mjs";import"../chunk-4LYB64T2.mjs";import"../chunk-OXXLVJVC.mjs";import"../chunk-JD53PFR4.mjs";export{a as DispatcherRepository};
|
|
2
2
|
//# sourceMappingURL=DispatcherRepository.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var f=Object.create;var d=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var L=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var v=(t,e)=>{for(var n in e)d(t,n,{get:e[n],enumerable:!0})},l=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of I(e))!m.call(t,i)&&i!==n&&d(t,i,{get:()=>e[i],enumerable:!(r=A(e,i))||r.enumerable});return t};var x=(t,e,n)=>(n=t!=null?f(L(t)):{},l(e||!t||!t.__esModule?d(n,"default",{value:t,enumerable:!0}):n,t)),y=t=>l(d({},"__esModule",{value:!0}),t);var h={};v(h,{DispatcherStateMachine:()=>g});module.exports=y(h);var a=x(require("debug")),p={debug:"debug",info:"info",warn:"warn",error:"error"},E=new Map;function N(t){if(t==null)return t;if(typeof t=="object")try{return JSON.stringify(t,null,2)}catch{return t}return t}function u(t){return(...e)=>{let n=e.map(N);t(...n)}}function D(t){if(!E.has(t)){let e=(0,a.default)(`${t}:${p.debug}`),n=(0,a.default)(`${t}:${p.info}`),r=(0,a.default)(`${t}:${p.warn}`),i=(0,a.default)(`${t}:${p.error}`);i.log=console.error.bind(console);let s={debug:u(e),info:u(n),warn:u(r),error:u(i)};E.set(t,s)}return E.get(t)}var b=D("StateMachine"),g=class{constructor(e,n,r){this.id=e;this.repository=n;this.emit=r}async transition(e,n,r,i){let s=e.state,c=e.status;s==="FINAL"&&n!=="FINAL"&&b.warn(`[transition] Attempting to move from FINAL back to ${n}`,{messageId:e.messageId}),e.state=n,e.status=r,i&&Object.assign(e,i);let o=new Date().toISOString();return r==="SENDING"&&!e.acceptedAt&&c!=="SENDING"&&(e.acceptedAt=o),r==="DELIVERED"&&!e.deliveredAt&&(e.deliveredAt=o),r==="READ"&&!e.readAt&&(e.readAt=o),r==="REPLIED"&&!e.repliedAt&&(e.repliedAt=o,e.readAt||(e.readAt=o)),r==="FAILED"&&!e.failedAt&&(e.failedAt=o),n==="DISPATCHED"&&!e.sentAt&&"PENDING",await this.repository.upsertMessage(e),c!==r&&(r==="REPLIED"&&c!=="READ"&&this.emit("read",e),this.emitStatusEvent(e,r)),n==="SCHEDULED"&&s!=="SCHEDULED"&&this.emit("scheduled",e),b.debug(`[transition] ${e.messageId} : ${s}/${c} -> ${n}/${r}`),e}emitStatusEvent(e,n){switch(n){case"SENDING":this.emit("sending",e);break;case"DELIVERED":this.emit("delivered",e);break;case"READ":this.emit("read",e);break;case"REPLIED":this.emit("replied",e);break;case"FAILED":this.emit("failed",e);break;case"CANCELED":this.emit("canceled",e);break}}};0&&(module.exports={DispatcherStateMachine});
|
|
2
2
|
//# sourceMappingURL=DispatcherStateMachine.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a}from"../chunk-UFUGEIFL.mjs";import"../chunk-
|
|
1
|
+
import{a}from"../chunk-UFUGEIFL.mjs";import"../chunk-4LYB64T2.mjs";import"../chunk-OXXLVJVC.mjs";import"../chunk-JD53PFR4.mjs";export{a as DispatcherStateMachine};
|
|
2
2
|
//# sourceMappingURL=DispatcherStateMachine.mjs.map
|
package/dist/errors/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=class extends Error{constructor(c,x){super(c);this.details=x;this.name=this.constructor.name,Error.captureStackTrace(this,this.constructor)}},t=class extends e{},p=class extends t{},u=class extends t{},l=class extends t{},n=class extends e{constructor(c,x,b){super(c,{cause:x,retryOptions:b});this.cause=x;this.retryOptions=b}},d=class extends e{},o=class extends t{};export{o as ConfigurationError,e as DispatcherError,u as InvalidConfigurationError,p as MessageInProgressError,l as NoValidShiftError,t as PermanentError,n as RetryableError,d as ServiceUnavailableError};
|
|
1
|
+
import"../chunk-JD53PFR4.mjs";var e=class extends Error{constructor(c,x){super(c);this.details=x;this.name=this.constructor.name,Error.captureStackTrace(this,this.constructor)}},t=class extends e{},p=class extends t{},u=class extends t{},l=class extends t{},n=class extends e{constructor(c,x,b){super(c,{cause:x,retryOptions:b});this.cause=x;this.retryOptions=b}},d=class extends e{},o=class extends t{};export{o as ConfigurationError,e as DispatcherError,u as InvalidConfigurationError,p as MessageInProgressError,l as NoValidShiftError,t as PermanentError,n as RetryableError,d as ServiceUnavailableError};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/errors/index.ts"],"sourcesContent":["/**\n * Error classes for the dispatcher system\n */\n\n/**\n * Base error class for all dispatcher errors\n */\nexport class DispatcherError extends Error {\n constructor(\n message: string,\n public details?: any\n ) {\n super(message);\n this.name = this.constructor.name;\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Permanent errors - do not retry\n */\nexport class PermanentError extends DispatcherError {}\n\n/**\n * Error when a message is already in progress for a contact + template\n */\nexport class MessageInProgressError extends PermanentError {}\n\n/**\n * Error for invalid configuration\n */\nexport class InvalidConfigurationError extends PermanentError {}\n\n/**\n * Error when no valid shift is found\n */\nexport class NoValidShiftError extends PermanentError {}\n\n/**\n * Retryable errors - trigger retry logic\n */\nexport class RetryableError extends DispatcherError {\n constructor(\n message: string,\n public cause?: Error,\n public retryOptions?: { backoffMultiplier?: number }\n ) {\n super(message, { cause, retryOptions });\n }\n}\n\n/**\n * Service errors - circuit breaker eligible\n */\nexport class ServiceUnavailableError extends DispatcherError {}\n\n/**\n * Error for configuration issues\n */\nexport class ConfigurationError extends PermanentError {}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/errors/index.ts"],"sourcesContent":["/**\n * Error classes for the dispatcher system\n */\n\n/**\n * Base error class for all dispatcher errors\n */\nexport class DispatcherError extends Error {\n constructor(\n message: string,\n public details?: any\n ) {\n super(message);\n this.name = this.constructor.name;\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Permanent errors - do not retry\n */\nexport class PermanentError extends DispatcherError {}\n\n/**\n * Error when a message is already in progress for a contact + template\n */\nexport class MessageInProgressError extends PermanentError {}\n\n/**\n * Error for invalid configuration\n */\nexport class InvalidConfigurationError extends PermanentError {}\n\n/**\n * Error when no valid shift is found\n */\nexport class NoValidShiftError extends PermanentError {}\n\n/**\n * Retryable errors - trigger retry logic\n */\nexport class RetryableError extends DispatcherError {\n constructor(\n message: string,\n public cause?: Error,\n public retryOptions?: { backoffMultiplier?: number }\n ) {\n super(message, { cause, retryOptions });\n }\n}\n\n/**\n * Service errors - circuit breaker eligible\n */\nexport class ServiceUnavailableError extends DispatcherError {}\n\n/**\n * Error for configuration issues\n */\nexport class ConfigurationError extends PermanentError {}\n"],"mappings":"8BAOO,IAAMA,EAAN,cAA8B,KAAM,CACzC,YACEC,EACOC,EACP,CACA,MAAMD,CAAO,EAFN,aAAAC,EAGP,KAAK,KAAO,KAAK,YAAY,KAC7B,MAAM,kBAAkB,KAAM,KAAK,WAAW,CAChD,CACF,EAKaC,EAAN,cAA6BH,CAAgB,CAAC,EAKxCI,EAAN,cAAqCD,CAAe,CAAC,EAK/CE,EAAN,cAAwCF,CAAe,CAAC,EAKlDG,EAAN,cAAgCH,CAAe,CAAC,EAK1CI,EAAN,cAA6BP,CAAgB,CAClD,YACEC,EACOO,EACAC,EACP,CACA,MAAMR,EAAS,CAAE,MAAAO,EAAO,aAAAC,CAAa,CAAC,EAH/B,WAAAD,EACA,kBAAAC,CAGT,CACF,EAKaC,EAAN,cAAsCV,CAAgB,CAAC,EAKjDW,EAAN,cAAiCR,CAAe,CAAC","names":["DispatcherError","message","details","PermanentError","MessageInProgressError","InvalidConfigurationError","NoValidShiftError","RetryableError","cause","retryOptions","ServiceUnavailableError","ConfigurationError"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ export { Blip, BlipError } from './core/Blip.mjs';
|
|
|
4
4
|
export { DispatcherMonitor, MonitorAlert, MonitorOptions, MonitorRule } from './core/DispatcherMonitor.mjs';
|
|
5
5
|
export { DispatcherRepository } from './core/DispatcherRepository.mjs';
|
|
6
6
|
export { enableLogger, getLogger } from './utils/logger.mjs';
|
|
7
|
-
export { CallbackEvent, ConnectionConfig, Contact, DispatchMessageOptions, DispatchState, DispatcherMetrics, DispatcherOptions, Intent, Message, MessageData, MessageDispatcherOptions, MessageOptions, MessagePayload, MessageState, MessageStatus, QueryFilter, Shift, Weekdays } from './types/index.mjs';
|
|
7
|
+
export { CallbackEvent, ConnectionConfig, Contact, DispatchMessageOptions, DispatchState, DispatcherManifest, DispatcherMetrics, DispatcherOptions, Intent, Message, MessageData, MessageDispatcherOptions, MessageOptions, MessagePayload, MessageState, MessageStatus, QueryFilter, Shift, Weekdays } from './types/index.mjs';
|
|
8
8
|
export { BlipConfig, DateInterval, PartiallyOptional, PartiallyRequiredStrict, Vnd } from './types/blip.mjs';
|
|
9
9
|
import './core/DispatcherQuery.mjs';
|
|
10
10
|
import 'events';
|