@kubun/store-workflow 0.13.0 → 0.14.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.
package/lib/api.d.ts CHANGED
@@ -51,8 +51,9 @@ export type ListFilter = {
51
51
  queue?: string;
52
52
  status?: WorkflowStatus;
53
53
  };
54
- export type WorkflowScheduleRow = Omit<WorkflowSchedule, 'params' | 'enabled'> & {
54
+ export type WorkflowScheduleRow = Omit<WorkflowSchedule, 'params' | 'policy' | 'enabled'> & {
55
55
  params: unknown;
56
+ policy: unknown;
56
57
  enabled: boolean;
57
58
  };
58
59
  export type InsertScheduleInput = {
@@ -63,6 +64,24 @@ export type InsertScheduleInput = {
63
64
  every?: string | null;
64
65
  nextRun: number;
65
66
  enabled: boolean;
67
+ kind?: 'interval' | 'adaptive';
68
+ policy?: unknown | null;
69
+ lastOutcome?: string | null;
70
+ consecutiveCount?: number;
71
+ };
72
+ export type AdvanceScheduleAdaptiveInput = {
73
+ nextRun: number;
74
+ lastOutcome: string | null;
75
+ consecutiveCount: number;
76
+ };
77
+ export type UpsertAdaptiveScheduleInput = {
78
+ id: string;
79
+ workflowName: string;
80
+ params: unknown;
81
+ policy: unknown;
82
+ nextRun: number;
83
+ enabled: boolean;
84
+ reactivate?: boolean;
66
85
  };
67
86
  export type WorkflowStoreAPI = {
68
87
  insertInstance(input: InsertInstanceInput): Promise<InsertInstanceResult>;
@@ -78,10 +97,13 @@ export type WorkflowStoreAPI = {
78
97
  get(instanceID: string): Promise<WorkflowInstanceRow | null>;
79
98
  list(filter?: ListFilter): Promise<Array<WorkflowInstanceRow>>;
80
99
  insertSchedule(input: InsertScheduleInput, now: number): Promise<void>;
100
+ upsertAdaptiveSchedule(input: UpsertAdaptiveScheduleInput, now: number): Promise<WorkflowScheduleRow>;
81
101
  getSchedule(id: string): Promise<WorkflowScheduleRow | null>;
82
102
  listSchedules(): Promise<Array<WorkflowScheduleRow>>;
83
103
  listDueSchedules(now: number): Promise<Array<WorkflowScheduleRow>>;
84
104
  advanceSchedule(id: string, nextRun: number, now: number): Promise<void>;
105
+ advanceScheduleAdaptive(id: string, input: AdvanceScheduleAdaptiveInput, now: number): Promise<void>;
85
106
  setScheduleEnabled(id: string, enabled: boolean, now: number): Promise<void>;
107
+ setSchedulePolicy(id: string, policy: unknown, now: number): Promise<void>;
86
108
  };
87
109
  export declare function createWorkflowStore(db: Kysely<WorkflowTables>, adapter: Adapter): WorkflowStoreAPI;
package/lib/api.js CHANGED
@@ -16,6 +16,7 @@ function rowToSchedule(row) {
16
16
  return {
17
17
  ...row,
18
18
  params: decodeJSON(row.params),
19
+ policy: decodeJSON(row.policy),
19
20
  // Stored as integer 0/1 across adapters.
20
21
  enabled: Number(row.enabled) === 1
21
22
  };
@@ -25,6 +26,7 @@ export function createWorkflowStore(db, adapter) {
25
26
  const encodeJSON = (value)=>adapter.encodeJSON(value);
26
27
  const encodeNextAction = (value)=>value != null ? adapter.encodeJSON(value) : null;
27
28
  const encodeParams = (value)=>adapter.encodeJSON(value);
29
+ const encodePolicy = (value)=>value != null ? adapter.encodeJSON(value) : null;
28
30
  async function readRow(tx, instanceID) {
29
31
  return await tx.selectFrom('kubun_workflow_instances').selectAll().where('instance_id', '=', instanceID).executeTakeFirst();
30
32
  }
@@ -219,12 +221,48 @@ export function createWorkflowStore(db, adapter) {
219
221
  params: encodeParams(input.params),
220
222
  cron: input.cron ?? null,
221
223
  every: input.every ?? null,
224
+ kind: input.kind ?? 'interval',
225
+ policy: encodePolicy(input.policy ?? null),
226
+ last_outcome: input.lastOutcome ?? null,
227
+ consecutive_count: input.consecutiveCount ?? 0,
222
228
  next_run: input.nextRun,
223
229
  enabled: input.enabled ? 1 : 0,
224
230
  created_at: now,
225
231
  updated_at: now
226
232
  }).execute();
227
233
  },
234
+ async upsertAdaptiveSchedule (input, now) {
235
+ const reactivate = input.reactivate ?? true;
236
+ // One statement, no preceding read: ON CONFLICT DO UPDATE lets two
237
+ // concurrent enables of the same id both commit — one inserts, the other
238
+ // updates — instead of the loser hitting the PK constraint. RETURNING
239
+ // hands back the row as committed by this statement. The update touches
240
+ // only next_run + enabled, so an existing row's params/policy and its
241
+ // last_outcome/consecutive_count streak survive a re-arm. Under
242
+ // reactivate:false the update keeps the stored `enabled` flag (a
243
+ // qualified self-reference, portable across SQLite and Postgres) so a
244
+ // user-paused row is not silently re-enabled; next_run still advances.
245
+ const row = await db.insertInto('kubun_workflow_schedules').values({
246
+ id: input.id,
247
+ workflow_name: input.workflowName,
248
+ params: encodeParams(input.params),
249
+ cron: null,
250
+ every: null,
251
+ kind: 'adaptive',
252
+ policy: encodePolicy(input.policy ?? null),
253
+ last_outcome: null,
254
+ consecutive_count: 0,
255
+ next_run: input.nextRun,
256
+ enabled: input.enabled ? 1 : 0,
257
+ created_at: now,
258
+ updated_at: now
259
+ }).onConflict((oc)=>oc.column('id').doUpdateSet((eb)=>({
260
+ next_run: input.nextRun,
261
+ enabled: reactivate ? input.enabled ? 1 : 0 : eb.ref('kubun_workflow_schedules.enabled'),
262
+ updated_at: now
263
+ }))).returningAll().executeTakeFirstOrThrow();
264
+ return rowToSchedule(row);
265
+ },
228
266
  async getSchedule (id) {
229
267
  const row = await db.selectFrom('kubun_workflow_schedules').selectAll().where('id', '=', id).executeTakeFirst();
230
268
  return row != null ? rowToSchedule(row) : null;
@@ -243,11 +281,25 @@ export function createWorkflowStore(db, adapter) {
243
281
  updated_at: now
244
282
  }).where('id', '=', id).execute();
245
283
  },
284
+ async advanceScheduleAdaptive (id, input, now) {
285
+ await db.updateTable('kubun_workflow_schedules').set({
286
+ next_run: input.nextRun,
287
+ last_outcome: input.lastOutcome,
288
+ consecutive_count: input.consecutiveCount,
289
+ updated_at: now
290
+ }).where('id', '=', id).execute();
291
+ },
246
292
  async setScheduleEnabled (id, enabled, now) {
247
293
  await db.updateTable('kubun_workflow_schedules').set({
248
294
  enabled: enabled ? 1 : 0,
249
295
  updated_at: now
250
296
  }).where('id', '=', id).execute();
297
+ },
298
+ async setSchedulePolicy (id, policy, now) {
299
+ await db.updateTable('kubun_workflow_schedules').set({
300
+ policy: encodePolicy(policy),
301
+ updated_at: now
302
+ }).where('id', '=', id).execute();
251
303
  }
252
304
  };
253
305
  }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { StoreProvider } from '@kubun/db';
2
2
  import type { WorkflowStoreAPI } from './api.js';
3
- export type { CheckpointInput, FailInput, FinishInput, InsertInstanceInput, InsertInstanceResult, InsertReason, InsertScheduleInput, ListFilter, WorkflowInstanceRow, WorkflowNextAction, WorkflowScheduleRow, WorkflowStatus, WorkflowStoreAPI, } from './api.js';
3
+ export type { AdvanceScheduleAdaptiveInput, CheckpointInput, FailInput, FinishInput, InsertInstanceInput, InsertInstanceResult, InsertReason, InsertScheduleInput, ListFilter, WorkflowInstanceRow, WorkflowNextAction, WorkflowScheduleRow, WorkflowStatus, WorkflowStoreAPI, } from './api.js';
4
4
  export { createWorkflowStore } from './api.js';
5
5
  export { workflowStoreDefinition } from './definition.js';
6
6
  export { WORKFLOW_SINGLETON_UNIQUE_INDEX } from './migrations.js';
package/lib/migrations.js CHANGED
@@ -40,7 +40,8 @@ export function getWorkflowMigrations(ctx) {
40
40
  ]).execute();
41
41
  await db.schema.createTable('kubun_workflow_schedules').ifNotExists().addColumn('id', t.text, (col)=>col.notNull().primaryKey()).addColumn('workflow_name', t.text, (col)=>col.notNull()).addColumn('params', t.json, (col)=>col.notNull())// Exactly one of cron / every is set; both nullable, the store persists
42
42
  // them without parsing.
43
- .addColumn('cron', t.text).addColumn('every', t.text)// ms-epoch as bigint (not the `timestamp` column type), matching the
43
+ .addColumn('cron', t.text).addColumn('every', t.text)// Default keeps existing interval inserts working unchanged.
44
+ .addColumn('kind', t.text, (col)=>col.notNull().defaultTo('interval')).addColumn('policy', t.json).addColumn('last_outcome', t.text).addColumn('consecutive_count', 'integer', (col)=>col.notNull().defaultTo(0))// ms-epoch as bigint (not the `timestamp` column type), matching the
44
45
  // instance time columns and compared against an explicit numeric `now`.
45
46
  .addColumn('next_run', t.bigint, (col)=>col.notNull())// 0/1: no portable boolean column type across the adapters.
46
47
  .addColumn('enabled', 'integer', (col)=>col.notNull().defaultTo(1)).addColumn('created_at', t.bigint, (col)=>col.notNull()).addColumn('updated_at', t.bigint, (col)=>col.notNull()).execute();
package/lib/tables.d.ts CHANGED
@@ -24,6 +24,10 @@ export type WorkflowScheduleTable = {
24
24
  params: unknown;
25
25
  cron: string | null;
26
26
  every: string | null;
27
+ kind: string;
28
+ policy: unknown | null;
29
+ last_outcome: string | null;
30
+ consecutive_count: number;
27
31
  next_run: number;
28
32
  enabled: number;
29
33
  created_at: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/store-workflow",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,14 +15,14 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "kysely": "^0.29.5",
19
- "@kubun/db": "^0.13.0",
20
- "@kubun/db-adapter": "^0.13.0"
18
+ "@kubun/db": "^0.14.0",
19
+ "@kubun/db-adapter": "^0.14.1",
20
+ "kysely": "^0.29.5"
21
21
  },
22
22
  "devDependencies": {
23
- "@testcontainers/postgresql": "^12.1.0",
24
- "@kubun/db-better-sqlite": "^0.13.0",
25
- "@kubun/db-postgres": "^0.13.0"
23
+ "@kubun/db-better-sqlite": "^0.14.0",
24
+ "@kubun/db-postgres": "^0.14.0",
25
+ "@testcontainers/postgresql": "^12.1.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"