@beeblock/svelar 0.4.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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/actions/index.d.ts +101 -0
- package/dist/actions/index.js +1 -0
- package/dist/api-keys/index.d.ts +58 -0
- package/dist/api-keys/index.js +1 -0
- package/dist/audit/index.d.ts +52 -0
- package/dist/audit/index.js +1 -0
- package/dist/auth/Auth.d.ts +283 -0
- package/dist/auth/Gate.d.ts +166 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +80 -0
- package/dist/broadcasting/client.d.ts +195 -0
- package/dist/broadcasting/client.js +1 -0
- package/dist/broadcasting/index.d.ts +318 -0
- package/dist/broadcasting/index.js +20 -0
- package/dist/cache/index.d.ts +77 -0
- package/dist/cache/index.js +1 -0
- package/dist/cli/Cli.d.ts +23 -0
- package/dist/cli/Command.d.ts +36 -0
- package/dist/cli/bin.d.ts +8 -0
- package/dist/cli/bin.js +5856 -0
- package/dist/cli/commands/KeyGenerateCommand.d.ts +16 -0
- package/dist/cli/commands/MakeActionCommand.d.ts +15 -0
- package/dist/cli/commands/MakeBroadcastingCommand.d.ts +29 -0
- package/dist/cli/commands/MakeChannelCommand.d.ts +18 -0
- package/dist/cli/commands/MakeCommandCommand.d.ts +16 -0
- package/dist/cli/commands/MakeConfigCommand.d.ts +13 -0
- package/dist/cli/commands/MakeControllerCommand.d.ts +28 -0
- package/dist/cli/commands/MakeDashboardCommand.d.ts +34 -0
- package/dist/cli/commands/MakeDockerCommand.d.ts +32 -0
- package/dist/cli/commands/MakeEventCommand.d.ts +11 -0
- package/dist/cli/commands/MakeJobCommand.d.ts +11 -0
- package/dist/cli/commands/MakeListenerCommand.d.ts +16 -0
- package/dist/cli/commands/MakeMiddlewareCommand.d.ts +11 -0
- package/dist/cli/commands/MakeMigrationCommand.d.ts +17 -0
- package/dist/cli/commands/MakeModelCommand.d.ts +25 -0
- package/dist/cli/commands/MakeObserverCommand.d.ts +23 -0
- package/dist/cli/commands/MakePluginCommand.d.ts +11 -0
- package/dist/cli/commands/MakeProviderCommand.d.ts +11 -0
- package/dist/cli/commands/MakeRepositoryCommand.d.ts +22 -0
- package/dist/cli/commands/MakeRequestCommand.d.ts +15 -0
- package/dist/cli/commands/MakeResourceCommand.d.ts +30 -0
- package/dist/cli/commands/MakeRouteCommand.d.ts +42 -0
- package/dist/cli/commands/MakeSchemaCommand.d.ts +20 -0
- package/dist/cli/commands/MakeSeederCommand.d.ts +11 -0
- package/dist/cli/commands/MakeServiceCommand.d.ts +28 -0
- package/dist/cli/commands/MakeTaskCommand.d.ts +12 -0
- package/dist/cli/commands/MigrateCommand.d.ts +26 -0
- package/dist/cli/commands/NewCommand.d.ts +21 -0
- package/dist/cli/commands/NewCommandTemplates.d.ts +123 -0
- package/dist/cli/commands/PluginInstallCommand.d.ts +16 -0
- package/dist/cli/commands/PluginListCommand.d.ts +11 -0
- package/dist/cli/commands/PluginPublishCommand.d.ts +22 -0
- package/dist/cli/commands/QueueFailedCommand.d.ts +9 -0
- package/dist/cli/commands/QueueFlushCommand.d.ts +9 -0
- package/dist/cli/commands/QueueRetryCommand.d.ts +16 -0
- package/dist/cli/commands/QueueWorkCommand.d.ts +25 -0
- package/dist/cli/commands/RoutesListCommand.d.ts +30 -0
- package/dist/cli/commands/ScheduleRunCommand.d.ts +15 -0
- package/dist/cli/commands/SeedCommand.d.ts +14 -0
- package/dist/cli/commands/TinkerCommand.d.ts +10 -0
- package/dist/cli/index.d.ts +36 -0
- package/dist/cli/index.js +1973 -0
- package/dist/cli/ts-resolve-hook.mjs +74 -0
- package/dist/cli/ts-resolver.mjs +8 -0
- package/dist/config/Config.d.ts +65 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/container/Application.d.ts +33 -0
- package/dist/container/Container.d.ts +70 -0
- package/dist/container/ServiceProvider.d.ts +21 -0
- package/dist/container/index.d.ts +3 -0
- package/dist/container/index.js +1 -0
- package/dist/dashboard/index.d.ts +123 -0
- package/dist/dashboard/index.js +5 -0
- package/dist/database/Connection.d.ts +80 -0
- package/dist/database/Migration.d.ts +76 -0
- package/dist/database/SchemaBuilder.d.ts +91 -0
- package/dist/database/Seeder.d.ts +9 -0
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.js +4 -0
- package/dist/email-templates/index.d.ts +51 -0
- package/dist/email-templates/index.js +57 -0
- package/dist/errors/Handler.d.ts +100 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +5 -0
- package/dist/events/EventServiceProvider.d.ts +82 -0
- package/dist/events/Listener.d.ts +28 -0
- package/dist/events/index.d.ts +80 -0
- package/dist/events/index.js +1 -0
- package/dist/excel/index.d.ts +154 -0
- package/dist/excel/index.js +1 -0
- package/dist/feature-flags/index.d.ts +158 -0
- package/dist/feature-flags/index.js +59 -0
- package/dist/forms/index.d.ts +81 -0
- package/dist/forms/index.js +1 -0
- package/dist/hashing/Hash.d.ts +51 -0
- package/dist/hashing/index.d.ts +1 -0
- package/dist/hashing/index.js +1 -0
- package/dist/hooks/index.d.ts +135 -0
- package/dist/hooks/index.js +5 -0
- package/dist/http/index.d.ts +201 -0
- package/dist/http/index.js +2 -0
- package/dist/i18n/index.d.ts +81 -0
- package/dist/i18n/index.js +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +127 -0
- package/dist/logging/LogViewer.d.ts +95 -0
- package/dist/logging/LogViewer.js +1 -0
- package/dist/logging/index.d.ts +83 -0
- package/dist/logging/index.js +3 -0
- package/dist/mail/index.d.ts +149 -0
- package/dist/mail/index.js +1 -0
- package/dist/middleware/Middleware.d.ts +208 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/notifications/index.d.ts +85 -0
- package/dist/notifications/index.js +2 -0
- package/dist/orm/Model.d.ts +123 -0
- package/dist/orm/Observer.d.ts +34 -0
- package/dist/orm/QueryBuilder.d.ts +119 -0
- package/dist/orm/Relationship.d.ts +58 -0
- package/dist/orm/index.d.ts +4 -0
- package/dist/orm/index.js +1 -0
- package/dist/pagination/index.d.ts +8 -0
- package/dist/pagination/index.js +0 -0
- package/dist/pdf/GeneratePdfJob.d.ts +99 -0
- package/dist/pdf/GeneratePdfJob.js +41 -0
- package/dist/pdf/index.d.ts +328 -0
- package/dist/pdf/index.js +41 -0
- package/dist/permissions/index.d.ts +161 -0
- package/dist/permissions/index.js +60 -0
- package/dist/plugins/BootstrapPlugins.d.ts +11 -0
- package/dist/plugins/PluginInstaller.d.ts +30 -0
- package/dist/plugins/PluginInstaller.js +1 -0
- package/dist/plugins/PluginPublisher.d.ts +32 -0
- package/dist/plugins/PluginPublisher.js +1 -0
- package/dist/plugins/PluginRegistry.d.ts +55 -0
- package/dist/plugins/PluginRegistry.js +1 -0
- package/dist/plugins/index.d.ts +206 -0
- package/dist/plugins/index.js +1 -0
- package/dist/queue/JobMonitor.d.ts +109 -0
- package/dist/queue/JobMonitor.js +5 -0
- package/dist/queue/index.d.ts +279 -0
- package/dist/queue/index.js +5 -0
- package/dist/repositories/index.d.ts +147 -0
- package/dist/repositories/index.js +1 -0
- package/dist/routing/Controller.d.ts +115 -0
- package/dist/routing/FormRequest.d.ts +94 -0
- package/dist/routing/Resource.d.ts +213 -0
- package/dist/routing/Response.d.ts +138 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +5 -0
- package/dist/scheduler/ScheduleMonitor.d.ts +141 -0
- package/dist/scheduler/ScheduleMonitor.js +1 -0
- package/dist/scheduler/SchedulerLock.d.ts +33 -0
- package/dist/scheduler/index.d.ts +208 -0
- package/dist/scheduler/index.js +34 -0
- package/dist/services/index.d.ts +79 -0
- package/dist/services/index.js +1 -0
- package/dist/session/Session.d.ts +166 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +16 -0
- package/dist/storage/index.d.ts +154 -0
- package/dist/storage/index.js +1 -0
- package/dist/support/Pipeline.d.ts +65 -0
- package/dist/support/date.d.ts +136 -0
- package/dist/support/date.js +1 -0
- package/dist/support/index.d.ts +8 -0
- package/dist/support/index.js +1 -0
- package/dist/support/singleton.d.ts +10 -0
- package/dist/support/uuid.d.ts +40 -0
- package/dist/teams/index.d.ts +91 -0
- package/dist/teams/index.js +78 -0
- package/dist/uploads/index.d.ts +63 -0
- package/dist/uploads/index.js +2 -0
- package/dist/validation/index.d.ts +46 -0
- package/dist/validation/index.js +1 -0
- package/dist/webhooks/index.d.ts +66 -0
- package/dist/webhooks/index.js +1 -0
- package/package.json +338 -0
- package/src/i18n/LanguageSwitcher.svelte +47 -0
- package/src/i18n/index.ts +113 -0
- package/src/ui/Alert.svelte +22 -0
- package/src/ui/Avatar.svelte +18 -0
- package/src/ui/AvatarFallback.svelte +18 -0
- package/src/ui/AvatarImage.svelte +12 -0
- package/src/ui/Badge.svelte +27 -0
- package/src/ui/Button.svelte +51 -0
- package/src/ui/Card.svelte +15 -0
- package/src/ui/CardContent.svelte +15 -0
- package/src/ui/CardDescription.svelte +15 -0
- package/src/ui/CardFooter.svelte +15 -0
- package/src/ui/CardHeader.svelte +15 -0
- package/src/ui/CardTitle.svelte +15 -0
- package/src/ui/Icon.svelte +81 -0
- package/src/ui/Input.svelte +40 -0
- package/src/ui/Label.svelte +20 -0
- package/src/ui/Separator.svelte +10 -0
- package/src/ui/Tabs.svelte +23 -0
- package/src/ui/TabsContent.svelte +27 -0
- package/src/ui/TabsList.svelte +19 -0
- package/src/ui/TabsTrigger.svelte +28 -0
- package/src/ui/Toaster.svelte +279 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/toast.ts +212 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Job Monitor
|
|
3
|
+
*
|
|
4
|
+
* Driver-agnostic utilities for querying queue job state.
|
|
5
|
+
* Used by the admin dashboard to display job metrics and manage jobs.
|
|
6
|
+
*/
|
|
7
|
+
export interface JobInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
jobClass: string;
|
|
10
|
+
queue: string;
|
|
11
|
+
status: 'waiting' | 'active' | 'completed' | 'failed' | 'delayed';
|
|
12
|
+
attempts: number;
|
|
13
|
+
maxAttempts: number;
|
|
14
|
+
payload: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
processedAt?: number;
|
|
18
|
+
failedAt?: number;
|
|
19
|
+
delayedUntil?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface JobCounts {
|
|
22
|
+
waiting: number;
|
|
23
|
+
active: number;
|
|
24
|
+
completed: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
delayed: number;
|
|
27
|
+
total: number;
|
|
28
|
+
}
|
|
29
|
+
export interface JobFilter {
|
|
30
|
+
status?: JobInfo['status'];
|
|
31
|
+
queue?: string;
|
|
32
|
+
jobClass?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
offset?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface QueueHealth {
|
|
37
|
+
driver: string;
|
|
38
|
+
queues: Record<string, JobCounts>;
|
|
39
|
+
oldestWaitingJob?: number;
|
|
40
|
+
failureRate: number;
|
|
41
|
+
throughput: number;
|
|
42
|
+
}
|
|
43
|
+
declare class JobMonitorService {
|
|
44
|
+
private _config;
|
|
45
|
+
private _bullmq;
|
|
46
|
+
private _metrics;
|
|
47
|
+
configure(config: {
|
|
48
|
+
driver: string;
|
|
49
|
+
default: string;
|
|
50
|
+
connections: Record<string, any>;
|
|
51
|
+
}): void;
|
|
52
|
+
/** Record a processed job (called internally by queue system) */
|
|
53
|
+
recordProcessed(): void;
|
|
54
|
+
/** Record a failed job (called internally by queue system) */
|
|
55
|
+
recordFailed(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Get job counts for a queue (or all queues)
|
|
58
|
+
*/
|
|
59
|
+
getCounts(queueName?: string): Promise<JobCounts>;
|
|
60
|
+
/**
|
|
61
|
+
* List jobs with filtering and pagination
|
|
62
|
+
*/
|
|
63
|
+
listJobs(filter?: JobFilter): Promise<JobInfo[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Get details for a single job
|
|
66
|
+
*/
|
|
67
|
+
getJob(jobId: string): Promise<JobInfo | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Retry a failed job
|
|
70
|
+
*/
|
|
71
|
+
retryJob(jobId: string): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Delete a job from the queue
|
|
74
|
+
*/
|
|
75
|
+
deleteJob(jobId: string): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Get overall queue health metrics
|
|
78
|
+
*/
|
|
79
|
+
getHealth(): Promise<QueueHealth>;
|
|
80
|
+
/**
|
|
81
|
+
* Flush all completed jobs (cleanup)
|
|
82
|
+
*/
|
|
83
|
+
flushCompleted(queueName?: string): Promise<number>;
|
|
84
|
+
/**
|
|
85
|
+
* Flush all failed jobs
|
|
86
|
+
*/
|
|
87
|
+
flushFailed(queueName?: string): Promise<number>;
|
|
88
|
+
private getBullMQ;
|
|
89
|
+
private getRedisConnection;
|
|
90
|
+
private getRedisQueue;
|
|
91
|
+
private _getRedisJobCounts;
|
|
92
|
+
private _listRedisJobs;
|
|
93
|
+
private _getRedisJob;
|
|
94
|
+
private _retryRedisJob;
|
|
95
|
+
private _deleteRedisJob;
|
|
96
|
+
private _flushRedisCompleted;
|
|
97
|
+
private _flushRedisFailed;
|
|
98
|
+
private _bullmqJobToInfo;
|
|
99
|
+
private getDbConnection;
|
|
100
|
+
private getDbTable;
|
|
101
|
+
private _getDatabaseJobCounts;
|
|
102
|
+
private _listDatabaseJobs;
|
|
103
|
+
private _getDatabaseJob;
|
|
104
|
+
private _retryDatabaseJob;
|
|
105
|
+
private _deleteDatabaseJob;
|
|
106
|
+
private _flushDatabaseFailed;
|
|
107
|
+
}
|
|
108
|
+
export declare const JobMonitor: JobMonitorService;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var C=Object.defineProperty;var p=(d,t)=>()=>(d&&(t=d(d=0)),t);var _=(d,t)=>{for(var e in t)C(d,e,{get:t[e],enumerable:!0})};function u(d,t){let e=Symbol.for(d),n=globalThis;return n[e]||(n[e]=t()),n[e]}var f=p(()=>{"use strict"});var w={};_(w,{Connection:()=>D});var b,D,y=p(()=>{"use strict";f();b=class{connections=new Map;config=null;defaultName="default";configure(t){this.config=t,this.defaultName=t.default}async connection(t){let e=t??this.defaultName;if(this.connections.has(e))return this.connections.get(e).drizzle;if(!this.config)throw new Error("Database not configured. Call Connection.configure() first, or register DatabaseServiceProvider.");let n=this.config.connections[e];if(!n)throw new Error(`Database connection "${e}" is not defined in configuration.`);let s=await this.createConnection(n);return this.connections.set(e,s),s.drizzle}async rawClient(t){let e=t??this.defaultName;return await this.connection(e),this.connections.get(e).rawClient}async raw(t,e=[],n){let s=await this.connection(n),a=this.getConfig(n);switch(a.driver){case"sqlite":{let i=await this.rawClient(n),r=e.map(o=>typeof o=="boolean"?o?1:0:o instanceof Date?o.toISOString():o),c=i.prepare(t),l=t.trimStart().toUpperCase();return l.startsWith("SELECT")||l.startsWith("PRAGMA")||l.startsWith("WITH")?c.all(...r):c.run(...r)}case"postgres":return(await this.rawClient(n))(t,...e);case"mysql":{let i=await this.rawClient(n),[r]=await i.execute(t,e);return r}default:throw new Error(`Unsupported driver: ${a.driver}`)}}getDriver(t){return this.getConfig(t).driver}getConfig(t){let e=t??this.defaultName;if(!this.config)throw new Error("Database not configured.");let n=this.config.connections[e];if(!n)throw new Error(`Database connection "${e}" is not defined.`);return n}async disconnect(t){if(t){let e=this.connections.get(t);e&&(await this.closeConnection(e),this.connections.delete(t))}else{for(let[e,n]of this.connections)await this.closeConnection(n);this.connections.clear()}}isConnected(t){return this.connections.has(t??this.defaultName)}async transaction(t,e){let n=this.getConfig(e),s=await this.rawClient(e);switch(n.driver){case"sqlite":{s.exec("BEGIN");try{let a=await t();return s.exec("COMMIT"),a}catch(a){throw s.exec("ROLLBACK"),a}}case"postgres":{await s`BEGIN`;try{let a=await t();return await s`COMMIT`,a}catch(a){throw await s`ROLLBACK`,a}}case"mysql":{let a=await s.getConnection();await a.beginTransaction();try{let i=await t();return await a.commit(),a.release(),i}catch(i){throw await a.rollback(),a.release(),i}}default:throw new Error(`Unsupported driver: ${n.driver}`)}}async createConnection(t){switch(t.driver){case"sqlite":return this.createSQLiteConnection(t);case"postgres":return this.createPostgresConnection(t);case"mysql":return this.createMySQLConnection(t);default:throw new Error(`Unsupported database driver: ${t.driver}`)}}async createSQLiteConnection(t){let e=t.filename??t.database??":memory:";try{let n=(await import("better-sqlite3")).default,{drizzle:s}=await import("drizzle-orm/better-sqlite3"),a=new n(e);return a.pragma("journal_mode = WAL"),a.pragma("foreign_keys = ON"),{drizzle:s(a),config:t,rawClient:a}}catch(n){let s;try{s=(await new Function("mod","return import(mod)")("node:sqlite")).DatabaseSync}catch{throw new Error(`No SQLite driver available. Install better-sqlite3 (npm install better-sqlite3) or use Node.js v22+ which includes built-in SQLite support. Original error: ${n instanceof Error?n.message:String(n)}`)}let a=new s(e);a.exec("PRAGMA journal_mode = WAL"),a.exec("PRAGMA foreign_keys = ON");let i={prepare(c){let l=a.prepare(c);return{all(...o){return l.all(...o)},run(...o){return l.run(...o)},get(...o){return l.get(...o)}}},exec(c){a.exec(c)},pragma(c){return a.prepare(`PRAGMA ${c}`).all()},close(){a.close()}},r;try{let{drizzle:c}=await import("drizzle-orm/better-sqlite3");r=c(i)}catch{r=i}return{drizzle:r,config:t,rawClient:i}}}async createPostgresConnection(t){let e=(await import("postgres")).default,{drizzle:n}=await import("drizzle-orm/postgres-js"),s=t.url??`postgres://${t.user}:${t.password}@${t.host??"localhost"}:${t.port??5432}/${t.database}`,a=e(s);return{drizzle:n(a),config:t,rawClient:a}}async createMySQLConnection(t){let e=await import("mysql2/promise"),{drizzle:n}=await import("drizzle-orm/mysql2"),s=e.createPool({host:t.host??"localhost",port:t.port??3306,database:t.database,user:t.user,password:t.password,uri:t.url});return{drizzle:n(s),config:t,rawClient:s}}async closeConnection(t){try{switch(t.config.driver){case"sqlite":t.rawClient.close();break;case"postgres":await t.rawClient.end();break;case"mysql":await t.rawClient.end();break}}catch{}}},D=u("svelar.connection",()=>new b)});f();var g=class{_config=null;_bullmq=null;_metrics={processed:[],failed:[]};configure(t){this._config=t}recordProcessed(){this._metrics.processed.push(Date.now());let t=Date.now()-36e5;this._metrics.processed=this._metrics.processed.filter(e=>e>t)}recordFailed(){this._metrics.failed.push(Date.now());let t=Date.now()-36e5;this._metrics.failed=this._metrics.failed.filter(e=>e>t)}async getCounts(t="default"){let e=this._config?.connections[this._config.default]?.driver;return e==="redis"?this._getRedisJobCounts(t):e==="database"?this._getDatabaseJobCounts(t):{waiting:0,active:0,completed:0,failed:0,delayed:0,total:0}}async listJobs(t={}){let e=this._config?.connections[this._config.default]?.driver;return e==="redis"?this._listRedisJobs(t):e==="database"?this._listDatabaseJobs(t):[]}async getJob(t){let e=this._config?.connections[this._config.default]?.driver;return e==="redis"?this._getRedisJob(t):e==="database"?this._getDatabaseJob(t):null}async retryJob(t){let e=this._config?.connections[this._config.default]?.driver;return e==="redis"?this._retryRedisJob(t):e==="database"?this._retryDatabaseJob(t):!1}async deleteJob(t){let e=this._config?.connections[this._config.default]?.driver;return e==="redis"?this._deleteRedisJob(t):e==="database"?this._deleteDatabaseJob(t):!1}async getHealth(){let t=this._config?.connections[this._config.default]?.driver??"sync",e=await this.getCounts("default"),n=Date.now()-36e5,s=this._metrics.processed.filter(r=>r>n).length,a=this._metrics.failed.filter(r=>r>n).length,i=s+a;return{driver:t,queues:{default:e},failureRate:i>0?a/i*100:0,throughput:s}}async flushCompleted(t="default"){return this._config?.connections[this._config.default]?.driver==="redis"?this._flushRedisCompleted(t):0}async flushFailed(t="default"){let e=this._config?.connections[this._config.default]?.driver;return e==="redis"?this._flushRedisFailed(t):e==="database"?this._flushDatabaseFailed(t):0}async getBullMQ(){if(this._bullmq)return this._bullmq;try{return this._bullmq=await Function('return import("bullmq")')(),this._bullmq}catch{throw new Error("bullmq is required for the Redis queue monitor.")}}getRedisConnection(){let t=this._config?.connections[this._config.default]??{};if(t.url){let e=new URL(t.url);return{host:e.hostname||"localhost",port:parseInt(e.port)||6379,password:e.password||t.password||void 0,db:parseInt(e.pathname?.slice(1)||"0")||t.db||0}}return{host:t.host??"localhost",port:t.port??6379,password:t.password,db:t.db??0}}async getRedisQueue(t){let e=await this.getBullMQ(),n=this.getRedisConnection(),s=this._config?.connections[this._config.default]??{};return new e.Queue(t,{connection:n,prefix:s.prefix??"svelar"})}async _getRedisJobCounts(t){let e=await this.getRedisQueue(t);try{let n=await e.getJobCounts("waiting","active","completed","failed","delayed");return{waiting:n.waiting??0,active:n.active??0,completed:n.completed??0,failed:n.failed??0,delayed:n.delayed??0,total:(n.waiting??0)+(n.active??0)+(n.completed??0)+(n.failed??0)+(n.delayed??0)}}finally{await e.close()}}async _listRedisJobs(t){let e=await this.getRedisQueue(t.queue??"default"),n=t.limit??50,s=t.offset??0;try{let a;return t.status?a=await e.getJobs([t.status],s,s+n-1):a=await e.getJobs(["waiting","active","completed","failed","delayed"],s,s+n-1),a.filter(i=>i!=null).filter(i=>!t.jobClass||i.name===t.jobClass).map(i=>this._bullmqJobToInfo(i))}finally{await e.close()}}async _getRedisJob(t){let e=await this.getRedisQueue("default");try{let n=await e.getJob(t);return n?this._bullmqJobToInfo(n):null}finally{await e.close()}}async _retryRedisJob(t){let e=await this.getRedisQueue("default");try{let n=await e.getJob(t);return n?(await n.retry(),!0):!1}catch{return!1}finally{await e.close()}}async _deleteRedisJob(t){let e=await this.getRedisQueue("default");try{let n=await e.getJob(t);return n?(await n.remove(),!0):!1}catch{return!1}finally{await e.close()}}async _flushRedisCompleted(t){let e=await this.getRedisQueue(t);try{let n=await e.getJobs(["completed"]),s=0;for(let a of n)a&&(await a.remove(),s++);return s}finally{await e.close()}}async _flushRedisFailed(t){let e=await this.getRedisQueue(t);try{let n=await e.getJobs(["failed"]),s=0;for(let a of n)a&&(await a.remove(),s++);return s}finally{await e.close()}}_bullmqJobToInfo(t){let e=t.finishedOn?t.failedReason?"failed":"completed":t.delay&&t.delay>0&&!t.processedOn?"delayed":t.processedOn?"active":"waiting";return{id:t.id,jobClass:t.name??t.data?.jobClass??"Unknown",queue:t.queueName??"default",status:e,attempts:t.attemptsMade??0,maxAttempts:t.opts?.attempts??3,payload:JSON.stringify(t.data??{}),error:t.failedReason??void 0,createdAt:t.timestamp??Date.now(),processedAt:t.processedOn??void 0,failedAt:t.failedReason?t.finishedOn:void 0,delayedUntil:t.delay?t.timestamp+t.delay:void 0}}async getDbConnection(){let{Connection:t}=await Promise.resolve().then(()=>(y(),w));return t}getDbTable(){return this._config?.connections[this._config.default]?.table??"svelar_jobs"}async _getDatabaseJobCounts(t){let e=await this.getDbConnection(),n=this.getDbTable(),s=Math.floor(Date.now()/1e3),i=(await e.raw(`SELECT
|
|
2
|
+
SUM(CASE WHEN reserved_at IS NULL AND available_at <= ? THEN 1 ELSE 0 END) as waiting,
|
|
3
|
+
SUM(CASE WHEN reserved_at IS NOT NULL THEN 1 ELSE 0 END) as active,
|
|
4
|
+
SUM(CASE WHEN reserved_at IS NULL AND available_at > ? THEN 1 ELSE 0 END) as delayed
|
|
5
|
+
FROM ${n} WHERE queue = ?`,[s,s,t]))?.[0]??{},r=Number(i.waiting)||0,c=Number(i.active)||0,l=Number(i.delayed)||0;return{waiting:r,active:c,completed:0,failed:0,delayed:l,total:r+c+l}}async _listDatabaseJobs(t){let e=await this.getDbConnection(),n=this.getDbTable(),s=t.limit??50,a=t.offset??0,i=Math.floor(Date.now()/1e3),r="WHERE 1=1",c=[];return t.queue&&(r+=" AND queue = ?",c.push(t.queue)),t.status==="waiting"?(r+=" AND reserved_at IS NULL AND available_at <= ?",c.push(i)):t.status==="active"?r+=" AND reserved_at IS NOT NULL":t.status==="delayed"&&(r+=" AND reserved_at IS NULL AND available_at > ?",c.push(i)),c.push(s,a),(await e.raw(`SELECT * FROM ${n} ${r} ORDER BY created_at DESC LIMIT ? OFFSET ?`,c)??[]).map(o=>{let v=JSON.parse(o.payload??"{}"),h=o.reserved_at!=null,m=!h&&o.available_at>i;return{id:o.id,jobClass:v.jobClass??"Unknown",queue:o.queue,status:h?"active":m?"delayed":"waiting",attempts:o.attempts??0,maxAttempts:o.max_attempts??3,payload:o.payload,createdAt:(o.created_at??0)*1e3,delayedUntil:m?o.available_at*1e3:void 0}})}async _getDatabaseJob(t){let e=await this.getDbConnection(),n=this.getDbTable(),s=await e.raw(`SELECT * FROM ${n} WHERE id = ?`,[t]);if(!s||s.length===0)return null;let a=s[0],i=JSON.parse(a.payload??"{}"),r=Math.floor(Date.now()/1e3);return{id:a.id,jobClass:i.jobClass??"Unknown",queue:a.queue,status:a.reserved_at?"active":a.available_at>r?"delayed":"waiting",attempts:a.attempts??0,maxAttempts:a.max_attempts??3,payload:a.payload,createdAt:(a.created_at??0)*1e3}}async _retryDatabaseJob(t){let e=await this.getDbConnection(),n=this.getDbTable(),s=Math.floor(Date.now()/1e3);try{return await e.raw(`UPDATE ${n} SET reserved_at = NULL, available_at = ?, attempts = 0 WHERE id = ?`,[s,t]),!0}catch{return!1}}async _deleteDatabaseJob(t){let e=await this.getDbConnection(),n=this.getDbTable();try{return await e.raw(`DELETE FROM ${n} WHERE id = ?`,[t]),!0}catch{return!1}}async _flushDatabaseFailed(t){let e=await this.getDbConnection(),n=this.getDbTable();return 0}},A=u("svelar.jobMonitor",()=>new g);export{A as JobMonitor};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelar Queue / Jobs
|
|
3
|
+
*
|
|
4
|
+
* Background job processing with sync, in-memory, and database-backed queues.
|
|
5
|
+
* Dispatch jobs from anywhere — controllers, services, model hooks, middleware,
|
|
6
|
+
* other jobs, or CLI commands.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Queue, Job } from 'svelar/queue';
|
|
11
|
+
*
|
|
12
|
+
* class SendWelcomeEmail extends Job {
|
|
13
|
+
* constructor(private userId: number) { super(); }
|
|
14
|
+
*
|
|
15
|
+
* async handle(): Promise<void> {
|
|
16
|
+
* const user = await User.findOrFail(this.userId);
|
|
17
|
+
* await Mailer.send({ to: user.email, template: 'welcome' });
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* failed(error: Error): void {
|
|
21
|
+
* console.error('Failed to send welcome email:', error);
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* // Register the job class (required for database driver deserialization)
|
|
26
|
+
* Queue.register(SendWelcomeEmail);
|
|
27
|
+
*
|
|
28
|
+
* // Dispatch to queue (async — processed by a worker)
|
|
29
|
+
* await Queue.dispatch(new SendWelcomeEmail(user.id));
|
|
30
|
+
*
|
|
31
|
+
* // Dispatch with delay (seconds)
|
|
32
|
+
* await Queue.dispatch(new SendWelcomeEmail(user.id), { delay: 60 });
|
|
33
|
+
*
|
|
34
|
+
* // Run synchronously regardless of configured driver
|
|
35
|
+
* await Queue.dispatchSync(new SendWelcomeEmail(user.id));
|
|
36
|
+
*
|
|
37
|
+
* // Chain jobs — run in sequence, stop on failure
|
|
38
|
+
* await Queue.chain([
|
|
39
|
+
* new ProcessPayment(orderId),
|
|
40
|
+
* new SendReceipt(orderId),
|
|
41
|
+
* new UpdateInventory(orderId),
|
|
42
|
+
* ]);
|
|
43
|
+
*
|
|
44
|
+
* // Process jobs (in a worker)
|
|
45
|
+
* await Queue.work();
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export type QueueDriver = 'sync' | 'memory' | 'database' | 'redis';
|
|
49
|
+
export interface QueueConfig {
|
|
50
|
+
default: string;
|
|
51
|
+
connections: Record<string, QueueConnectionConfig>;
|
|
52
|
+
}
|
|
53
|
+
export interface QueueConnectionConfig {
|
|
54
|
+
driver: QueueDriver;
|
|
55
|
+
/** Queue name (default: 'default') */
|
|
56
|
+
queue?: string;
|
|
57
|
+
/** Max retry attempts */
|
|
58
|
+
maxAttempts?: number;
|
|
59
|
+
/** Retry delay in seconds */
|
|
60
|
+
retryDelay?: number;
|
|
61
|
+
/** Database table for database driver */
|
|
62
|
+
table?: string;
|
|
63
|
+
/** Redis connection URL for redis driver (default: redis://localhost:6379) */
|
|
64
|
+
url?: string;
|
|
65
|
+
/** Redis host (alternative to url) */
|
|
66
|
+
host?: string;
|
|
67
|
+
/** Redis port (default: 6379) */
|
|
68
|
+
port?: number;
|
|
69
|
+
/** Redis password */
|
|
70
|
+
password?: string;
|
|
71
|
+
/** Redis database index (default: 0) */
|
|
72
|
+
db?: number;
|
|
73
|
+
/** BullMQ queue prefix (default: 'svelar') */
|
|
74
|
+
prefix?: string;
|
|
75
|
+
/** Default job options for BullMQ */
|
|
76
|
+
defaultJobOptions?: {
|
|
77
|
+
removeOnComplete?: boolean | number;
|
|
78
|
+
removeOnFail?: boolean | number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface DispatchOptions {
|
|
82
|
+
/** Queue name */
|
|
83
|
+
queue?: string;
|
|
84
|
+
/** Delay in seconds before processing */
|
|
85
|
+
delay?: number;
|
|
86
|
+
/** Max retry attempts */
|
|
87
|
+
maxAttempts?: number;
|
|
88
|
+
}
|
|
89
|
+
export declare abstract class Job {
|
|
90
|
+
/** Number of times this job has been attempted */
|
|
91
|
+
attempts: number;
|
|
92
|
+
/** Maximum attempts before failing permanently */
|
|
93
|
+
maxAttempts: number;
|
|
94
|
+
/** Delay before retry (in seconds) */
|
|
95
|
+
retryDelay: number;
|
|
96
|
+
/** Queue to dispatch to */
|
|
97
|
+
queue: string;
|
|
98
|
+
/**
|
|
99
|
+
* Handle the job — implement your logic here
|
|
100
|
+
*/
|
|
101
|
+
abstract handle(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Called when the job has failed after all retries
|
|
104
|
+
*/
|
|
105
|
+
failed(error: Error): void;
|
|
106
|
+
/**
|
|
107
|
+
* Called before each retry
|
|
108
|
+
*/
|
|
109
|
+
retrying(attempt: number): void;
|
|
110
|
+
/**
|
|
111
|
+
* Serialize the job for storage.
|
|
112
|
+
* Override this if your job needs custom serialization.
|
|
113
|
+
*/
|
|
114
|
+
serialize(): string;
|
|
115
|
+
/**
|
|
116
|
+
* Restore job properties from deserialized data.
|
|
117
|
+
* Called when reconstructing a job from the database.
|
|
118
|
+
* Override this if your job needs custom deserialization.
|
|
119
|
+
*/
|
|
120
|
+
restore(data: Record<string, any>): void;
|
|
121
|
+
}
|
|
122
|
+
export interface FailedJobRecord {
|
|
123
|
+
id: string;
|
|
124
|
+
queue: string;
|
|
125
|
+
jobClass: string;
|
|
126
|
+
payload: string;
|
|
127
|
+
exception: string;
|
|
128
|
+
failedAt: number;
|
|
129
|
+
}
|
|
130
|
+
declare class QueueManager {
|
|
131
|
+
private config;
|
|
132
|
+
private drivers;
|
|
133
|
+
private processing;
|
|
134
|
+
private jobRegistry;
|
|
135
|
+
private failedStore;
|
|
136
|
+
private _activeWorker;
|
|
137
|
+
configure(config: QueueConfig): void;
|
|
138
|
+
/**
|
|
139
|
+
* Register a job class for database driver deserialization.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* Queue.register(SendWelcomeEmail);
|
|
143
|
+
* Queue.register(ProcessPayment);
|
|
144
|
+
*/
|
|
145
|
+
register(JobClass: new (...args: any[]) => Job): void;
|
|
146
|
+
/**
|
|
147
|
+
* Register multiple job classes at once.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* Queue.registerAll([SendWelcomeEmail, ProcessPayment, GenerateReport]);
|
|
151
|
+
*/
|
|
152
|
+
registerAll(classes: Array<new (...args: any[]) => Job>): void;
|
|
153
|
+
/**
|
|
154
|
+
* Dispatch a job to the configured queue driver.
|
|
155
|
+
* The job will be processed asynchronously by a worker (unless using the sync driver).
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* // Basic dispatch
|
|
159
|
+
* await Queue.dispatch(new SendWelcomeEmail(user.id));
|
|
160
|
+
*
|
|
161
|
+
* // Dispatch with options
|
|
162
|
+
* await Queue.dispatch(new SendWelcomeEmail(user.id), {
|
|
163
|
+
* queue: 'emails',
|
|
164
|
+
* delay: 60, // Wait 60 seconds before processing
|
|
165
|
+
* maxAttempts: 5,
|
|
166
|
+
* });
|
|
167
|
+
*/
|
|
168
|
+
dispatch(job: Job, options?: DispatchOptions): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Dispatch a job synchronously, bypassing the configured queue driver.
|
|
171
|
+
* The job runs immediately in the current process and the promise
|
|
172
|
+
* resolves when the job completes (or rejects if it fails all retries).
|
|
173
|
+
*
|
|
174
|
+
* Use this when you need the result right away, in tests, or for
|
|
175
|
+
* jobs that must complete before the response is sent.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* // Run immediately regardless of configured driver
|
|
179
|
+
* await Queue.dispatchSync(new SendWelcomeEmail(user.id));
|
|
180
|
+
*
|
|
181
|
+
* // Useful in tests
|
|
182
|
+
* await Queue.dispatchSync(new ProcessPayment(orderId));
|
|
183
|
+
*/
|
|
184
|
+
dispatchSync(job: Job): Promise<void>;
|
|
185
|
+
/**
|
|
186
|
+
* Chain multiple jobs to run in sequence.
|
|
187
|
+
* Each job runs after the previous one completes successfully.
|
|
188
|
+
* If any job fails (after all retries), the chain stops.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* await Queue.chain([
|
|
192
|
+
* new ProcessPayment(orderId),
|
|
193
|
+
* new SendReceipt(orderId),
|
|
194
|
+
* new UpdateInventory(orderId),
|
|
195
|
+
* ]);
|
|
196
|
+
*
|
|
197
|
+
* // Chain with options
|
|
198
|
+
* await Queue.chain([
|
|
199
|
+
* new ProcessPayment(orderId),
|
|
200
|
+
* new SendReceipt(orderId),
|
|
201
|
+
* ], { queue: 'orders' });
|
|
202
|
+
*/
|
|
203
|
+
chain(jobs: Job[], options?: DispatchOptions): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Process jobs from the queue (worker loop).
|
|
206
|
+
*
|
|
207
|
+
* For sync/memory/database drivers this runs a poll loop.
|
|
208
|
+
* For the redis driver this starts a BullMQ Worker that processes
|
|
209
|
+
* jobs natively — no polling required.
|
|
210
|
+
*
|
|
211
|
+
* @returns Number of processed jobs (poll drivers) or 0 (redis — the
|
|
212
|
+
* worker runs indefinitely until stop() is called).
|
|
213
|
+
*/
|
|
214
|
+
work(options?: {
|
|
215
|
+
queue?: string;
|
|
216
|
+
maxJobs?: number;
|
|
217
|
+
sleep?: number;
|
|
218
|
+
/** BullMQ concurrency (redis driver only, default: 1) */
|
|
219
|
+
concurrency?: number;
|
|
220
|
+
}): Promise<number>;
|
|
221
|
+
/**
|
|
222
|
+
* Stop the worker loop (poll drivers) or close the BullMQ Worker (redis).
|
|
223
|
+
*/
|
|
224
|
+
stop(): Promise<void>;
|
|
225
|
+
/**
|
|
226
|
+
* Get the size of a queue
|
|
227
|
+
*/
|
|
228
|
+
size(queue?: string): Promise<number>;
|
|
229
|
+
/**
|
|
230
|
+
* Clear all jobs from a queue
|
|
231
|
+
*/
|
|
232
|
+
clear(queue?: string): Promise<void>;
|
|
233
|
+
/**
|
|
234
|
+
* List all failed jobs.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* const failures = await Queue.failed();
|
|
238
|
+
* for (const f of failures) {
|
|
239
|
+
* console.log(f.jobClass, f.exception);
|
|
240
|
+
* }
|
|
241
|
+
*/
|
|
242
|
+
failed(): Promise<FailedJobRecord[]>;
|
|
243
|
+
/**
|
|
244
|
+
* Retry a failed job by its ID.
|
|
245
|
+
* The job is re-dispatched to the queue and removed from the failed_jobs table.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* await Queue.retry('some-uuid');
|
|
249
|
+
*/
|
|
250
|
+
retry(failedJobId: string): Promise<boolean>;
|
|
251
|
+
/**
|
|
252
|
+
* Retry all failed jobs.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* const count = await Queue.retryAll();
|
|
256
|
+
* console.log(`Retried ${count} jobs`);
|
|
257
|
+
*/
|
|
258
|
+
retryAll(): Promise<number>;
|
|
259
|
+
/**
|
|
260
|
+
* Remove a single failed job record.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* await Queue.forgetFailed('some-uuid');
|
|
264
|
+
*/
|
|
265
|
+
forgetFailed(failedJobId: string): Promise<boolean>;
|
|
266
|
+
/**
|
|
267
|
+
* Remove all failed job records.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* const count = await Queue.flushFailed();
|
|
271
|
+
*/
|
|
272
|
+
flushFailed(): Promise<number>;
|
|
273
|
+
private resolveDriver;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Global Queue singleton
|
|
277
|
+
*/
|
|
278
|
+
export declare const Queue: QueueManager;
|
|
279
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var R=Object.defineProperty;var P=(l,e)=>()=>(l&&(e=l(l=0)),e);var Q=(l,e)=>{for(var t in e)R(l,t,{get:e[t],enumerable:!0})};function p(l,e){let t=Symbol.for(l),r=globalThis;return r[t]||(r[t]=e()),r[t]}var g=P(()=>{"use strict"});var b={};Q(b,{Connection:()=>j});var w,j,y=P(()=>{"use strict";g();w=class{connections=new Map;config=null;defaultName="default";configure(e){this.config=e,this.defaultName=e.default}async connection(e){let t=e??this.defaultName;if(this.connections.has(t))return this.connections.get(t).drizzle;if(!this.config)throw new Error("Database not configured. Call Connection.configure() first, or register DatabaseServiceProvider.");let r=this.config.connections[t];if(!r)throw new Error(`Database connection "${t}" is not defined in configuration.`);let a=await this.createConnection(r);return this.connections.set(t,a),a.drizzle}async rawClient(e){let t=e??this.defaultName;return await this.connection(t),this.connections.get(t).rawClient}async raw(e,t=[],r){let a=await this.connection(r),s=this.getConfig(r);switch(s.driver){case"sqlite":{let i=await this.rawClient(r),u=t.map(c=>typeof c=="boolean"?c?1:0:c instanceof Date?c.toISOString():c),n=i.prepare(e),o=e.trimStart().toUpperCase();return o.startsWith("SELECT")||o.startsWith("PRAGMA")||o.startsWith("WITH")?n.all(...u):n.run(...u)}case"postgres":return(await this.rawClient(r))(e,...t);case"mysql":{let i=await this.rawClient(r),[u]=await i.execute(e,t);return u}default:throw new Error(`Unsupported driver: ${s.driver}`)}}getDriver(e){return this.getConfig(e).driver}getConfig(e){let t=e??this.defaultName;if(!this.config)throw new Error("Database not configured.");let r=this.config.connections[t];if(!r)throw new Error(`Database connection "${t}" is not defined.`);return r}async disconnect(e){if(e){let t=this.connections.get(e);t&&(await this.closeConnection(t),this.connections.delete(e))}else{for(let[t,r]of this.connections)await this.closeConnection(r);this.connections.clear()}}isConnected(e){return this.connections.has(e??this.defaultName)}async transaction(e,t){let r=this.getConfig(t),a=await this.rawClient(t);switch(r.driver){case"sqlite":{a.exec("BEGIN");try{let s=await e();return a.exec("COMMIT"),s}catch(s){throw a.exec("ROLLBACK"),s}}case"postgres":{await a`BEGIN`;try{let s=await e();return await a`COMMIT`,s}catch(s){throw await a`ROLLBACK`,s}}case"mysql":{let s=await a.getConnection();await s.beginTransaction();try{let i=await e();return await s.commit(),s.release(),i}catch(i){throw await s.rollback(),s.release(),i}}default:throw new Error(`Unsupported driver: ${r.driver}`)}}async createConnection(e){switch(e.driver){case"sqlite":return this.createSQLiteConnection(e);case"postgres":return this.createPostgresConnection(e);case"mysql":return this.createMySQLConnection(e);default:throw new Error(`Unsupported database driver: ${e.driver}`)}}async createSQLiteConnection(e){let t=e.filename??e.database??":memory:";try{let r=(await import("better-sqlite3")).default,{drizzle:a}=await import("drizzle-orm/better-sqlite3"),s=new r(t);return s.pragma("journal_mode = WAL"),s.pragma("foreign_keys = ON"),{drizzle:a(s),config:e,rawClient:s}}catch(r){let a;try{a=(await new Function("mod","return import(mod)")("node:sqlite")).DatabaseSync}catch{throw new Error(`No SQLite driver available. Install better-sqlite3 (npm install better-sqlite3) or use Node.js v22+ which includes built-in SQLite support. Original error: ${r instanceof Error?r.message:String(r)}`)}let s=new a(t);s.exec("PRAGMA journal_mode = WAL"),s.exec("PRAGMA foreign_keys = ON");let i={prepare(n){let o=s.prepare(n);return{all(...c){return o.all(...c)},run(...c){return o.run(...c)},get(...c){return o.get(...c)}}},exec(n){s.exec(n)},pragma(n){return s.prepare(`PRAGMA ${n}`).all()},close(){s.close()}},u;try{let{drizzle:n}=await import("drizzle-orm/better-sqlite3");u=n(i)}catch{u=i}return{drizzle:u,config:e,rawClient:i}}}async createPostgresConnection(e){let t=(await import("postgres")).default,{drizzle:r}=await import("drizzle-orm/postgres-js"),a=e.url??`postgres://${e.user}:${e.password}@${e.host??"localhost"}:${e.port??5432}/${e.database}`,s=t(a);return{drizzle:r(s),config:e,rawClient:s}}async createMySQLConnection(e){let t=await import("mysql2/promise"),{drizzle:r}=await import("drizzle-orm/mysql2"),a=t.createPool({host:e.host??"localhost",port:e.port??3306,database:e.database,user:e.user,password:e.password,uri:e.url});return{drizzle:r(a),config:e,rawClient:a}}async closeConnection(e){try{switch(e.config.driver){case"sqlite":e.rawClient.close();break;case"postgres":await e.rawClient.end();break;case"mysql":await e.rawClient.end();break}}catch{}}},j=p("svelar.connection",()=>new w)});g();var v=class{attempts=0;maxAttempts=3;retryDelay=60;queue="default";failed(e){console.error(`[Queue] Job ${this.constructor.name} permanently failed:`,e.message)}retrying(e){}serialize(){let e={};for(let[t,r]of Object.entries(this))typeof r!="function"&&(e[t]=r);return JSON.stringify(e)}restore(e){for(let[t,r]of Object.entries(e))t!=="attempts"&&t!=="maxAttempts"&&t!=="retryDelay"&&t!=="queue"&&(this[t]=r)}},h=class{async push(e){try{e.job.attempts=1,await e.job.handle()}catch(t){if(e.attempts+1<e.maxAttempts)return e.attempts++,e.job.attempts=e.attempts+1,e.job.retrying(e.job.attempts),this.push(e);e.job.failed(t)}}async pop(){return null}async size(){return 0}async clear(){}},C=class{queues=new Map;async push(e){let t=e.queue;this.queues.has(t)||this.queues.set(t,[]),this.queues.get(t).push(e)}async pop(e="default"){let t=this.queues.get(e)??[],r=Date.now(),a=t.findIndex(s=>s.availableAt<=r);return a===-1?null:t.splice(a,1)[0]}async size(e="default"){return this.queues.get(e)?.length??0}async clear(e){e?this.queues.delete(e):this.queues.clear()}},m=class{constructor(e,t){this.table=e;this.registry=t}async getConnection(){let{Connection:e}=await Promise.resolve().then(()=>(y(),b));return e}async push(e){await(await this.getConnection()).raw(`INSERT INTO ${this.table} (id, queue, payload, attempts, max_attempts, available_at, created_at)
|
|
2
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,[e.id,e.queue,JSON.stringify({jobClass:e.jobClass,payload:e.payload}),e.attempts,e.maxAttempts,Math.floor(e.availableAt/1e3),Math.floor(e.createdAt/1e3)])}async pop(e="default"){let t=await this.getConnection(),r=Math.floor(Date.now()/1e3),a=await t.raw(`SELECT * FROM ${this.table}
|
|
3
|
+
WHERE queue = ? AND available_at <= ? AND reserved_at IS NULL
|
|
4
|
+
ORDER BY created_at ASC LIMIT 1`,[e,r]);if(!a||a.length===0)return null;let s=a[0];await t.raw(`UPDATE ${this.table} SET reserved_at = ?, attempts = attempts + 1 WHERE id = ?`,[r,s.id]);let i=JSON.parse(s.payload),u=this.registry.resolve(i.jobClass,i.payload);return{id:s.id,jobClass:i.jobClass,payload:i.payload,queue:s.queue,attempts:s.attempts+1,maxAttempts:s.max_attempts,availableAt:s.available_at*1e3,createdAt:s.created_at*1e3,job:u}}async size(e="default"){return(await(await this.getConnection()).raw(`SELECT COUNT(*) as count FROM ${this.table} WHERE queue = ? AND reserved_at IS NULL`,[e]))?.[0]?.count??0}async clear(e){let t=await this.getConnection();e?await t.raw(`DELETE FROM ${this.table} WHERE queue = ?`,[e]):await t.raw(`DELETE FROM ${this.table}`,[])}async delete(e){await(await this.getConnection()).raw(`DELETE FROM ${this.table} WHERE id = ?`,[e])}async release(e,t=0){let r=await this.getConnection(),a=Math.floor(Date.now()/1e3)+t;await r.raw(`UPDATE ${this.table} SET reserved_at = NULL, available_at = ? WHERE id = ?`,[a,e])}},f=class{queues=new Map;config;registry;_bullmq=null;constructor(e,t){this.config=e,this.registry=t}async getBullMQ(){if(this._bullmq)return this._bullmq;try{return this._bullmq=await Function('return import("bullmq")')(),this._bullmq}catch{throw new Error("bullmq is required for the Redis queue driver. Install it with: npm install bullmq")}}getRedisConnection(){if(this.config.url){let e=new URL(this.config.url);return{host:e.hostname||"localhost",port:parseInt(e.port)||6379,password:e.password||this.config.password||void 0,db:parseInt(e.pathname?.slice(1)||"0")||this.config.db||0}}return{host:this.config.host??"localhost",port:this.config.port??6379,password:this.config.password,db:this.config.db??0}}async getQueue(e){if(this.queues.has(e))return this.queues.get(e);let t=await this.getBullMQ(),r=this.getRedisConnection(),a=this.config.prefix??"svelar",s=new t.Queue(e,{connection:r,prefix:a,defaultJobOptions:{removeOnComplete:this.config.defaultJobOptions?.removeOnComplete??100,removeOnFail:this.config.defaultJobOptions?.removeOnFail??500}});return this.queues.set(e,s),s}async push(e){let t=await this.getQueue(e.queue),r=Math.max(0,e.availableAt-Date.now());await t.add(e.jobClass,{jobClass:e.jobClass,payload:e.payload},{jobId:e.id,delay:r>0?r:void 0,attempts:e.maxAttempts,backoff:{type:"fixed",delay:(e.job.retryDelay??60)*1e3}})}async pop(e){return null}async size(e="default"){let r=await(await this.getQueue(e)).getJobCounts("waiting","delayed","active");return r.waiting+r.delayed+r.active}async clear(e){if(e)await(await this.getQueue(e)).obliterate({force:!0});else for(let t of this.queues.values())await t.obliterate({force:!0})}async createWorker(e,t,r,a){let s=await this.getBullMQ(),i=this.getRedisConnection(),u=this.config.prefix??"svelar",n=new s.Worker(e,async o=>{let c=o.data,d=t.resolve(c.jobClass,c.payload);d.attempts=o.attemptsMade+1,await d.handle()},{connection:i,prefix:u,concurrency:a?.concurrency??1});return n.on("failed",async(o,c)=>{let d=o?.data;if(d)try{let x=t.resolve(d.jobClass,d.payload);o.attemptsMade>=(o.opts?.attempts??3)&&(x.failed(c),await r.store({id:o.id,jobClass:d.jobClass,payload:d.payload,queue:e,attempts:o.attemptsMade,maxAttempts:o.opts?.attempts??3,availableAt:Date.now(),createdAt:o.timestamp??Date.now(),job:x},c))}catch{console.error("[Queue] Failed to resolve job for failure handler:",c.message)}}),n}},D=class{table="svelar_failed_jobs";async getConnection(){let{Connection:e}=await Promise.resolve().then(()=>(y(),b));return e}async store(e,t){try{await(await this.getConnection()).raw(`INSERT INTO ${this.table} (id, queue, job_class, payload, exception, failed_at)
|
|
5
|
+
VALUES (?, ?, ?, ?, ?, ?)`,[crypto.randomUUID(),e.queue,e.jobClass,e.payload,t.stack??t.message,Math.floor(Date.now()/1e3)])}catch{console.error("[Queue] Could not persist failed job (run migration to create svelar_failed_jobs table)")}}async all(){return(await(await this.getConnection()).raw(`SELECT * FROM ${this.table} ORDER BY failed_at DESC`,[])??[]).map(r=>({id:r.id,queue:r.queue,jobClass:r.job_class,payload:r.payload,exception:r.exception,failedAt:r.failed_at}))}async find(e){let r=await(await this.getConnection()).raw(`SELECT * FROM ${this.table} WHERE id = ? LIMIT 1`,[e]);if(!r||r.length===0)return null;let a=r[0];return{id:a.id,queue:a.queue,jobClass:a.job_class,payload:a.payload,exception:a.exception,failedAt:a.failed_at}}async forget(e){return await(await this.getConnection()).raw(`DELETE FROM ${this.table} WHERE id = ?`,[e]),!0}async flush(){let e=await this.getConnection(),r=(await e.raw(`SELECT COUNT(*) as count FROM ${this.table}`,[]))?.[0]?.count??0;return await e.raw(`DELETE FROM ${this.table}`,[]),r}},q=class{jobs=new Map;register(e){this.jobs.set(e.name,e)}registerAll(e){for(let t of e)this.register(t)}resolve(e,t){let r=this.jobs.get(e);if(!r)throw new Error(`Job class "${e}" is not registered. Call Queue.register(${e}) in your app bootstrap. Registered jobs: [${[...this.jobs.keys()].join(", ")}]`);let a=Object.create(r.prototype);a.attempts=0,a.maxAttempts=3,a.retryDelay=60,a.queue="default";try{let s=JSON.parse(t);a.restore(s)}catch{}return a}has(e){return this.jobs.has(e)}},E=class{config={default:"sync",connections:{sync:{driver:"sync"}}};drivers=new Map;processing=!1;jobRegistry=new q;failedStore=new D;_activeWorker=null;configure(e){this.config=e,this.drivers.clear()}register(e){this.jobRegistry.register(e)}registerAll(e){this.jobRegistry.registerAll(e)}async dispatch(e,t){let r=this.config.default,a=this.config.connections[r],s=this.resolveDriver(r);t?.queue&&(e.queue=t.queue),t?.maxAttempts!==void 0&&(e.maxAttempts=t.maxAttempts);let i={id:crypto.randomUUID(),jobClass:e.constructor.name,payload:e.serialize(),queue:e.queue??a?.queue??"default",attempts:0,maxAttempts:e.maxAttempts,availableAt:Date.now()+(t?.delay??0)*1e3,createdAt:Date.now(),job:e};await s.push(i)}async dispatchSync(e){let t=new h,r={id:crypto.randomUUID(),jobClass:e.constructor.name,payload:e.serialize(),queue:e.queue,attempts:0,maxAttempts:e.maxAttempts,availableAt:Date.now(),createdAt:Date.now(),job:e};await t.push(r)}async chain(e,t){if(e.length===0)return;let r=new A(e);t?.queue&&(r.queue=t.queue),t?.maxAttempts!==void 0&&(r.maxAttempts=t.maxAttempts),await this.dispatch(r,t)}async work(e){let t=this.config.default,r=this.resolveDriver(t),a=e?.queue??"default";if(r instanceof f){let n=await r.createWorker(a,this.jobRegistry,this.failedStore,{concurrency:e?.concurrency??1});return this.processing=!0,this._activeWorker=n,await new Promise(o=>{let c=()=>{this.processing?setTimeout(c,500):n.close().then(o).catch(o)};c()}),0}let s=e?.maxJobs??1/0,i=(e?.sleep??1)*1e3,u=0;for(this.processing=!0;this.processing&&u<s;){let n=await r.pop(a);if(!n){if(s===1/0){await new Promise(o=>setTimeout(o,i));continue}break}n.attempts++,n.job.attempts=n.attempts;try{await n.job.handle(),u++,r instanceof m&&await r.delete(n.id)}catch(o){if(n.attempts<n.maxAttempts){n.job.retrying(n.attempts);let c=n.job.retryDelay??60;r instanceof m?await r.release(n.id,c):(n.availableAt=Date.now()+c*1e3,await r.push(n))}else n.job.failed(o),await this.failedStore.store(n,o),r instanceof m&&await r.delete(n.id)}}return u}async stop(){this.processing=!1,this._activeWorker&&(await this._activeWorker.close(),this._activeWorker=null)}async size(e){return this.resolveDriver(this.config.default).size(e)}async clear(e){return this.resolveDriver(this.config.default).clear(e)}async failed(){return this.failedStore.all()}async retry(e){let t=await this.failedStore.find(e);if(!t)return!1;let r=this.jobRegistry.resolve(t.jobClass,t.payload);return r.queue=t.queue,await this.dispatch(r,{queue:t.queue}),await this.failedStore.forget(e),!0}async retryAll(){let e=await this.failedStore.all(),t=0;for(let r of e)try{let a=this.jobRegistry.resolve(r.jobClass,r.payload);a.queue=r.queue,await this.dispatch(a,{queue:r.queue}),await this.failedStore.forget(r.id),t++}catch{}return t}async forgetFailed(e){return this.failedStore.forget(e)}async flushFailed(){return this.failedStore.flush()}resolveDriver(e){if(this.drivers.has(e))return this.drivers.get(e);let t=this.config.connections[e];if(!t)throw new Error(`Queue connection "${e}" is not defined.`);let r;switch(t.driver){case"sync":r=new h;break;case"memory":r=new C;break;case"database":r=new m(t.table??"svelar_jobs",this.jobRegistry);break;case"redis":r=new f(t,this.jobRegistry);break;default:throw new Error(`Unknown queue driver: ${t.driver}`)}return this.drivers.set(e,r),r}},A=class extends v{remainingJobs;constructor(e){super(),this.remainingJobs=[...e],this.maxAttempts=1}async handle(){for(let e of this.remainingJobs){let t=null,r=!1;for(let a=1;a<=e.maxAttempts;a++){e.attempts=a;try{await e.handle(),r=!0;break}catch(s){t=s,a<e.maxAttempts&&(e.retrying(a),e.retryDelay>0&&await new Promise(i=>setTimeout(i,e.retryDelay*1e3)))}}if(!r&&t)throw e.failed(t),new Error(`Chain stopped: ${e.constructor.name} failed after ${e.maxAttempts} attempt(s). Remaining jobs: [${this.remainingJobs.slice(this.remainingJobs.indexOf(e)+1).map(a=>a.constructor.name).join(", ")}]`)}}serialize(){return JSON.stringify({jobs:this.remainingJobs.map(e=>({jobClass:e.constructor.name,payload:e.serialize()}))})}},J=p("svelar.queue",()=>new E);export{v as Job,J as Queue};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelar Repository
|
|
3
|
+
*
|
|
4
|
+
* Base repository classes that abstract data access from business logic.
|
|
5
|
+
* Repositories wrap Model queries and provide a clean interface for
|
|
6
|
+
* services and actions to work with.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Repository } from 'svelar/repositories';
|
|
11
|
+
* import { User } from '../models/User';
|
|
12
|
+
*
|
|
13
|
+
* class UserRepository extends Repository<User> {
|
|
14
|
+
* model() {
|
|
15
|
+
* return User;
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* async findByEmail(email: string): Promise<User | null> {
|
|
19
|
+
* return this.query().where('email', email).first();
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* async findActive(): Promise<User[]> {
|
|
23
|
+
* return this.query()
|
|
24
|
+
* .where('active', true)
|
|
25
|
+
* .orderBy('name')
|
|
26
|
+
* .get();
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* async findWithPosts(id: number): Promise<User | null> {
|
|
30
|
+
* return this.query()
|
|
31
|
+
* .with('posts')
|
|
32
|
+
* .find(id);
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export interface PaginationMeta {
|
|
38
|
+
total: number;
|
|
39
|
+
page: number;
|
|
40
|
+
perPage: number;
|
|
41
|
+
lastPage: number;
|
|
42
|
+
hasNextPage: boolean;
|
|
43
|
+
hasPrevPage: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface PaginatedResult<T> {
|
|
46
|
+
data: T[];
|
|
47
|
+
meta: PaginationMeta;
|
|
48
|
+
}
|
|
49
|
+
export interface SortOption {
|
|
50
|
+
column: string;
|
|
51
|
+
direction: 'asc' | 'desc';
|
|
52
|
+
}
|
|
53
|
+
export interface FilterCriteria {
|
|
54
|
+
column: string;
|
|
55
|
+
operator: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'like' | 'in' | 'not_in';
|
|
56
|
+
value: any;
|
|
57
|
+
}
|
|
58
|
+
export declare abstract class Repository<TModel = any> {
|
|
59
|
+
/**
|
|
60
|
+
* Return the Model class this repository works with
|
|
61
|
+
*/
|
|
62
|
+
abstract model(): any;
|
|
63
|
+
/**
|
|
64
|
+
* Get a fresh query builder for the model
|
|
65
|
+
*/
|
|
66
|
+
protected query(): any;
|
|
67
|
+
/**
|
|
68
|
+
* Get all records
|
|
69
|
+
*/
|
|
70
|
+
all(): Promise<TModel[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Find a record by primary key
|
|
73
|
+
*/
|
|
74
|
+
findById(id: any): Promise<TModel | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Find a record by primary key or throw
|
|
77
|
+
*/
|
|
78
|
+
findByIdOrFail(id: any): Promise<TModel>;
|
|
79
|
+
/**
|
|
80
|
+
* Find records matching a where clause
|
|
81
|
+
*/
|
|
82
|
+
findWhere(column: string, value: any): Promise<TModel[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Find the first record matching a where clause
|
|
85
|
+
*/
|
|
86
|
+
findFirstWhere(column: string, value: any): Promise<TModel | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Create a new record
|
|
89
|
+
*/
|
|
90
|
+
create(attributes: Record<string, any>): Promise<TModel>;
|
|
91
|
+
/**
|
|
92
|
+
* Update a record by primary key
|
|
93
|
+
*/
|
|
94
|
+
update(id: any, attributes: Record<string, any>): Promise<TModel>;
|
|
95
|
+
/**
|
|
96
|
+
* Delete a record by primary key
|
|
97
|
+
*/
|
|
98
|
+
delete(id: any): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Create multiple records
|
|
101
|
+
*/
|
|
102
|
+
createMany(items: Record<string, any>[]): Promise<TModel[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Delete records matching a where clause
|
|
105
|
+
*/
|
|
106
|
+
deleteWhere(column: string, value: any): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Count all records
|
|
109
|
+
*/
|
|
110
|
+
count(): Promise<number>;
|
|
111
|
+
/**
|
|
112
|
+
* Count records matching a where clause
|
|
113
|
+
*/
|
|
114
|
+
countWhere(column: string, value: any): Promise<number>;
|
|
115
|
+
/**
|
|
116
|
+
* Check if a record exists with the given criteria
|
|
117
|
+
*/
|
|
118
|
+
exists(column: string, value: any): Promise<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* Paginate results
|
|
121
|
+
*/
|
|
122
|
+
paginate(page?: number, perPage?: number): Promise<PaginatedResult<TModel>>;
|
|
123
|
+
/**
|
|
124
|
+
* Get the latest records
|
|
125
|
+
*/
|
|
126
|
+
latest(limit?: number, column?: string): Promise<TModel[]>;
|
|
127
|
+
/**
|
|
128
|
+
* Get the oldest records
|
|
129
|
+
*/
|
|
130
|
+
oldest(limit?: number, column?: string): Promise<TModel[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Find or create a record
|
|
133
|
+
*/
|
|
134
|
+
firstOrCreate(searchAttributes: Record<string, any>, createAttributes?: Record<string, any>): Promise<TModel>;
|
|
135
|
+
/**
|
|
136
|
+
* Update or create a record
|
|
137
|
+
*/
|
|
138
|
+
updateOrCreate(searchAttributes: Record<string, any>, updateAttributes: Record<string, any>): Promise<TModel>;
|
|
139
|
+
/**
|
|
140
|
+
* Apply multiple filter criteria
|
|
141
|
+
*/
|
|
142
|
+
filter(criteria: FilterCriteria[], sort?: SortOption): Promise<TModel[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Pluck a single column from all records
|
|
145
|
+
*/
|
|
146
|
+
pluck(column: string): Promise<any[]>;
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=class{query(){return this.model().query()}async all(){return this.model().all()}async findById(e){return this.model().find(e)}async findByIdOrFail(e){return this.model().findOrFail(e)}async findWhere(e,t){return this.model().where(e,t).get()}async findFirstWhere(e,t){return this.model().where(e,t).first()}async create(e){return this.model().create(e)}async update(e,t){let r=await this.findByIdOrFail(e);return await r.update(t),r}async delete(e){await(await this.findByIdOrFail(e)).delete()}async createMany(e){let t=[];for(let r of e)t.push(await this.create(r));return t}async deleteWhere(e,t){return this.query().where(e,t).delete()}async count(){return this.model().count()}async countWhere(e,t){return this.query().where(e,t).count()}async exists(e,t){return this.query().where(e,t).exists()}async paginate(e=1,t=15){let r=await this.query().paginate(e,t);return{data:r.data,meta:{total:r.total,page:r.currentPage,perPage:r.perPage,lastPage:r.lastPage,hasNextPage:r.currentPage<r.lastPage,hasPrevPage:r.currentPage>1}}}async latest(e,t="created_at"){let r=this.query().orderBy(t,"desc");return e&&r.limit(e),r.get()}async oldest(e,t="created_at"){let r=this.query().orderBy(t,"asc");return e&&r.limit(e),r.get()}async firstOrCreate(e,t){let r=Object.entries(e),a=this.query();for(let[i,s]of r)a=a.where(i,s);let n=await a.first();return n||this.create({...e,...t})}async updateOrCreate(e,t){let r=Object.entries(e),a=this.query();for(let[i,s]of r)a=a.where(i,s);let n=await a.first();return n?(await n.update(t),n):this.create({...e,...t})}async filter(e,t){let r=this.query();for(let{column:a,operator:n,value:i}of e)switch(n){case"in":r=r.whereIn(a,i);break;case"not_in":r=r.whereNotIn(a,i);break;case"like":r=r.where(a,"like",i);break;default:r=r.where(a,n,i)}return t&&(r=r.orderBy(t.column,t.direction)),r.get()}async pluck(e){return this.query().pluck(e)}};export{o as Repository};
|