@sonamu-kit/tasks 0.0.2 → 0.1.1

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.
@@ -40,18 +40,76 @@ interface BackendPostgresOptions {
40
40
  * Manages a connection to a Postgres database for workflow operations.
41
41
  */
42
42
  export class BackendPostgres implements Backend {
43
- private knex: Knex;
43
+ private config: Knex.Config;
44
44
  private namespaceId: string;
45
45
  private usePubSub: boolean;
46
46
  private pubsub: PostgresPubSub | null = null;
47
+ private initialized: boolean = false;
48
+ private runMigrations: boolean;
49
+
50
+ private _knex: Knex | null = null;
51
+ private get knex(): Knex {
52
+ if (!this._knex) {
53
+ this._knex = knex(this.config);
54
+ }
55
+
56
+ return this._knex;
57
+ }
58
+
59
+ constructor(config: Knex.Config, options?: BackendPostgresOptions) {
60
+ this.config = {
61
+ ...config,
62
+ postProcessResponse: (result, _queryContext) => {
63
+ if (result === null || result === undefined) {
64
+ return result;
65
+ }
66
+
67
+ if (config?.postProcessResponse) {
68
+ result = config.postProcessResponse(result, _queryContext);
69
+ }
70
+
71
+ const camelizeRow = (row: Record<string, unknown>) =>
72
+ Object.fromEntries(
73
+ Object.entries(row).map(([key, value]) => [camelize(key, true), value]),
74
+ );
75
+
76
+ if (Array.isArray(result)) {
77
+ return result.map(camelizeRow);
78
+ }
79
+
80
+ return camelizeRow(result);
81
+ },
82
+ };
83
+
84
+ const { namespaceId, usePubSub, runMigrations } = {
85
+ namespaceId: DEFAULT_NAMESPACE_ID,
86
+ usePubSub: true,
87
+ runMigrations: true,
88
+ ...options,
89
+ };
47
90
 
48
- private constructor(knex: Knex, namespaceId: string, usePubSub: boolean) {
49
- this.knex = knex;
50
91
  this.namespaceId = namespaceId;
51
92
  this.usePubSub = usePubSub;
93
+ this.runMigrations = runMigrations;
94
+ }
95
+
96
+ async initialize() {
97
+ if (this.initialized) {
98
+ return;
99
+ }
100
+
101
+ if (this.runMigrations) {
102
+ await migrate(this.config, DEFAULT_SCHEMA);
103
+ }
104
+
105
+ this.initialized = true;
52
106
  }
53
107
 
54
108
  async subscribe(callback: OnSubscribed) {
109
+ if (!this.initialized) {
110
+ throw new Error("Backend not initialized");
111
+ }
112
+
55
113
  if (!this.usePubSub) {
56
114
  return;
57
115
  }
@@ -64,6 +122,10 @@ export class BackendPostgres implements Backend {
64
122
  }
65
123
 
66
124
  async publish(payload?: string): Promise<void> {
125
+ if (!this.initialized) {
126
+ throw new Error("Backend not initialized");
127
+ }
128
+
67
129
  if (!this.usePubSub) {
68
130
  return;
69
131
  }
@@ -75,56 +137,21 @@ export class BackendPostgres implements Backend {
75
137
  );
76
138
  }
77
139
 
78
- /**
79
- * Create and initialize a new BackendPostgres instance. This will
80
- * automatically run migrations on startup unless `runMigrations` is set to
81
- * false.
82
- */
83
- static async connect(
84
- dbConf: Knex.Config,
85
- options?: BackendPostgresOptions,
86
- ): Promise<BackendPostgres> {
87
- const postProcessResponse: Knex.Config["postProcessResponse"] = (result, _queryContext) => {
88
- if (result === null || result === undefined) {
89
- return result;
90
- }
91
-
92
- if (dbConf?.postProcessResponse) {
93
- result = dbConf.postProcessResponse(result, _queryContext);
94
- }
95
-
96
- const camelizeRow = (row: Record<string, unknown>) =>
97
- Object.fromEntries(Object.entries(row).map(([key, value]) => [camelize(key, true), value]));
98
-
99
- if (Array.isArray(result)) {
100
- return result.map(camelizeRow);
101
- }
102
-
103
- return camelizeRow(result);
104
- };
105
-
106
- const { namespaceId, runMigrations, usePubSub } = {
107
- namespaceId: DEFAULT_NAMESPACE_ID,
108
- runMigrations: true,
109
- usePubSub: true,
110
- ...options,
111
- };
112
-
113
- const knexInstance = knex({ ...dbConf, postProcessResponse });
114
- if (runMigrations) {
115
- await migrate(knexInstance, DEFAULT_SCHEMA);
140
+ async stop(): Promise<void> {
141
+ if (!this.initialized) {
142
+ return;
116
143
  }
117
144
 
118
- return new BackendPostgres(knexInstance, namespaceId, usePubSub);
119
- }
120
-
121
- async stop(): Promise<void> {
122
145
  await this.pubsub?.destroy();
123
146
  this.pubsub = null;
124
147
  await this.knex.destroy();
125
148
  }
126
149
 
127
150
  async createWorkflowRun(params: CreateWorkflowRunParams): Promise<WorkflowRun> {
151
+ if (!this.initialized) {
152
+ throw new Error("Backend not initialized");
153
+ }
154
+
128
155
  const qb = this.knex
129
156
  .withSchema(DEFAULT_SCHEMA)
130
157
  .table("workflow_runs")
@@ -155,6 +182,10 @@ export class BackendPostgres implements Backend {
155
182
  }
156
183
 
157
184
  async getWorkflowRun(params: GetWorkflowRunParams): Promise<WorkflowRun | null> {
185
+ if (!this.initialized) {
186
+ throw new Error("Backend not initialized");
187
+ }
188
+
158
189
  const workflowRun = await this.knex
159
190
  .withSchema(DEFAULT_SCHEMA)
160
191
  .table("workflow_runs")
@@ -189,6 +220,10 @@ export class BackendPostgres implements Backend {
189
220
  }
190
221
 
191
222
  async listWorkflowRuns(params: ListWorkflowRunsParams): Promise<PaginatedResponse<WorkflowRun>> {
223
+ if (!this.initialized) {
224
+ throw new Error("Backend not initialized");
225
+ }
226
+
192
227
  const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
193
228
  const { after, before } = params;
194
229
 
@@ -232,6 +267,10 @@ export class BackendPostgres implements Backend {
232
267
  }
233
268
 
234
269
  async claimWorkflowRun(params: ClaimWorkflowRunParams): Promise<WorkflowRun | null> {
270
+ if (!this.initialized) {
271
+ throw new Error("Backend not initialized");
272
+ }
273
+
235
274
  const claimed = await this.knex
236
275
  .with("expired", (qb) =>
237
276
  qb
@@ -288,6 +327,10 @@ export class BackendPostgres implements Backend {
288
327
  }
289
328
 
290
329
  async extendWorkflowRunLease(params: ExtendWorkflowRunLeaseParams): Promise<WorkflowRun> {
330
+ if (!this.initialized) {
331
+ throw new Error("Backend not initialized");
332
+ }
333
+
291
334
  const [updated] = await this.knex
292
335
  .withSchema(DEFAULT_SCHEMA)
293
336
  .table("workflow_runs")
@@ -309,6 +352,10 @@ export class BackendPostgres implements Backend {
309
352
  }
310
353
 
311
354
  async sleepWorkflowRun(params: SleepWorkflowRunParams): Promise<WorkflowRun> {
355
+ if (!this.initialized) {
356
+ throw new Error("Backend not initialized");
357
+ }
358
+
312
359
  // 'succeeded' status is deprecated
313
360
  const [updated] = await this.knex
314
361
  .withSchema(DEFAULT_SCHEMA)
@@ -333,6 +380,10 @@ export class BackendPostgres implements Backend {
333
380
  }
334
381
 
335
382
  async completeWorkflowRun(params: CompleteWorkflowRunParams): Promise<WorkflowRun> {
383
+ if (!this.initialized) {
384
+ throw new Error("Backend not initialized");
385
+ }
386
+
336
387
  const [updated] = await this.knex
337
388
  .withSchema(DEFAULT_SCHEMA)
338
389
  .table("workflow_runs")
@@ -359,6 +410,10 @@ export class BackendPostgres implements Backend {
359
410
  }
360
411
 
361
412
  async failWorkflowRun(params: FailWorkflowRunParams): Promise<WorkflowRun> {
413
+ if (!this.initialized) {
414
+ throw new Error("Backend not initialized");
415
+ }
416
+
362
417
  const { workflowRunId, error } = params;
363
418
  const { initialIntervalMs, backoffCoefficient, maximumIntervalMs } = DEFAULT_RETRY_POLICY;
364
419
 
@@ -403,6 +458,10 @@ export class BackendPostgres implements Backend {
403
458
  }
404
459
 
405
460
  async cancelWorkflowRun(params: CancelWorkflowRunParams): Promise<WorkflowRun> {
461
+ if (!this.initialized) {
462
+ throw new Error("Backend not initialized");
463
+ }
464
+
406
465
  const [updated] = await this.knex
407
466
  .withSchema(DEFAULT_SCHEMA)
408
467
  .table("workflow_runs")
@@ -447,6 +506,10 @@ export class BackendPostgres implements Backend {
447
506
  }
448
507
 
449
508
  async createStepAttempt(params: CreateStepAttemptParams): Promise<StepAttempt> {
509
+ if (!this.initialized) {
510
+ throw new Error("Backend not initialized");
511
+ }
512
+
450
513
  const [stepAttempt] = await this.knex
451
514
  .withSchema(DEFAULT_SCHEMA)
452
515
  .table("step_attempts")
@@ -473,6 +536,10 @@ export class BackendPostgres implements Backend {
473
536
  }
474
537
 
475
538
  async getStepAttempt(params: GetStepAttemptParams): Promise<StepAttempt | null> {
539
+ if (!this.initialized) {
540
+ throw new Error("Backend not initialized");
541
+ }
542
+
476
543
  const stepAttempt = await this.knex
477
544
  .withSchema(DEFAULT_SCHEMA)
478
545
  .table("step_attempts")
@@ -484,6 +551,10 @@ export class BackendPostgres implements Backend {
484
551
  }
485
552
 
486
553
  async listStepAttempts(params: ListStepAttemptsParams): Promise<PaginatedResponse<StepAttempt>> {
554
+ if (!this.initialized) {
555
+ throw new Error("Backend not initialized");
556
+ }
557
+
487
558
  const limit = params.limit ?? DEFAULT_PAGINATION_PAGE_SIZE;
488
559
  const { after, before } = params;
489
560
 
@@ -569,6 +640,10 @@ export class BackendPostgres implements Backend {
569
640
  }
570
641
 
571
642
  async completeStepAttempt(params: CompleteStepAttemptParams): Promise<StepAttempt> {
643
+ if (!this.initialized) {
644
+ throw new Error("Backend not initialized");
645
+ }
646
+
572
647
  const [updated] = await this.knex
573
648
  .withSchema(DEFAULT_SCHEMA)
574
649
  .table("step_attempts as sa")
@@ -598,6 +673,10 @@ export class BackendPostgres implements Backend {
598
673
  }
599
674
 
600
675
  async failStepAttempt(params: FailStepAttemptParams): Promise<StepAttempt> {
676
+ if (!this.initialized) {
677
+ throw new Error("Backend not initialized");
678
+ }
679
+
601
680
  const [updated] = await this.knex
602
681
  .withSchema(DEFAULT_SCHEMA)
603
682
  .table("step_attempts as sa")
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import type { Knex } from "knex";
2
+ import knex, { type Knex } from "knex";
3
3
 
4
4
  export const DEFAULT_SCHEMA = "sonamu_tasks";
5
5
 
@@ -7,12 +7,17 @@ export const DEFAULT_SCHEMA = "sonamu_tasks";
7
7
  * migrate applies pending migrations to the database. Does nothing if the
8
8
  * database is already up to date.
9
9
  */
10
- export async function migrate(knex: Knex, schema: string) {
11
- await knex.schema.createSchemaIfNotExists(schema);
12
- await knex.migrate.latest({
13
- directory: path.join(import.meta.dirname, "migrations"),
14
- schemaName: schema,
15
- });
10
+ export async function migrate(config: Knex.Config, schema: string) {
11
+ const instance = knex({ ...config, pool: { min: 1, max: 1 } });
12
+ try {
13
+ await instance.schema.createSchemaIfNotExists(schema);
14
+ await instance.migrate.latest({
15
+ directory: path.join(import.meta.dirname, "migrations"),
16
+ schemaName: schema,
17
+ });
18
+ } finally {
19
+ await instance.destroy();
20
+ }
16
21
  }
17
22
 
18
23
  /**
@@ -8,10 +8,11 @@ describe("StepExecutor", () => {
8
8
  let backend: BackendPostgres;
9
9
 
10
10
  beforeAll(async () => {
11
- backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
11
+ backend = new BackendPostgres(KNEX_GLOBAL_CONFIG, {
12
12
  namespaceId: randomUUID(),
13
13
  runMigrations: false,
14
14
  });
15
+ await backend.initialize();
15
16
  });
16
17
 
17
18
  afterAll(async () => {
@@ -164,10 +165,11 @@ describe("executeWorkflow", () => {
164
165
  let backend: BackendPostgres;
165
166
 
166
167
  beforeAll(async () => {
167
- backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
168
+ backend = new BackendPostgres(KNEX_GLOBAL_CONFIG, {
168
169
  namespaceId: randomUUID(),
169
170
  runMigrations: false,
170
171
  });
172
+ await backend.initialize();
171
173
  });
172
174
 
173
175
  afterAll(async () => {
@@ -10,11 +10,12 @@ async function getBackend(): Promise<BackendPostgres> {
10
10
  return _backend;
11
11
  }
12
12
 
13
- _backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
13
+ _backend = new BackendPostgres(KNEX_GLOBAL_CONFIG, {
14
14
  runMigrations: true,
15
15
  namespaceId: randomUUID(),
16
16
  });
17
17
 
18
+ await _backend.initialize();
18
19
  return _backend;
19
20
  }
20
21
 
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import knex, { type Knex } from "knex";
2
+ import type { Knex } from "knex";
3
3
  import { BackendPostgres } from "../database/backend";
4
4
  import { migrate as baseMigrate, DEFAULT_SCHEMA } from "../database/base";
5
5
 
@@ -20,7 +20,7 @@ export const KNEX_GLOBAL_CONFIG: Knex.Config = {
20
20
  } as const;
21
21
 
22
22
  export async function migrate(): Promise<void> {
23
- await baseMigrate(knex(KNEX_GLOBAL_CONFIG), DEFAULT_SCHEMA);
23
+ await baseMigrate(KNEX_GLOBAL_CONFIG, DEFAULT_SCHEMA);
24
24
  }
25
25
 
26
26
  export async function createBackend(): Promise<BackendPostgres> {
@@ -28,9 +28,11 @@ export async function createBackend(): Promise<BackendPostgres> {
28
28
  return backend;
29
29
  }
30
30
 
31
- backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
31
+ backend = new BackendPostgres(KNEX_GLOBAL_CONFIG, {
32
32
  namespaceId: randomUUID(),
33
+ runMigrations: false,
33
34
  });
35
+ await backend.initialize();
34
36
 
35
37
  return backend;
36
38
  }
@@ -8,10 +8,11 @@ describe("Worker", () => {
8
8
  let backend: BackendPostgres;
9
9
 
10
10
  beforeEach(async () => {
11
- backend = await BackendPostgres.connect(KNEX_GLOBAL_CONFIG, {
11
+ backend = new BackendPostgres(KNEX_GLOBAL_CONFIG, {
12
12
  namespaceId: randomUUID(),
13
13
  runMigrations: false,
14
14
  });
15
+ await backend.initialize();
15
16
  });
16
17
 
17
18
  afterEach(async () => {
@@ -13,7 +13,7 @@ const config: Knex.Config = {
13
13
  } as const;
14
14
 
15
15
  // Use Postgres (configured with Knex config)
16
- const backend = await BackendPostgres.connect(config, {
16
+ const backend = new BackendPostgres(config, {
17
17
  runMigrations: false,
18
18
  });
19
19