@kubun/plugin-workflow 0.17.0 → 0.18.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.
@@ -44,8 +44,17 @@ export function createWorkflowControlPlaneHelpers(getP2P) {
44
44
  discoverWorkflows: async (groupID, options)=>(await getP2P()).discoverWorkflows(groupID, options),
45
45
  listWorkflows: async (groupID, targetDID)=>(await getP2P()).listWorkflows(groupID, targetDID),
46
46
  workflowStatus: async (groupID, targetDID, instanceID)=>(await getP2P()).workflowStatus(groupID, targetDID, instanceID),
47
- enqueueWorkflow: async (groupID, targetDID, name, params, opts)=>unwrap(await (await getP2P()).enqueueWorkflow(groupID, targetDID, name, params, opts)),
48
- cancelWorkflow: async (groupID, targetDID, instanceID)=>unwrap(await (await getP2P()).cancelWorkflow(groupID, targetDID, instanceID)),
49
- retryWorkflow: async (groupID, targetDID, instanceID)=>unwrap(await (await getP2P()).retryWorkflow(groupID, targetDID, instanceID))
47
+ enqueueWorkflow: async (groupID, targetDID, name, params, opts)=>{
48
+ const p2p = await getP2P();
49
+ return unwrap(await p2p.enqueueWorkflow(groupID, targetDID, name, params, opts));
50
+ },
51
+ cancelWorkflow: async (groupID, targetDID, instanceID)=>{
52
+ const p2p = await getP2P();
53
+ return unwrap(await p2p.cancelWorkflow(groupID, targetDID, instanceID));
54
+ },
55
+ retryWorkflow: async (groupID, targetDID, instanceID)=>{
56
+ const p2p = await getP2P();
57
+ return unwrap(await p2p.retryWorkflow(groupID, targetDID, instanceID));
58
+ }
50
59
  };
51
60
  }
package/lib/engine.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Logger } from '@kubun/logger';
2
2
  import type { RetryPolicy, RetryRefusalReason, WorkflowAPI } from '@kubun/plugin-workflow-api';
3
3
  import type { WorkflowStoreAPI } from '@kubun/store-workflow';
4
4
  import type { GetRandomID } from '@sozai/runtime';
5
+ import { type ResolvedFinishedInstanceCleanup } from './instance-cleanup.js';
5
6
  export declare function backoff(attempts: number, policy?: RetryPolicy): number;
6
7
  export declare function parseEvery(spec: string): number;
7
8
  export declare class WorkflowRetryError extends Error {
@@ -15,15 +16,18 @@ export type WorkflowEngineDeps = {
15
16
  getStore: () => Promise<WorkflowStoreAPI>;
16
17
  logger: Logger;
17
18
  now?: () => number;
18
- getRandomID?: GetRandomID;
19
+ getRandomID: GetRandomID;
19
20
  tickMs?: number;
20
21
  disposeTimeoutMs?: number;
21
22
  ready?: () => Promise<void>;
23
+ finishedInstanceCleanup?: ResolvedFinishedInstanceCleanup | null;
24
+ cleanupBatchSize?: number;
22
25
  };
23
26
  export type WorkflowEngine = {
24
27
  api: WorkflowAPI;
25
28
  recover(): Promise<Array<string>>;
26
29
  tick(): Promise<void>;
30
+ cleanupFinished(): Promise<number>;
27
31
  dispose(): Promise<void>;
28
32
  };
29
33
  export declare function createWorkflowEngine(deps: WorkflowEngineDeps): WorkflowEngine;
package/lib/engine.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { getWorkflowStore } from '@kubun/store-workflow';
1
2
  import { EventEmitter } from '@sozai/event';
2
3
  import { createFlow } from '@sozai/flow';
3
4
  import { computeNextDelay, validateAdaptivePolicy } from './adaptive.js';
5
+ import { FINISHED_INSTANCE_CLEANUP_BATCH_SIZE } from './instance-cleanup.js';
4
6
  // One runner may hold an instance's lease for this long before another boot may
5
7
  // reclaim it as stranded.
6
8
  const LEASE_MS = 30_000;
@@ -62,7 +64,8 @@ function toStatus(row) {
62
64
  };
63
65
  }
64
66
  // The active (pending/running) instance for (name, singletonKey) if one exists,
65
- // else the most recent terminal by updated_at, else null. Instances are promoted
67
+ // else the most recent terminal by updated_at (a tie goes to the highest
68
+ // instance_id, as the finished-instance cleanup keeps), else null. Instances are promoted
66
69
  // with singleton_key = subject, so the subject keys the lookup.
67
70
  async function currentInstanceRow(s, name, singletonKey) {
68
71
  const rows = await s.list({
@@ -70,7 +73,7 @@ async function currentInstanceRow(s, name, singletonKey) {
70
73
  });
71
74
  const forSubject = rows.filter((r)=>r.singleton_key === singletonKey);
72
75
  const active = forSubject.find((r)=>r.status === 'pending' || r.status === 'running');
73
- return active ?? forSubject.reduce((best, r)=>best == null || r.updated_at > best.updated_at ? r : best, null);
76
+ return active ?? forSubject.reduce((best, r)=>best == null || r.updated_at > best.updated_at || r.updated_at === best.updated_at && r.instance_id > best.instance_id ? r : best, null);
74
77
  }
75
78
  // Project an adaptive schedule row as a PeriodicSync. `currentStatus` is the
76
79
  // schedule subject's current instance (see `currentInstanceRow`), null if never
@@ -97,7 +100,7 @@ export function createWorkflowEngine(deps) {
97
100
  const registry = new Map();
98
101
  // A per-process identity for lease ownership. A fresh engine over the same DB
99
102
  // (a "reboot") gets a new one, which is what makes crash-reclaim observable.
100
- const bootID = (deps.getRandomID ?? (()=>crypto.randomUUID()))();
103
+ const bootID = deps.getRandomID();
101
104
  // Instances currently being driven in THIS process, so `cancel` can signal the
102
105
  // running flow via its AbortController.
103
106
  const running = new Map();
@@ -183,9 +186,12 @@ export function createWorkflowEngine(deps) {
183
186
  handlers: def.handlers,
184
187
  stateValidator: def.stateValidator
185
188
  });
189
+ const initialAction = row.next_action ?? def.initialAction;
190
+ let currentAction = initialAction;
191
+ let stepInputState = row.state;
186
192
  const flow = generate({
187
- state: row.state,
188
- action: row.next_action ?? def.initialAction,
193
+ state: stepInputState,
194
+ action: initialAction,
189
195
  signal: controller.signal
190
196
  });
191
197
  events.fire('started', {
@@ -207,11 +213,14 @@ export function createWorkflowEngine(deps) {
207
213
  const held = await s.renew(instanceID, bootID, LEASE_MS, now());
208
214
  if (!held) return;
209
215
  const nextAction = value.status === 'action' ? value.action : null;
216
+ const checkpointState = flow.getState();
210
217
  await s.checkpoint(instanceID, {
211
- state: flow.getState(),
218
+ state: checkpointState,
212
219
  nextAction,
213
220
  status: 'running'
214
221
  }, now());
222
+ stepInputState = checkpointState;
223
+ currentAction = nextAction;
215
224
  events.fire('checkpoint', {
216
225
  instanceID,
217
226
  name: def.name
@@ -241,7 +250,7 @@ export function createWorkflowEngine(deps) {
241
250
  fireTerminal(def, instanceID, 'cancelled');
242
251
  return;
243
252
  }
244
- // status === 'error'. `getState()` here is the PRE-handler input state.
253
+ // status === 'error'. Persist the failing step with the state it received.
245
254
  const error = String(value.error);
246
255
  const attempts = row.attempts + 1;
247
256
  const maxAttempts = def.retry?.maxAttempts ?? 1;
@@ -249,8 +258,8 @@ export function createWorkflowEngine(deps) {
249
258
  // Retry rides the queue: re-queue `pending` with a future run gate; the
250
259
  // scheduler re-claims it when due. Not terminal, so no terminal event.
251
260
  await s.fail(instanceID, {
252
- state: flow.getState(),
253
- nextAction: row.next_action,
261
+ state: stepInputState,
262
+ nextAction: currentAction,
254
263
  error,
255
264
  attempts,
256
265
  runAfter: now() + backoff(attempts, def.retry),
@@ -259,8 +268,8 @@ export function createWorkflowEngine(deps) {
259
268
  return;
260
269
  }
261
270
  const wrote = await s.fail(instanceID, {
262
- state: flow.getState(),
263
- nextAction: row.next_action,
271
+ state: stepInputState,
272
+ nextAction: currentAction,
264
273
  error,
265
274
  attempts,
266
275
  runAfter: now(),
@@ -280,6 +289,58 @@ export function createWorkflowEngine(deps) {
280
289
  running.delete(instanceID);
281
290
  }
282
291
  }
292
+ // --- Finished-instance retention cleanup ---
293
+ const cleanup = deps.finishedInstanceCleanup ?? null;
294
+ const cleanupBatchSize = deps.cleanupBatchSize ?? FINISHED_INSTANCE_CLEANUP_BATCH_SIZE;
295
+ // Due immediately: the first scheduler pass after `ready` runs it.
296
+ let nextCleanupAt = Number.NEGATIVE_INFINITY;
297
+ let cleanupRun = null;
298
+ // One batch per statement until a short batch, or until dispose(). Errors are
299
+ // logged, never thrown: the next interval retries.
300
+ async function collectFinished(retentionMs) {
301
+ let total = 0;
302
+ try {
303
+ const s = await store();
304
+ const before = now() - retentionMs;
305
+ while(!disposed){
306
+ const removed = await s.purgeFinished({
307
+ before,
308
+ limit: cleanupBatchSize
309
+ });
310
+ total += removed;
311
+ if (removed < cleanupBatchSize) break;
312
+ }
313
+ if (total > 0) {
314
+ logger.debug('workflow cleanup removed {count} finished instances', {
315
+ count: total
316
+ });
317
+ }
318
+ } catch (cause) {
319
+ logger.warn('workflow finished-instance cleanup failed', {
320
+ error: cause
321
+ });
322
+ }
323
+ return total;
324
+ }
325
+ function cleanupFinished() {
326
+ if (cleanup == null || disposed) return Promise.resolve(0);
327
+ if (cleanupRun == null) {
328
+ const run = collectFinished(cleanup.retentionMs).finally(()=>{
329
+ cleanupRun = null;
330
+ });
331
+ cleanupRun = run;
332
+ // Tracked so dispose() drains a run in progress before the store closes.
333
+ track(run.then(()=>{}));
334
+ }
335
+ return cleanupRun;
336
+ }
337
+ // Called from each scheduler pass; starts a run once the interval is due. The
338
+ // pass does not wait for it, so a long cleanup never delays claims.
339
+ function maybeCleanup(at) {
340
+ if (cleanup == null || cleanupRun != null || at < nextCleanupAt) return;
341
+ nextCleanupAt = at + cleanup.intervalMs;
342
+ void cleanupFinished();
343
+ }
283
344
  // Per-lane concurrency. Defaults to 1; `defineQueue` raises a lane's cap.
284
345
  const concurrency = new Map();
285
346
  function laneConcurrency(lane) {
@@ -379,6 +440,7 @@ export function createWorkflowEngine(deps) {
379
440
  // in-flight when dispose() fired from claiming fresh work after shutdown began.
380
441
  if (disposed) return;
381
442
  const at = now();
443
+ maybeCleanup(at);
382
444
  // Promote due schedules first so their instances are claimable this pass.
383
445
  await promoteSchedules(s, at);
384
446
  if (disposed) return;
@@ -470,7 +532,7 @@ export function createWorkflowEngine(deps) {
470
532
  async schedule (name, params, when) {
471
533
  const s = await store();
472
534
  const everyMs = parseEvery(when.every);
473
- const id = crypto.randomUUID();
535
+ const id = deps.getRandomID();
474
536
  const at = now();
475
537
  // First fire one interval out: a restart storm must not refire everything
476
538
  // that was ever scheduled.
@@ -486,7 +548,8 @@ export function createWorkflowEngine(deps) {
486
548
  const handle = {
487
549
  id,
488
550
  async disable () {
489
- await (await store()).setScheduleEnabled(id, false, now());
551
+ const s2 = await store();
552
+ await s2.setScheduleEnabled(id, false, now());
490
553
  },
491
554
  async enable () {
492
555
  // Re-arm one interval out from re-enable so a long-disabled schedule
@@ -498,7 +561,8 @@ export function createWorkflowEngine(deps) {
498
561
  },
499
562
  async cancel () {
500
563
  // No store delete today: permanent disable is the cancel semantics.
501
- await (await store()).setScheduleEnabled(id, false, now());
564
+ const s2 = await store();
565
+ await s2.setScheduleEnabled(id, false, now());
502
566
  }
503
567
  };
504
568
  return handle;
@@ -525,7 +589,8 @@ export function createWorkflowEngine(deps) {
525
589
  const handle = {
526
590
  id,
527
591
  async disable () {
528
- await (await store()).setScheduleEnabled(id, false, now());
592
+ const s2 = await store();
593
+ await s2.setScheduleEnabled(id, false, now());
529
594
  },
530
595
  async enable () {
531
596
  // Re-arm eagerly (now + changed), mirroring reactivation above.
@@ -540,7 +605,8 @@ export function createWorkflowEngine(deps) {
540
605
  await s2.setScheduleEnabled(id, true, reAt);
541
606
  },
542
607
  async cancel () {
543
- await (await store()).setScheduleEnabled(id, false, now());
608
+ const s2 = await store();
609
+ await s2.setScheduleEnabled(id, false, now());
544
610
  }
545
611
  };
546
612
  return handle;
@@ -551,7 +617,7 @@ export function createWorkflowEngine(deps) {
551
617
  throw new Error(`Workflow "${name}" is not registered`);
552
618
  }
553
619
  const s = await store();
554
- const instanceID = crypto.randomUUID();
620
+ const instanceID = deps.getRandomID();
555
621
  const createdAt = now();
556
622
  const initialAction = params !== undefined ? {
557
623
  name: def.initialAction.name,
@@ -585,6 +651,53 @@ export function createWorkflowEngine(deps) {
585
651
  instanceID
586
652
  };
587
653
  },
654
+ async enqueueTx (stores, name, params, opts) {
655
+ const def = registry.get(name);
656
+ if (def == null) {
657
+ throw new Error(`Workflow "${name}" is not registered`);
658
+ }
659
+ // Resolve the store from the CALLER's request-scoped provider, not the
660
+ // top-level `store()` — when `stores` is a tx-scoped StoreProvider
661
+ // (e.g. from `db.withTransaction`), this binds the returned store to
662
+ // that already-open transaction.
663
+ const s = await getWorkflowStore(stores);
664
+ const instanceID = deps.getRandomID();
665
+ const createdAt = now();
666
+ const initialAction = params !== undefined ? {
667
+ name: def.initialAction.name,
668
+ params
669
+ } : def.initialAction;
670
+ const input = {
671
+ instanceID,
672
+ name,
673
+ queue: opts?.queue ?? def.queue ?? name,
674
+ idempotencyKey: opts?.idempotencyKey ?? null,
675
+ singletonKey: opts?.singletonKey ?? null,
676
+ priority: opts?.priority ?? 0,
677
+ state: {},
678
+ nextAction: initialAction,
679
+ runAfter: createdAt,
680
+ createdAt
681
+ };
682
+ // insertInstanceTx runs directly on the tx-bound store — no nested
683
+ // transaction — inheriting atomicity from the caller's own transaction.
684
+ const result = await s.insertInstanceTx(input);
685
+ // Idempotent or singleton hit: return the existing instance, drive nothing.
686
+ if (!result.created) {
687
+ return {
688
+ instanceID: result.row.instance_id
689
+ };
690
+ }
691
+ // Defer the wake until AFTER the caller's transaction commits: `stores`
692
+ // IS that transaction here, so an inline scheduleTick() (as `enqueue`
693
+ // does above) could wake the scheduler to claim a row whose insert has
694
+ // not committed yet. onCommit fires post-commit, or never if the
695
+ // transaction rolls back.
696
+ stores.onCommit(()=>scheduleTick());
697
+ return {
698
+ instanceID
699
+ };
700
+ },
588
701
  async cancel (instanceID) {
589
702
  const controller = running.get(instanceID);
590
703
  if (controller != null) {
@@ -720,6 +833,7 @@ export function createWorkflowEngine(deps) {
720
833
  track(p);
721
834
  return p;
722
835
  },
836
+ cleanupFinished,
723
837
  async dispose () {
724
838
  disposed = true;
725
839
  if (interval != null) {
package/lib/index.d.ts CHANGED
@@ -1,10 +1,30 @@
1
1
  import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
2
- export type WorkflowPluginOptions = Record<string, never>;
3
- export declare function createWorkflowPlugin(_options?: WorkflowPluginOptions): (params: PluginFactoryParams) => KubunPlugin;
2
+ import { type FinishedInstanceCleanupOptions } from './instance-cleanup.js';
3
+ export type WorkflowPluginOptions = {
4
+ /**
5
+ * Retention cleanup of finished workflow instances. Every run of a workflow,
6
+ * scheduled ones included, leaves an instance row; once it has been finished
7
+ * (completed, failed or cancelled) for `retentionMs`, the cleanup deletes it.
8
+ * Defaults: runs every 6 hours, keeps 30 days. Omit it for the defaults,
9
+ * pass an object to change either value, or `false` to keep every instance.
10
+ *
11
+ * It never deletes a pending or running instance, nor the newest instance of
12
+ * a periodic sync's subject, which is what its status and
13
+ * `getCurrentInstance` report. A deleted instance is unknown to `get`, cannot be retried,
14
+ * and no longer holds its idempotency key: an enqueue reusing that key
15
+ * creates a new instance.
16
+ *
17
+ * Each value must be a positive finite number, or the plugin throws when it
18
+ * is created.
19
+ */
20
+ finishedInstanceCleanup?: FinishedInstanceCleanupOptions;
21
+ };
22
+ export declare function createWorkflowPlugin(options?: WorkflowPluginOptions): (params: PluginFactoryParams) => KubunPlugin;
4
23
  export { computeNextDelay, validateAdaptivePolicy } from './adaptive.js';
5
24
  export { createWorkflowControlPlaneHelpers, WorkflowCommandRefusedError, type WorkflowControlPlaneHelpers, } from './control-plane.js';
6
25
  export { defineWorkflow, isRemotelyCommandable, isRemotelyObservable } from './definition.js';
7
26
  export type { WorkflowEngine, WorkflowEngineDeps } from './engine.js';
8
27
  export { createWorkflowEngine, parseEvery, WorkflowRetryError } from './engine.js';
28
+ export { DEFAULT_FINISHED_INSTANCE_CLEANUP_INTERVAL_MS, DEFAULT_FINISHED_INSTANCE_RETENTION_MS, FINISHED_INSTANCE_CLEANUP_BATCH_SIZE, type FinishedInstanceCleanupOptions, type ResolvedFinishedInstanceCleanup, resolveFinishedInstanceCleanup, } from './instance-cleanup.js';
9
29
  export type { WorkflowContext, WorkflowEventResult } from './schema.js';
10
30
  export { createWorkflowContextFactory, createWorkflowSchemaExtension, subscribeToWorkflowEvents, } from './schema.js';
package/lib/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { getWorkflowStore, workflowStoreDefinition } from '@kubun/store-workflow';
2
2
  import { createWorkflowControlPlaneHelpers } from './control-plane.js';
3
3
  import { createWorkflowEngine } from './engine.js';
4
+ import { resolveFinishedInstanceCleanup } from './instance-cleanup.js';
4
5
  import { createWorkflowContextFactory, createWorkflowSchemaExtension } from './schema.js';
5
- export function createWorkflowPlugin(_options = {}) {
6
+ export function createWorkflowPlugin(options = {}) {
7
+ // Validated here, so a bad value fails when the plugin is created.
8
+ const cleanupSettings = resolveFinishedInstanceCleanup(options.finishedInstanceCleanup);
6
9
  return (params)=>{
7
10
  params.db.register(workflowStoreDefinition);
8
11
  const logger = params.getLogger('workflow');
@@ -14,7 +17,8 @@ export function createWorkflowPlugin(_options = {}) {
14
17
  // the interval claim no work until `ready()` resolves, and an identity
15
18
  // mismatch leaves the engine inert rather than driving jobs on a database it
16
19
  // was rejected from.
17
- ready: ()=>params.engine.ready()
20
+ ready: ()=>params.engine.ready(),
21
+ finishedInstanceCleanup: cleanupSettings
18
22
  });
19
23
  // Re-drive stranded instances once on boot, but only after the host engine
20
24
  // verifies its database identity — a re-driven instance mints under the engine's
@@ -44,4 +48,5 @@ export { computeNextDelay, validateAdaptivePolicy } from './adaptive.js';
44
48
  export { createWorkflowControlPlaneHelpers, WorkflowCommandRefusedError } from './control-plane.js';
45
49
  export { defineWorkflow, isRemotelyCommandable, isRemotelyObservable } from './definition.js';
46
50
  export { createWorkflowEngine, parseEvery, WorkflowRetryError } from './engine.js';
51
+ export { DEFAULT_FINISHED_INSTANCE_CLEANUP_INTERVAL_MS, DEFAULT_FINISHED_INSTANCE_RETENTION_MS, FINISHED_INSTANCE_CLEANUP_BATCH_SIZE, resolveFinishedInstanceCleanup } from './instance-cleanup.js';
47
52
  export { createWorkflowContextFactory, createWorkflowSchemaExtension, subscribeToWorkflowEvents } from './schema.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Retention cleanup of finished workflow instances (completed, failed or
3
+ * cancelled). `false` turns it off; an object (or the option left out) turns
4
+ * it on, each value defaulting when absent.
5
+ */
6
+ export type FinishedInstanceCleanupOptions = false | {
7
+ /** How often the cleanup runs, in milliseconds. Default 6 hours. */
8
+ intervalMs?: number;
9
+ /** How long a finished instance is kept, in milliseconds. Default 30 days. */
10
+ retentionMs?: number;
11
+ };
12
+ export declare const DEFAULT_FINISHED_INSTANCE_CLEANUP_INTERVAL_MS: number;
13
+ export declare const DEFAULT_FINISHED_INSTANCE_RETENTION_MS: number;
14
+ /** The most rows one cleanup statement deletes. */
15
+ export declare const FINISHED_INSTANCE_CLEANUP_BATCH_SIZE = 500;
16
+ export type ResolvedFinishedInstanceCleanup = {
17
+ intervalMs: number;
18
+ retentionMs: number;
19
+ };
20
+ /**
21
+ * The cleanup settings `options` describe, or null when cleanup is off.
22
+ * Throws on a value that is not a positive whole number of milliseconds.
23
+ */
24
+ export declare function resolveFinishedInstanceCleanup(options: FinishedInstanceCleanupOptions | undefined): ResolvedFinishedInstanceCleanup | null;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Retention cleanup of finished workflow instances (completed, failed or
3
+ * cancelled). `false` turns it off; an object (or the option left out) turns
4
+ * it on, each value defaulting when absent.
5
+ */ export const DEFAULT_FINISHED_INSTANCE_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
6
+ export const DEFAULT_FINISHED_INSTANCE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
7
+ /** The most rows one cleanup statement deletes. */ export const FINISHED_INSTANCE_CLEANUP_BATCH_SIZE = 500;
8
+ function assertDuration(value, label) {
9
+ // Whole milliseconds only: the cutoff is compared with integer timestamp
10
+ // columns, which Postgres will not compare with a fractional parameter.
11
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
12
+ throw new Error(`finishedInstanceCleanup.${label} must be a positive whole number of milliseconds`);
13
+ }
14
+ return value;
15
+ }
16
+ /**
17
+ * The cleanup settings `options` describe, or null when cleanup is off.
18
+ * Throws on a value that is not a positive whole number of milliseconds.
19
+ */ export function resolveFinishedInstanceCleanup(options) {
20
+ if (options === false) return null;
21
+ if (options != null && (typeof options !== 'object' || Array.isArray(options))) {
22
+ throw new Error('finishedInstanceCleanup must be false or an object');
23
+ }
24
+ return {
25
+ intervalMs: assertDuration(options?.intervalMs ?? DEFAULT_FINISHED_INSTANCE_CLEANUP_INTERVAL_MS, 'intervalMs'),
26
+ retentionMs: assertDuration(options?.retentionMs ?? DEFAULT_FINISHED_INSTANCE_RETENTION_MS, 'retentionMs')
27
+ };
28
+ }
package/lib/schema.js CHANGED
@@ -143,6 +143,9 @@ export function createWorkflowContextFactory(api) {
143
143
  // connector exposes numeric time (`ConnectorSyncEvent.duration`). `Int` would
144
144
  // overflow GraphQL's 32-bit range for a millisecond epoch. `state`/`next_action`
145
145
  // have no field here — the projection cannot leak them.
146
+ // Retry rejects missing, active, or non-resumable instances and instances whose
147
+ // singleton key is already held. Cancellation only requests a transition, so
148
+ // the returned status may still be non-terminal.
146
149
  const SDL = `
147
150
  type WorkflowInstanceStatus {
148
151
  instanceID: ID!
@@ -183,12 +186,7 @@ extend type Query {
183
186
  extend type Mutation {
184
187
  setPeriodicSyncEnabled(id: ID!, enabled: Boolean!): PeriodicSync!
185
188
  setPeriodicSyncPolicy(id: ID!, policy: JSON!): PeriodicSync!
186
- # Operator re-arm of a failed instance. Errors (not a null result) when the
187
- # instance is missing, not failed, has no resumable action, or a live
188
- # singleton already holds its key.
189
189
  retryWorkflow(instanceID: ID!): WorkflowInstanceStatus!
190
- # Operator-initiated cancel. Async: cancel() only requests cancellation, so the
191
- # returned status is a re-fetch right after and may not yet be terminal.
192
190
  cancelWorkflow(instanceID: ID!): WorkflowInstanceStatus!
193
191
  }
194
192
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/plugin-workflow",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,23 +15,23 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "@kubun/db": "^0.17.0",
19
- "@kubun/engine": "^0.17.0",
20
- "@kubun/graphql": "^0.17.0",
21
- "@kubun/id": "^0.17.0",
22
- "@kubun/logger": "^0.17.0",
23
- "@kubun/plugin-p2p-api": "^0.17.0",
24
- "@kubun/plugin-workflow-api": "^0.17.0",
25
- "@kubun/store-workflow": "^0.17.0",
26
- "@sozai/event": "^0.1.3",
18
+ "@kubun/db": "^0.18.0",
19
+ "@kubun/engine": "^0.18.0",
20
+ "@kubun/graphql": "^0.18.0",
21
+ "@kubun/id": "^0.18.0",
22
+ "@kubun/logger": "^0.18.0",
23
+ "@kubun/plugin-p2p-api": "^0.18.0",
24
+ "@kubun/plugin-workflow-api": "^0.18.0",
25
+ "@kubun/store-workflow": "^0.18.0",
26
+ "@sozai/event": "^0.1.6",
27
27
  "@sozai/flow": "^0.3.1",
28
28
  "@sozai/runtime": "^0.1.0",
29
- "@sozai/schema": "^0.1.2"
29
+ "@sozai/schema": "^0.1.4"
30
30
  },
31
31
  "devDependencies": {
32
- "@kubun/db-better-sqlite": "^0.17.0",
33
- "@kubun/db-postgres": "^0.17.0",
34
- "@testcontainers/postgresql": "^12.1.0"
32
+ "@kubun/db-better-sqlite": "^0.18.0",
33
+ "@kubun/db-postgres": "^0.18.0",
34
+ "@testcontainers/postgresql": "^12.2.1"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"