@getstrata/cli 2.1.0 → 2.2.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.
- package/CHANGELOG.md +17 -0
- package/package.json +3 -3
- package/src/queueWorker.ts +82 -26
- package/src/schedule.ts +77 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @getstrata/cli changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
Consistent model scopes.
|
|
6
|
+
Changed-field model writes.
|
|
7
|
+
Awaited provider startup.
|
|
8
|
+
Shared cache correctness.
|
|
9
|
+
Native HTTP controls and body limits.
|
|
10
|
+
Graceful lifecycle.
|
|
11
|
+
Validated async discovery.
|
|
12
|
+
Stronger model input types.
|
|
13
|
+
OpenTelemetry tracing.
|
|
14
|
+
Distributed scheduler ownership.
|
|
15
|
+
App-owned quota policy.
|
|
16
|
+
Bounded memory throttling.
|
|
17
|
+
|
|
3
18
|
## 2.1.0
|
|
4
19
|
|
|
5
20
|
- refactor(starter): reuse shared config and base repositories - fix(database): await password casts before model writes - fix(database): preserve concrete model types in query results
|
|
@@ -48,6 +63,8 @@
|
|
|
48
63
|
|
|
49
64
|
## Unreleased
|
|
50
65
|
|
|
66
|
+
- Await queue/scheduler application startup and close resources after failed startup or completed work; preserve original failures when cleanup also fails.
|
|
67
|
+
|
|
51
68
|
- 2.0: migrations no longer implicitly call an app seed export. Seeding requires --seed; fresh accepts the same explicit flag. Missing seed exports are rejected before applying migrations or resetting data.
|
|
52
69
|
|
|
53
70
|
## 1.1.9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Strata CLI: framework commands for Bun apps (dev, start, migrate, run)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
],
|
|
47
47
|
"scripts": {},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@getstrata/bootstrap": "^2.
|
|
50
|
-
"@getstrata/core": "^2.
|
|
49
|
+
"@getstrata/bootstrap": "^2.2.0",
|
|
50
|
+
"@getstrata/core": "^2.2.0"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
package/src/queueWorker.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
installGracefulShutdownSignals,
|
|
3
|
-
|
|
3
|
+
LifecycleCoordinator,
|
|
4
4
|
} from "@getstrata/core/lifecycle/gracefulShutdown";
|
|
5
5
|
import { createFailedJobService, createQueueWorker } from "@getstrata/core/queue/createAppQueue";
|
|
6
6
|
|
|
@@ -10,35 +10,91 @@ type QueueWorkerClose = () => unknown | Promise<unknown>;
|
|
|
10
10
|
async function runQueueWorkerCommand(options: {
|
|
11
11
|
boot: QueueWorkerBoot;
|
|
12
12
|
close?: QueueWorkerClose;
|
|
13
|
+
flush?: QueueWorkerClose;
|
|
14
|
+
drain?: QueueWorkerClose;
|
|
15
|
+
lifecycle?: LifecycleCoordinator;
|
|
13
16
|
failedJobs?: ReturnType<typeof createFailedJobService>;
|
|
14
17
|
}): Promise<void> {
|
|
15
18
|
const redisUrl = process.env.REDIS_URL;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
if (!redisUrl) throw new Error("queue:work requires REDIS_URL to be set.");
|
|
20
|
+
const lifecycle = options.lifecycle ?? new LifecycleCoordinator();
|
|
21
|
+
if (lifecycle.isShuttingDown) throw new Error("Cannot start a worker during shutdown.");
|
|
22
|
+
let worker: ReturnType<typeof createQueueWorker> | undefined;
|
|
23
|
+
let running: Promise<void> | undefined;
|
|
24
|
+
let booting: Promise<unknown> | undefined;
|
|
25
|
+
let stopping = false;
|
|
26
|
+
const name = `queue:${crypto.randomUUID()}`;
|
|
27
|
+
lifecycle.register(
|
|
28
|
+
`${name}:stop`,
|
|
29
|
+
() => {
|
|
30
|
+
stopping = true;
|
|
31
|
+
worker?.requestStop();
|
|
32
|
+
},
|
|
33
|
+
"stop",
|
|
34
|
+
);
|
|
35
|
+
lifecycle.register(
|
|
36
|
+
`${name}:drain`,
|
|
37
|
+
async () => {
|
|
38
|
+
// Completion, including failed startup/run, means no admitted worker remains.
|
|
39
|
+
await booting?.catch(() => {});
|
|
40
|
+
await running?.catch(() => {});
|
|
41
|
+
await options.drain?.();
|
|
42
|
+
},
|
|
43
|
+
"drain",
|
|
44
|
+
);
|
|
45
|
+
lifecycle.register(
|
|
46
|
+
`${name}:flush`,
|
|
47
|
+
async () => {
|
|
48
|
+
await options.flush?.();
|
|
49
|
+
},
|
|
50
|
+
"flush",
|
|
51
|
+
);
|
|
52
|
+
lifecycle.register(
|
|
53
|
+
`${name}:close`,
|
|
54
|
+
async () => {
|
|
55
|
+
try {
|
|
56
|
+
await worker?.close?.();
|
|
57
|
+
} finally {
|
|
58
|
+
await options.close?.();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"close",
|
|
62
|
+
);
|
|
63
|
+
lifecycle.register(
|
|
64
|
+
`${name}:force`,
|
|
65
|
+
() => {
|
|
66
|
+
worker?.requestStop();
|
|
67
|
+
worker?.close?.();
|
|
68
|
+
},
|
|
69
|
+
"force",
|
|
70
|
+
);
|
|
71
|
+
const uninstall = installGracefulShutdownSignals(undefined, lifecycle);
|
|
72
|
+
let failure: unknown;
|
|
73
|
+
let failed = false;
|
|
74
|
+
try {
|
|
75
|
+
booting = (async () => {
|
|
76
|
+
await options.boot();
|
|
77
|
+
})();
|
|
78
|
+
await booting;
|
|
79
|
+
if (!stopping && !lifecycle.isShuttingDown) {
|
|
80
|
+
worker = createQueueWorker(redisUrl, options.failedJobs ?? createFailedJobService());
|
|
81
|
+
console.log("[queue:work] Listening for jobs on Redis...");
|
|
82
|
+
running = worker.run();
|
|
83
|
+
await running;
|
|
84
|
+
console.log("[queue:work] Worker stopped.");
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
failed = true;
|
|
88
|
+
failure = error;
|
|
19
89
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
registerShutdownHandler("queue-worker", async () => {
|
|
29
|
-
worker.requestStop();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (options.close) {
|
|
33
|
-
registerShutdownHandler("database", async () => {
|
|
34
|
-
await options.close?.();
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
installGracefulShutdownSignals();
|
|
39
|
-
|
|
40
|
-
await worker.run();
|
|
41
|
-
console.log("[queue:work] Worker stopped.");
|
|
90
|
+
const result = await lifecycle.shutdown("WORKER_FINISHED");
|
|
91
|
+
uninstall();
|
|
92
|
+
if (!result.successful)
|
|
93
|
+
throw new AggregateError(
|
|
94
|
+
[...(failed ? [failure] : []), ...result.errors.map((entry) => entry.error)],
|
|
95
|
+
"Queue startup/runtime or shutdown failed.",
|
|
96
|
+
);
|
|
97
|
+
if (failed) throw failure;
|
|
42
98
|
}
|
|
43
99
|
|
|
44
100
|
export type { QueueWorkerBoot, QueueWorkerClose };
|
package/src/schedule.ts
CHANGED
|
@@ -1,25 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
installGracefulShutdownSignals,
|
|
3
|
+
LifecycleCoordinator,
|
|
4
|
+
} from "@getstrata/core/lifecycle/gracefulShutdown";
|
|
1
5
|
import { appSchedule, runDueScheduledTasks } from "@getstrata/core/scheduler/schedule";
|
|
2
6
|
|
|
3
7
|
type LoadAppSchedule = () => void | Promise<void>;
|
|
8
|
+
type CloseScheduledApp = () => void | Promise<void>;
|
|
4
9
|
|
|
5
|
-
function createScheduleRunCommand(
|
|
10
|
+
function createScheduleRunCommand(
|
|
11
|
+
loadSchedule: LoadAppSchedule,
|
|
12
|
+
close?: CloseScheduledApp,
|
|
13
|
+
options: {
|
|
14
|
+
lifecycle?: LifecycleCoordinator;
|
|
15
|
+
flush?: CloseScheduledApp;
|
|
16
|
+
drain?: CloseScheduledApp;
|
|
17
|
+
} = {},
|
|
18
|
+
) {
|
|
6
19
|
return async function scheduleRunCommand(): Promise<void> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
const lifecycle = options.lifecycle ?? new LifecycleCoordinator();
|
|
21
|
+
if (lifecycle.isShuttingDown) throw new Error("Cannot start scheduling during shutdown.");
|
|
22
|
+
const admission = new AbortController();
|
|
23
|
+
let stopping = false;
|
|
24
|
+
let execution: Promise<void> | undefined;
|
|
25
|
+
const name = `scheduler:${crypto.randomUUID()}`;
|
|
26
|
+
lifecycle.register(
|
|
27
|
+
`${name}:stop`,
|
|
28
|
+
() => {
|
|
29
|
+
stopping = true;
|
|
30
|
+
admission.abort();
|
|
31
|
+
},
|
|
32
|
+
"stop",
|
|
33
|
+
);
|
|
34
|
+
lifecycle.register(
|
|
35
|
+
`${name}:drain`,
|
|
36
|
+
async () => {
|
|
37
|
+
await execution?.catch(() => {});
|
|
38
|
+
await options.drain?.();
|
|
39
|
+
},
|
|
40
|
+
"drain",
|
|
41
|
+
);
|
|
42
|
+
lifecycle.register(
|
|
43
|
+
`${name}:flush`,
|
|
44
|
+
async () => {
|
|
45
|
+
await options.flush?.();
|
|
46
|
+
},
|
|
47
|
+
"flush",
|
|
48
|
+
);
|
|
49
|
+
lifecycle.register(
|
|
50
|
+
`${name}:close`,
|
|
51
|
+
async () => {
|
|
52
|
+
await close?.();
|
|
53
|
+
},
|
|
54
|
+
"close",
|
|
55
|
+
);
|
|
56
|
+
const uninstall = installGracefulShutdownSignals(undefined, lifecycle);
|
|
57
|
+
let failed = false;
|
|
58
|
+
let failure: unknown;
|
|
59
|
+
try {
|
|
60
|
+
execution = (async () => {
|
|
61
|
+
await loadSchedule();
|
|
62
|
+
if (stopping) return;
|
|
63
|
+
const due = appSchedule.dueTasks();
|
|
64
|
+
if (due.length === 0) console.log("No scheduled tasks due.");
|
|
65
|
+
else {
|
|
66
|
+
for (const task of due) console.log(`Running scheduled task: ${task.name}`);
|
|
67
|
+
await runDueScheduledTasks(appSchedule, new Date(), { signal: admission.signal });
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
await execution;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
failed = true;
|
|
73
|
+
failure = error;
|
|
14
74
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
75
|
+
const result = await lifecycle.shutdown("SCHEDULE_FINISHED");
|
|
76
|
+
uninstall();
|
|
77
|
+
if (!result.successful)
|
|
78
|
+
throw new AggregateError(
|
|
79
|
+
[...(failed ? [failure] : []), ...result.errors.map((entry) => entry.error)],
|
|
80
|
+
"Scheduler startup/runtime or shutdown failed.",
|
|
81
|
+
);
|
|
82
|
+
if (failed) throw failure;
|
|
21
83
|
};
|
|
22
84
|
}
|
|
23
85
|
|
|
24
|
-
export type { LoadAppSchedule };
|
|
86
|
+
export type { CloseScheduledApp, LoadAppSchedule };
|
|
25
87
|
export { createScheduleRunCommand };
|