feat(queue): implement local adapter with JSON persistence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
87
packages/queue/src/adapters/local.ts
Normal file
87
packages/queue/src/adapters/local.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
import type { QueueAdapter, QueueConfig, TaskPayload } from '../types.js';
|
||||
|
||||
const DEFAULT_DATA_DIR = '.mosaic/queue';
|
||||
|
||||
export function createLocalAdapter(config: QueueConfig): QueueAdapter {
|
||||
if (config.type !== 'local') {
|
||||
throw new Error(`Expected config type "local", got "${config.type}"`);
|
||||
}
|
||||
|
||||
const dataDir = config.dataDir ?? DEFAULT_DATA_DIR;
|
||||
const queues = new Map<string, TaskPayload[]>();
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
|
||||
// Load existing JSON files on startup
|
||||
for (const file of readdirSync(dataDir)) {
|
||||
if (!file.endsWith('.json')) continue;
|
||||
const queueName = file.slice(0, -5);
|
||||
try {
|
||||
const raw = readFileSync(join(dataDir, file), 'utf-8');
|
||||
const items = JSON.parse(raw) as TaskPayload[];
|
||||
if (Array.isArray(items)) {
|
||||
queues.set(queueName, items);
|
||||
}
|
||||
} catch {
|
||||
// Ignore corrupt files
|
||||
}
|
||||
}
|
||||
|
||||
function persist(queueName: string): void {
|
||||
const items = queues.get(queueName) ?? [];
|
||||
writeFileSync(join(dataDir, `${queueName}.json`), JSON.stringify(items), 'utf-8');
|
||||
}
|
||||
|
||||
function getQueue(queueName: string): TaskPayload[] {
|
||||
let q = queues.get(queueName);
|
||||
if (!q) {
|
||||
q = [];
|
||||
queues.set(queueName, q);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'local',
|
||||
|
||||
async enqueue(queueName: string, payload: TaskPayload): Promise<void> {
|
||||
getQueue(queueName).push(payload);
|
||||
persist(queueName);
|
||||
},
|
||||
|
||||
async dequeue(queueName: string): Promise<TaskPayload | null> {
|
||||
const q = getQueue(queueName);
|
||||
const item = q.shift() ?? null;
|
||||
persist(queueName);
|
||||
return item;
|
||||
},
|
||||
|
||||
async length(queueName: string): Promise<number> {
|
||||
return getQueue(queueName).length;
|
||||
},
|
||||
|
||||
async publish(channel: string, message: string): Promise<void> {
|
||||
emitter.emit(channel, message);
|
||||
},
|
||||
|
||||
subscribe(channel: string, handler: (message: string) => void): () => void {
|
||||
emitter.on(channel, handler);
|
||||
return () => {
|
||||
emitter.off(channel, handler);
|
||||
};
|
||||
},
|
||||
|
||||
async close(): Promise<void> {
|
||||
for (const queueName of queues.keys()) {
|
||||
persist(queueName);
|
||||
}
|
||||
queues.clear();
|
||||
emitter.removeAllListeners();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user