@rangka/core 0.1.1 → 0.1.3

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 (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -25
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -1,93 +0,0 @@
1
- import type { Kysely } from 'kysely';
2
- import { sql } from 'kysely';
3
- import type { EnqueueOptions, BackoffStrategy } from './types.js';
4
-
5
- export interface EnqueueParams {
6
- name: string;
7
- data?: unknown;
8
- options?: EnqueueOptions;
9
- }
10
-
11
- /**
12
- * Insert a job row into rangka_jobs and return its ID.
13
- * If `unique` is set, deduplicates against active/created jobs with the same key.
14
- */
15
- export async function enqueue(
16
- db: Kysely<unknown>,
17
- name: string,
18
- data: unknown = null,
19
- options: EnqueueOptions = {},
20
- ): Promise<string> {
21
- const maxRetries = options.retries ?? 0;
22
- const backoff: BackoffStrategy = options.backoff ?? 'exponential';
23
- const startAfter = options.delay ? new Date(Date.now() + options.delay) : null;
24
- const uniqueKey = options.unique ? (options.uniqueKey ?? name) : null;
25
-
26
- if (uniqueKey) {
27
- return insertUniqueJob(db, name, data, maxRetries, backoff, startAfter, uniqueKey);
28
- }
29
-
30
- return insertJob(db, name, data, maxRetries, backoff, startAfter);
31
- }
32
-
33
- /** Insert a job with a unique_key constraint — returns the existing job ID if a duplicate exists. */
34
- async function insertUniqueJob(
35
- db: Kysely<unknown>,
36
- name: string,
37
- data: unknown,
38
- maxRetries: number,
39
- backoff: BackoffStrategy,
40
- startAfter: Date | null,
41
- uniqueKey: string,
42
- ): Promise<string> {
43
- const result = await sql<{ id: string }>`
44
- INSERT INTO rangka_jobs (name, data, state, max_retries, backoff, start_after, unique_key)
45
- VALUES (
46
- ${name},
47
- ${JSON.stringify(data)}::jsonb,
48
- 'created',
49
- ${maxRetries},
50
- ${backoff},
51
- ${startAfter},
52
- ${uniqueKey}
53
- )
54
- ON CONFLICT (unique_key) WHERE state IN ('created', 'active')
55
- DO NOTHING
56
- RETURNING id
57
- `.execute(db);
58
-
59
- // If insert was skipped due to conflict, look up the existing job's ID
60
- if (result.rows.length === 0) {
61
- const existing = await sql<{ id: string }>`
62
- SELECT id FROM rangka_jobs WHERE unique_key = ${uniqueKey} AND state IN ('created', 'active') LIMIT 1
63
- `.execute(db);
64
- return existing.rows[0]?.id ?? '';
65
- }
66
-
67
- return result.rows[0].id;
68
- }
69
-
70
- /** Insert a standard (non-unique) job row. */
71
- async function insertJob(
72
- db: Kysely<unknown>,
73
- name: string,
74
- data: unknown,
75
- maxRetries: number,
76
- backoff: BackoffStrategy,
77
- startAfter: Date | null,
78
- ): Promise<string> {
79
- const result = await sql<{ id: string }>`
80
- INSERT INTO rangka_jobs (name, data, state, max_retries, backoff, start_after)
81
- VALUES (
82
- ${name},
83
- ${JSON.stringify(data)}::jsonb,
84
- 'created',
85
- ${maxRetries},
86
- ${backoff},
87
- ${startAfter}
88
- )
89
- RETURNING id
90
- `.execute(db);
91
-
92
- return result.rows[0].id;
93
- }
package/src/jobs/index.ts DELETED
@@ -1,14 +0,0 @@
1
- export type {
2
- JobRecord,
3
- DeadLetterRecord,
4
- ScheduledJobRecord,
5
- JobState,
6
- BackoffStrategy,
7
- JobWorkerConfig,
8
- RegisteredJob,
9
- EnqueueOptions,
10
- } from './types.js';
11
- export { JobRegistry } from './registry.js';
12
- export { enqueue } from './enqueue.js';
13
- export { JobWorker } from './worker.js';
14
- export { ScheduleManager } from './scheduler.js';
@@ -1,92 +0,0 @@
1
- import type { JobConfig } from '@rangka/shared';
2
- import type { RegisteredJob } from './types.js';
3
-
4
- /**
5
- * Stores and validates background job definitions.
6
- * Jobs can be one-off or scheduled via cron expressions.
7
- */
8
- export class JobRegistry {
9
- private readonly jobs: Map<string, RegisteredJob> = new Map();
10
-
11
- /** Register a new job after validating its configuration. */
12
- register(name: string, config: JobConfig): void {
13
- if (this.jobs.has(name)) {
14
- throw new Error(`Job "${name}" is already registered`);
15
- }
16
-
17
- const validationErrors = this.validate(name, config);
18
- if (validationErrors.length > 0) {
19
- throw new Error(`Invalid job config for "${name}": ${validationErrors.join('; ')}`);
20
- }
21
-
22
- this.jobs.set(name, { name, config });
23
- }
24
-
25
- get(name: string): RegisteredJob | undefined {
26
- return this.jobs.get(name);
27
- }
28
-
29
- has(name: string): boolean {
30
- return this.jobs.has(name);
31
- }
32
-
33
- getAll(): RegisteredJob[] {
34
- return Array.from(this.jobs.values());
35
- }
36
-
37
- /** Return only jobs that have a cron schedule defined. */
38
- getScheduled(): RegisteredJob[] {
39
- return this.getAll().filter((job) => !!job.config.schedule);
40
- }
41
-
42
- /** Validate job name and config, returning a list of human-readable errors. */
43
- private validate(name: string, config: JobConfig): string[] {
44
- const errors: string[] = [];
45
-
46
- if (!name || name.trim().length === 0) {
47
- errors.push('Job name must not be empty');
48
- }
49
-
50
- if (typeof config.handler !== 'function') {
51
- errors.push('Job handler must be a function');
52
- }
53
-
54
- this.validateConcurrency(config, errors);
55
- this.validateRetries(config, errors);
56
- this.validateBackoff(config, errors);
57
- this.validateSchedule(config, errors);
58
-
59
- return errors;
60
- }
61
-
62
- private validateConcurrency(config: JobConfig, errors: string[]): void {
63
- if (
64
- config.concurrency !== undefined &&
65
- (config.concurrency < 1 || !Number.isInteger(config.concurrency))
66
- ) {
67
- errors.push('Concurrency must be a positive integer');
68
- }
69
- }
70
-
71
- private validateRetries(config: JobConfig, errors: string[]): void {
72
- if (config.retries !== undefined && (config.retries < 0 || !Number.isInteger(config.retries))) {
73
- errors.push('Retries must be a non-negative integer');
74
- }
75
- }
76
-
77
- private validateBackoff(config: JobConfig, errors: string[]): void {
78
- const allowedStrategies = ['exponential', 'linear', 'fixed'];
79
- if (config.backoff !== undefined && !allowedStrategies.includes(config.backoff)) {
80
- errors.push('Backoff must be one of: exponential, linear, fixed');
81
- }
82
- }
83
-
84
- private validateSchedule(config: JobConfig, errors: string[]): void {
85
- if (config.schedule !== undefined) {
86
- const cronFields = config.schedule.trim().split(/\s+/);
87
- if (cronFields.length !== 5) {
88
- errors.push('Schedule must be a valid 5-field cron expression');
89
- }
90
- }
91
- }
92
- }
@@ -1,205 +0,0 @@
1
- import type { Kysely } from 'kysely';
2
- import { sql } from 'kysely';
3
- import type { JobRegistry } from './registry.js';
4
- import { toCount } from '../helpers/coerce.js';
5
-
6
- /**
7
- * ScheduleManager polls the database for cron-scheduled jobs that are due,
8
- * enqueues them, and advances their next_run_at timestamps.
9
- */
10
- export class ScheduleManager {
11
- private running = false;
12
- private pollTimer: ReturnType<typeof setTimeout> | null = null;
13
- private readonly pollInterval: number;
14
- private readonly db: Kysely<unknown>;
15
- private readonly registry: JobRegistry;
16
-
17
- constructor(db: Kysely<unknown>, registry: JobRegistry, pollInterval = 60000) {
18
- this.db = db;
19
- this.registry = registry;
20
- this.pollInterval = pollInterval;
21
- }
22
-
23
- // ─── Public API ────────────────────────────────────────────────────────────
24
-
25
- /** Upserts all registered scheduled jobs into the database. */
26
- async syncSchedules(): Promise<void> {
27
- const scheduledJobs = this.registry.getScheduled();
28
-
29
- for (const job of scheduledJobs) {
30
- const cronExpr = job.config.schedule!;
31
- const nextRun = this.computeNextRun(cronExpr);
32
-
33
- await sql`
34
- INSERT INTO rangka_scheduled_jobs (name, cron, data, next_run_at)
35
- VALUES (${job.name}, ${cronExpr}, NULL, ${nextRun})
36
- ON CONFLICT (name) DO UPDATE SET cron = ${cronExpr}, next_run_at = COALESCE(rangka_scheduled_jobs.next_run_at, ${nextRun})
37
- `.execute(this.db);
38
- }
39
- }
40
-
41
- /** Starts the polling loop. */
42
- start(): void {
43
- if (this.running) return;
44
- this.running = true;
45
- this.schedulePoll();
46
- }
47
-
48
- /** Stops the polling loop. */
49
- async stop(): Promise<void> {
50
- this.running = false;
51
- if (this.pollTimer) {
52
- clearTimeout(this.pollTimer);
53
- this.pollTimer = null;
54
- }
55
- }
56
-
57
- isRunning(): boolean {
58
- return this.running;
59
- }
60
-
61
- /**
62
- * Parses a 5-field cron expression and returns the next matching minute
63
- * after `from`. Scans up to ~1 year of minutes before giving up.
64
- */
65
- computeNextRun(cronExpr: string, from: Date = new Date()): Date {
66
- const fields = cronExpr.trim().split(/\s+/);
67
- if (fields.length !== 5) {
68
- throw new Error(`Invalid cron expression: "${cronExpr}"`);
69
- }
70
-
71
- const [minuteExpr, hourExpr, dayOfMonthExpr, monthExpr, dayOfWeekExpr] = fields;
72
-
73
- // Start scanning from the next whole minute after `from`
74
- const candidate = new Date(from.getTime());
75
- candidate.setSeconds(0, 0);
76
- candidate.setMinutes(candidate.getMinutes() + 1);
77
-
78
- const ONE_YEAR_IN_MINUTES = 525_960;
79
-
80
- for (let i = 0; i < ONE_YEAR_IN_MINUTES; i++) {
81
- const matchesAllFields =
82
- this.matchesCronField(minuteExpr, candidate.getMinutes(), 0, 59) &&
83
- this.matchesCronField(hourExpr, candidate.getHours(), 0, 23) &&
84
- this.matchesCronField(dayOfMonthExpr, candidate.getDate(), 1, 31) &&
85
- this.matchesCronField(monthExpr, candidate.getMonth() + 1, 1, 12) &&
86
- this.matchesCronField(dayOfWeekExpr, candidate.getDay(), 0, 6);
87
-
88
- if (matchesAllFields) {
89
- return candidate;
90
- }
91
-
92
- candidate.setMinutes(candidate.getMinutes() + 1);
93
- }
94
-
95
- throw new Error(`Could not compute next run for cron: "${cronExpr}"`);
96
- }
97
-
98
- // ─── Private helpers ───────────────────────────────────────────────────────
99
-
100
- /** Arms the next poll timer. */
101
- private schedulePoll(): void {
102
- this.pollTimer = setTimeout(() => this.poll(), this.pollInterval);
103
- }
104
-
105
- /** Runs one poll cycle, then re-arms the timer if still running. */
106
- private poll(): void {
107
- if (!this.running) return;
108
-
109
- this.checkSchedules().finally(() => {
110
- if (this.running) {
111
- this.schedulePoll();
112
- }
113
- });
114
- }
115
-
116
- /**
117
- * Finds all schedules that are due and processes each one:
118
- * - Skips enqueuing if the job already has a pending/active instance.
119
- * - Otherwise enqueues a new job instance.
120
- * In both cases, advances the schedule's next_run_at.
121
- */
122
- private async checkSchedules(): Promise<void> {
123
- const dueSchedules = await sql<{ id: string; name: string; cron: string; data: unknown }>`
124
- SELECT id, name, cron, data FROM rangka_scheduled_jobs
125
- WHERE next_run_at <= NOW()
126
- FOR UPDATE SKIP LOCKED
127
- `.execute(this.db);
128
-
129
- for (const schedule of dueSchedules.rows) {
130
- const alreadyPending = await this.hasPendingJob(schedule.name);
131
-
132
- if (!alreadyPending) {
133
- await this.enqueueJob(schedule.name, schedule.data);
134
- }
135
-
136
- await this.advanceSchedule(schedule.id, schedule.cron);
137
- }
138
- }
139
-
140
- /** Inserts a new job instance into the jobs table. */
141
- private async enqueueJob(name: string, data: unknown): Promise<void> {
142
- await sql`
143
- INSERT INTO rangka_jobs (name, data, state, max_retries, backoff)
144
- VALUES (${name}, ${JSON.stringify(data)}::jsonb, 'created', 0, 'exponential')
145
- `.execute(this.db);
146
- }
147
-
148
- /** Updates a schedule's next_run_at and records last_run_at. */
149
- private async advanceSchedule(scheduleId: string, cronExpr: string): Promise<void> {
150
- const nextRun = this.computeNextRun(cronExpr);
151
- await sql`
152
- UPDATE rangka_scheduled_jobs
153
- SET next_run_at = ${nextRun}, last_run_at = NOW()
154
- WHERE id = ${scheduleId}
155
- `.execute(this.db);
156
- }
157
-
158
- /** Returns true if a job with the given name is already created or active. */
159
- private async hasPendingJob(name: string): Promise<boolean> {
160
- const result = await sql<{ count: string }>`
161
- SELECT COUNT(*) as count FROM rangka_jobs
162
- WHERE name = ${name} AND state IN ('created', 'active')
163
- `.execute(this.db);
164
- return toCount(result.rows[0]) > 0;
165
- }
166
-
167
- /**
168
- * Checks whether a single cron field expression matches the given value.
169
- * Supports: wildcard (*), lists (1,5,10), ranges (1-5), and steps (star/5, 1-10/2).
170
- */
171
- private matchesCronField(expr: string, value: number, min: number, _max: number): boolean {
172
- if (expr === '*') return true;
173
-
174
- // A field can be a comma-separated list of segments
175
- for (const segment of expr.split(',')) {
176
- if (this.segmentMatches(segment, value, min)) {
177
- return true;
178
- }
179
- }
180
-
181
- return false;
182
- }
183
-
184
- /** Evaluates a single segment of a cron field (step, range, or literal). */
185
- private segmentMatches(segment: string, value: number, fieldMin: number): boolean {
186
- if (segment.includes('/')) {
187
- // Step expression, e.g. "*/5" or "10-30/5"
188
- const [rangeExpr, stepStr] = segment.split('/');
189
- const step = parseInt(stepStr, 10);
190
- const start = rangeExpr === '*' ? fieldMin : parseInt(rangeExpr, 10);
191
- return value >= start && (value - start) % step === 0;
192
- }
193
-
194
- if (segment.includes('-')) {
195
- // Range expression, e.g. "1-5"
196
- const [startStr, endStr] = segment.split('-');
197
- const rangeStart = parseInt(startStr, 10);
198
- const rangeEnd = parseInt(endStr, 10);
199
- return value >= rangeStart && value <= rangeEnd;
200
- }
201
-
202
- // Exact value, e.g. "30"
203
- return parseInt(segment, 10) === value;
204
- }
205
- }
@@ -1,132 +0,0 @@
1
- import type { TableDefinition } from '../db/types.js';
2
-
3
- export function getJobTables(): TableDefinition[] {
4
- return [buildJobsTable(), buildDeadLettersTable(), buildScheduledJobsTable()];
5
- }
6
-
7
- function buildJobsTable(): TableDefinition {
8
- return {
9
- name: 'rangka_jobs',
10
- columns: [
11
- {
12
- name: 'id',
13
- type: 'UUID',
14
- nullable: false,
15
- primaryKey: true,
16
- defaultValue: 'gen_random_uuid()',
17
- },
18
- { name: 'name', type: 'TEXT', nullable: false },
19
- { name: 'data', type: 'JSONB', nullable: true },
20
- { name: 'state', type: 'TEXT', nullable: false, defaultValue: "'created'" },
21
- { name: 'retry_count', type: 'INTEGER', nullable: false, defaultValue: '0' },
22
- { name: 'max_retries', type: 'INTEGER', nullable: false, defaultValue: '0' },
23
- { name: 'backoff', type: 'TEXT', nullable: false, defaultValue: "'exponential'" },
24
- { name: 'start_after', type: 'TIMESTAMPTZ', nullable: true },
25
- { name: 'started_at', type: 'TIMESTAMPTZ', nullable: true },
26
- { name: 'completed_at', type: 'TIMESTAMPTZ', nullable: true },
27
- { name: 'failed_at', type: 'TIMESTAMPTZ', nullable: true },
28
- { name: 'error', type: 'TEXT', nullable: true },
29
- { name: 'unique_key', type: 'TEXT', nullable: true },
30
- { name: 'created_at', type: 'TIMESTAMPTZ', nullable: false, defaultValue: 'NOW()' },
31
- { name: 'expire_in', type: 'INTERVAL', nullable: false, defaultValue: "'15 minutes'" },
32
- ],
33
- foreignKeys: [],
34
- checkConstraints: [
35
- {
36
- name: 'chk_rangka_jobs_state',
37
- column: 'state',
38
- expression: "state IN ('created', 'active', 'completed', 'failed')",
39
- },
40
- ],
41
- indexes: [
42
- {
43
- name: 'idx_rangka_jobs_state_start_after',
44
- table: 'rangka_jobs',
45
- columns: ['state', 'start_after'],
46
- unique: false,
47
- },
48
- {
49
- name: 'idx_rangka_jobs_name_state',
50
- table: 'rangka_jobs',
51
- columns: ['name', 'state'],
52
- unique: false,
53
- },
54
- {
55
- name: 'idx_rangka_jobs_unique_key_active',
56
- table: 'rangka_jobs',
57
- columns: ['unique_key'],
58
- unique: true,
59
- where: "state IN ('created', 'active')",
60
- },
61
- ],
62
- };
63
- }
64
-
65
- function buildDeadLettersTable(): TableDefinition {
66
- return {
67
- name: 'rangka_dead_letters',
68
- columns: [
69
- {
70
- name: 'id',
71
- type: 'UUID',
72
- nullable: false,
73
- primaryKey: true,
74
- defaultValue: 'gen_random_uuid()',
75
- },
76
- { name: 'job_id', type: 'UUID', nullable: false },
77
- { name: 'name', type: 'TEXT', nullable: false },
78
- { name: 'data', type: 'JSONB', nullable: true },
79
- { name: 'error', type: 'TEXT', nullable: true },
80
- { name: 'retry_count', type: 'INTEGER', nullable: false, defaultValue: '0' },
81
- { name: 'failed_at', type: 'TIMESTAMPTZ', nullable: false, defaultValue: 'NOW()' },
82
- { name: 'created_at', type: 'TIMESTAMPTZ', nullable: false, defaultValue: 'NOW()' },
83
- ],
84
- foreignKeys: [],
85
- checkConstraints: [],
86
- indexes: [
87
- {
88
- name: 'idx_rangka_dead_letters_name',
89
- table: 'rangka_dead_letters',
90
- columns: ['name'],
91
- unique: false,
92
- },
93
- ],
94
- };
95
- }
96
-
97
- function buildScheduledJobsTable(): TableDefinition {
98
- return {
99
- name: 'rangka_scheduled_jobs',
100
- columns: [
101
- {
102
- name: 'id',
103
- type: 'UUID',
104
- nullable: false,
105
- primaryKey: true,
106
- defaultValue: 'gen_random_uuid()',
107
- },
108
- { name: 'name', type: 'TEXT', nullable: false },
109
- { name: 'cron', type: 'TEXT', nullable: false },
110
- { name: 'data', type: 'JSONB', nullable: true },
111
- { name: 'last_run_at', type: 'TIMESTAMPTZ', nullable: true },
112
- { name: 'next_run_at', type: 'TIMESTAMPTZ', nullable: true },
113
- { name: 'created_at', type: 'TIMESTAMPTZ', nullable: false, defaultValue: 'NOW()' },
114
- ],
115
- foreignKeys: [],
116
- checkConstraints: [],
117
- indexes: [
118
- {
119
- name: 'uidx_rangka_scheduled_jobs_name',
120
- table: 'rangka_scheduled_jobs',
121
- columns: ['name'],
122
- unique: true,
123
- },
124
- {
125
- name: 'idx_rangka_scheduled_jobs_next_run',
126
- table: 'rangka_scheduled_jobs',
127
- columns: ['next_run_at'],
128
- unique: false,
129
- },
130
- ],
131
- };
132
- }
package/src/jobs/types.ts DELETED
@@ -1,62 +0,0 @@
1
- import type { JobConfig } from '@rangka/shared';
2
-
3
- export type JobState = 'created' | 'active' | 'completed' | 'failed';
4
-
5
- export type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
6
-
7
- export interface JobRecord {
8
- id: string;
9
- name: string;
10
- data: unknown;
11
- state: JobState;
12
- retry_count: number;
13
- max_retries: number;
14
- backoff: BackoffStrategy;
15
- start_after: Date | null;
16
- started_at: Date | null;
17
- completed_at: Date | null;
18
- failed_at: Date | null;
19
- error: string | null;
20
- unique_key: string | null;
21
- created_at: Date;
22
- expire_in: string;
23
- }
24
-
25
- export interface DeadLetterRecord {
26
- id: string;
27
- job_id: string;
28
- name: string;
29
- data: unknown;
30
- error: string | null;
31
- retry_count: number;
32
- failed_at: Date;
33
- created_at: Date;
34
- }
35
-
36
- export interface ScheduledJobRecord {
37
- id: string;
38
- name: string;
39
- cron: string;
40
- data: unknown;
41
- last_run_at: Date | null;
42
- next_run_at: Date | null;
43
- created_at: Date;
44
- }
45
-
46
- export interface JobWorkerConfig {
47
- pollInterval?: number;
48
- enabled?: boolean;
49
- }
50
-
51
- export interface RegisteredJob {
52
- name: string;
53
- config: JobConfig;
54
- }
55
-
56
- export interface EnqueueOptions {
57
- delay?: number;
58
- unique?: boolean;
59
- uniqueKey?: string;
60
- retries?: number;
61
- backoff?: BackoffStrategy;
62
- }