@kubun/plugin-workflow 0.16.0 → 0.17.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/control-plane.js +12 -3
- package/lib/engine.d.ts +5 -1
- package/lib/engine.js +120 -10
- package/lib/index.d.ts +22 -2
- package/lib/index.js +7 -2
- package/lib/instance-cleanup.d.ts +24 -0
- package/lib/instance-cleanup.js +28 -0
- package/package.json +13 -13
package/lib/control-plane.js
CHANGED
|
@@ -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)=>
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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;
|
|
@@ -27,7 +29,9 @@ const INTERVAL_UNITS = {
|
|
|
27
29
|
export function parseEvery(spec) {
|
|
28
30
|
const match = /^(\d+)(ms|s|m|h)?$/.exec(spec.trim());
|
|
29
31
|
if (match == null) throw new Error(`Invalid interval "${spec}"`);
|
|
30
|
-
|
|
32
|
+
const unit = match[2];
|
|
33
|
+
const multiplier = unit != null ? INTERVAL_UNITS[unit] ?? 1 : 1;
|
|
34
|
+
return Number(match[1]) * multiplier;
|
|
31
35
|
}
|
|
32
36
|
const TERMINAL = new Set([
|
|
33
37
|
'completed',
|
|
@@ -60,7 +64,8 @@ function toStatus(row) {
|
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
// The active (pending/running) instance for (name, singletonKey) if one exists,
|
|
63
|
-
// else the most recent terminal by updated_at
|
|
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
|
|
64
69
|
// with singleton_key = subject, so the subject keys the lookup.
|
|
65
70
|
async function currentInstanceRow(s, name, singletonKey) {
|
|
66
71
|
const rows = await s.list({
|
|
@@ -68,7 +73,7 @@ async function currentInstanceRow(s, name, singletonKey) {
|
|
|
68
73
|
});
|
|
69
74
|
const forSubject = rows.filter((r)=>r.singleton_key === singletonKey);
|
|
70
75
|
const active = forSubject.find((r)=>r.status === 'pending' || r.status === 'running');
|
|
71
|
-
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);
|
|
72
77
|
}
|
|
73
78
|
// Project an adaptive schedule row as a PeriodicSync. `currentStatus` is the
|
|
74
79
|
// schedule subject's current instance (see `currentInstanceRow`), null if never
|
|
@@ -95,7 +100,7 @@ export function createWorkflowEngine(deps) {
|
|
|
95
100
|
const registry = new Map();
|
|
96
101
|
// A per-process identity for lease ownership. A fresh engine over the same DB
|
|
97
102
|
// (a "reboot") gets a new one, which is what makes crash-reclaim observable.
|
|
98
|
-
const bootID =
|
|
103
|
+
const bootID = deps.getRandomID();
|
|
99
104
|
// Instances currently being driven in THIS process, so `cancel` can signal the
|
|
100
105
|
// running flow via its AbortController.
|
|
101
106
|
const running = new Map();
|
|
@@ -278,6 +283,58 @@ export function createWorkflowEngine(deps) {
|
|
|
278
283
|
running.delete(instanceID);
|
|
279
284
|
}
|
|
280
285
|
}
|
|
286
|
+
// --- Finished-instance retention cleanup ---
|
|
287
|
+
const cleanup = deps.finishedInstanceCleanup ?? null;
|
|
288
|
+
const cleanupBatchSize = deps.cleanupBatchSize ?? FINISHED_INSTANCE_CLEANUP_BATCH_SIZE;
|
|
289
|
+
// Due immediately: the first scheduler pass after `ready` runs it.
|
|
290
|
+
let nextCleanupAt = Number.NEGATIVE_INFINITY;
|
|
291
|
+
let cleanupRun = null;
|
|
292
|
+
// One batch per statement until a short batch, or until dispose(). Errors are
|
|
293
|
+
// logged, never thrown: the next interval retries.
|
|
294
|
+
async function collectFinished(retentionMs) {
|
|
295
|
+
let total = 0;
|
|
296
|
+
try {
|
|
297
|
+
const s = await store();
|
|
298
|
+
const before = now() - retentionMs;
|
|
299
|
+
while(!disposed){
|
|
300
|
+
const removed = await s.purgeFinished({
|
|
301
|
+
before,
|
|
302
|
+
limit: cleanupBatchSize
|
|
303
|
+
});
|
|
304
|
+
total += removed;
|
|
305
|
+
if (removed < cleanupBatchSize) break;
|
|
306
|
+
}
|
|
307
|
+
if (total > 0) {
|
|
308
|
+
logger.debug('workflow cleanup removed {count} finished instances', {
|
|
309
|
+
count: total
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} catch (cause) {
|
|
313
|
+
logger.warn('workflow finished-instance cleanup failed', {
|
|
314
|
+
error: cause
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return total;
|
|
318
|
+
}
|
|
319
|
+
function cleanupFinished() {
|
|
320
|
+
if (cleanup == null || disposed) return Promise.resolve(0);
|
|
321
|
+
if (cleanupRun == null) {
|
|
322
|
+
const run = collectFinished(cleanup.retentionMs).finally(()=>{
|
|
323
|
+
cleanupRun = null;
|
|
324
|
+
});
|
|
325
|
+
cleanupRun = run;
|
|
326
|
+
// Tracked so dispose() drains a run in progress before the store closes.
|
|
327
|
+
track(run.then(()=>{}));
|
|
328
|
+
}
|
|
329
|
+
return cleanupRun;
|
|
330
|
+
}
|
|
331
|
+
// Called from each scheduler pass; starts a run once the interval is due. The
|
|
332
|
+
// pass does not wait for it, so a long cleanup never delays claims.
|
|
333
|
+
function maybeCleanup(at) {
|
|
334
|
+
if (cleanup == null || cleanupRun != null || at < nextCleanupAt) return;
|
|
335
|
+
nextCleanupAt = at + cleanup.intervalMs;
|
|
336
|
+
void cleanupFinished();
|
|
337
|
+
}
|
|
281
338
|
// Per-lane concurrency. Defaults to 1; `defineQueue` raises a lane's cap.
|
|
282
339
|
const concurrency = new Map();
|
|
283
340
|
function laneConcurrency(lane) {
|
|
@@ -377,6 +434,7 @@ export function createWorkflowEngine(deps) {
|
|
|
377
434
|
// in-flight when dispose() fired from claiming fresh work after shutdown began.
|
|
378
435
|
if (disposed) return;
|
|
379
436
|
const at = now();
|
|
437
|
+
maybeCleanup(at);
|
|
380
438
|
// Promote due schedules first so their instances are claimable this pass.
|
|
381
439
|
await promoteSchedules(s, at);
|
|
382
440
|
if (disposed) return;
|
|
@@ -468,7 +526,7 @@ export function createWorkflowEngine(deps) {
|
|
|
468
526
|
async schedule (name, params, when) {
|
|
469
527
|
const s = await store();
|
|
470
528
|
const everyMs = parseEvery(when.every);
|
|
471
|
-
const id =
|
|
529
|
+
const id = deps.getRandomID();
|
|
472
530
|
const at = now();
|
|
473
531
|
// First fire one interval out: a restart storm must not refire everything
|
|
474
532
|
// that was ever scheduled.
|
|
@@ -484,7 +542,8 @@ export function createWorkflowEngine(deps) {
|
|
|
484
542
|
const handle = {
|
|
485
543
|
id,
|
|
486
544
|
async disable () {
|
|
487
|
-
|
|
545
|
+
const s2 = await store();
|
|
546
|
+
await s2.setScheduleEnabled(id, false, now());
|
|
488
547
|
},
|
|
489
548
|
async enable () {
|
|
490
549
|
// Re-arm one interval out from re-enable so a long-disabled schedule
|
|
@@ -496,7 +555,8 @@ export function createWorkflowEngine(deps) {
|
|
|
496
555
|
},
|
|
497
556
|
async cancel () {
|
|
498
557
|
// No store delete today: permanent disable is the cancel semantics.
|
|
499
|
-
|
|
558
|
+
const s2 = await store();
|
|
559
|
+
await s2.setScheduleEnabled(id, false, now());
|
|
500
560
|
}
|
|
501
561
|
};
|
|
502
562
|
return handle;
|
|
@@ -523,7 +583,8 @@ export function createWorkflowEngine(deps) {
|
|
|
523
583
|
const handle = {
|
|
524
584
|
id,
|
|
525
585
|
async disable () {
|
|
526
|
-
|
|
586
|
+
const s2 = await store();
|
|
587
|
+
await s2.setScheduleEnabled(id, false, now());
|
|
527
588
|
},
|
|
528
589
|
async enable () {
|
|
529
590
|
// Re-arm eagerly (now + changed), mirroring reactivation above.
|
|
@@ -538,7 +599,8 @@ export function createWorkflowEngine(deps) {
|
|
|
538
599
|
await s2.setScheduleEnabled(id, true, reAt);
|
|
539
600
|
},
|
|
540
601
|
async cancel () {
|
|
541
|
-
|
|
602
|
+
const s2 = await store();
|
|
603
|
+
await s2.setScheduleEnabled(id, false, now());
|
|
542
604
|
}
|
|
543
605
|
};
|
|
544
606
|
return handle;
|
|
@@ -549,7 +611,7 @@ export function createWorkflowEngine(deps) {
|
|
|
549
611
|
throw new Error(`Workflow "${name}" is not registered`);
|
|
550
612
|
}
|
|
551
613
|
const s = await store();
|
|
552
|
-
const instanceID =
|
|
614
|
+
const instanceID = deps.getRandomID();
|
|
553
615
|
const createdAt = now();
|
|
554
616
|
const initialAction = params !== undefined ? {
|
|
555
617
|
name: def.initialAction.name,
|
|
@@ -583,6 +645,53 @@ export function createWorkflowEngine(deps) {
|
|
|
583
645
|
instanceID
|
|
584
646
|
};
|
|
585
647
|
},
|
|
648
|
+
async enqueueTx (stores, name, params, opts) {
|
|
649
|
+
const def = registry.get(name);
|
|
650
|
+
if (def == null) {
|
|
651
|
+
throw new Error(`Workflow "${name}" is not registered`);
|
|
652
|
+
}
|
|
653
|
+
// Resolve the store from the CALLER's request-scoped provider, not the
|
|
654
|
+
// top-level `store()` — when `stores` is a tx-scoped StoreProvider
|
|
655
|
+
// (e.g. from `db.withTransaction`), this binds the returned store to
|
|
656
|
+
// that already-open transaction.
|
|
657
|
+
const s = await getWorkflowStore(stores);
|
|
658
|
+
const instanceID = deps.getRandomID();
|
|
659
|
+
const createdAt = now();
|
|
660
|
+
const initialAction = params !== undefined ? {
|
|
661
|
+
name: def.initialAction.name,
|
|
662
|
+
params
|
|
663
|
+
} : def.initialAction;
|
|
664
|
+
const input = {
|
|
665
|
+
instanceID,
|
|
666
|
+
name,
|
|
667
|
+
queue: opts?.queue ?? def.queue ?? name,
|
|
668
|
+
idempotencyKey: opts?.idempotencyKey ?? null,
|
|
669
|
+
singletonKey: opts?.singletonKey ?? null,
|
|
670
|
+
priority: opts?.priority ?? 0,
|
|
671
|
+
state: {},
|
|
672
|
+
nextAction: initialAction,
|
|
673
|
+
runAfter: createdAt,
|
|
674
|
+
createdAt
|
|
675
|
+
};
|
|
676
|
+
// insertInstanceTx runs directly on the tx-bound store — no nested
|
|
677
|
+
// transaction — inheriting atomicity from the caller's own transaction.
|
|
678
|
+
const result = await s.insertInstanceTx(input);
|
|
679
|
+
// Idempotent or singleton hit: return the existing instance, drive nothing.
|
|
680
|
+
if (!result.created) {
|
|
681
|
+
return {
|
|
682
|
+
instanceID: result.row.instance_id
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
// Defer the wake until AFTER the caller's transaction commits: `stores`
|
|
686
|
+
// IS that transaction here, so an inline scheduleTick() (as `enqueue`
|
|
687
|
+
// does above) could wake the scheduler to claim a row whose insert has
|
|
688
|
+
// not committed yet. onCommit fires post-commit, or never if the
|
|
689
|
+
// transaction rolls back.
|
|
690
|
+
stores.onCommit(()=>scheduleTick());
|
|
691
|
+
return {
|
|
692
|
+
instanceID
|
|
693
|
+
};
|
|
694
|
+
},
|
|
586
695
|
async cancel (instanceID) {
|
|
587
696
|
const controller = running.get(instanceID);
|
|
588
697
|
if (controller != null) {
|
|
@@ -718,6 +827,7 @@ export function createWorkflowEngine(deps) {
|
|
|
718
827
|
track(p);
|
|
719
828
|
return p;
|
|
720
829
|
},
|
|
830
|
+
cleanupFinished,
|
|
721
831
|
async dispose () {
|
|
722
832
|
disposed = true;
|
|
723
833
|
if (interval != null) {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
|
|
2
|
-
|
|
3
|
-
export
|
|
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(
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,22 +15,22 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/db": "^0.
|
|
19
|
-
"@kubun/engine": "^0.
|
|
20
|
-
"@kubun/graphql": "^0.
|
|
21
|
-
"@kubun/id": "^0.
|
|
22
|
-
"@kubun/logger": "^0.
|
|
23
|
-
"@kubun/plugin-p2p-api": "^0.
|
|
24
|
-
"@kubun/plugin-workflow-api": "^0.
|
|
25
|
-
"@kubun/store-workflow": "^0.
|
|
18
|
+
"@kubun/db": "^0.17.0",
|
|
19
|
+
"@kubun/engine": "^0.17.2",
|
|
20
|
+
"@kubun/graphql": "^0.17.2",
|
|
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.1",
|
|
25
|
+
"@kubun/store-workflow": "^0.17.1",
|
|
26
26
|
"@sozai/event": "^0.1.3",
|
|
27
27
|
"@sozai/flow": "^0.3.1",
|
|
28
28
|
"@sozai/runtime": "^0.1.0",
|
|
29
29
|
"@sozai/schema": "^0.1.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@kubun/db-better-sqlite": "^0.
|
|
33
|
-
"@kubun/db-postgres": "^0.
|
|
32
|
+
"@kubun/db-better-sqlite": "^0.17.0",
|
|
33
|
+
"@kubun/db-postgres": "^0.17.1",
|
|
34
34
|
"@testcontainers/postgresql": "^12.1.0"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
41
41
|
"build:clean": "del lib",
|
|
42
|
-
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
43
|
-
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
42
|
+
"build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
43
|
+
"build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
44
44
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
45
45
|
"test": "pnpm run test:types && pnpm run test:unit",
|
|
46
46
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|