express-file-cluster 0.1.0

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.
Files changed (53) hide show
  1. package/dist/auth/index.cjs +98 -0
  2. package/dist/auth/index.cjs.map +1 -0
  3. package/dist/auth/index.d.cts +16 -0
  4. package/dist/auth/index.d.ts +16 -0
  5. package/dist/auth/index.js +59 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/cli/index.cjs +380 -0
  8. package/dist/cli/index.cjs.map +1 -0
  9. package/dist/cli/index.d.cts +1 -0
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +357 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/index.cjs +541 -0
  14. package/dist/index.cjs.map +1 -0
  15. package/dist/index.d.cts +23 -0
  16. package/dist/index.d.ts +23 -0
  17. package/dist/index.js +498 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/tasks/index.cjs +70 -0
  20. package/dist/tasks/index.cjs.map +1 -0
  21. package/dist/tasks/index.d.cts +16 -0
  22. package/dist/tasks/index.d.ts +16 -0
  23. package/dist/tasks/index.js +41 -0
  24. package/dist/tasks/index.js.map +1 -0
  25. package/dist/types-DNdkDUPX.d.cts +70 -0
  26. package/dist/types-DNdkDUPX.d.ts +70 -0
  27. package/package.json +69 -0
  28. package/src/auth/index.ts +71 -0
  29. package/src/cli/commands/build.ts +43 -0
  30. package/src/cli/commands/diagnostics.ts +124 -0
  31. package/src/cli/commands/generate.ts +91 -0
  32. package/src/cli/commands/run.ts +28 -0
  33. package/src/cli/commands/start.ts +74 -0
  34. package/src/cli/index.ts +22 -0
  35. package/src/cluster/index.ts +31 -0
  36. package/src/compose.ts +26 -0
  37. package/src/db/index.ts +31 -0
  38. package/src/db/model.ts +95 -0
  39. package/src/db/mongo.ts +22 -0
  40. package/src/errors.ts +10 -0
  41. package/src/index.ts +127 -0
  42. package/src/router/mount.test.ts +75 -0
  43. package/src/router/mount.ts +49 -0
  44. package/src/router/scan.test.ts +23 -0
  45. package/src/router/scan.ts +55 -0
  46. package/src/tasks/bullmq-backend.ts +76 -0
  47. package/src/tasks/index.ts +51 -0
  48. package/src/tasks/scanner.test.ts +58 -0
  49. package/src/tasks/scanner.ts +30 -0
  50. package/src/tasks/thread-runner.ts +40 -0
  51. package/src/types.ts +68 -0
  52. package/tsconfig.json +9 -0
  53. package/tsup.config.ts +26 -0
@@ -0,0 +1,76 @@
1
+ import type { Job } from 'bullmq';
2
+ import { taskRegistry, setEnqueueImpl } from './index.js';
3
+ import { runInThread } from './thread-runner.js';
4
+
5
+ interface BullMQOpts {
6
+ redisUrl: string;
7
+ concurrency: number;
8
+ }
9
+
10
+ export async function initBullMQ(opts: BullMQOpts): Promise<void> {
11
+ let bullmq: typeof import('bullmq');
12
+ try {
13
+ bullmq = await import('bullmq');
14
+ } catch {
15
+ throw new Error('[EFC] bullmq is not installed. Run: npm install bullmq');
16
+ }
17
+
18
+ const connection = parseRedisUrl(opts.redisUrl);
19
+ const queue = new bullmq.Queue('efc', { connection });
20
+
21
+ const worker = new bullmq.Worker(
22
+ 'efc',
23
+ async (job: Job) => {
24
+ const def = taskRegistry.get(job.name);
25
+ if (!def) {
26
+ throw new Error(`[EFC] No task registered for name: ${job.name}`);
27
+ }
28
+
29
+ if (def.options.thread && def.filePath) {
30
+ await runInThread(def.filePath, job.data);
31
+ } else {
32
+ await def.handler(job.data);
33
+ }
34
+ },
35
+ { connection, concurrency: opts.concurrency },
36
+ );
37
+
38
+ worker.on('failed', (job: Job | undefined, err: Error) => {
39
+ console.error(`[EFC] Task ${job?.name ?? 'unknown'} failed:`, err.message);
40
+ });
41
+
42
+ worker.on('completed', (job: Job) => {
43
+ console.log(`[EFC] Task ${job.name} completed (id=${job.id})`);
44
+ });
45
+
46
+ setEnqueueImpl(async (name, payload) => {
47
+ const def = taskRegistry.get(name);
48
+ if (!def) {
49
+ throw new Error(`[EFC] Cannot enqueue unknown task: "${name}". Is the task file in tasksDir?`);
50
+ }
51
+
52
+ await queue.add(name, payload, {
53
+ attempts: def.options.retries ?? 3,
54
+ backoff: {
55
+ type: def.options.backoff ?? 'exponential',
56
+ delay: 1000,
57
+ },
58
+ });
59
+ });
60
+
61
+ console.log('[EFC] BullMQ backend initialised');
62
+ }
63
+
64
+ function parseRedisUrl(url: string): { host: string; port: number; password?: string } {
65
+ try {
66
+ const u = new URL(url);
67
+ const result: { host: string; port: number; password?: string } = {
68
+ host: u.hostname || 'localhost',
69
+ port: parseInt(u.port || '6379', 10),
70
+ };
71
+ if (u.password) result.password = u.password;
72
+ return result;
73
+ } catch {
74
+ return { host: 'localhost', port: 6379 };
75
+ }
76
+ }
@@ -0,0 +1,51 @@
1
+ import type { TaskDefinition, TaskOptions } from '../types.js';
2
+
3
+ export const taskRegistry = new Map<string, TaskDefinition>();
4
+
5
+ type HandlerFn<T> = (payload: T) => Promise<void>;
6
+ type DefineTaskOverload = {
7
+ <T>(handler: HandlerFn<T>): TaskDefinition<T>;
8
+ <T>(options: TaskOptions, handler: HandlerFn<T>): TaskDefinition<T>;
9
+ };
10
+
11
+ export const defineTask: DefineTaskOverload = <T>(
12
+ handlerOrOptions: HandlerFn<T> | TaskOptions,
13
+ maybeHandler?: HandlerFn<T>,
14
+ ): TaskDefinition<T> => {
15
+ let options: TaskOptions = {};
16
+ let handler: HandlerFn<T>;
17
+
18
+ if (typeof handlerOrOptions === 'function') {
19
+ handler = handlerOrOptions;
20
+ } else {
21
+ options = handlerOrOptions;
22
+ if (!maybeHandler) throw new Error('[EFC] defineTask: handler function is required');
23
+ handler = maybeHandler;
24
+ }
25
+
26
+ return {
27
+ handler: handler as (payload: unknown) => Promise<void>,
28
+ options,
29
+ name: '',
30
+ };
31
+ };
32
+
33
+ type EnqueueImpl = (name: string, payload: unknown) => Promise<void>;
34
+ let _impl: EnqueueImpl | null = null;
35
+
36
+ export function setEnqueueImpl(impl: EnqueueImpl): void {
37
+ _impl = impl;
38
+ }
39
+
40
+ export async function enqueue<T>(name: string, payload: T): Promise<void> {
41
+ if (!_impl) {
42
+ throw new Error(
43
+ `[EFC] Task queue not initialised. Set tasks.backend in ignite() config.`,
44
+ );
45
+ }
46
+ return _impl(name, payload as unknown);
47
+ }
48
+
49
+ export function registerTask(name: string, def: TaskDefinition): void {
50
+ taskRegistry.set(name, { ...def, name });
51
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { scanTasks } from './scanner.js';
6
+ import { taskRegistry } from './index.js';
7
+
8
+ describe('scanTasks', () => {
9
+ let tmpDir: string;
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'efc-test-'));
13
+ taskRegistry.clear();
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it('returns silently when directory does not exist', async () => {
21
+ await expect(scanTasks('/nonexistent/path/xyz')).resolves.toBeUndefined();
22
+ expect(taskRegistry.size).toBe(0);
23
+ });
24
+
25
+ it('returns silently for an empty directory', async () => {
26
+ await scanTasks(tmpDir);
27
+ expect(taskRegistry.size).toBe(0);
28
+ });
29
+
30
+ it('registers a valid task from a .js file', async () => {
31
+ const taskFile = path.join(tmpDir, 'MyTask.js');
32
+ fs.writeFileSync(
33
+ taskFile,
34
+ `export default { handler: async (p) => {}, options: {}, name: '' };`,
35
+ );
36
+
37
+ await scanTasks(tmpDir);
38
+ expect(taskRegistry.has('MyTask')).toBe(true);
39
+ const def = taskRegistry.get('MyTask');
40
+ expect(typeof def?.handler).toBe('function');
41
+ });
42
+
43
+ it('skips files with no valid default export and logs a warning', async () => {
44
+ const taskFile = path.join(tmpDir, 'BadTask.js');
45
+ fs.writeFileSync(taskFile, `export const foo = 42;`);
46
+
47
+ await scanTasks(tmpDir);
48
+ expect(taskRegistry.has('BadTask')).toBe(false);
49
+ });
50
+
51
+ it('skips unimportable files gracefully', async () => {
52
+ const taskFile = path.join(tmpDir, 'BrokenTask.js');
53
+ fs.writeFileSync(taskFile, `this is not valid javascript !!!`);
54
+
55
+ await expect(scanTasks(tmpDir)).resolves.toBeUndefined();
56
+ expect(taskRegistry.has('BrokenTask')).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { registerTask } from './index.js';
4
+ import type { TaskDefinition } from '../types.js';
5
+
6
+ export async function scanTasks(tasksDir: string): Promise<void> {
7
+ if (!fs.existsSync(tasksDir)) return;
8
+
9
+ const files = fs.readdirSync(tasksDir).filter((f) => /\.(ts|js|mts|mjs)$/.test(f));
10
+
11
+ for (const file of files) {
12
+ const filePath = path.join(tasksDir, file);
13
+ const taskName = path.basename(file, path.extname(file));
14
+
15
+ try {
16
+ const mod = await import(filePath) as Record<string, unknown>;
17
+ const def = mod['default'] as TaskDefinition | undefined;
18
+
19
+ if (!def || typeof def.handler !== 'function') {
20
+ console.warn(`[EFC] Task file ${file} has no valid default export — skipping`);
21
+ continue;
22
+ }
23
+
24
+ registerTask(taskName, { ...def, filePath });
25
+ console.log(`[EFC] Registered task: ${taskName}`);
26
+ } catch (err) {
27
+ console.warn(`[EFC] Failed to load task ${file}:`, err);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,40 @@
1
+ import { Worker, workerData, parentPort, isMainThread } from 'node:worker_threads';
2
+ import type { TaskDefinition } from '../types.js';
3
+
4
+ // ── Worker-thread side ────────────────────────────────────────────────────────
5
+ if (!isMainThread) {
6
+ const { handlerPath, payload } = workerData as { handlerPath: string; payload: unknown };
7
+
8
+ (async () => {
9
+ try {
10
+ const mod = await import(handlerPath) as Record<string, unknown>;
11
+ const def = mod['default'] as TaskDefinition | undefined;
12
+ if (!def || typeof def.handler !== 'function') {
13
+ throw new Error(`[EFC] Thread runner: no valid task export in ${handlerPath}`);
14
+ }
15
+ await def.handler(payload);
16
+ parentPort?.postMessage({ ok: true });
17
+ } catch (err) {
18
+ parentPort?.postMessage({ ok: false, error: String(err) });
19
+ }
20
+ })();
21
+ }
22
+
23
+ // ── Main-thread side ──────────────────────────────────────────────────────────
24
+ export function runInThread(handlerPath: string, payload: unknown): Promise<void> {
25
+ return new Promise((resolve, reject) => {
26
+ const worker = new Worker(new URL('./thread-runner.js', import.meta.url), {
27
+ workerData: { handlerPath, payload },
28
+ });
29
+
30
+ worker.on('message', (msg: { ok: boolean; error?: string }) => {
31
+ if (msg.ok) resolve();
32
+ else reject(new Error(msg.error ?? 'Thread task failed'));
33
+ });
34
+
35
+ worker.on('error', reject);
36
+ worker.on('exit', (code) => {
37
+ if (code !== 0) reject(new Error(`Thread worker exited with code ${code}`));
38
+ });
39
+ });
40
+ }
package/src/types.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { RequestHandler, ErrorRequestHandler } from 'express';
2
+
3
+ export type DatabaseEngine = 'mongodb' | 'postgresql';
4
+ export type AuthStrategy = 'http-only' | 'localStorage';
5
+ export type TaskBackend = 'bullmq' | 'pg-boss';
6
+
7
+ export interface TaskConfig {
8
+ backend: TaskBackend;
9
+ redisUrl?: string;
10
+ concurrency?: number;
11
+ }
12
+
13
+ export interface EFCConfig {
14
+ port?: number;
15
+ apiDir: string;
16
+ tasksDir?: string;
17
+ database?: DatabaseEngine;
18
+ databaseUrl?: string;
19
+ authStrategy?: AuthStrategy;
20
+ jwtSecret?: string;
21
+ cluster?: boolean;
22
+ workers?: number;
23
+ tasks?: TaskConfig | false;
24
+ globalMiddlewares?: RequestHandler[];
25
+ onWorkerReady?: (id: number) => void;
26
+ onWorkerCrash?: (id: number, code: number) => void;
27
+ onError?: ErrorRequestHandler;
28
+ }
29
+
30
+ export interface RouteEntry {
31
+ urlPath: string;
32
+ filePath: string;
33
+ params: string[];
34
+ }
35
+
36
+ export interface TaskOptions {
37
+ thread?: boolean;
38
+ retries?: number;
39
+ backoff?: 'fixed' | 'exponential';
40
+ concurrency?: number;
41
+ schedule?: string;
42
+ }
43
+
44
+ export interface TaskDefinition<TPayload = unknown> {
45
+ handler: (payload: TPayload) => Promise<void>;
46
+ options: TaskOptions;
47
+ name: string;
48
+ filePath?: string;
49
+ }
50
+
51
+ export interface FieldDefinition {
52
+ type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array';
53
+ required?: boolean;
54
+ unique?: boolean;
55
+ default?: unknown;
56
+ }
57
+
58
+ export type ModelSchema = Record<string, FieldDefinition>;
59
+
60
+ export interface ModelCRUD<T extends Record<string, unknown>> {
61
+ find(filter?: Partial<T>): Promise<T[]>;
62
+ findById(id: string): Promise<(T & { id: string }) | null>;
63
+ findOne(filter: Partial<T>): Promise<(T & { id: string }) | null>;
64
+ create(data: Partial<T>): Promise<T & { id: string }>;
65
+ update(id: string, data: Partial<T>): Promise<(T & { id: string }) | null>;
66
+ delete(id: string): Promise<void>;
67
+ count(filter?: Partial<T>): Promise<number>;
68
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: 'src/index.ts',
6
+ 'auth/index': 'src/auth/index.ts',
7
+ 'tasks/index': 'src/tasks/index.ts',
8
+ 'cli/index': 'src/cli/index.ts',
9
+ },
10
+ format: ['cjs', 'esm'],
11
+ dts: true,
12
+ clean: true,
13
+ splitting: false,
14
+ sourcemap: true,
15
+ target: 'node18',
16
+ banner: {
17
+ js: '',
18
+ },
19
+ esbuildOptions(options, context) {
20
+ if (context.format === 'esm') {
21
+ options.banner = {
22
+ js: context.entry === 'src/cli/index.ts' ? '#!/usr/bin/env node' : '',
23
+ };
24
+ }
25
+ },
26
+ });