Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

qified

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

qified - npm Package Compare versions

Comparing version
0.5.3
to
0.6.0
+418
-8
dist/index.cjs

@@ -24,4 +24,6 @@ "use strict";

MemoryMessageProvider: () => MemoryMessageProvider,
MemoryTaskProvider: () => MemoryTaskProvider,
Qified: () => Qified,
QifiedEvents: () => QifiedEvents
QifiedEvents: () => QifiedEvents,
QifiedHooks: () => QifiedHooks
});

@@ -131,2 +133,343 @@ module.exports = __toCommonJS(index_exports);

// src/memory/task.ts
var defaultMemoryTaskId = "@qified/memory";
var defaultTimeout = 3e4;
var defaultRetries = 3;
var MemoryTaskProvider = class {
_id;
_timeout;
_retries;
_taskHandlers;
_queues;
_processing;
// Map of queue -> Set of task IDs being processed
_deadLetterQueue;
_taskIdCounter = 0;
_active = true;
/**
* Creates an instance of MemoryTaskProvider.
* @param {MemoryTaskProviderOptions} options - Optional configuration for the provider.
*/
constructor(options) {
this._id = options?.id ?? defaultMemoryTaskId;
this._timeout = options?.timeout ?? defaultTimeout;
this._retries = options?.retries ?? defaultRetries;
this._taskHandlers = /* @__PURE__ */ new Map();
this._queues = /* @__PURE__ */ new Map();
this._processing = /* @__PURE__ */ new Map();
this._deadLetterQueue = /* @__PURE__ */ new Map();
}
/**
* Gets the provider ID for the memory task provider.
* @returns {string} The provider ID.
*/
get id() {
return this._id;
}
/**
* Sets the provider ID for the memory task provider.
* @param {string} id The new provider ID.
*/
set id(id) {
this._id = id;
}
/**
* Gets the default timeout for task processing.
* @returns {number} The timeout in milliseconds.
*/
get timeout() {
return this._timeout;
}
/**
* Sets the default timeout for task processing.
* @param {number} timeout The timeout in milliseconds.
*/
set timeout(timeout) {
this._timeout = timeout;
}
/**
* Gets the default maximum retry attempts.
* @returns {number} The maximum retry attempts.
*/
get retries() {
return this._retries;
}
/**
* Sets the default maximum retry attempts.
* @param {number} retries The maximum retry attempts.
*/
set retries(retries) {
this._retries = retries;
}
/**
* Gets the task handlers map.
* @returns {Map<string, TaskHandler[]>} The task handlers map.
*/
get taskHandlers() {
return this._taskHandlers;
}
/**
* Sets the task handlers map.
* @param {Map<string, TaskHandler[]>} value The new task handlers map.
*/
set taskHandlers(value) {
this._taskHandlers = value;
}
/**
* Generates a unique task ID.
* @returns {string} A unique task ID.
*/
generateTaskId() {
return `task-${Date.now()}-${++this._taskIdCounter}`;
}
/**
* Enqueues a task to a specific queue.
* Automatically assigns ID and timestamp to the task.
* @param {string} queue - The queue name to enqueue to.
* @param {EnqueueTask} taskData - The task data to enqueue.
* @returns {Promise<string>} The ID of the enqueued task.
*/
async enqueue(queue, taskData) {
if (!this._active) {
throw new Error("TaskProvider has been disconnected");
}
const task = {
id: this.generateTaskId(),
timestamp: Date.now(),
...taskData
};
const queuedTask = {
task,
attempt: 0,
deadlineAt: 0,
processing: false
};
if (!this._queues.has(queue)) {
this._queues.set(queue, []);
}
this._queues.get(queue)?.push(queuedTask);
await this.processQueue(queue);
return task.id;
}
/**
* Registers a handler to process tasks from a queue.
* Starts processing any pending tasks in the queue.
* @param {string} queue - The queue name to dequeue from.
* @param {TaskHandler} handler - The handler configuration.
* @returns {Promise<void>}
*/
async dequeue(queue, handler) {
if (!this._active) {
throw new Error("TaskProvider has been disconnected");
}
if (!this._taskHandlers.has(queue)) {
this._taskHandlers.set(queue, []);
}
this._taskHandlers.get(queue)?.push(handler);
await this.processQueue(queue);
}
/**
* Processes tasks in a queue by delivering them to registered handlers.
* @param {string} queue - The queue name to process.
*/
async processQueue(queue) {
if (!this._active) {
return;
}
const handlers = this._taskHandlers.get(queue);
if (!handlers || handlers.length === 0) {
return;
}
const queuedTasks = this._queues.get(queue);
if (!queuedTasks || queuedTasks.length === 0) {
return;
}
const processingSet = this._processing.get(queue) ?? /* @__PURE__ */ new Set();
this._processing.set(queue, processingSet);
for (const queuedTask of queuedTasks) {
if (queuedTask.processing || processingSet.has(queuedTask.task.id)) {
continue;
}
if (queuedTask.task.scheduledAt && queuedTask.task.scheduledAt > Date.now()) {
continue;
}
queuedTask.processing = true;
processingSet.add(queuedTask.task.id);
for (const handler of handlers) {
void this.processTask(queue, queuedTask, handler);
}
}
}
/**
* Processes a single task with a handler.
* @param {string} queue - The queue name.
* @param {QueuedTask} queuedTask - The queued task to process.
* @param {TaskHandler} handler - The handler to process the task.
*/
async processTask(queue, queuedTask, handler) {
const { task } = queuedTask;
const maxRetries = task.maxRetries ?? this._retries;
const timeout = task.timeout ?? this._timeout;
queuedTask.attempt++;
queuedTask.deadlineAt = Date.now() + timeout;
let acknowledged = false;
let rejected = false;
const context = {
ack: async () => {
if (acknowledged || rejected) {
return;
}
acknowledged = true;
await this.removeTask(queue, task.id);
},
reject: async (requeue = true) => {
if (acknowledged || rejected) {
return;
}
rejected = true;
if (requeue && queuedTask.attempt < maxRetries) {
queuedTask.processing = false;
this._processing.get(queue)?.delete(task.id);
setTimeout(() => {
void this.processQueue(queue);
}, 100);
} else {
await this.moveToDeadLetter(queue, task);
await this.removeTask(queue, task.id);
}
},
extend: async (ttl) => {
if (acknowledged || rejected) {
return;
}
queuedTask.deadlineAt = Date.now() + ttl;
if (queuedTask.timeoutHandle) {
clearTimeout(queuedTask.timeoutHandle);
}
queuedTask.timeoutHandle = setTimeout(() => {
if (!acknowledged && !rejected) {
void context.reject(true);
}
}, ttl);
},
metadata: {
attempt: queuedTask.attempt,
maxRetries
}
};
queuedTask.timeoutHandle = setTimeout(() => {
if (!acknowledged && !rejected) {
void context.reject(true);
}
}, timeout);
try {
await handler.handler(task, context);
if (!acknowledged && !rejected) {
await context.ack();
}
} catch (_error) {
if (!acknowledged && !rejected) {
await context.reject(true);
}
} finally {
if (queuedTask.timeoutHandle) {
clearTimeout(queuedTask.timeoutHandle);
}
}
}
/**
* Removes a task from the queue.
* @param {string} queue - The queue name.
* @param {string} taskId - The task ID to remove.
*/
async removeTask(queue, taskId) {
const queuedTasks = this._queues.get(queue);
if (queuedTasks) {
const index = queuedTasks.findIndex((qt) => qt.task.id === taskId);
if (index !== -1) {
queuedTasks.splice(index, 1);
}
}
this._processing.get(queue)?.delete(taskId);
}
/**
* Moves a task to the dead-letter queue.
* @param {string} queue - The original queue name.
* @param {Task} task - The task to move.
*/
async moveToDeadLetter(queue, task) {
const dlqKey = `${queue}:dead-letter`;
if (!this._deadLetterQueue.has(dlqKey)) {
this._deadLetterQueue.set(dlqKey, []);
}
this._deadLetterQueue.get(dlqKey)?.push(task);
}
/**
* Unsubscribes a handler from a queue.
* @param {string} queue - The queue name to unsubscribe from.
* @param {string} [id] - Optional handler ID. If not provided, removes all handlers.
* @returns {Promise<void>}
*/
async unsubscribe(queue, id) {
if (id) {
const handlers = this._taskHandlers.get(queue);
if (handlers) {
this._taskHandlers.set(
queue,
handlers.filter((h) => h.id !== id)
);
}
} else {
this._taskHandlers.delete(queue);
}
}
/**
* Disconnects and clears all queues and handlers.
* Stops all task processing.
* @returns {Promise<void>}
*/
async disconnect() {
this._active = false;
for (const queuedTasks of this._queues.values()) {
for (const queuedTask of queuedTasks) {
if (queuedTask.timeoutHandle) {
clearTimeout(queuedTask.timeoutHandle);
}
}
}
this._taskHandlers.clear();
this._queues.clear();
this._processing.clear();
this._deadLetterQueue.clear();
}
/**
* Gets all tasks in the dead-letter queue for a specific queue.
* Useful for debugging and monitoring failed tasks.
* @param {string} queue - The queue name.
* @returns {Task[]} Array of tasks in the dead-letter queue.
*/
getDeadLetterTasks(queue) {
const dlqKey = `${queue}:dead-letter`;
return this._deadLetterQueue.get(dlqKey) ?? [];
}
/**
* Gets the current state of a queue.
* Useful for monitoring and debugging.
* @param {string} queue - The queue name.
* @returns {Object} Queue statistics.
*/
getQueueStats(queue) {
const queuedTasks = this._queues.get(queue) ?? [];
const processing = this._processing.get(queue)?.size ?? 0;
const waiting = queuedTasks.filter((qt) => !qt.processing).length;
const dlqKey = `${queue}:dead-letter`;
const deadLetter = this._deadLetterQueue.get(dlqKey)?.length ?? 0;
return {
waiting,
processing,
deadLetter
};
}
};
// src/index.ts

@@ -143,4 +486,16 @@ var QifiedEvents = /* @__PURE__ */ ((QifiedEvents2) => {

})(QifiedEvents || {});
var QifiedHooks = /* @__PURE__ */ ((QifiedHooks2) => {
QifiedHooks2["beforeSubscribe"] = "before:subscribe";
QifiedHooks2["afterSubscribe"] = "after:subscribe";
QifiedHooks2["beforePublish"] = "before:publish";
QifiedHooks2["afterPublish"] = "after:publish";
QifiedHooks2["beforeUnsubscribe"] = "before:unsubscribe";
QifiedHooks2["afterUnsubscribe"] = "after:unsubscribe";
QifiedHooks2["beforeDisconnect"] = "before:disconnect";
QifiedHooks2["afterDisconnect"] = "after:disconnect";
return QifiedHooks2;
})(QifiedHooks || {});
var Qified = class extends import_hookified.Hookified {
_messageProviders = [];
_taskProviders = [];
/**

@@ -159,2 +514,9 @@ * Creates an instance of Qified.

}
if (options?.taskProviders) {
if (Array.isArray(options?.taskProviders)) {
this._taskProviders = options.taskProviders;
} else {
this._taskProviders = [options?.taskProviders];
}
}
}

@@ -176,2 +538,16 @@ /**

/**
* Gets or sets the task providers.
* @returns {TaskProvider[]} The array of task providers.
*/
get taskProviders() {
return this._taskProviders;
}
/**
* Sets the task providers.
* @param {TaskProvider[]} providers - The array of task providers to set.
*/
set taskProviders(providers) {
this._taskProviders = providers;
}
/**
* Subscribes to a topic. If you have multiple message providers, it will subscribe to the topic on all of them.

@@ -183,7 +559,16 @@ * @param {string} topic - The topic to subscribe to.

try {
const context = { topic, handler };
await this.hook("before:subscribe" /* beforeSubscribe */, context);
const promises = this._messageProviders.map(
async (provider) => provider.subscribe(topic, handler)
async (provider) => provider.subscribe(context.topic, context.handler)
);
await Promise.all(promises);
this.emit("subscribe" /* subscribe */, { topic, handler });
await this.hook("after:subscribe" /* afterSubscribe */, {
topic: context.topic,
handler: context.handler
});
this.emit("subscribe" /* subscribe */, {
topic: context.topic,
handler: context.handler
});
} catch (error) {

@@ -200,7 +585,16 @@ this.emit("error" /* error */, error);

try {
const context = { topic, message };
await this.hook("before:publish" /* beforePublish */, context);
const promises = this._messageProviders.map(
async (provider) => provider.publish(topic, message)
async (provider) => provider.publish(context.topic, context.message)
);
await Promise.all(promises);
this.emit("publish" /* publish */, { topic, message });
await this.hook("after:publish" /* afterPublish */, {
topic: context.topic,
message: context.message
});
this.emit("publish" /* publish */, {
topic: context.topic,
message: context.message
});
} catch (error) {

@@ -218,7 +612,16 @@ this.emit("error" /* error */, error);

try {
const context = { topic, id };
await this.hook("before:unsubscribe" /* beforeUnsubscribe */, context);
const promises = this._messageProviders.map(
async (provider) => provider.unsubscribe(topic, id)
async (provider) => provider.unsubscribe(context.topic, context.id)
);
await Promise.all(promises);
this.emit("unsubscribe" /* unsubscribe */, { topic, id });
await this.hook("after:unsubscribe" /* afterUnsubscribe */, {
topic: context.topic,
id: context.id
});
this.emit("unsubscribe" /* unsubscribe */, {
topic: context.topic,
id: context.id
});
} catch (error) {

@@ -234,2 +637,4 @@ this.emit("error" /* error */, error);

try {
const context = { providerCount: this._messageProviders.length };
await this.hook("before:disconnect" /* beforeDisconnect */, context);
const promises = this._messageProviders.map(

@@ -240,2 +645,5 @@ async (provider) => provider.disconnect()

this._messageProviders = [];
await this.hook("after:disconnect" /* afterDisconnect */, {
providerCount: context.providerCount
});
this.emit("disconnect" /* disconnect */);

@@ -250,5 +658,7 @@ } catch (error) {

MemoryMessageProvider,
MemoryTaskProvider,
Qified,
QifiedEvents
QifiedEvents,
QifiedHooks
});
/* v8 ignore next -- @preserve */

@@ -197,2 +197,26 @@ import { HookifiedOptions, Hookified } from 'hookified';

/**
* Configuration options for task providers
* Allows customization of default behavior for task processing
*/
type TaskProviderOptions = {
/**
* Default timeout for task processing in milliseconds
* Tasks exceeding this duration may be requeued or marked as failed
* @type {number}
*/
timeout?: number;
/**
* Default maximum number of retry attempts for failed tasks
* After this many failures, tasks may be sent to dead-letter queue
* @type {number}
*/
retries?: number;
/**
* Name of the dead-letter queue for failed tasks
* If not provided, dead-letter functionality is disabled
* @type {string}
*/
deadLetterQueue?: string;
};
/**
* TaskProvider interface for task queue management

@@ -340,2 +364,150 @@ * Handles enqueueing, dequeueing, and lifecycle management of tasks

/**
* Configuration options for the memory task provider.
*/
type MemoryTaskProviderOptions = TaskProviderOptions & {
/**
* The unique identifier for this provider instance.
* @default "@qified/memory-task"
*/
id?: string;
};
/**
* In-memory task provider for testing and simple use cases.
* Tasks are stored and processed in memory without persistence.
* Supports task acknowledgment, rejection, retry, and timeout handling.
*/
declare class MemoryTaskProvider implements TaskProvider {
private _id;
private _timeout;
private _retries;
private _taskHandlers;
private _queues;
private _processing;
private _deadLetterQueue;
private _taskIdCounter;
private _active;
/**
* Creates an instance of MemoryTaskProvider.
* @param {MemoryTaskProviderOptions} options - Optional configuration for the provider.
*/
constructor(options?: MemoryTaskProviderOptions);
/**
* Gets the provider ID for the memory task provider.
* @returns {string} The provider ID.
*/
get id(): string;
/**
* Sets the provider ID for the memory task provider.
* @param {string} id The new provider ID.
*/
set id(id: string);
/**
* Gets the default timeout for task processing.
* @returns {number} The timeout in milliseconds.
*/
get timeout(): number;
/**
* Sets the default timeout for task processing.
* @param {number} timeout The timeout in milliseconds.
*/
set timeout(timeout: number);
/**
* Gets the default maximum retry attempts.
* @returns {number} The maximum retry attempts.
*/
get retries(): number;
/**
* Sets the default maximum retry attempts.
* @param {number} retries The maximum retry attempts.
*/
set retries(retries: number);
/**
* Gets the task handlers map.
* @returns {Map<string, TaskHandler[]>} The task handlers map.
*/
get taskHandlers(): Map<string, TaskHandler[]>;
/**
* Sets the task handlers map.
* @param {Map<string, TaskHandler[]>} value The new task handlers map.
*/
set taskHandlers(value: Map<string, TaskHandler[]>);
/**
* Generates a unique task ID.
* @returns {string} A unique task ID.
*/
private generateTaskId;
/**
* Enqueues a task to a specific queue.
* Automatically assigns ID and timestamp to the task.
* @param {string} queue - The queue name to enqueue to.
* @param {EnqueueTask} taskData - The task data to enqueue.
* @returns {Promise<string>} The ID of the enqueued task.
*/
enqueue(queue: string, taskData: EnqueueTask): Promise<string>;
/**
* Registers a handler to process tasks from a queue.
* Starts processing any pending tasks in the queue.
* @param {string} queue - The queue name to dequeue from.
* @param {TaskHandler} handler - The handler configuration.
* @returns {Promise<void>}
*/
dequeue(queue: string, handler: TaskHandler): Promise<void>;
/**
* Processes tasks in a queue by delivering them to registered handlers.
* @param {string} queue - The queue name to process.
*/
private processQueue;
/**
* Processes a single task with a handler.
* @param {string} queue - The queue name.
* @param {QueuedTask} queuedTask - The queued task to process.
* @param {TaskHandler} handler - The handler to process the task.
*/
private processTask;
/**
* Removes a task from the queue.
* @param {string} queue - The queue name.
* @param {string} taskId - The task ID to remove.
*/
private removeTask;
/**
* Moves a task to the dead-letter queue.
* @param {string} queue - The original queue name.
* @param {Task} task - The task to move.
*/
private moveToDeadLetter;
/**
* Unsubscribes a handler from a queue.
* @param {string} queue - The queue name to unsubscribe from.
* @param {string} [id] - Optional handler ID. If not provided, removes all handlers.
* @returns {Promise<void>}
*/
unsubscribe(queue: string, id?: string): Promise<void>;
/**
* Disconnects and clears all queues and handlers.
* Stops all task processing.
* @returns {Promise<void>}
*/
disconnect(): Promise<void>;
/**
* Gets all tasks in the dead-letter queue for a specific queue.
* Useful for debugging and monitoring failed tasks.
* @param {string} queue - The queue name.
* @returns {Task[]} Array of tasks in the dead-letter queue.
*/
getDeadLetterTasks(queue: string): Task[];
/**
* Gets the current state of a queue.
* Useful for monitoring and debugging.
* @param {string} queue - The queue name.
* @returns {Object} Queue statistics.
*/
getQueueStats(queue: string): {
waiting: number;
processing: number;
deadLetter: number;
};
}
/**
* Standard events emitted by Qified.

@@ -352,2 +524,17 @@ */

}
/**
* Hook event names for before/after lifecycle hooks.
* Before hooks receive a mutable context object that can be modified.
* After hooks receive the final context after the operation completes.
*/
declare enum QifiedHooks {
beforeSubscribe = "before:subscribe",
afterSubscribe = "after:subscribe",
beforePublish = "before:publish",
afterPublish = "after:publish",
beforeUnsubscribe = "before:unsubscribe",
afterUnsubscribe = "after:unsubscribe",
beforeDisconnect = "before:disconnect",
afterDisconnect = "after:disconnect"
}
type QifiedOptions = {

@@ -361,6 +548,7 @@ /**

*/
taskProviders?: TaskProvider[];
taskProviders?: TaskProvider | TaskProvider[];
} & HookifiedOptions;
declare class Qified extends Hookified {
private _messageProviders;
private _taskProviders;
/**

@@ -382,2 +570,12 @@ * Creates an instance of Qified.

/**
* Gets or sets the task providers.
* @returns {TaskProvider[]} The array of task providers.
*/
get taskProviders(): TaskProvider[];
/**
* Sets the task providers.
* @param {TaskProvider[]} providers - The array of task providers to set.
*/
set taskProviders(providers: TaskProvider[]);
/**
* Subscribes to a topic. If you have multiple message providers, it will subscribe to the topic on all of them.

@@ -408,2 +606,2 @@ * @param {string} topic - The topic to subscribe to.

export { MemoryMessageProvider, type Message, type MessageProvider, Qified, QifiedEvents, type QifiedOptions, type TaskProvider, type TopicHandler };
export { MemoryMessageProvider, MemoryTaskProvider, type Message, type MessageProvider, Qified, QifiedEvents, QifiedHooks, type QifiedOptions, type TaskProvider, type TopicHandler };

@@ -197,2 +197,26 @@ import { HookifiedOptions, Hookified } from 'hookified';

/**
* Configuration options for task providers
* Allows customization of default behavior for task processing
*/
type TaskProviderOptions = {
/**
* Default timeout for task processing in milliseconds
* Tasks exceeding this duration may be requeued or marked as failed
* @type {number}
*/
timeout?: number;
/**
* Default maximum number of retry attempts for failed tasks
* After this many failures, tasks may be sent to dead-letter queue
* @type {number}
*/
retries?: number;
/**
* Name of the dead-letter queue for failed tasks
* If not provided, dead-letter functionality is disabled
* @type {string}
*/
deadLetterQueue?: string;
};
/**
* TaskProvider interface for task queue management

@@ -340,2 +364,150 @@ * Handles enqueueing, dequeueing, and lifecycle management of tasks

/**
* Configuration options for the memory task provider.
*/
type MemoryTaskProviderOptions = TaskProviderOptions & {
/**
* The unique identifier for this provider instance.
* @default "@qified/memory-task"
*/
id?: string;
};
/**
* In-memory task provider for testing and simple use cases.
* Tasks are stored and processed in memory without persistence.
* Supports task acknowledgment, rejection, retry, and timeout handling.
*/
declare class MemoryTaskProvider implements TaskProvider {
private _id;
private _timeout;
private _retries;
private _taskHandlers;
private _queues;
private _processing;
private _deadLetterQueue;
private _taskIdCounter;
private _active;
/**
* Creates an instance of MemoryTaskProvider.
* @param {MemoryTaskProviderOptions} options - Optional configuration for the provider.
*/
constructor(options?: MemoryTaskProviderOptions);
/**
* Gets the provider ID for the memory task provider.
* @returns {string} The provider ID.
*/
get id(): string;
/**
* Sets the provider ID for the memory task provider.
* @param {string} id The new provider ID.
*/
set id(id: string);
/**
* Gets the default timeout for task processing.
* @returns {number} The timeout in milliseconds.
*/
get timeout(): number;
/**
* Sets the default timeout for task processing.
* @param {number} timeout The timeout in milliseconds.
*/
set timeout(timeout: number);
/**
* Gets the default maximum retry attempts.
* @returns {number} The maximum retry attempts.
*/
get retries(): number;
/**
* Sets the default maximum retry attempts.
* @param {number} retries The maximum retry attempts.
*/
set retries(retries: number);
/**
* Gets the task handlers map.
* @returns {Map<string, TaskHandler[]>} The task handlers map.
*/
get taskHandlers(): Map<string, TaskHandler[]>;
/**
* Sets the task handlers map.
* @param {Map<string, TaskHandler[]>} value The new task handlers map.
*/
set taskHandlers(value: Map<string, TaskHandler[]>);
/**
* Generates a unique task ID.
* @returns {string} A unique task ID.
*/
private generateTaskId;
/**
* Enqueues a task to a specific queue.
* Automatically assigns ID and timestamp to the task.
* @param {string} queue - The queue name to enqueue to.
* @param {EnqueueTask} taskData - The task data to enqueue.
* @returns {Promise<string>} The ID of the enqueued task.
*/
enqueue(queue: string, taskData: EnqueueTask): Promise<string>;
/**
* Registers a handler to process tasks from a queue.
* Starts processing any pending tasks in the queue.
* @param {string} queue - The queue name to dequeue from.
* @param {TaskHandler} handler - The handler configuration.
* @returns {Promise<void>}
*/
dequeue(queue: string, handler: TaskHandler): Promise<void>;
/**
* Processes tasks in a queue by delivering them to registered handlers.
* @param {string} queue - The queue name to process.
*/
private processQueue;
/**
* Processes a single task with a handler.
* @param {string} queue - The queue name.
* @param {QueuedTask} queuedTask - The queued task to process.
* @param {TaskHandler} handler - The handler to process the task.
*/
private processTask;
/**
* Removes a task from the queue.
* @param {string} queue - The queue name.
* @param {string} taskId - The task ID to remove.
*/
private removeTask;
/**
* Moves a task to the dead-letter queue.
* @param {string} queue - The original queue name.
* @param {Task} task - The task to move.
*/
private moveToDeadLetter;
/**
* Unsubscribes a handler from a queue.
* @param {string} queue - The queue name to unsubscribe from.
* @param {string} [id] - Optional handler ID. If not provided, removes all handlers.
* @returns {Promise<void>}
*/
unsubscribe(queue: string, id?: string): Promise<void>;
/**
* Disconnects and clears all queues and handlers.
* Stops all task processing.
* @returns {Promise<void>}
*/
disconnect(): Promise<void>;
/**
* Gets all tasks in the dead-letter queue for a specific queue.
* Useful for debugging and monitoring failed tasks.
* @param {string} queue - The queue name.
* @returns {Task[]} Array of tasks in the dead-letter queue.
*/
getDeadLetterTasks(queue: string): Task[];
/**
* Gets the current state of a queue.
* Useful for monitoring and debugging.
* @param {string} queue - The queue name.
* @returns {Object} Queue statistics.
*/
getQueueStats(queue: string): {
waiting: number;
processing: number;
deadLetter: number;
};
}
/**
* Standard events emitted by Qified.

@@ -352,2 +524,17 @@ */

}
/**
* Hook event names for before/after lifecycle hooks.
* Before hooks receive a mutable context object that can be modified.
* After hooks receive the final context after the operation completes.
*/
declare enum QifiedHooks {
beforeSubscribe = "before:subscribe",
afterSubscribe = "after:subscribe",
beforePublish = "before:publish",
afterPublish = "after:publish",
beforeUnsubscribe = "before:unsubscribe",
afterUnsubscribe = "after:unsubscribe",
beforeDisconnect = "before:disconnect",
afterDisconnect = "after:disconnect"
}
type QifiedOptions = {

@@ -361,6 +548,7 @@ /**

*/
taskProviders?: TaskProvider[];
taskProviders?: TaskProvider | TaskProvider[];
} & HookifiedOptions;
declare class Qified extends Hookified {
private _messageProviders;
private _taskProviders;
/**

@@ -382,2 +570,12 @@ * Creates an instance of Qified.

/**
* Gets or sets the task providers.
* @returns {TaskProvider[]} The array of task providers.
*/
get taskProviders(): TaskProvider[];
/**
* Sets the task providers.
* @param {TaskProvider[]} providers - The array of task providers to set.
*/
set taskProviders(providers: TaskProvider[]);
/**
* Subscribes to a topic. If you have multiple message providers, it will subscribe to the topic on all of them.

@@ -408,2 +606,2 @@ * @param {string} topic - The topic to subscribe to.

export { MemoryMessageProvider, type Message, type MessageProvider, Qified, QifiedEvents, type QifiedOptions, type TaskProvider, type TopicHandler };
export { MemoryMessageProvider, MemoryTaskProvider, type Message, type MessageProvider, Qified, QifiedEvents, QifiedHooks, type QifiedOptions, type TaskProvider, type TopicHandler };

@@ -104,2 +104,343 @@ // src/index.ts

// src/memory/task.ts
var defaultMemoryTaskId = "@qified/memory";
var defaultTimeout = 3e4;
var defaultRetries = 3;
var MemoryTaskProvider = class {
_id;
_timeout;
_retries;
_taskHandlers;
_queues;
_processing;
// Map of queue -> Set of task IDs being processed
_deadLetterQueue;
_taskIdCounter = 0;
_active = true;
/**
* Creates an instance of MemoryTaskProvider.
* @param {MemoryTaskProviderOptions} options - Optional configuration for the provider.
*/
constructor(options) {
this._id = options?.id ?? defaultMemoryTaskId;
this._timeout = options?.timeout ?? defaultTimeout;
this._retries = options?.retries ?? defaultRetries;
this._taskHandlers = /* @__PURE__ */ new Map();
this._queues = /* @__PURE__ */ new Map();
this._processing = /* @__PURE__ */ new Map();
this._deadLetterQueue = /* @__PURE__ */ new Map();
}
/**
* Gets the provider ID for the memory task provider.
* @returns {string} The provider ID.
*/
get id() {
return this._id;
}
/**
* Sets the provider ID for the memory task provider.
* @param {string} id The new provider ID.
*/
set id(id) {
this._id = id;
}
/**
* Gets the default timeout for task processing.
* @returns {number} The timeout in milliseconds.
*/
get timeout() {
return this._timeout;
}
/**
* Sets the default timeout for task processing.
* @param {number} timeout The timeout in milliseconds.
*/
set timeout(timeout) {
this._timeout = timeout;
}
/**
* Gets the default maximum retry attempts.
* @returns {number} The maximum retry attempts.
*/
get retries() {
return this._retries;
}
/**
* Sets the default maximum retry attempts.
* @param {number} retries The maximum retry attempts.
*/
set retries(retries) {
this._retries = retries;
}
/**
* Gets the task handlers map.
* @returns {Map<string, TaskHandler[]>} The task handlers map.
*/
get taskHandlers() {
return this._taskHandlers;
}
/**
* Sets the task handlers map.
* @param {Map<string, TaskHandler[]>} value The new task handlers map.
*/
set taskHandlers(value) {
this._taskHandlers = value;
}
/**
* Generates a unique task ID.
* @returns {string} A unique task ID.
*/
generateTaskId() {
return `task-${Date.now()}-${++this._taskIdCounter}`;
}
/**
* Enqueues a task to a specific queue.
* Automatically assigns ID and timestamp to the task.
* @param {string} queue - The queue name to enqueue to.
* @param {EnqueueTask} taskData - The task data to enqueue.
* @returns {Promise<string>} The ID of the enqueued task.
*/
async enqueue(queue, taskData) {
if (!this._active) {
throw new Error("TaskProvider has been disconnected");
}
const task = {
id: this.generateTaskId(),
timestamp: Date.now(),
...taskData
};
const queuedTask = {
task,
attempt: 0,
deadlineAt: 0,
processing: false
};
if (!this._queues.has(queue)) {
this._queues.set(queue, []);
}
this._queues.get(queue)?.push(queuedTask);
await this.processQueue(queue);
return task.id;
}
/**
* Registers a handler to process tasks from a queue.
* Starts processing any pending tasks in the queue.
* @param {string} queue - The queue name to dequeue from.
* @param {TaskHandler} handler - The handler configuration.
* @returns {Promise<void>}
*/
async dequeue(queue, handler) {
if (!this._active) {
throw new Error("TaskProvider has been disconnected");
}
if (!this._taskHandlers.has(queue)) {
this._taskHandlers.set(queue, []);
}
this._taskHandlers.get(queue)?.push(handler);
await this.processQueue(queue);
}
/**
* Processes tasks in a queue by delivering them to registered handlers.
* @param {string} queue - The queue name to process.
*/
async processQueue(queue) {
if (!this._active) {
return;
}
const handlers = this._taskHandlers.get(queue);
if (!handlers || handlers.length === 0) {
return;
}
const queuedTasks = this._queues.get(queue);
if (!queuedTasks || queuedTasks.length === 0) {
return;
}
const processingSet = this._processing.get(queue) ?? /* @__PURE__ */ new Set();
this._processing.set(queue, processingSet);
for (const queuedTask of queuedTasks) {
if (queuedTask.processing || processingSet.has(queuedTask.task.id)) {
continue;
}
if (queuedTask.task.scheduledAt && queuedTask.task.scheduledAt > Date.now()) {
continue;
}
queuedTask.processing = true;
processingSet.add(queuedTask.task.id);
for (const handler of handlers) {
void this.processTask(queue, queuedTask, handler);
}
}
}
/**
* Processes a single task with a handler.
* @param {string} queue - The queue name.
* @param {QueuedTask} queuedTask - The queued task to process.
* @param {TaskHandler} handler - The handler to process the task.
*/
async processTask(queue, queuedTask, handler) {
const { task } = queuedTask;
const maxRetries = task.maxRetries ?? this._retries;
const timeout = task.timeout ?? this._timeout;
queuedTask.attempt++;
queuedTask.deadlineAt = Date.now() + timeout;
let acknowledged = false;
let rejected = false;
const context = {
ack: async () => {
if (acknowledged || rejected) {
return;
}
acknowledged = true;
await this.removeTask(queue, task.id);
},
reject: async (requeue = true) => {
if (acknowledged || rejected) {
return;
}
rejected = true;
if (requeue && queuedTask.attempt < maxRetries) {
queuedTask.processing = false;
this._processing.get(queue)?.delete(task.id);
setTimeout(() => {
void this.processQueue(queue);
}, 100);
} else {
await this.moveToDeadLetter(queue, task);
await this.removeTask(queue, task.id);
}
},
extend: async (ttl) => {
if (acknowledged || rejected) {
return;
}
queuedTask.deadlineAt = Date.now() + ttl;
if (queuedTask.timeoutHandle) {
clearTimeout(queuedTask.timeoutHandle);
}
queuedTask.timeoutHandle = setTimeout(() => {
if (!acknowledged && !rejected) {
void context.reject(true);
}
}, ttl);
},
metadata: {
attempt: queuedTask.attempt,
maxRetries
}
};
queuedTask.timeoutHandle = setTimeout(() => {
if (!acknowledged && !rejected) {
void context.reject(true);
}
}, timeout);
try {
await handler.handler(task, context);
if (!acknowledged && !rejected) {
await context.ack();
}
} catch (_error) {
if (!acknowledged && !rejected) {
await context.reject(true);
}
} finally {
if (queuedTask.timeoutHandle) {
clearTimeout(queuedTask.timeoutHandle);
}
}
}
/**
* Removes a task from the queue.
* @param {string} queue - The queue name.
* @param {string} taskId - The task ID to remove.
*/
async removeTask(queue, taskId) {
const queuedTasks = this._queues.get(queue);
if (queuedTasks) {
const index = queuedTasks.findIndex((qt) => qt.task.id === taskId);
if (index !== -1) {
queuedTasks.splice(index, 1);
}
}
this._processing.get(queue)?.delete(taskId);
}
/**
* Moves a task to the dead-letter queue.
* @param {string} queue - The original queue name.
* @param {Task} task - The task to move.
*/
async moveToDeadLetter(queue, task) {
const dlqKey = `${queue}:dead-letter`;
if (!this._deadLetterQueue.has(dlqKey)) {
this._deadLetterQueue.set(dlqKey, []);
}
this._deadLetterQueue.get(dlqKey)?.push(task);
}
/**
* Unsubscribes a handler from a queue.
* @param {string} queue - The queue name to unsubscribe from.
* @param {string} [id] - Optional handler ID. If not provided, removes all handlers.
* @returns {Promise<void>}
*/
async unsubscribe(queue, id) {
if (id) {
const handlers = this._taskHandlers.get(queue);
if (handlers) {
this._taskHandlers.set(
queue,
handlers.filter((h) => h.id !== id)
);
}
} else {
this._taskHandlers.delete(queue);
}
}
/**
* Disconnects and clears all queues and handlers.
* Stops all task processing.
* @returns {Promise<void>}
*/
async disconnect() {
this._active = false;
for (const queuedTasks of this._queues.values()) {
for (const queuedTask of queuedTasks) {
if (queuedTask.timeoutHandle) {
clearTimeout(queuedTask.timeoutHandle);
}
}
}
this._taskHandlers.clear();
this._queues.clear();
this._processing.clear();
this._deadLetterQueue.clear();
}
/**
* Gets all tasks in the dead-letter queue for a specific queue.
* Useful for debugging and monitoring failed tasks.
* @param {string} queue - The queue name.
* @returns {Task[]} Array of tasks in the dead-letter queue.
*/
getDeadLetterTasks(queue) {
const dlqKey = `${queue}:dead-letter`;
return this._deadLetterQueue.get(dlqKey) ?? [];
}
/**
* Gets the current state of a queue.
* Useful for monitoring and debugging.
* @param {string} queue - The queue name.
* @returns {Object} Queue statistics.
*/
getQueueStats(queue) {
const queuedTasks = this._queues.get(queue) ?? [];
const processing = this._processing.get(queue)?.size ?? 0;
const waiting = queuedTasks.filter((qt) => !qt.processing).length;
const dlqKey = `${queue}:dead-letter`;
const deadLetter = this._deadLetterQueue.get(dlqKey)?.length ?? 0;
return {
waiting,
processing,
deadLetter
};
}
};
// src/index.ts

@@ -116,4 +457,16 @@ var QifiedEvents = /* @__PURE__ */ ((QifiedEvents2) => {

})(QifiedEvents || {});
var QifiedHooks = /* @__PURE__ */ ((QifiedHooks2) => {
QifiedHooks2["beforeSubscribe"] = "before:subscribe";
QifiedHooks2["afterSubscribe"] = "after:subscribe";
QifiedHooks2["beforePublish"] = "before:publish";
QifiedHooks2["afterPublish"] = "after:publish";
QifiedHooks2["beforeUnsubscribe"] = "before:unsubscribe";
QifiedHooks2["afterUnsubscribe"] = "after:unsubscribe";
QifiedHooks2["beforeDisconnect"] = "before:disconnect";
QifiedHooks2["afterDisconnect"] = "after:disconnect";
return QifiedHooks2;
})(QifiedHooks || {});
var Qified = class extends Hookified {
_messageProviders = [];
_taskProviders = [];
/**

@@ -132,2 +485,9 @@ * Creates an instance of Qified.

}
if (options?.taskProviders) {
if (Array.isArray(options?.taskProviders)) {
this._taskProviders = options.taskProviders;
} else {
this._taskProviders = [options?.taskProviders];
}
}
}

@@ -149,2 +509,16 @@ /**

/**
* Gets or sets the task providers.
* @returns {TaskProvider[]} The array of task providers.
*/
get taskProviders() {
return this._taskProviders;
}
/**
* Sets the task providers.
* @param {TaskProvider[]} providers - The array of task providers to set.
*/
set taskProviders(providers) {
this._taskProviders = providers;
}
/**
* Subscribes to a topic. If you have multiple message providers, it will subscribe to the topic on all of them.

@@ -156,7 +530,16 @@ * @param {string} topic - The topic to subscribe to.

try {
const context = { topic, handler };
await this.hook("before:subscribe" /* beforeSubscribe */, context);
const promises = this._messageProviders.map(
async (provider) => provider.subscribe(topic, handler)
async (provider) => provider.subscribe(context.topic, context.handler)
);
await Promise.all(promises);
this.emit("subscribe" /* subscribe */, { topic, handler });
await this.hook("after:subscribe" /* afterSubscribe */, {
topic: context.topic,
handler: context.handler
});
this.emit("subscribe" /* subscribe */, {
topic: context.topic,
handler: context.handler
});
} catch (error) {

@@ -173,7 +556,16 @@ this.emit("error" /* error */, error);

try {
const context = { topic, message };
await this.hook("before:publish" /* beforePublish */, context);
const promises = this._messageProviders.map(
async (provider) => provider.publish(topic, message)
async (provider) => provider.publish(context.topic, context.message)
);
await Promise.all(promises);
this.emit("publish" /* publish */, { topic, message });
await this.hook("after:publish" /* afterPublish */, {
topic: context.topic,
message: context.message
});
this.emit("publish" /* publish */, {
topic: context.topic,
message: context.message
});
} catch (error) {

@@ -191,7 +583,16 @@ this.emit("error" /* error */, error);

try {
const context = { topic, id };
await this.hook("before:unsubscribe" /* beforeUnsubscribe */, context);
const promises = this._messageProviders.map(
async (provider) => provider.unsubscribe(topic, id)
async (provider) => provider.unsubscribe(context.topic, context.id)
);
await Promise.all(promises);
this.emit("unsubscribe" /* unsubscribe */, { topic, id });
await this.hook("after:unsubscribe" /* afterUnsubscribe */, {
topic: context.topic,
id: context.id
});
this.emit("unsubscribe" /* unsubscribe */, {
topic: context.topic,
id: context.id
});
} catch (error) {

@@ -207,2 +608,4 @@ this.emit("error" /* error */, error);

try {
const context = { providerCount: this._messageProviders.length };
await this.hook("before:disconnect" /* beforeDisconnect */, context);
const promises = this._messageProviders.map(

@@ -213,2 +616,5 @@ async (provider) => provider.disconnect()

this._messageProviders = [];
await this.hook("after:disconnect" /* afterDisconnect */, {
providerCount: context.providerCount
});
this.emit("disconnect" /* disconnect */);

@@ -222,5 +628,7 @@ } catch (error) {

MemoryMessageProvider,
MemoryTaskProvider,
Qified,
QifiedEvents
QifiedEvents,
QifiedHooks
};
/* v8 ignore next -- @preserve */
+5
-5
{
"name": "qified",
"version": "0.5.3",
"version": "0.6.0",
"description": "Task and Message Queues with Multiple Providers",

@@ -36,8 +36,8 @@ "type": "module",

"devDependencies": {
"@biomejs/biome": "^2.3.5",
"@vitest/coverage-v8": "^4.0.8",
"@biomejs/biome": "^2.3.10",
"@vitest/coverage-v8": "^4.0.16",
"rimraf": "^6.1.0",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"vitest": "^4.0.8"
"vitest": "^4.0.16"
},

@@ -49,3 +49,3 @@ "files": [

"dependencies": {
"hookified": "^1.13.0"
"hookified": "^1.14.0"
},

@@ -52,0 +52,0 @@ "scripts": {

+163
-0

@@ -308,2 +308,165 @@ [![logo.svg](https://qified.org/logo.svg)](https://qified.org)

# Hooks
Qified provides before and after hooks for all major operations, allowing you to intercept and modify data before an operation executes, or perform actions after it completes. Hooks are powered by [Hookified](https://hookified.org).
## Available Hooks
The following hooks are available via the `QifiedHooks` enum:
| Hook | Description | Context Properties |
|------|-------------|-------------------|
| `beforeSubscribe` | Called before subscribing to a topic | `{ topic, handler }` |
| `afterSubscribe` | Called after subscribing to a topic | `{ topic, handler }` |
| `beforePublish` | Called before publishing a message | `{ topic, message }` |
| `afterPublish` | Called after publishing a message | `{ topic, message }` |
| `beforeUnsubscribe` | Called before unsubscribing from a topic | `{ topic, id }` |
| `afterUnsubscribe` | Called after unsubscribing from a topic | `{ topic, id }` |
| `beforeDisconnect` | Called before disconnecting from providers | `{ providerCount }` |
| `afterDisconnect` | Called after disconnecting from providers | `{ providerCount }` |
## Using Hooks
Use the `onHook()` method to register a hook handler:
```js
import { Qified, MemoryMessageProvider, QifiedHooks } from 'qified';
const qified = new Qified({
messageProviders: new MemoryMessageProvider()
});
// Register a before hook
qified.onHook(QifiedHooks.beforePublish, async (context) => {
console.log('About to publish to:', context.topic);
});
// Register an after hook
qified.onHook(QifiedHooks.afterPublish, async (context) => {
console.log('Published message:', context.message.id);
});
```
## Modifying Data with Before Hooks
Before hooks receive a mutable context object. Any changes you make to the context will be applied to the operation:
```js
import { Qified, MemoryMessageProvider, QifiedHooks } from 'qified';
const qified = new Qified({
messageProviders: new MemoryMessageProvider()
});
// Add timestamp and headers to all messages
qified.onHook(QifiedHooks.beforePublish, async (context) => {
// Add timestamp if not present
context.message.timestamp = context.message.timestamp ?? Date.now();
// Add custom headers
context.message.headers = {
...context.message.headers,
'x-processed-by': 'qified',
'x-environment': process.env.NODE_ENV
};
});
// Modify message data
qified.onHook(QifiedHooks.beforePublish, async (context) => {
// Add metadata to the message data
context.message.data = {
...context.message.data,
_meta: {
version: '1.0',
source: 'api'
}
};
});
// Subscribe to receive messages
await qified.subscribe('events', {
id: 'handler1',
handler: async (message) => {
// Message will have timestamp, headers, and modified data
console.log('Timestamp:', message.timestamp);
console.log('Headers:', message.headers);
console.log('Data:', message.data);
}
});
// Publish a message - hooks will modify it before sending
await qified.publish('events', {
id: 'msg-1',
data: { text: 'Hello!' }
});
```
## Modifying Topics with Before Hooks
You can also modify the topic in before hooks:
```js
// Route all messages to a prefixed topic
qified.onHook(QifiedHooks.beforePublish, async (context) => {
context.topic = `production/${context.topic}`;
});
// Subscribe to the prefixed topic
await qified.subscribe('production/events', {
id: 'handler1',
handler: async (message) => {
console.log('Received:', message.data);
}
});
// This publishes to 'production/events' due to the hook
await qified.publish('events', {
id: 'msg-1',
data: { text: 'Hello!' }
});
```
## Multiple Hooks
Multiple hooks for the same event execute in the order they were registered:
```js
// First hook - runs first
qified.onHook(QifiedHooks.beforePublish, async (context) => {
context.message.timestamp = Date.now();
});
// Second hook - runs second, can see changes from first hook
qified.onHook(QifiedHooks.beforePublish, async (context) => {
context.message.headers = { 'x-timestamp': String(context.message.timestamp) };
});
// Third hook - runs third
qified.onHook(QifiedHooks.beforePublish, async (context) => {
console.log('Final message:', context.message);
});
```
## Hooks vs Events
Both hooks and events are available, but they serve different purposes:
| Feature | Hooks | Events |
|---------|-------|--------|
| Timing | Before and after operations | After operations only |
| Data modification | Yes (before hooks) | No |
| Use case | Intercepting/transforming data | Logging, monitoring, side effects |
```js
// Hook - can modify the message before it's published
qified.onHook(QifiedHooks.beforePublish, async (context) => {
context.message.timestamp = Date.now();
});
// Event - notified after publish completes (cannot modify)
qified.on(QifiedEvents.publish, async (data) => {
console.log('Published:', data.message.id);
});
```
# Providers

@@ -310,0 +473,0 @@