@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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/dist/actions/index.d.ts +101 -0
  4. package/dist/actions/index.js +1 -0
  5. package/dist/api-keys/index.d.ts +58 -0
  6. package/dist/api-keys/index.js +1 -0
  7. package/dist/audit/index.d.ts +52 -0
  8. package/dist/audit/index.js +1 -0
  9. package/dist/auth/Auth.d.ts +283 -0
  10. package/dist/auth/Gate.d.ts +166 -0
  11. package/dist/auth/index.d.ts +2 -0
  12. package/dist/auth/index.js +80 -0
  13. package/dist/broadcasting/client.d.ts +195 -0
  14. package/dist/broadcasting/client.js +1 -0
  15. package/dist/broadcasting/index.d.ts +318 -0
  16. package/dist/broadcasting/index.js +20 -0
  17. package/dist/cache/index.d.ts +77 -0
  18. package/dist/cache/index.js +1 -0
  19. package/dist/cli/Cli.d.ts +23 -0
  20. package/dist/cli/Command.d.ts +36 -0
  21. package/dist/cli/bin.d.ts +8 -0
  22. package/dist/cli/bin.js +5856 -0
  23. package/dist/cli/commands/KeyGenerateCommand.d.ts +16 -0
  24. package/dist/cli/commands/MakeActionCommand.d.ts +15 -0
  25. package/dist/cli/commands/MakeBroadcastingCommand.d.ts +29 -0
  26. package/dist/cli/commands/MakeChannelCommand.d.ts +18 -0
  27. package/dist/cli/commands/MakeCommandCommand.d.ts +16 -0
  28. package/dist/cli/commands/MakeConfigCommand.d.ts +13 -0
  29. package/dist/cli/commands/MakeControllerCommand.d.ts +28 -0
  30. package/dist/cli/commands/MakeDashboardCommand.d.ts +34 -0
  31. package/dist/cli/commands/MakeDockerCommand.d.ts +32 -0
  32. package/dist/cli/commands/MakeEventCommand.d.ts +11 -0
  33. package/dist/cli/commands/MakeJobCommand.d.ts +11 -0
  34. package/dist/cli/commands/MakeListenerCommand.d.ts +16 -0
  35. package/dist/cli/commands/MakeMiddlewareCommand.d.ts +11 -0
  36. package/dist/cli/commands/MakeMigrationCommand.d.ts +17 -0
  37. package/dist/cli/commands/MakeModelCommand.d.ts +25 -0
  38. package/dist/cli/commands/MakeObserverCommand.d.ts +23 -0
  39. package/dist/cli/commands/MakePluginCommand.d.ts +11 -0
  40. package/dist/cli/commands/MakeProviderCommand.d.ts +11 -0
  41. package/dist/cli/commands/MakeRepositoryCommand.d.ts +22 -0
  42. package/dist/cli/commands/MakeRequestCommand.d.ts +15 -0
  43. package/dist/cli/commands/MakeResourceCommand.d.ts +30 -0
  44. package/dist/cli/commands/MakeRouteCommand.d.ts +42 -0
  45. package/dist/cli/commands/MakeSchemaCommand.d.ts +20 -0
  46. package/dist/cli/commands/MakeSeederCommand.d.ts +11 -0
  47. package/dist/cli/commands/MakeServiceCommand.d.ts +28 -0
  48. package/dist/cli/commands/MakeTaskCommand.d.ts +12 -0
  49. package/dist/cli/commands/MigrateCommand.d.ts +26 -0
  50. package/dist/cli/commands/NewCommand.d.ts +21 -0
  51. package/dist/cli/commands/NewCommandTemplates.d.ts +123 -0
  52. package/dist/cli/commands/PluginInstallCommand.d.ts +16 -0
  53. package/dist/cli/commands/PluginListCommand.d.ts +11 -0
  54. package/dist/cli/commands/PluginPublishCommand.d.ts +22 -0
  55. package/dist/cli/commands/QueueFailedCommand.d.ts +9 -0
  56. package/dist/cli/commands/QueueFlushCommand.d.ts +9 -0
  57. package/dist/cli/commands/QueueRetryCommand.d.ts +16 -0
  58. package/dist/cli/commands/QueueWorkCommand.d.ts +25 -0
  59. package/dist/cli/commands/RoutesListCommand.d.ts +30 -0
  60. package/dist/cli/commands/ScheduleRunCommand.d.ts +15 -0
  61. package/dist/cli/commands/SeedCommand.d.ts +14 -0
  62. package/dist/cli/commands/TinkerCommand.d.ts +10 -0
  63. package/dist/cli/index.d.ts +36 -0
  64. package/dist/cli/index.js +1973 -0
  65. package/dist/cli/ts-resolve-hook.mjs +74 -0
  66. package/dist/cli/ts-resolver.mjs +8 -0
  67. package/dist/config/Config.d.ts +65 -0
  68. package/dist/config/index.d.ts +1 -0
  69. package/dist/config/index.js +1 -0
  70. package/dist/container/Application.d.ts +33 -0
  71. package/dist/container/Container.d.ts +70 -0
  72. package/dist/container/ServiceProvider.d.ts +21 -0
  73. package/dist/container/index.d.ts +3 -0
  74. package/dist/container/index.js +1 -0
  75. package/dist/dashboard/index.d.ts +123 -0
  76. package/dist/dashboard/index.js +5 -0
  77. package/dist/database/Connection.d.ts +80 -0
  78. package/dist/database/Migration.d.ts +76 -0
  79. package/dist/database/SchemaBuilder.d.ts +91 -0
  80. package/dist/database/Seeder.d.ts +9 -0
  81. package/dist/database/index.d.ts +4 -0
  82. package/dist/database/index.js +4 -0
  83. package/dist/email-templates/index.d.ts +51 -0
  84. package/dist/email-templates/index.js +57 -0
  85. package/dist/errors/Handler.d.ts +100 -0
  86. package/dist/errors/index.d.ts +1 -0
  87. package/dist/errors/index.js +5 -0
  88. package/dist/events/EventServiceProvider.d.ts +82 -0
  89. package/dist/events/Listener.d.ts +28 -0
  90. package/dist/events/index.d.ts +80 -0
  91. package/dist/events/index.js +1 -0
  92. package/dist/excel/index.d.ts +154 -0
  93. package/dist/excel/index.js +1 -0
  94. package/dist/feature-flags/index.d.ts +158 -0
  95. package/dist/feature-flags/index.js +59 -0
  96. package/dist/forms/index.d.ts +81 -0
  97. package/dist/forms/index.js +1 -0
  98. package/dist/hashing/Hash.d.ts +51 -0
  99. package/dist/hashing/index.d.ts +1 -0
  100. package/dist/hashing/index.js +1 -0
  101. package/dist/hooks/index.d.ts +135 -0
  102. package/dist/hooks/index.js +5 -0
  103. package/dist/http/index.d.ts +201 -0
  104. package/dist/http/index.js +2 -0
  105. package/dist/i18n/index.d.ts +81 -0
  106. package/dist/i18n/index.js +1 -0
  107. package/dist/index.d.ts +54 -0
  108. package/dist/index.js +127 -0
  109. package/dist/logging/LogViewer.d.ts +95 -0
  110. package/dist/logging/LogViewer.js +1 -0
  111. package/dist/logging/index.d.ts +83 -0
  112. package/dist/logging/index.js +3 -0
  113. package/dist/mail/index.d.ts +149 -0
  114. package/dist/mail/index.js +1 -0
  115. package/dist/middleware/Middleware.d.ts +208 -0
  116. package/dist/middleware/index.d.ts +1 -0
  117. package/dist/middleware/index.js +1 -0
  118. package/dist/notifications/index.d.ts +85 -0
  119. package/dist/notifications/index.js +2 -0
  120. package/dist/orm/Model.d.ts +123 -0
  121. package/dist/orm/Observer.d.ts +34 -0
  122. package/dist/orm/QueryBuilder.d.ts +119 -0
  123. package/dist/orm/Relationship.d.ts +58 -0
  124. package/dist/orm/index.d.ts +4 -0
  125. package/dist/orm/index.js +1 -0
  126. package/dist/pagination/index.d.ts +8 -0
  127. package/dist/pagination/index.js +0 -0
  128. package/dist/pdf/GeneratePdfJob.d.ts +99 -0
  129. package/dist/pdf/GeneratePdfJob.js +41 -0
  130. package/dist/pdf/index.d.ts +328 -0
  131. package/dist/pdf/index.js +41 -0
  132. package/dist/permissions/index.d.ts +161 -0
  133. package/dist/permissions/index.js +60 -0
  134. package/dist/plugins/BootstrapPlugins.d.ts +11 -0
  135. package/dist/plugins/PluginInstaller.d.ts +30 -0
  136. package/dist/plugins/PluginInstaller.js +1 -0
  137. package/dist/plugins/PluginPublisher.d.ts +32 -0
  138. package/dist/plugins/PluginPublisher.js +1 -0
  139. package/dist/plugins/PluginRegistry.d.ts +55 -0
  140. package/dist/plugins/PluginRegistry.js +1 -0
  141. package/dist/plugins/index.d.ts +206 -0
  142. package/dist/plugins/index.js +1 -0
  143. package/dist/queue/JobMonitor.d.ts +109 -0
  144. package/dist/queue/JobMonitor.js +5 -0
  145. package/dist/queue/index.d.ts +279 -0
  146. package/dist/queue/index.js +5 -0
  147. package/dist/repositories/index.d.ts +147 -0
  148. package/dist/repositories/index.js +1 -0
  149. package/dist/routing/Controller.d.ts +115 -0
  150. package/dist/routing/FormRequest.d.ts +94 -0
  151. package/dist/routing/Resource.d.ts +213 -0
  152. package/dist/routing/Response.d.ts +138 -0
  153. package/dist/routing/index.d.ts +4 -0
  154. package/dist/routing/index.js +5 -0
  155. package/dist/scheduler/ScheduleMonitor.d.ts +141 -0
  156. package/dist/scheduler/ScheduleMonitor.js +1 -0
  157. package/dist/scheduler/SchedulerLock.d.ts +33 -0
  158. package/dist/scheduler/index.d.ts +208 -0
  159. package/dist/scheduler/index.js +34 -0
  160. package/dist/services/index.d.ts +79 -0
  161. package/dist/services/index.js +1 -0
  162. package/dist/session/Session.d.ts +166 -0
  163. package/dist/session/index.d.ts +1 -0
  164. package/dist/session/index.js +16 -0
  165. package/dist/storage/index.d.ts +154 -0
  166. package/dist/storage/index.js +1 -0
  167. package/dist/support/Pipeline.d.ts +65 -0
  168. package/dist/support/date.d.ts +136 -0
  169. package/dist/support/date.js +1 -0
  170. package/dist/support/index.d.ts +8 -0
  171. package/dist/support/index.js +1 -0
  172. package/dist/support/singleton.d.ts +10 -0
  173. package/dist/support/uuid.d.ts +40 -0
  174. package/dist/teams/index.d.ts +91 -0
  175. package/dist/teams/index.js +78 -0
  176. package/dist/uploads/index.d.ts +63 -0
  177. package/dist/uploads/index.js +2 -0
  178. package/dist/validation/index.d.ts +46 -0
  179. package/dist/validation/index.js +1 -0
  180. package/dist/webhooks/index.d.ts +66 -0
  181. package/dist/webhooks/index.js +1 -0
  182. package/package.json +338 -0
  183. package/src/i18n/LanguageSwitcher.svelte +47 -0
  184. package/src/i18n/index.ts +113 -0
  185. package/src/ui/Alert.svelte +22 -0
  186. package/src/ui/Avatar.svelte +18 -0
  187. package/src/ui/AvatarFallback.svelte +18 -0
  188. package/src/ui/AvatarImage.svelte +12 -0
  189. package/src/ui/Badge.svelte +27 -0
  190. package/src/ui/Button.svelte +51 -0
  191. package/src/ui/Card.svelte +15 -0
  192. package/src/ui/CardContent.svelte +15 -0
  193. package/src/ui/CardDescription.svelte +15 -0
  194. package/src/ui/CardFooter.svelte +15 -0
  195. package/src/ui/CardHeader.svelte +15 -0
  196. package/src/ui/CardTitle.svelte +15 -0
  197. package/src/ui/Icon.svelte +81 -0
  198. package/src/ui/Input.svelte +40 -0
  199. package/src/ui/Label.svelte +20 -0
  200. package/src/ui/Separator.svelte +10 -0
  201. package/src/ui/Tabs.svelte +23 -0
  202. package/src/ui/TabsContent.svelte +27 -0
  203. package/src/ui/TabsList.svelte +19 -0
  204. package/src/ui/TabsTrigger.svelte +28 -0
  205. package/src/ui/Toaster.svelte +279 -0
  206. package/src/ui/index.ts +31 -0
  207. package/src/ui/toast.ts +212 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Schedule Monitor
3
+ *
4
+ * Utility for inspecting and managing scheduled tasks from the admin dashboard.
5
+ * Tracks task execution history, health metrics, and provides task management
6
+ * capabilities.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { ScheduleMonitor } from 'svelar/scheduler';
11
+ *
12
+ * // Configure with your scheduler instance
13
+ * ScheduleMonitor.configure(scheduler);
14
+ *
15
+ * // List all tasks with their status
16
+ * const tasks = ScheduleMonitor.listTasks();
17
+ *
18
+ * // Get health metrics
19
+ * const health = ScheduleMonitor.getHealth();
20
+ *
21
+ * // Manually run a task
22
+ * await ScheduleMonitor.runTask('SendDailyReport');
23
+ *
24
+ * // Toggle task enabled state
25
+ * ScheduleMonitor.disableTask('SendDailyReport');
26
+ * ```
27
+ */
28
+ import type { Scheduler } from './index.js';
29
+ export interface TaskRunRecord {
30
+ /** When the task was run */
31
+ timestamp: Date;
32
+ /** Whether execution succeeded */
33
+ success: boolean;
34
+ /** How long the task took (ms) */
35
+ duration: number;
36
+ /** Error message if failed */
37
+ error?: string;
38
+ }
39
+ export interface TaskInfo {
40
+ /** Task name */
41
+ name: string;
42
+ /** Cron expression */
43
+ expression: string;
44
+ /** Human-readable schedule description */
45
+ humanReadable: string;
46
+ /** Next scheduled run time */
47
+ nextRun: Date;
48
+ /** Last execution timestamp */
49
+ lastRun?: Date;
50
+ /** Duration of last execution (ms) */
51
+ lastDuration?: number;
52
+ /** Success/failure status of last run */
53
+ lastStatus?: 'success' | 'failed';
54
+ /** Whether the task is enabled */
55
+ enabled: boolean;
56
+ /** Whether the task is currently running */
57
+ isRunning: boolean;
58
+ /** Recent execution history */
59
+ history: TaskRunRecord[];
60
+ }
61
+ export interface SchedulerHealth {
62
+ /** Total registered tasks */
63
+ totalTasks: number;
64
+ /** Number of enabled tasks */
65
+ enabledTasks: number;
66
+ /** Number of currently running tasks */
67
+ runningTasks: number;
68
+ /** Recent errors */
69
+ lastErrors: Array<{
70
+ task: string;
71
+ error: string;
72
+ timestamp: Date;
73
+ }>;
74
+ /** Scheduler uptime in milliseconds */
75
+ uptime: number;
76
+ }
77
+ declare class ScheduleMonitorService {
78
+ private scheduler;
79
+ private startTime;
80
+ private taskEnabled;
81
+ /**
82
+ * Configure the monitor with a scheduler instance
83
+ */
84
+ configure(scheduler: Scheduler): void;
85
+ /**
86
+ * List all registered tasks with their current status.
87
+ * Reads run history from the database so all processes (CLI, web) share the same data.
88
+ */
89
+ listTasks(): Promise<TaskInfo[]>;
90
+ /**
91
+ * Get info for a specific task by name
92
+ */
93
+ getTask(name: string): Promise<TaskInfo | null>;
94
+ /**
95
+ * Enable a task
96
+ */
97
+ enableTask(name: string): void;
98
+ /**
99
+ * Disable a task
100
+ */
101
+ disableTask(name: string): void;
102
+ /**
103
+ * Manually trigger a task to run (if not disabled).
104
+ * Persists the result to the database.
105
+ */
106
+ runTask(name: string): Promise<void>;
107
+ /**
108
+ * Get execution history for a task from the database
109
+ */
110
+ getTaskHistory(name: string, limit?: number): Promise<TaskRunRecord[]>;
111
+ /**
112
+ * Get overall scheduler health metrics
113
+ */
114
+ getHealth(): Promise<SchedulerHealth>;
115
+ /**
116
+ * Load task run history from the database.
117
+ * Single query fetches the latest N records per task.
118
+ */
119
+ private loadHistoryFromDb;
120
+ /**
121
+ * Persist a task result to the database
122
+ */
123
+ private persistResult;
124
+ /**
125
+ * Create a TaskInfo object from a ScheduledTask and its DB history
126
+ */
127
+ private createTaskInfo;
128
+ /**
129
+ * Convert a cron expression to a human-readable description
130
+ */
131
+ private humanizeExpression;
132
+ /**
133
+ * Calculate the next run time for a task given its cron expression
134
+ */
135
+ private getNextRun;
136
+ }
137
+ /**
138
+ * Export as a singleton instance
139
+ */
140
+ export declare const ScheduleMonitor: ScheduleMonitorService;
141
+ export {};
@@ -0,0 +1 @@
1
+ var v=Object.defineProperty;var g=(c,t)=>()=>(c&&(t=c(c=0)),t);var R=(c,t)=>{for(var e in t)v(c,e,{get:t[e],enumerable:!0})};import{hostname as S}from"os";var C,k=g(()=>{"use strict";C=`${S()}:${process.pid}:${Math.random().toString(36).slice(2,10)}`});function d(c,t){let e=Symbol.for(c),s=globalThis;return s[e]||(s[e]=t()),s[e]}var T=g(()=>{"use strict"});var m={};R(m,{Connection:()=>D});var f,D,p=g(()=>{"use strict";T();f=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 s=this.config.connections[e];if(!s)throw new Error(`Database connection "${e}" is not defined in configuration.`);let n=await this.createConnection(s);return this.connections.set(e,n),n.drizzle}async rawClient(t){let e=t??this.defaultName;return await this.connection(e),this.connections.get(e).rawClient}async raw(t,e=[],s){let n=await this.connection(s),r=this.getConfig(s);switch(r.driver){case"sqlite":{let i=await this.rawClient(s),o=e.map(l=>typeof l=="boolean"?l?1:0:l instanceof Date?l.toISOString():l),a=i.prepare(t),u=t.trimStart().toUpperCase();return u.startsWith("SELECT")||u.startsWith("PRAGMA")||u.startsWith("WITH")?a.all(...o):a.run(...o)}case"postgres":return(await this.rawClient(s))(t,...e);case"mysql":{let i=await this.rawClient(s),[o]=await i.execute(t,e);return o}default:throw new Error(`Unsupported driver: ${r.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 s=this.config.connections[e];if(!s)throw new Error(`Database connection "${e}" is not defined.`);return s}async disconnect(t){if(t){let e=this.connections.get(t);e&&(await this.closeConnection(e),this.connections.delete(t))}else{for(let[e,s]of this.connections)await this.closeConnection(s);this.connections.clear()}}isConnected(t){return this.connections.has(t??this.defaultName)}async transaction(t,e){let s=this.getConfig(e),n=await this.rawClient(e);switch(s.driver){case"sqlite":{n.exec("BEGIN");try{let r=await t();return n.exec("COMMIT"),r}catch(r){throw n.exec("ROLLBACK"),r}}case"postgres":{await n`BEGIN`;try{let r=await t();return await n`COMMIT`,r}catch(r){throw await n`ROLLBACK`,r}}case"mysql":{let r=await n.getConnection();await r.beginTransaction();try{let i=await t();return await r.commit(),r.release(),i}catch(i){throw await r.rollback(),r.release(),i}}default:throw new Error(`Unsupported driver: ${s.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 s=(await import("better-sqlite3")).default,{drizzle:n}=await import("drizzle-orm/better-sqlite3"),r=new s(e);return r.pragma("journal_mode = WAL"),r.pragma("foreign_keys = ON"),{drizzle:n(r),config:t,rawClient:r}}catch(s){let n;try{n=(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: ${s instanceof Error?s.message:String(s)}`)}let r=new n(e);r.exec("PRAGMA journal_mode = WAL"),r.exec("PRAGMA foreign_keys = ON");let i={prepare(a){let u=r.prepare(a);return{all(...l){return u.all(...l)},run(...l){return u.run(...l)},get(...l){return u.get(...l)}}},exec(a){r.exec(a)},pragma(a){return r.prepare(`PRAGMA ${a}`).all()},close(){r.close()}},o;try{let{drizzle:a}=await import("drizzle-orm/better-sqlite3");o=a(i)}catch{o=i}return{drizzle:o,config:t,rawClient:i}}}async createPostgresConnection(t){let e=(await import("postgres")).default,{drizzle:s}=await import("drizzle-orm/postgres-js"),n=t.url??`postgres://${t.user}:${t.password}@${t.host??"localhost"}:${t.port??5432}/${t.database}`,r=e(n);return{drizzle:s(r),config:t,rawClient:r}}async createMySQLConnection(t){let e=await import("mysql2/promise"),{drizzle:s}=await import("drizzle-orm/mysql2"),n=e.createPool({host:t.host??"localhost",port:t.port??3306,database:t.database,user:t.user,password:t.password,uri:t.url});return{drizzle:s(n),config:t,rawClient:n}}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=d("svelar.connection",()=>new f)});k();function h(c,t,e){if(c==="*")return null;let s=new Set;for(let n of c.split(",")){let[r,i]=n.split("/"),o=i?parseInt(i,10):1;if(r==="*")for(let a=t;a<=e;a+=o)s.add(a);else if(r.includes("-")){let[a,u]=r.split("-"),l=parseInt(a,10),b=parseInt(u,10);for(let w=l;w<=b;w+=o)s.add(w)}else s.add(parseInt(r,10))}return[...s].sort((n,r)=>n-r)}function I(c){let t=c.trim().split(/\s+/);if(t.length!==5)throw new Error(`Invalid cron expression: "${c}". Expected 5 fields.`);return{minute:h(t[0],0,59),hour:h(t[1],0,23),dayOfMonth:h(t[2],1,31),month:h(t[3],1,12),dayOfWeek:h(t[4],0,6)}}function E(c,t){let e=I(c),s=t.getMinutes(),n=t.getHours(),r=t.getDate(),i=t.getMonth()+1,o=t.getDay();return!(e.minute&&!e.minute.includes(s)||e.hour&&!e.hour.includes(n)||e.dayOfMonth&&!e.dayOfMonth.includes(r)||e.month&&!e.month.includes(i)||e.dayOfWeek&&!e.dayOfWeek.includes(o))}T();var y=class{scheduler=null;startTime=new Date;taskEnabled=new Map;configure(t){this.scheduler=t,this.startTime=new Date;for(let e of t.getTasks())this.taskEnabled.has(e.name)||this.taskEnabled.set(e.name,!0)}async listTasks(){if(!this.scheduler)return[];let t=this.scheduler.getTasks(),e=await this.loadHistoryFromDb(t.map(s=>s.name));return t.map(s=>this.createTaskInfo(s,e.get(s.name)||[]))}async getTask(t){if(!this.scheduler)return null;let e=this.scheduler.getTasks().find(n=>n.name===t);if(!e)return null;let s=await this.loadHistoryFromDb([t]);return this.createTaskInfo(e,s.get(t)||[])}enableTask(t){this.taskEnabled.set(t,!0)}disableTask(t){this.taskEnabled.set(t,!1)}async runTask(t){if(!this.scheduler)throw new Error("Scheduler not configured");if(this.taskEnabled.has(t)&&!this.taskEnabled.get(t))throw new Error(`Task "${t}" is disabled`);let e=this.scheduler.getTasks().find(n=>n.name===t);if(!e)throw new Error(`Task "${t}" not found`);let s=await e.executeTask();await this.persistResult(s).catch(()=>{})}async getTaskHistory(t,e=10){return(await this.loadHistoryFromDb([t],e)).get(t)||[]}async getHealth(){if(!this.scheduler)return{totalTasks:0,enabledTasks:0,runningTasks:0,lastErrors:[],uptime:0};let t=this.scheduler.getTasks(),e=t.filter(i=>!this.taskEnabled.has(i.name)||this.taskEnabled.get(i.name)).length,s=t.filter(i=>i.isRunning()).length,n=[];try{let{Connection:i}=await Promise.resolve().then(()=>(p(),m)),o=await i.raw("SELECT task, error, ran_at FROM scheduled_task_runs WHERE error IS NOT NULL ORDER BY ran_at DESC LIMIT 10");for(let a of o)n.push({task:a.task,error:a.error,timestamp:new Date(a.ran_at)})}catch{}let r=Date.now()-this.startTime.getTime();return{totalTasks:t.length,enabledTasks:e,runningTasks:s,lastErrors:n,uptime:r}}async loadHistoryFromDb(t,e=20){let s=new Map;if(t.length===0)return s;try{let{Connection:n}=await Promise.resolve().then(()=>(p(),m)),r=t.map(()=>"?").join(", "),i=await n.raw(`SELECT task, success, duration, error, ran_at FROM scheduled_task_runs WHERE task IN (${r}) ORDER BY ran_at DESC LIMIT ?`,[...t,t.length*e]);for(let o of i){s.has(o.task)||s.set(o.task,[]);let a=s.get(o.task);a.length<e&&a.push({timestamp:new Date(o.ran_at),success:!!o.success,duration:o.duration,error:o.error||void 0})}}catch{}return s}async persistResult(t){let{Connection:e}=await Promise.resolve().then(()=>(p(),m));await e.raw("INSERT INTO scheduled_task_runs (task, success, duration, error, ran_at) VALUES (?, ?, ?, ?, ?)",[t.task,t.success,t.duration,t.error||null,t.timestamp.toISOString()])}createTaskInfo(t,e){let s=t.getExpression(),n=e.length>0?e[0]:null,r=!this.taskEnabled.has(t.name)||this.taskEnabled.get(t.name)===!0;return{name:t.name,expression:s,humanReadable:this.humanizeExpression(s),nextRun:this.getNextRun(s),lastRun:n?.timestamp,lastDuration:n?.duration,lastStatus:n?.success?"success":"failed",enabled:r,isRunning:t.isRunning(),history:e.slice(0,5)}}humanizeExpression(t){let e=t.trim().split(/\s+/);if(e.length!==5)return t;let[s,n,r,i,o]=e;if(t==="* * * * *")return"Every minute";if(t==="*/5 * * * *")return"Every 5 minutes";if(t==="*/10 * * * *")return"Every 10 minutes";if(t==="*/15 * * * *")return"Every 15 minutes";if(t==="*/30 * * * *")return"Every 30 minutes";if(t==="0 * * * *")return"Every hour";if(t==="0 0 * * *")return"Daily at midnight";if(t==="0 0 * * 0")return"Weekly (Sunday at midnight)";if(t==="0 0 1 * *")return"Monthly (1st at midnight)";if(t==="0 0 1 1 *")return"Yearly (Jan 1st at midnight)";if(r==="*"&&i==="*"&&o==="*"&&n!=="*"&&s!=="*"){let a=parseInt(n,10),u=parseInt(s,10);return`Daily at ${a.toString().padStart(2,"0")}:${u.toString().padStart(2,"0")}`}if(o==="1-5"&&r==="*"&&i==="*"&&n!=="*"&&s!=="*"){let a=parseInt(n,10),u=parseInt(s,10);return`Weekdays at ${a.toString().padStart(2,"0")}:${u.toString().padStart(2,"0")}`}return t}getNextRun(t){let e=new Date,s=new Date(e);s.setSeconds(0),s.setMilliseconds(0),s.setMinutes(s.getMinutes()+1);let n=366*24*60,r=0;for(;r<n;){if(E(t,s))return s;s.setMinutes(s.getMinutes()+1),r++}return e}},z=d("svelar.scheduleMonitor",()=>new y);export{z as ScheduleMonitor};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Distributed Scheduler Lock
3
+ *
4
+ * Database-backed distributed locking for scheduled tasks.
5
+ * Prevents duplicate task execution across multiple scheduler instances.
6
+ * Supports SQLite, PostgreSQL, and MySQL via the Connection abstraction.
7
+ *
8
+ * The `scheduler_locks` table is auto-created on first use.
9
+ */
10
+ export declare class SchedulerLock {
11
+ /**
12
+ * Get the unique owner ID for this process.
13
+ */
14
+ static getOwnerId(): string;
15
+ /**
16
+ * Ensure the scheduler_locks table exists (auto-created on first use).
17
+ */
18
+ static ensureTable(): Promise<void>;
19
+ /**
20
+ * Try to acquire a distributed lock for a task.
21
+ * Returns true if the lock was acquired, false if another process holds it.
22
+ */
23
+ static acquire(taskKey: string, ttlMinutes?: number): Promise<boolean>;
24
+ /**
25
+ * Release a lock (only if this process owns it).
26
+ */
27
+ static release(taskKey: string): Promise<void>;
28
+ /**
29
+ * Release all locks held by this process.
30
+ * Called during graceful shutdown.
31
+ */
32
+ static releaseAll(): Promise<void>;
33
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Svelar Task Scheduler
3
+ *
4
+ * Laravel-inspired task scheduler with cron expressions.
5
+ * Schedule recurring tasks, chain operations, and manage
6
+ * background work.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Scheduler, ScheduledTask } from 'svelar/scheduler';
11
+ *
12
+ * // Define tasks
13
+ * class PruneExpiredTokens extends ScheduledTask {
14
+ * schedule() { return this.daily(); }
15
+ *
16
+ * async handle(): Promise<void> {
17
+ * await Connection.raw(
18
+ * 'DELETE FROM tokens WHERE expires_at < ?',
19
+ * [new Date().toISOString()]
20
+ * );
21
+ * }
22
+ * }
23
+ *
24
+ * class SendDailyReport extends ScheduledTask {
25
+ * schedule() { return this.dailyAt('09:00'); }
26
+ *
27
+ * async handle(): Promise<void> {
28
+ * const users = await User.count();
29
+ * await Mailer.send({ to: 'admin@example.com', body: `Users: ${users}` });
30
+ * }
31
+ * }
32
+ *
33
+ * // Register tasks
34
+ * const scheduler = new Scheduler();
35
+ * scheduler.register(new PruneExpiredTokens());
36
+ * scheduler.register(new SendDailyReport());
37
+ *
38
+ * // Run the scheduler (call every minute, e.g., via cron or setInterval)
39
+ * await scheduler.run();
40
+ *
41
+ * // Or start the built-in ticker
42
+ * scheduler.start();
43
+ * ```
44
+ */
45
+ interface CronFields {
46
+ minute: number[] | null;
47
+ hour: number[] | null;
48
+ dayOfMonth: number[] | null;
49
+ month: number[] | null;
50
+ dayOfWeek: number[] | null;
51
+ }
52
+ declare function parseCron(expression: string): CronFields;
53
+ declare function cronMatches(expression: string, date: Date): boolean;
54
+ export interface TaskSchedule {
55
+ expression: string;
56
+ timezone?: string;
57
+ withoutOverlapping?: boolean;
58
+ onSuccess?: () => void | Promise<void>;
59
+ onFailure?: (error: Error) => void | Promise<void>;
60
+ }
61
+ export interface TaskResult {
62
+ task: string;
63
+ success: boolean;
64
+ duration: number;
65
+ error?: string;
66
+ timestamp: Date;
67
+ }
68
+ export declare abstract class ScheduledTask {
69
+ /** Task name (defaults to class name) */
70
+ name: string;
71
+ /** Whether this task is currently running */
72
+ private _running;
73
+ /** Whether to prevent overlapping execution */
74
+ protected withoutOverlapping: boolean;
75
+ /** Lock TTL in minutes for distributed locking (default: 5) */
76
+ protected _lockTtlMinutes: number;
77
+ /** The cron expression */
78
+ private _expression;
79
+ /**
80
+ * Define the schedule for this task.
81
+ * Return `this` after calling a scheduling method.
82
+ */
83
+ schedule(): this;
84
+ /**
85
+ * The task logic — override this.
86
+ */
87
+ abstract handle(): Promise<void>;
88
+ /**
89
+ * Called when the task succeeds
90
+ */
91
+ onSuccess(): void | Promise<void>;
92
+ /**
93
+ * Called when the task fails
94
+ */
95
+ onFailure(error: Error): void | Promise<void>;
96
+ /** Run every minute */
97
+ everyMinute(): this;
98
+ /** Run every N minutes */
99
+ everyMinutes(n: number): this;
100
+ /** Run every 5 minutes */
101
+ everyFiveMinutes(): this;
102
+ /** Run every 10 minutes */
103
+ everyTenMinutes(): this;
104
+ /** Run every 15 minutes */
105
+ everyFifteenMinutes(): this;
106
+ /** Run every 30 minutes */
107
+ everyThirtyMinutes(): this;
108
+ /** Run every hour */
109
+ hourly(): this;
110
+ /** Run every hour at a specific minute */
111
+ hourlyAt(minute: number): this;
112
+ /** Run every day at midnight */
113
+ daily(): this;
114
+ /** Run every day at a specific time (HH:MM) */
115
+ dailyAt(time: string): this;
116
+ /** Run twice daily at specific hours */
117
+ twiceDaily(hour1?: number, hour2?: number): this;
118
+ /** Run every week (Sunday at midnight) */
119
+ weekly(): this;
120
+ /** Run on a specific day and time */
121
+ weeklyOn(day: number, time?: string): this;
122
+ /** Run on weekdays (Mon-Fri) */
123
+ weekdays(): this;
124
+ /** Run on weekends (Sat-Sun) */
125
+ weekends(): this;
126
+ /** Run monthly (1st at midnight) */
127
+ monthly(): this;
128
+ /** Run monthly on a specific day and time */
129
+ monthlyOn(day: number, time?: string): this;
130
+ /** Run quarterly (1st of Jan, Apr, Jul, Oct) */
131
+ quarterly(): this;
132
+ /** Run yearly (Jan 1st at midnight) */
133
+ yearly(): this;
134
+ /** Set a custom cron expression */
135
+ cron(expression: string): this;
136
+ /** Prevent overlapping execution (uses distributed lock across processes) */
137
+ preventOverlap(): this;
138
+ /** Set the distributed lock TTL (default: 5 minutes). If a task takes longer, increase this. */
139
+ lockExpiresAfter(minutes: number): this;
140
+ /** @internal */
141
+ getExpression(): string;
142
+ /** @internal */
143
+ isRunning(): boolean;
144
+ /** @internal */
145
+ executeTask(): Promise<TaskResult>;
146
+ }
147
+ export declare class Scheduler {
148
+ private tasks;
149
+ private timer;
150
+ private history;
151
+ private maxHistory;
152
+ private _persistToDb;
153
+ /**
154
+ * Enable database persistence for task run history.
155
+ * Requires the `scheduled_task_runs` table to exist.
156
+ */
157
+ persistToDatabase(): this;
158
+ /**
159
+ * Register a scheduled task
160
+ */
161
+ register(task: ScheduledTask): this;
162
+ /**
163
+ * Register multiple tasks
164
+ */
165
+ registerMany(tasks: ScheduledTask[]): this;
166
+ /**
167
+ * Run all due tasks (call this every minute)
168
+ */
169
+ run(now?: Date): Promise<TaskResult[]>;
170
+ /**
171
+ * Start the scheduler, aligned to the top of each minute like crontab.
172
+ */
173
+ start(): void;
174
+ /**
175
+ * Stop the scheduler and release all distributed locks held by this process.
176
+ */
177
+ stop(): Promise<void>;
178
+ /**
179
+ * Get all registered tasks
180
+ */
181
+ getTasks(): ScheduledTask[];
182
+ /**
183
+ * Get task execution history (in-memory)
184
+ */
185
+ getHistory(): TaskResult[];
186
+ /**
187
+ * Check which tasks are due right now
188
+ */
189
+ dueTasks(now?: Date): ScheduledTask[];
190
+ /**
191
+ * Remove a task by name
192
+ */
193
+ remove(name: string): boolean;
194
+ /**
195
+ * Clear all tasks
196
+ */
197
+ clear(): void;
198
+ private addToHistory;
199
+ private _historyTableEnsured;
200
+ private ensureHistoryTable;
201
+ private persistResult;
202
+ }
203
+ /**
204
+ * Create a scheduled task from a simple function
205
+ */
206
+ export declare function task(name: string, handler: () => Promise<void>, scheduleConfig?: (task: ScheduledTask) => void): ScheduledTask;
207
+ export { parseCron, cronMatches };
208
+ export { SchedulerLock } from './SchedulerLock.js';
@@ -0,0 +1,34 @@
1
+ var R=Object.defineProperty;var E=(a,t)=>()=>(a&&(t=a(a=0)),t);var b=(a,t)=>{for(var e in t)R(a,e,{get:t[e],enumerable:!0})};function N(a,t){let e=Symbol.for(a),s=globalThis;return s[e]||(s[e]=t()),s[e]}var C=E(()=>{"use strict"});var m={};b(m,{Connection:()=>A});var k,A,T=E(()=>{"use strict";C();k=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 s=this.config.connections[e];if(!s)throw new Error(`Database connection "${e}" is not defined in configuration.`);let n=await this.createConnection(s);return this.connections.set(e,n),n.drizzle}async rawClient(t){let e=t??this.defaultName;return await this.connection(e),this.connections.get(e).rawClient}async raw(t,e=[],s){let n=await this.connection(s),r=this.getConfig(s);switch(r.driver){case"sqlite":{let i=await this.rawClient(s),c=e.map(u=>typeof u=="boolean"?u?1:0:u instanceof Date?u.toISOString():u),o=i.prepare(t),l=t.trimStart().toUpperCase();return l.startsWith("SELECT")||l.startsWith("PRAGMA")||l.startsWith("WITH")?o.all(...c):o.run(...c)}case"postgres":return(await this.rawClient(s))(t,...e);case"mysql":{let i=await this.rawClient(s),[c]=await i.execute(t,e);return c}default:throw new Error(`Unsupported driver: ${r.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 s=this.config.connections[e];if(!s)throw new Error(`Database connection "${e}" is not defined.`);return s}async disconnect(t){if(t){let e=this.connections.get(t);e&&(await this.closeConnection(e),this.connections.delete(t))}else{for(let[e,s]of this.connections)await this.closeConnection(s);this.connections.clear()}}isConnected(t){return this.connections.has(t??this.defaultName)}async transaction(t,e){let s=this.getConfig(e),n=await this.rawClient(e);switch(s.driver){case"sqlite":{n.exec("BEGIN");try{let r=await t();return n.exec("COMMIT"),r}catch(r){throw n.exec("ROLLBACK"),r}}case"postgres":{await n`BEGIN`;try{let r=await t();return await n`COMMIT`,r}catch(r){throw await n`ROLLBACK`,r}}case"mysql":{let r=await n.getConnection();await r.beginTransaction();try{let i=await t();return await r.commit(),r.release(),i}catch(i){throw await r.rollback(),r.release(),i}}default:throw new Error(`Unsupported driver: ${s.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 s=(await import("better-sqlite3")).default,{drizzle:n}=await import("drizzle-orm/better-sqlite3"),r=new s(e);return r.pragma("journal_mode = WAL"),r.pragma("foreign_keys = ON"),{drizzle:n(r),config:t,rawClient:r}}catch(s){let n;try{n=(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: ${s instanceof Error?s.message:String(s)}`)}let r=new n(e);r.exec("PRAGMA journal_mode = WAL"),r.exec("PRAGMA foreign_keys = ON");let i={prepare(o){let l=r.prepare(o);return{all(...u){return l.all(...u)},run(...u){return l.run(...u)},get(...u){return l.get(...u)}}},exec(o){r.exec(o)},pragma(o){return r.prepare(`PRAGMA ${o}`).all()},close(){r.close()}},c;try{let{drizzle:o}=await import("drizzle-orm/better-sqlite3");c=o(i)}catch{c=i}return{drizzle:c,config:t,rawClient:i}}}async createPostgresConnection(t){let e=(await import("postgres")).default,{drizzle:s}=await import("drizzle-orm/postgres-js"),n=t.url??`postgres://${t.user}:${t.password}@${t.host??"localhost"}:${t.port??5432}/${t.database}`,r=e(n);return{drizzle:s(r),config:t,rawClient:r}}async createMySQLConnection(t){let e=await import("mysql2/promise"),{drizzle:s}=await import("drizzle-orm/mysql2"),n=e.createPool({host:t.host??"localhost",port:t.port??3306,database:t.database,user:t.user,password:t.password,uri:t.url});return{drizzle:s(n),config:t,rawClient:n}}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{}}},A=N("svelar.connection",()=>new k)});var g={};b(g,{SchedulerLock:()=>y});import{hostname as L}from"os";async function d(){let{Connection:a}=await Promise.resolve().then(()=>(T(),m));return a}async function D(){return(await d()).getDriver()}var _,h,y,p=E(()=>{"use strict";_=!1,h=`${L()}:${process.pid}:${Math.random().toString(36).slice(2,10)}`;y=class{static getOwnerId(){return h}static async ensureTable(){if(_)return;let t=await d();switch(t.getDriver()){case"sqlite":await t.raw(`CREATE TABLE IF NOT EXISTS scheduler_locks (
2
+ task_key TEXT PRIMARY KEY,
3
+ owner TEXT NOT NULL,
4
+ expires_at TEXT NOT NULL
5
+ )`);break;case"postgres":await t.raw(`CREATE TABLE IF NOT EXISTS scheduler_locks (
6
+ task_key VARCHAR(255) PRIMARY KEY,
7
+ owner VARCHAR(255) NOT NULL,
8
+ expires_at TIMESTAMPTZ NOT NULL
9
+ )`);break;case"mysql":await t.raw(`CREATE TABLE IF NOT EXISTS scheduler_locks (
10
+ task_key VARCHAR(255) PRIMARY KEY,
11
+ owner VARCHAR(255) NOT NULL,
12
+ expires_at DATETIME NOT NULL
13
+ ) ENGINE=InnoDB`);break}_=!0}static async acquire(t,e=5){await this.ensureTable();let s=await d(),n=await D(),r=new Date().toISOString(),i=new Date(Date.now()+e*6e4).toISOString();try{await s.transaction(async()=>{switch(await s.raw("DELETE FROM scheduler_locks WHERE task_key = ? AND expires_at < ?",[t,r]),n){case"sqlite":await s.raw("INSERT OR IGNORE INTO scheduler_locks (task_key, owner, expires_at) VALUES (?, ?, ?)",[t,h,i]);break;case"postgres":await s.raw("INSERT INTO scheduler_locks (task_key, owner, expires_at) VALUES ($1, $2, $3) ON CONFLICT (task_key) DO NOTHING",[t,h,i]);break;case"mysql":await s.raw("INSERT IGNORE INTO scheduler_locks (task_key, owner, expires_at) VALUES (?, ?, ?)",[t,h,i]);break}});let c=await s.raw("SELECT owner FROM scheduler_locks WHERE task_key = ?",[t]);return c.length>0&&c[0].owner===h}catch{return!1}}static async release(t){try{await(await d()).raw("DELETE FROM scheduler_locks WHERE task_key = ? AND owner = ?",[t,h])}catch{}}static async releaseAll(){try{await(await d()).raw("DELETE FROM scheduler_locks WHERE owner = ?",[h])}catch{}}}});p();function w(a,t,e){if(a==="*")return null;let s=new Set;for(let n of a.split(",")){let[r,i]=n.split("/"),c=i?parseInt(i,10):1;if(r==="*")for(let o=t;o<=e;o+=c)s.add(o);else if(r.includes("-")){let[o,l]=r.split("-"),u=parseInt(o,10),S=parseInt(l,10);for(let f=u;f<=S;f+=c)s.add(f)}else s.add(parseInt(r,10))}return[...s].sort((n,r)=>n-r)}function x(a){let t=a.trim().split(/\s+/);if(t.length!==5)throw new Error(`Invalid cron expression: "${a}". Expected 5 fields.`);return{minute:w(t[0],0,59),hour:w(t[1],0,23),dayOfMonth:w(t[2],1,31),month:w(t[3],1,12),dayOfWeek:w(t[4],0,6)}}function I(a,t){let e=x(a),s=t.getMinutes(),n=t.getHours(),r=t.getDate(),i=t.getMonth()+1,c=t.getDay();return!(e.minute&&!e.minute.includes(s)||e.hour&&!e.hour.includes(n)||e.dayOfMonth&&!e.dayOfMonth.includes(r)||e.month&&!e.month.includes(i)||e.dayOfWeek&&!e.dayOfWeek.includes(c))}var v=class{name=this.constructor.name;_running=!1;withoutOverlapping=!1;_lockTtlMinutes=5;_expression="* * * * *";schedule(){return this}onSuccess(){}onFailure(t){console.error(`[Scheduler] Task "${this.name}" failed:`,t.message)}everyMinute(){return this._expression="* * * * *",this}everyMinutes(t){return this._expression=`*/${t} * * * *`,this}everyFiveMinutes(){return this.everyMinutes(5)}everyTenMinutes(){return this.everyMinutes(10)}everyFifteenMinutes(){return this.everyMinutes(15)}everyThirtyMinutes(){return this.everyMinutes(30)}hourly(){return this._expression="0 * * * *",this}hourlyAt(t){return this._expression=`${t} * * * *`,this}daily(){return this._expression="0 0 * * *",this}dailyAt(t){let[e,s]=t.split(":").map(Number);return this._expression=`${s??0} ${e} * * *`,this}twiceDaily(t=1,e=13){return this._expression=`0 ${t},${e} * * *`,this}weekly(){return this._expression="0 0 * * 0",this}weeklyOn(t,e="00:00"){let[s,n]=e.split(":").map(Number);return this._expression=`${n??0} ${s} * * ${t}`,this}weekdays(){return this._expression=`${this._expression.split(" ").slice(0,4).join(" ")} 1-5`,this}weekends(){return this._expression=`${this._expression.split(" ").slice(0,4).join(" ")} 0,6`,this}monthly(){return this._expression="0 0 1 * *",this}monthlyOn(t,e="00:00"){let[s,n]=e.split(":").map(Number);return this._expression=`${n??0} ${s} ${t} * *`,this}quarterly(){return this._expression="0 0 1 1,4,7,10 *",this}yearly(){return this._expression="0 0 1 1 *",this}cron(t){return this._expression=t,this}preventOverlap(){return this.withoutOverlapping=!0,this}lockExpiresAfter(t){return this._lockTtlMinutes=t,this}getExpression(){return this.schedule(),this._expression}isRunning(){return this._running}async executeTask(){if(this.withoutOverlapping&&this._running)return{task:this.name,success:!0,duration:0,timestamp:new Date};let t=!1;if(this.withoutOverlapping)try{let{SchedulerLock:s}=await Promise.resolve().then(()=>(p(),g));if(t=await s.acquire(this.name,this._lockTtlMinutes),!t)return{task:this.name,success:!0,duration:0,timestamp:new Date}}catch{}this._running=!0;let e=Date.now();try{await this.handle();let s=Date.now()-e;return await this.onSuccess(),{task:this.name,success:!0,duration:s,timestamp:new Date}}catch(s){let n=Date.now()-e;return await this.onFailure(s),{task:this.name,success:!1,duration:n,error:s.message,timestamp:new Date}}finally{if(this._running=!1,t)try{let{SchedulerLock:s}=await Promise.resolve().then(()=>(p(),g));await s.release(this.name)}catch{}}}},O=class{tasks=[];timer=null;history=[];maxHistory=100;_persistToDb=!1;persistToDatabase(){return this._persistToDb=!0,this}register(t){return this.tasks.push(t),this}registerMany(t){for(let e of t)this.register(e);return this}async run(t){let e=t??new Date,s=[];for(let n of this.tasks){let r=n.getExpression();if(I(r,e)){let i=await n.executeTask();s.push(i),this.addToHistory(i)}}return s}start(){if(this.timer)return;this.run().catch(s=>console.error("[Scheduler] Error:",s));let e=6e4-Date.now()%6e4;this.timer=setTimeout(()=>{this.run().catch(s=>console.error("[Scheduler] Error:",s)),this.timer=setInterval(()=>{this.run().catch(s=>console.error("[Scheduler] Error:",s))},6e4)},e),console.log(`[Scheduler] Started with ${this.tasks.length} task(s). Next tick in ${Math.round(e/1e3)}s.`)}async stop(){this.timer&&(clearTimeout(this.timer),clearInterval(this.timer),this.timer=null);try{let{SchedulerLock:t}=await Promise.resolve().then(()=>(p(),g));await t.releaseAll()}catch{}console.log("[Scheduler] Stopped.")}getTasks(){return[...this.tasks]}getHistory(){return[...this.history]}dueTasks(t){let e=t??new Date;return this.tasks.filter(s=>I(s.getExpression(),e))}remove(t){let e=this.tasks.findIndex(s=>s.name===t);return e!==-1?(this.tasks.splice(e,1),!0):!1}clear(){this.tasks=[]}addToHistory(t){this.history.push(t),this.history.length>this.maxHistory&&this.history.shift(),this._persistToDb&&this.persistResult(t).catch(()=>{})}_historyTableEnsured=!1;async ensureHistoryTable(){if(this._historyTableEnsured)return;let{Connection:t}=await Promise.resolve().then(()=>(T(),m));switch(t.getDriver()){case"sqlite":await t.raw(`CREATE TABLE IF NOT EXISTS scheduled_task_runs (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ task TEXT NOT NULL,
16
+ success INTEGER NOT NULL,
17
+ duration INTEGER NOT NULL,
18
+ error TEXT,
19
+ ran_at TEXT NOT NULL
20
+ )`);break;case"postgres":await t.raw(`CREATE TABLE IF NOT EXISTS scheduled_task_runs (
21
+ id SERIAL PRIMARY KEY,
22
+ task VARCHAR(255) NOT NULL,
23
+ success BOOLEAN NOT NULL,
24
+ duration INTEGER NOT NULL,
25
+ error TEXT,
26
+ ran_at TIMESTAMPTZ NOT NULL
27
+ )`);break;case"mysql":await t.raw(`CREATE TABLE IF NOT EXISTS scheduled_task_runs (
28
+ id INT AUTO_INCREMENT PRIMARY KEY,
29
+ task VARCHAR(255) NOT NULL,
30
+ success TINYINT NOT NULL,
31
+ duration INT NOT NULL,
32
+ error TEXT,
33
+ ran_at DATETIME NOT NULL
34
+ ) ENGINE=InnoDB`);break}this._historyTableEnsured=!0}async persistResult(t){try{await this.ensureHistoryTable();let{Connection:e}=await Promise.resolve().then(()=>(T(),m));await e.raw("INSERT INTO scheduled_task_runs (task, success, duration, error, ran_at) VALUES (?, ?, ?, ?, ?)",[t.task,t.success,t.duration,t.error||null,t.timestamp.toISOString()])}catch{}}};function U(a,t,e){class s extends v{name=a;schedule(){return e&&e(this),this}async handle(){return t()}}return new s}export{v as ScheduledTask,O as Scheduler,y as SchedulerLock,I as cronMatches,x as parseCron,U as task};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Svelar Service Layer
3
+ *
4
+ * Base classes for the service layer in a hybrid DDD architecture.
5
+ * Services orchestrate business logic, coordinate actions, and
6
+ * interact with repositories.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { Service } from 'svelar/services';
11
+ *
12
+ * class UserService extends Service {
13
+ * constructor(private userRepo: UserRepository) {
14
+ * super();
15
+ * }
16
+ *
17
+ * async register(data: RegisterDTO): Promise<User> {
18
+ * const exists = await this.userRepo.findByEmail(data.email);
19
+ * if (exists) throw new ConflictError('Email already taken');
20
+ *
21
+ * const hashedPassword = await Hash.make(data.password);
22
+ * const user = await this.userRepo.create({
23
+ * ...data,
24
+ * password: hashedPassword,
25
+ * });
26
+ *
27
+ * await this.emit('user:registered', user);
28
+ * return user;
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export interface ServiceResult<T = any> {
34
+ success: boolean;
35
+ data?: T;
36
+ error?: string;
37
+ errors?: Record<string, string[]>;
38
+ }
39
+ export declare abstract class Service {
40
+ private eventDispatchFn?;
41
+ /**
42
+ * Set the event dispatch function for this service
43
+ */
44
+ setEventDispatcher(dispatchFn: (event: any) => Promise<void>): void;
45
+ /**
46
+ * Emit a domain event object
47
+ */
48
+ protected emit(event: any): Promise<void>;
49
+ /**
50
+ * Return a success result
51
+ */
52
+ protected ok<T>(data?: T): ServiceResult<T>;
53
+ /**
54
+ * Return a failure result
55
+ */
56
+ protected fail(error: string, errors?: Record<string, string[]>): ServiceResult<never>;
57
+ /**
58
+ * Execute an action within a try/catch, returning a ServiceResult
59
+ */
60
+ protected attempt<T>(fn: () => Promise<T>): Promise<ServiceResult<T>>;
61
+ }
62
+ /**
63
+ * Pre-built CRUD service that delegates to a repository.
64
+ * Extend to add custom business logic.
65
+ */
66
+ export declare abstract class CrudService<TModel = any, TCreateDTO = any, TUpdateDTO = any> extends Service {
67
+ protected abstract repository(): Repository<TModel>;
68
+ findAll(options?: {
69
+ page?: number;
70
+ perPage?: number;
71
+ }): Promise<TModel[]>;
72
+ findById(id: any): Promise<TModel | null>;
73
+ findByIdOrFail(id: any): Promise<TModel>;
74
+ create(data: TCreateDTO): Promise<TModel>;
75
+ update(id: any, data: TUpdateDTO): Promise<TModel>;
76
+ delete(id: any): Promise<void>;
77
+ }
78
+ import { Repository } from '../repositories/index.js';
79
+ export { Repository };
@@ -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,r){return this.model().where(e,r).get()}async findFirstWhere(e,r){return this.model().where(e,r).first()}async create(e){return this.model().create(e)}async update(e,r){let t=await this.findByIdOrFail(e);return await t.update(r),t}async delete(e){await(await this.findByIdOrFail(e)).delete()}async createMany(e){let r=[];for(let t of e)r.push(await this.create(t));return r}async deleteWhere(e,r){return this.query().where(e,r).delete()}async count(){return this.model().count()}async countWhere(e,r){return this.query().where(e,r).count()}async exists(e,r){return this.query().where(e,r).exists()}async paginate(e=1,r=15){let t=await this.query().paginate(e,r);return{data:t.data,meta:{total:t.total,page:t.currentPage,perPage:t.perPage,lastPage:t.lastPage,hasNextPage:t.currentPage<t.lastPage,hasPrevPage:t.currentPage>1}}}async latest(e,r="created_at"){let t=this.query().orderBy(r,"desc");return e&&t.limit(e),t.get()}async oldest(e,r="created_at"){let t=this.query().orderBy(r,"asc");return e&&t.limit(e),t.get()}async firstOrCreate(e,r){let t=Object.entries(e),a=this.query();for(let[s,i]of t)a=a.where(s,i);let n=await a.first();return n||this.create({...e,...r})}async updateOrCreate(e,r){let t=Object.entries(e),a=this.query();for(let[s,i]of t)a=a.where(s,i);let n=await a.first();return n?(await n.update(r),n):this.create({...e,...r})}async filter(e,r){let t=this.query();for(let{column:a,operator:n,value:s}of e)switch(n){case"in":t=t.whereIn(a,s);break;case"not_in":t=t.whereNotIn(a,s);break;case"like":t=t.where(a,"like",s);break;default:t=t.where(a,n,s)}return r&&(t=t.orderBy(r.column,r.direction)),t.get()}async pluck(e){return this.query().pluck(e)}};var c=class{eventDispatchFn;setEventDispatcher(e){this.eventDispatchFn=e}async emit(e){this.eventDispatchFn&&await this.eventDispatchFn(e)}ok(e){return{success:!0,data:e}}fail(e,r){return{success:!1,error:e,errors:r}}async attempt(e){try{let r=await e();return this.ok(r)}catch(r){return this.fail(r.message)}}},l=class extends c{async findAll(e){return e?.page?(await this.repository().paginate(e.page,e.perPage)).data:this.repository().all()}async findById(e){return this.repository().findById(e)}async findByIdOrFail(e){let r=await this.repository().findById(e);if(!r)throw new Error(`Record with id ${e} not found`);return r}async create(e){return this.repository().create(e)}async update(e,r){return this.repository().update(e,r)}async delete(e){return this.repository().delete(e)}};export{l as CrudService,o as Repository,c as Service};