diff --git a/apps/example/.gitignore b/apps/example/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/apps/example/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/apps/example/README.md b/apps/example/README.md new file mode 100644 index 0000000..35305d3 --- /dev/null +++ b/apps/example/README.md @@ -0,0 +1,15 @@ +# @tinyjobs/example + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.1.26. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/apps/example/package.json b/apps/example/package.json new file mode 100644 index 0000000..9a92280 --- /dev/null +++ b/apps/example/package.json @@ -0,0 +1,17 @@ +{ + "name": "@tinyjobs/example", + "module": "src/index.ts", + "type": "module", + "scripts": { + "dev": "bun src/index.ts --watch" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "tinyjobs": "*" + } +} \ No newline at end of file diff --git a/apps/example/src/index.ts b/apps/example/src/index.ts new file mode 100644 index 0000000..b1f3928 --- /dev/null +++ b/apps/example/src/index.ts @@ -0,0 +1,11 @@ +import TinyJob from "tinyjobs"; +import type TinyJobsTypes from "./jobs/jobs.types"; + +const JobHandler = new TinyJob(); +JobHandler.loadJobs(); + +const runExample = await JobHandler.queueJob("ExampleJob", { + name: "World", +}); + +console.log("ExampleJob queued:", runExample.id); diff --git a/apps/example/src/jobs/ExampleJob.ts b/apps/example/src/jobs/ExampleJob.ts new file mode 100644 index 0000000..09c6145 --- /dev/null +++ b/apps/example/src/jobs/ExampleJob.ts @@ -0,0 +1,12 @@ +import { Job } from "tinyjobs"; + +export default class ExampleJob extends Job { + constructor() { + super(); + this.name = "ExampleJob"; + } + + async run({ name }: { name: string }) { + console.log(`Hello from ExampleJob ${name}!`); + } +} diff --git a/apps/example/src/jobs/jobs.types.d.ts b/apps/example/src/jobs/jobs.types.d.ts new file mode 100644 index 0000000..fb8a2c9 --- /dev/null +++ b/apps/example/src/jobs/jobs.types.d.ts @@ -0,0 +1,8 @@ + +interface ExampleJobParams { name: string }; + +type TinyJobsTypes = { + 'ExampleJob': ExampleJobParams +}; + +export default TinyJobsTypes; diff --git a/apps/example/tinyjobs.json b/apps/example/tinyjobs.json new file mode 100644 index 0000000..bef624a --- /dev/null +++ b/apps/example/tinyjobs.json @@ -0,0 +1,4 @@ +{ + "language": "typescript", + "jobsDir": "src/jobs" +} \ No newline at end of file diff --git a/apps/example/tsconfig.json b/apps/example/tsconfig.json new file mode 100644 index 0000000..ded8a40 --- /dev/null +++ b/apps/example/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "paths": { + "tinyjobs": ["../../packages/tinyjobs/src/index.ts"], + } + } +} diff --git a/bun.lockb b/bun.lockb index ca66c02..9e86e6a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index bf86f96..260828d 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,5 @@ { "name": "tinyjobs", - "private": true, - "scripts": { - "build": "turbo build", - "dev": "turbo dev", - "lint": "turbo lint", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "ci:version": "bunx changeset version && bun install", - "ci:publish": "bunx changeset publish" - }, "devDependencies": { "prettier": "^3.3.3", "turbo": "^2.0.14", @@ -19,6 +10,15 @@ "node": ">=18" }, "packageManager": "bun@1.1.26", + "private": true, + "scripts": { + "build": "turbo build", + "dev": "turbo dev", + "lint": "turbo lint", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "ci:version": "bunx changeset version && bun install", + "ci:publish": "bunx changeset publish" + }, "workspaces": [ "apps/*", "packages/*" diff --git a/packages/tinyjobs/package.json b/packages/tinyjobs/package.json index fc24197..df5cdd2 100644 --- a/packages/tinyjobs/package.json +++ b/packages/tinyjobs/package.json @@ -42,6 +42,7 @@ ], "scripts": { "build": "tsup src/index.ts --format esm,cjs --dts", + "dev": "bun run build --watch", "prepack": "bun run build" }, "devDependencies": { @@ -54,6 +55,6 @@ }, "peerDependencies": {}, "dependencies": { - + "bullmq": "^5.12.11" } } \ No newline at end of file diff --git a/packages/tinyjobs/src/index.ts b/packages/tinyjobs/src/index.ts index e69de29..8f41d33 100644 --- a/packages/tinyjobs/src/index.ts +++ b/packages/tinyjobs/src/index.ts @@ -0,0 +1,5 @@ +import TinyJobsHandler from "./lib/TinyJobsHandler"; +import Job from "./structures/Job"; + +export default TinyJobsHandler; +export { Job }; diff --git a/packages/tinyjobs/src/lib/TinyJobsHandler.ts b/packages/tinyjobs/src/lib/TinyJobsHandler.ts new file mode 100644 index 0000000..9ead481 --- /dev/null +++ b/packages/tinyjobs/src/lib/TinyJobsHandler.ts @@ -0,0 +1,85 @@ +import { Queue, Worker, Job as BullJob } from "bullmq"; +import type { JobsOptions, ConnectionOptions } from "bullmq"; +import path from "path"; + +import TinyJob from "../structures/Job"; +import { generateRandomUid } from "../utils/utils"; +import { getConfig } from "../utils/config"; +import { loadJobsFromDir } from "../utils/jobs"; + +type TinyJobsConstructorTypes = { + connection?: ConnectionOptions; + queueOptions?: JobsOptions; + queueName?: string; +}; + +class TinyJobs { + private queue: Queue; + private worker: Worker; + private jobs = new Map TinyJob>(); + + constructor(tinyJobsParams?: TinyJobsConstructorTypes) { + const { + connection, + queueOptions, + queueName = `tjq-${generateRandomUid()}`, + } = tinyJobsParams ?? {}; + + this.queue = new Queue(queueName, { + connection: connection ?? {}, + }); + + this.worker = new Worker(queueName, this.processQueue.bind(this), { + connection: connection ?? {}, + }); + } + + private async processQueue(job: BullJob) { + const JobClass = this.jobs.get(job.name); + if (!JobClass) + throw new Error(`No handler registered for job type: ${job.name}`); + + if (JobClass.prototype instanceof TinyJob) { + const jobInstance = new (JobClass as any)(); + await jobInstance.handle(job.data); + } else { + throw new Error("Invalid job type."); + } + } + + public async queueJob( + jobName: K, + data: T[K], + options?: JobsOptions + ) { + return this.queue.add(jobName as string, data, options ? options : {}); + } + + public registerJob(job: new () => TinyJob) { + this.jobs.set(job.name, job); + } + + public async loadJobs(dir?: string) { + const config = await getConfig(); + const jobsDir = path.resolve( + process.cwd(), + dir ?? (config?.jobsDir as string) + ); + + if (!jobsDir) + throw new Error( + "Jobs directory not configured. Please run 'tinyjobs init'." + ); + + const jobs = await loadJobsFromDir(jobsDir); + for (const job of jobs) { + if (typeof job === "function") { + this.registerJob(job); + } else { + throw new Error(`Invalid job type: ${typeof job}`); + } + } + } +} + +export default TinyJobs; diff --git a/packages/tinyjobs/src/structures/Job.ts b/packages/tinyjobs/src/structures/Job.ts new file mode 100644 index 0000000..c754b67 --- /dev/null +++ b/packages/tinyjobs/src/structures/Job.ts @@ -0,0 +1,26 @@ +/** + * @description The base class for all jobs + * @abstract + * @class + * @name TinyJob + * @example + * class MyJob extends TinyJob { + * constructor() { + * super(); + * this.name = "MyJob"; + * this.cron = "* * * * *"; + * } + * + * run({ name }: { name: string }) { + * console.log(`Hello, ${name}!`); + * } + * } + **/ +abstract class Job { + name!: string; + cron?: string; + + abstract run(payload: Record): void; +} + +export default Job; diff --git a/packages/tinyjobs/src/utils/config.ts b/packages/tinyjobs/src/utils/config.ts new file mode 100644 index 0000000..350c733 --- /dev/null +++ b/packages/tinyjobs/src/utils/config.ts @@ -0,0 +1,19 @@ +import fs from "fs"; +import path from "path"; + +interface TinyJobConfig { + jobsDir: string; + language: "typescript" | "javascript"; +} + +const configFileName = "tinyjobs.json"; + +export const getConfig = async () => { + if (!fs.existsSync(path.resolve(process.cwd(), configFileName))) { + return undefined; + } + + return JSON.parse( + fs.readFileSync(path.resolve(process.cwd(), configFileName), "utf-8") + ) as TinyJobConfig; +}; diff --git a/packages/tinyjobs/src/utils/jobs.ts b/packages/tinyjobs/src/utils/jobs.ts new file mode 100644 index 0000000..d6960f8 --- /dev/null +++ b/packages/tinyjobs/src/utils/jobs.ts @@ -0,0 +1,24 @@ +import fs from "fs"; +import path from "path"; +import TinyJob from "../structures/Job"; + +export const loadJobsFromDir = async (dir: string) => { + const files = fs + .readdirSync(dir) + .filter( + (file) => + (file.endsWith(".js") || file.endsWith(".ts")) && + !file.endsWith(".d.ts") + ); + + const jobs: TinyJob[] = []; + + for (const file of files) { + const JobClass = require(path.join(dir, file)).default; + if (JobClass.prototype instanceof TinyJob) { + jobs.push(JobClass); + } + } + + return jobs; +}; diff --git a/packages/tinyjobs/src/utils/utils.ts b/packages/tinyjobs/src/utils/utils.ts new file mode 100644 index 0000000..797e239 --- /dev/null +++ b/packages/tinyjobs/src/utils/utils.ts @@ -0,0 +1,4 @@ +export const generateRandomUid = () => { + const uuid = crypto.randomUUID(); + return uuid.replace(/-/g, ""); +};