@c9up/ream 0.2.8 → 0.2.9
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/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +51 -3
- package/dist/Ignitor.js.map +1 -1
- package/dist/commands/Migrate.d.ts +9 -0
- package/dist/commands/Migrate.d.ts.map +1 -0
- package/dist/commands/Migrate.js +16 -0
- package/dist/commands/Migrate.js.map +1 -0
- package/dist/commands/MigrateBase.d.ts +27 -0
- package/dist/commands/MigrateBase.d.ts.map +1 -0
- package/dist/commands/MigrateBase.js +77 -0
- package/dist/commands/MigrateBase.js.map +1 -0
- package/dist/commands/MigrateRollback.d.ts +9 -0
- package/dist/commands/MigrateRollback.d.ts.map +1 -0
- package/dist/commands/MigrateRollback.js +16 -0
- package/dist/commands/MigrateRollback.js.map +1 -0
- package/dist/commands/MigrateStatus.d.ts +9 -0
- package/dist/commands/MigrateStatus.d.ts.map +1 -0
- package/dist/commands/MigrateStatus.js +19 -0
- package/dist/commands/MigrateStatus.js.map +1 -0
- package/dist/commands/ScheduleList.d.ts +18 -0
- package/dist/commands/ScheduleList.d.ts.map +1 -0
- package/dist/commands/ScheduleList.js +55 -0
- package/dist/commands/ScheduleList.js.map +1 -0
- package/dist/commands/ScheduleRun.d.ts +19 -0
- package/dist/commands/ScheduleRun.d.ts.map +1 -0
- package/dist/commands/ScheduleRun.js +61 -0
- package/dist/commands/ScheduleRun.js.map +1 -0
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +63 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/scheduler_resolver.d.ts +12 -0
- package/dist/commands/scheduler_resolver.d.ts.map +1 -0
- package/dist/commands/scheduler_resolver.js +19 -0
- package/dist/commands/scheduler_resolver.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +5 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Ignitor.ts +51 -3
- package/src/commands/Migrate.ts +18 -0
- package/src/commands/MigrateBase.ts +81 -0
- package/src/commands/MigrateRollback.ts +18 -0
- package/src/commands/MigrateStatus.ts +22 -0
- package/src/commands/ScheduleList.ts +66 -0
- package/src/commands/ScheduleRun.ts +65 -0
- package/src/commands/index.ts +73 -0
- package/src/commands/scheduler_resolver.ts +23 -0
- package/src/index.ts +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `schedule:run <name>` — run one registered task immediately.
|
|
3
|
+
*
|
|
4
|
+
* An admin override: it calls the task body directly, so it bypasses both the
|
|
5
|
+
* cron schedule and the distributed lock. That is the point — you reach for it
|
|
6
|
+
* when a task must run now, or to see it fail with your own eyes — and it is
|
|
7
|
+
* also why the exit codes are distinct: `1` the task failed, `2` no such task,
|
|
8
|
+
* `3` an invocation of it is already in flight.
|
|
9
|
+
*/
|
|
10
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
11
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
14
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
|
+
};
|
|
16
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
17
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
18
|
+
};
|
|
19
|
+
import { BaseCommand } from '../console/BaseCommand.js';
|
|
20
|
+
import { args } from '../console/decorators.js';
|
|
21
|
+
import { resolveScheduler } from './scheduler_resolver.js';
|
|
22
|
+
export default class ScheduleRun extends BaseCommand {
|
|
23
|
+
static commandName = 'schedule:run';
|
|
24
|
+
static description = 'Run a registered scheduled task once, now (bypasses the lock)';
|
|
25
|
+
static options = { startApp: true };
|
|
26
|
+
async run() {
|
|
27
|
+
// Rejected here rather than in the scheduler so the message arrives without
|
|
28
|
+
// the operator first paying for a boot.
|
|
29
|
+
if (this.name.trim() === '') {
|
|
30
|
+
this.logger.error('Task name cannot be empty. Usage: schedule:run <name>');
|
|
31
|
+
this.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const scheduler = await resolveScheduler(this);
|
|
35
|
+
if (!scheduler)
|
|
36
|
+
return;
|
|
37
|
+
const result = await scheduler.runTaskNow(this.name);
|
|
38
|
+
const duration = Math.round(result.durationMs);
|
|
39
|
+
if (result.outcome === 'unknown') {
|
|
40
|
+
this.logger.error(`Unknown task: ${this.name}. Run "schedule:list" to see the registered tasks.`);
|
|
41
|
+
this.exitCode = 2;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (result.outcome === 'already-running') {
|
|
45
|
+
this.logger.error(`Task ${this.name} is already running in this process — skipped. Try again once it completes.`);
|
|
46
|
+
this.exitCode = 3;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (result.outcome === 'completed') {
|
|
50
|
+
this.logger.success(`${this.name} completed in ${duration} ms`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.logger.error(`${this.name} failed after ${duration} ms: ${result.error?.message || 'unknown error'}`);
|
|
54
|
+
this.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
__decorate([
|
|
58
|
+
args.string({ description: 'Task name, as `schedule:list` prints it' }),
|
|
59
|
+
__metadata("design:type", String)
|
|
60
|
+
], ScheduleRun.prototype, "name", void 0);
|
|
61
|
+
//# sourceMappingURL=ScheduleRun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScheduleRun.js","sourceRoot":"","sources":["../../src/commands/ScheduleRun.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;;;;;;;;;;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AAE/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAE1D,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,WAAW;IAClD,MAAM,CAAU,WAAW,GAAG,cAAc,CAAA;IAC5C,MAAM,CAAU,WAAW,GAAG,+DAA+D,CAAA;IAC7F,MAAM,CAAU,OAAO,GAAmB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IAK5D,KAAK,CAAC,GAAG;QACP,4EAA4E;QAC5E,wCAAwC;QACxC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAA;YAC1E,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;YACjB,OAAM;QACR,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAA;QAC9C,IAAI,CAAC,SAAS;YAAE,OAAM;QAEtB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QAE9C,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,iBAAiB,IAAI,CAAC,IAAI,oDAAoD,CAC/E,CAAA;YACD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;YACjB,OAAM;QACR,CAAC;QAED,IAAI,MAAM,CAAC,OAAO,KAAK,iBAAiB,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,QAAQ,IAAI,CAAC,IAAI,6EAA6E,CAC/F,CAAA;YACD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;YACjB,OAAM;QACR,CAAC;QAED,IAAI,MAAM,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC;YACnC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,IAAI,iBAAiB,QAAQ,KAAK,CAAC,CAAA;YAC/D,OAAM;QACR,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,GAAG,IAAI,CAAC,IAAI,iBAAiB,QAAQ,QAAQ,MAAM,CAAC,KAAK,EAAE,OAAO,IAAI,eAAe,EAAE,CACxF,CAAA;QACD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;IACnB,CAAC;;AA1CO;IADP,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;yCACpD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/ream/commands` — the commands the framework itself ships.
|
|
3
|
+
*
|
|
4
|
+
* The shape AdonisJS uses for `@adonisjs/core/commands`: a module answering
|
|
5
|
+
* `getMetaData()` then `getCommand()`, so listing a command does not import it.
|
|
6
|
+
*
|
|
7
|
+
* The kernel registers this loader on its own rather than waiting for an entry
|
|
8
|
+
* in `reamrc.commands` — the deviation from AdonisJS, where the starter kit
|
|
9
|
+
* lists it. These commands were part of the `ream` binary before they were
|
|
10
|
+
* classes, and an application that upgrades must not silently lose them.
|
|
11
|
+
*/
|
|
12
|
+
import type { CommandClass, SerializedCommand } from '../console/types.js';
|
|
13
|
+
export declare function getMetaData(): Promise<SerializedCommand[]>;
|
|
14
|
+
export declare function getCommand(metadata: SerializedCommand): Promise<CommandClass | null>;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AA4C1E,wBAAsB,WAAW,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAUhE;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAI1F"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@c9up/ream/commands` — the commands the framework itself ships.
|
|
3
|
+
*
|
|
4
|
+
* The shape AdonisJS uses for `@adonisjs/core/commands`: a module answering
|
|
5
|
+
* `getMetaData()` then `getCommand()`, so listing a command does not import it.
|
|
6
|
+
*
|
|
7
|
+
* The kernel registers this loader on its own rather than waiting for an entry
|
|
8
|
+
* in `reamrc.commands` — the deviation from AdonisJS, where the starter kit
|
|
9
|
+
* lists it. These commands were part of the `ream` binary before they were
|
|
10
|
+
* classes, and an application that upgrades must not silently lose them.
|
|
11
|
+
*/
|
|
12
|
+
const COMMANDS = [
|
|
13
|
+
// The registry commands first: a data package registers its runner, and
|
|
14
|
+
// these drive whatever registered — they name no store.
|
|
15
|
+
{
|
|
16
|
+
commandName: 'migrate',
|
|
17
|
+
description: 'Run pending migrations for every registered migration source',
|
|
18
|
+
load: () => import('./Migrate.js'),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
commandName: 'migrate:rollback',
|
|
22
|
+
description: 'Roll back the last batch for every registered migration source',
|
|
23
|
+
load: () => import('./MigrateRollback.js'),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
commandName: 'migrate:status',
|
|
27
|
+
description: 'Show applied and pending migrations for every source',
|
|
28
|
+
load: () => import('./MigrateStatus.js'),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
commandName: 'schedule:list',
|
|
32
|
+
description: 'List every registered scheduled task, its next run and its stats',
|
|
33
|
+
load: () => import('./ScheduleList.js'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
commandName: 'schedule:run',
|
|
37
|
+
description: 'Run a registered scheduled task once, now (bypasses the lock)',
|
|
38
|
+
load: () => import('./ScheduleRun.js'),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
/** `migrate:status` → `migrate`; a name without a colon has no namespace. */
|
|
42
|
+
function namespaceOf(commandName) {
|
|
43
|
+
const colon = commandName.indexOf(':');
|
|
44
|
+
return colon === -1 ? null : commandName.slice(0, colon);
|
|
45
|
+
}
|
|
46
|
+
export async function getMetaData() {
|
|
47
|
+
return COMMANDS.map((entry) => ({
|
|
48
|
+
commandName: entry.commandName,
|
|
49
|
+
namespace: namespaceOf(entry.commandName),
|
|
50
|
+
description: entry.description,
|
|
51
|
+
aliases: [],
|
|
52
|
+
options: { startApp: true },
|
|
53
|
+
args: [],
|
|
54
|
+
flags: [],
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
export async function getCommand(metadata) {
|
|
58
|
+
const entry = COMMANDS.find((command) => command.commandName === metadata.commandName);
|
|
59
|
+
if (!entry)
|
|
60
|
+
return null;
|
|
61
|
+
return (await entry.load()).default;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH,MAAM,QAAQ,GAAqB;IACjC,wEAAwE;IACxE,wDAAwD;IACxD;QACE,WAAW,EAAE,SAAS;QACtB,WAAW,EAAE,8DAA8D;QAC3E,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC;KACnC;IACD;QACE,WAAW,EAAE,kBAAkB;QAC/B,WAAW,EAAE,gEAAgE;QAC7E,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC;KAC3C;IACD;QACE,WAAW,EAAE,gBAAgB;QAC7B,WAAW,EAAE,sDAAsD;QACnE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC;KACzC;IACD;QACE,WAAW,EAAE,eAAe;QAC5B,WAAW,EAAE,kEAAkE;QAC/E,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC;KACxC;IACD;QACE,WAAW,EAAE,cAAc;QAC3B,WAAW,EAAE,+DAA+D;QAC5E,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC;KACvC;CACF,CAAA;AAED,6EAA6E;AAC7E,SAAS,WAAW,CAAC,WAAmB;IACtC,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACtC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC9B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC;QACzC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO,EAAE,EAAE;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;QAC3B,IAAI,EAAE,EAAE;QACR,KAAK,EAAE,EAAE;KACV,CAAC,CAAC,CAAA;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAA2B;IAC1D,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,KAAK,QAAQ,CAAC,WAAW,CAAC,CAAA;IACtF,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAA;AACrC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the scheduler for the `schedule:*` commands.
|
|
3
|
+
*
|
|
4
|
+
* Shared because the failure is the same one on both, and it is a
|
|
5
|
+
* configuration mistake rather than a crash: the provider is simply not in the
|
|
6
|
+
* list, and the message has to say so — a container error mentioning an
|
|
7
|
+
* unbound `scheduler` binding tells an operator nothing.
|
|
8
|
+
*/
|
|
9
|
+
import type { BaseCommand } from '../console/BaseCommand.js';
|
|
10
|
+
import type { Scheduler } from '../scheduler/Scheduler.js';
|
|
11
|
+
export declare function resolveScheduler(command: BaseCommand): Promise<Scheduler | undefined>;
|
|
12
|
+
//# sourceMappingURL=scheduler_resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler_resolver.d.ts","sourceRoot":"","sources":["../../src/commands/scheduler_resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAE1D,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAU3F"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the scheduler for the `schedule:*` commands.
|
|
3
|
+
*
|
|
4
|
+
* Shared because the failure is the same one on both, and it is a
|
|
5
|
+
* configuration mistake rather than a crash: the provider is simply not in the
|
|
6
|
+
* list, and the message has to say so — a container error mentioning an
|
|
7
|
+
* unbound `scheduler` binding tells an operator nothing.
|
|
8
|
+
*/
|
|
9
|
+
export async function resolveScheduler(command) {
|
|
10
|
+
try {
|
|
11
|
+
return await command.app.container.resolve('scheduler');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
command.logger.error('No ScheduleProvider is registered. Add ScheduleProvider to the providers list in reamrc.ts.');
|
|
15
|
+
command.exitCode = 2;
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=scheduler_resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler_resolver.js","sourceRoot":"","sources":["../../src/commands/scheduler_resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAoB;IACzD,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAY,WAAW,CAAC,CAAA;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6FAA6F,CAC9F,CAAA;QACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -119,5 +119,5 @@ export { BaseEvent, Emitter, type EmitterResolver, type ListenerClass, } from '.
|
|
|
119
119
|
export { type BufferedEvent, type BufferedEventName, type EventFinder, EventsBuffer, } from './events/EventsBuffer.js';
|
|
120
120
|
export type { ErrorReporter, LockBackend, RunTaskOutcome, ScheduleEvent, ScheduleEventSink, ScheduleInvocation, ScheduleMetadata, ScheduleProviderOptions, SchedulerOptions, ScheduleTaskCompletedEvent, ScheduleTaskFailedEvent, ScheduleTaskSkippedEvent, ScheduleTaskStartedEvent, TaskInfo, TaskStats, } from './scheduler/index.js';
|
|
121
121
|
export { getScheduleMetadata, MemoryLockBackend, SCHEDULE_METADATA_KEY, Schedule, ScheduleProvider, Scheduler, StatsTracker, } from './scheduler/index.js';
|
|
122
|
-
export declare const VERSION = "0.2.
|
|
122
|
+
export declare const VERSION = "0.2.9";
|
|
123
123
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -91,5 +91,5 @@ export { defineConfig as defineEventsConfig } from './events/config.js';
|
|
|
91
91
|
export { BaseEvent, Emitter, } from './events/Emitter.js';
|
|
92
92
|
export { EventsBuffer, } from './events/EventsBuffer.js';
|
|
93
93
|
export { getScheduleMetadata, MemoryLockBackend, SCHEDULE_METADATA_KEY, Schedule, ScheduleProvider, Scheduler, StatsTracker, } from './scheduler/index.js';
|
|
94
|
-
export const VERSION = '0.2.
|
|
94
|
+
export const VERSION = '0.2.9';
|
|
95
95
|
//# sourceMappingURL=index.js.map
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/ream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Ream — Rust-powered Node.js application framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/http/Exception.d.ts",
|
|
20
20
|
"import": "./dist/http/Exception.js"
|
|
21
21
|
},
|
|
22
|
+
"./commands": {
|
|
23
|
+
"types": "./dist/commands/index.d.ts",
|
|
24
|
+
"import": "./dist/commands/index.js"
|
|
25
|
+
},
|
|
22
26
|
"./console": {
|
|
23
27
|
"types": "./dist/console/index.d.ts",
|
|
24
28
|
"import": "./dist/console/index.js"
|
|
Binary file
|
package/src/Ignitor.ts
CHANGED
|
@@ -1137,6 +1137,7 @@ export class ConsoleKernel {
|
|
|
1137
1137
|
|
|
1138
1138
|
return {
|
|
1139
1139
|
getMetaData: async () => {
|
|
1140
|
+
await this.#loadFrameworkCommands(found)
|
|
1140
1141
|
await this.#discoverAppCommands(kernel, found)
|
|
1141
1142
|
await this.#loadPackageCommands(found)
|
|
1142
1143
|
// Imported here, not at the top of the file: the console stack is only
|
|
@@ -1149,6 +1150,24 @@ export class ConsoleKernel {
|
|
|
1149
1150
|
}
|
|
1150
1151
|
}
|
|
1151
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* Register the framework's own commands (`@c9up/ream/commands`).
|
|
1155
|
+
*
|
|
1156
|
+
* AdonisJS lists its equivalent in the rc file, which its starter kit writes.
|
|
1157
|
+
* Ream registers them here instead, and first: these were subcommands of the
|
|
1158
|
+
* `ream` binary before they were classes, so an application that upgrades
|
|
1159
|
+
* would otherwise lose them without ever having asked for them. Loading them
|
|
1160
|
+
* first also means an application that ships a command of the same name wins
|
|
1161
|
+
* — the later registration replaces this one.
|
|
1162
|
+
*/
|
|
1163
|
+
async #loadFrameworkCommands(found: Map<string, CommandClass>): Promise<void> {
|
|
1164
|
+
const loader = await import('./commands/index.js')
|
|
1165
|
+
for (const metadata of await loader.getMetaData()) {
|
|
1166
|
+
const command = await loader.getCommand(metadata)
|
|
1167
|
+
if (command !== null) found.set(command.commandName, command)
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1152
1171
|
/**
|
|
1153
1172
|
* Load every command in the app's `commands/` directory.
|
|
1154
1173
|
*
|
|
@@ -1200,13 +1219,30 @@ export class ConsoleKernel {
|
|
|
1200
1219
|
if (!declared) return
|
|
1201
1220
|
|
|
1202
1221
|
for (const entry of declared) {
|
|
1203
|
-
const
|
|
1222
|
+
const module = await entry()
|
|
1223
|
+
|
|
1224
|
+
// A package with several commands ships ONE loader — `getMetaData()` +
|
|
1225
|
+
// `getCommand()` — so its rc entry is one line and `ream list` reads
|
|
1226
|
+
// metadata without importing a single command class. That is what makes
|
|
1227
|
+
// this channel usable for a package that has six of them, and why
|
|
1228
|
+
// requiring a default export per command pushed packages to wire
|
|
1229
|
+
// themselves into the CLI binary instead.
|
|
1230
|
+
const loader = loaderOf(module)
|
|
1231
|
+
if (loader) {
|
|
1232
|
+
for (const metadata of await loader.getMetaData()) {
|
|
1233
|
+
const loaded = await loader.getCommand(metadata)
|
|
1234
|
+
if (loaded !== null) found.set(loaded.commandName, loaded)
|
|
1235
|
+
}
|
|
1236
|
+
continue
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const command = commandOf(module)
|
|
1204
1240
|
if (command === undefined) {
|
|
1205
1241
|
throw new ReamError(
|
|
1206
1242
|
'E_CONSOLE_INVALID_COMMAND',
|
|
1207
|
-
'An entry of reamrc.ts `commands`
|
|
1243
|
+
'An entry of reamrc.ts `commands` is neither a command loader nor a default-exported command class.',
|
|
1208
1244
|
{
|
|
1209
|
-
hint: '
|
|
1245
|
+
hint: 'Ship `export function getMetaData()` + `export function getCommand(metadata)` for a package with several commands, or `export default class X { static commandName = "..."; static description = "..."; run() {} }` for one.',
|
|
1210
1246
|
},
|
|
1211
1247
|
)
|
|
1212
1248
|
}
|
|
@@ -1246,6 +1282,18 @@ interface ConsoleCommandKernel {
|
|
|
1246
1282
|
}
|
|
1247
1283
|
|
|
1248
1284
|
/** The command a module default-exports, or undefined when it exports none. */
|
|
1285
|
+
/**
|
|
1286
|
+
* A module that enumerates its own commands — the shape AdonisJS packages ship
|
|
1287
|
+
* and the one `CommandLoader` already describes.
|
|
1288
|
+
*/
|
|
1289
|
+
function loaderOf(mod: unknown): CommandLoader | undefined {
|
|
1290
|
+
if (typeof mod !== 'object' || mod === null) return undefined
|
|
1291
|
+
const getMetaData = Reflect.get(mod, 'getMetaData')
|
|
1292
|
+
const getCommand = Reflect.get(mod, 'getCommand')
|
|
1293
|
+
if (typeof getMetaData !== 'function' || typeof getCommand !== 'function') return undefined
|
|
1294
|
+
return mod as CommandLoader
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1249
1297
|
function commandOf(mod: unknown): CommandClass | undefined {
|
|
1250
1298
|
if (typeof mod !== 'object' || mod === null) return undefined
|
|
1251
1299
|
const value = Reflect.get(mod, 'default')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** `migrate` — apply every pending migration, in every registered store. */
|
|
2
|
+
|
|
3
|
+
import type { RegisteredMigrationSource } from '../migrations/types.js'
|
|
4
|
+
import { MigrateBase } from './MigrateBase.js'
|
|
5
|
+
|
|
6
|
+
export default class Migrate extends MigrateBase {
|
|
7
|
+
static override commandName = 'migrate'
|
|
8
|
+
static override description = 'Run pending migrations for every registered migration source'
|
|
9
|
+
|
|
10
|
+
protected async migrateSource(source: RegisteredMigrationSource): Promise<void> {
|
|
11
|
+
const executed = await source.runner.migrate()
|
|
12
|
+
if (executed.length === 0) {
|
|
13
|
+
this.report(source, 'nothing to migrate.')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
for (const name of executed) this.report(source, `migrated: ${name}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `migrate`, `migrate:rollback` and `migrate:status` share.
|
|
3
|
+
*
|
|
4
|
+
* All three drive the SAME thing — the `migrations` registry, where each data
|
|
5
|
+
* package puts its runner — and differ only in which method they call and how
|
|
6
|
+
* they print the result. A ream particularity, deliberate: AdonisJS has one
|
|
7
|
+
* store and can name it, while an application here may hold a relational store
|
|
8
|
+
* and a time-series one at once, so the command names none of them.
|
|
9
|
+
*
|
|
10
|
+
* Sequentially, never in parallel: two stores on one database server would
|
|
11
|
+
* contend on locks, and interleaved output makes a failure impossible to
|
|
12
|
+
* attribute to the store that caused it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { BaseCommand } from '../console/BaseCommand.js'
|
|
16
|
+
import { flags } from '../console/decorators.js'
|
|
17
|
+
import type { CommandOptions } from '../console/types.js'
|
|
18
|
+
import type { MigrationRegistry } from '../migrations/MigrationRegistry.js'
|
|
19
|
+
import type { RegisteredMigrationSource } from '../migrations/types.js'
|
|
20
|
+
|
|
21
|
+
export abstract class MigrateBase extends BaseCommand {
|
|
22
|
+
static override options: CommandOptions = { startApp: true }
|
|
23
|
+
|
|
24
|
+
@flags.string({
|
|
25
|
+
description: 'Only this migration source, by the name its package registered',
|
|
26
|
+
})
|
|
27
|
+
declare only?: string
|
|
28
|
+
|
|
29
|
+
/** What to do with one store. Implemented by each command. */
|
|
30
|
+
protected abstract migrateSource(source: RegisteredMigrationSource): Promise<void>
|
|
31
|
+
|
|
32
|
+
async run(): Promise<void> {
|
|
33
|
+
const sources = await this.#resolveSources()
|
|
34
|
+
if (sources === undefined) return
|
|
35
|
+
|
|
36
|
+
for (const source of sources) {
|
|
37
|
+
await this.migrateSource(source)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The line every message is prefixed with, so multi-store output stays legible. */
|
|
42
|
+
protected report(source: RegisteredMigrationSource, message: string): void {
|
|
43
|
+
this.logger.log(` [${source.name}] ${message}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The sources this run covers, or `undefined` when the run should stop —
|
|
48
|
+
* having said why.
|
|
49
|
+
*
|
|
50
|
+
* An empty registry is NOT a failure: an application may legitimately have no
|
|
51
|
+
* data package yet. Naming which thing is missing beats exiting non-zero on a
|
|
52
|
+
* state the user may have chosen.
|
|
53
|
+
*/
|
|
54
|
+
async #resolveSources(): Promise<RegisteredMigrationSource[] | undefined> {
|
|
55
|
+
const registry = await this.app.container.resolve<MigrationRegistry>('migrations')
|
|
56
|
+
|
|
57
|
+
if (this.only !== undefined) {
|
|
58
|
+
const picked = registry.get(this.only)
|
|
59
|
+
if (picked === undefined) {
|
|
60
|
+
const names = registry.names()
|
|
61
|
+
this.logger.error(`No migration source named "${this.only}".`)
|
|
62
|
+
this.logger.error(
|
|
63
|
+
names.length > 0
|
|
64
|
+
? ` Registered: ${names.join(', ')}`
|
|
65
|
+
: ' No data package registered one. Is its provider in reamrc.ts?',
|
|
66
|
+
)
|
|
67
|
+
this.exitCode = 1
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
return [picked]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sources = registry.all()
|
|
74
|
+
if (sources.length === 0) {
|
|
75
|
+
this.logger.info('No migration source registered.')
|
|
76
|
+
this.logger.info(' A data package registers one from its provider — check reamrc.ts.')
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
return sources
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** `migrate:rollback` — undo the last batch, in every registered store. */
|
|
2
|
+
|
|
3
|
+
import type { RegisteredMigrationSource } from '../migrations/types.js'
|
|
4
|
+
import { MigrateBase } from './MigrateBase.js'
|
|
5
|
+
|
|
6
|
+
export default class MigrateRollback extends MigrateBase {
|
|
7
|
+
static override commandName = 'migrate:rollback'
|
|
8
|
+
static override description = 'Roll back the last batch for every registered migration source'
|
|
9
|
+
|
|
10
|
+
protected async migrateSource(source: RegisteredMigrationSource): Promise<void> {
|
|
11
|
+
const rolled = await source.runner.rollback()
|
|
12
|
+
if (rolled.length === 0) {
|
|
13
|
+
this.report(source, 'nothing to rollback.')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
for (const name of rolled) this.report(source, `rolled back: ${name}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** `migrate:status` — what has run and what has not, in every registered store. */
|
|
2
|
+
|
|
3
|
+
import type { RegisteredMigrationSource } from '../migrations/types.js'
|
|
4
|
+
import { MigrateBase } from './MigrateBase.js'
|
|
5
|
+
|
|
6
|
+
export default class MigrateStatus extends MigrateBase {
|
|
7
|
+
static override commandName = 'migrate:status'
|
|
8
|
+
static override description = 'Show applied and pending migrations for every source'
|
|
9
|
+
|
|
10
|
+
protected async migrateSource(source: RegisteredMigrationSource): Promise<void> {
|
|
11
|
+
const statuses = await source.runner.status()
|
|
12
|
+
if (statuses.length === 0) {
|
|
13
|
+
this.report(source, 'no migrations found.')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
for (const node of statuses) {
|
|
17
|
+
const mark = node.status === 'applied' ? '✓' : '○'
|
|
18
|
+
const batch = node.batch === undefined ? '' : ` (batch ${node.batch})`
|
|
19
|
+
this.report(source, `${mark} ${node.name}${batch}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `schedule:list` — every registered task, when it next fires, and how its past
|
|
3
|
+
* runs went.
|
|
4
|
+
*
|
|
5
|
+
* The columns are the operator's questions in order: is my task registered at
|
|
6
|
+
* all, on what schedule, when does it fire next, when did it last fire, and is
|
|
7
|
+
* it failing. Stats come from the scheduler's own tracker, so a task that has
|
|
8
|
+
* never run reads as `—` rather than a zero that looks like a result.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BaseCommand } from '../console/BaseCommand.js'
|
|
12
|
+
import type { CommandOptions } from '../console/types.js'
|
|
13
|
+
import { resolveScheduler } from './scheduler_resolver.js'
|
|
14
|
+
|
|
15
|
+
/** Over-long names and cron expressions are truncated so the table stays a table. */
|
|
16
|
+
function truncate(value: string, width: number): string {
|
|
17
|
+
return value.length > width ? `${value.slice(0, width - 1)}…` : value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function asDate(ms: number | null): string {
|
|
21
|
+
return ms === null ? '—' : new Date(ms).toISOString()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default class ScheduleList extends BaseCommand {
|
|
25
|
+
static override commandName = 'schedule:list'
|
|
26
|
+
static override description = 'List every registered scheduled task, its next run and its stats'
|
|
27
|
+
static override options: CommandOptions = { startApp: true }
|
|
28
|
+
|
|
29
|
+
async run(): Promise<void> {
|
|
30
|
+
const scheduler = await resolveScheduler(this)
|
|
31
|
+
if (!scheduler) return
|
|
32
|
+
|
|
33
|
+
const tasks = scheduler.listTasks()
|
|
34
|
+
if (tasks.length === 0) {
|
|
35
|
+
this.logger.info('No scheduled tasks registered.')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.logger.log(
|
|
40
|
+
[
|
|
41
|
+
'NAME'.padEnd(32),
|
|
42
|
+
'CRON'.padEnd(18),
|
|
43
|
+
'NEXT RUN'.padEnd(22),
|
|
44
|
+
'LAST RUN'.padEnd(22),
|
|
45
|
+
'RUNS'.padStart(6),
|
|
46
|
+
'ERR'.padStart(5),
|
|
47
|
+
'AVG(ms)'.padStart(9),
|
|
48
|
+
].join(' '),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
for (const task of tasks) {
|
|
52
|
+
const stats = scheduler.getStats(task.name)
|
|
53
|
+
this.logger.log(
|
|
54
|
+
[
|
|
55
|
+
truncate(task.name, 32).padEnd(32),
|
|
56
|
+
truncate(task.cronExpr, 18).padEnd(18),
|
|
57
|
+
asDate(task.nextRun).padEnd(22),
|
|
58
|
+
asDate(stats.lastRunMs).padEnd(22),
|
|
59
|
+
String(stats.runCount).padStart(6),
|
|
60
|
+
String(stats.errorCount).padStart(5),
|
|
61
|
+
String(Math.round(stats.avgDurationMs)).padStart(9),
|
|
62
|
+
].join(' '),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `schedule:run <name>` — run one registered task immediately.
|
|
3
|
+
*
|
|
4
|
+
* An admin override: it calls the task body directly, so it bypasses both the
|
|
5
|
+
* cron schedule and the distributed lock. That is the point — you reach for it
|
|
6
|
+
* when a task must run now, or to see it fail with your own eyes — and it is
|
|
7
|
+
* also why the exit codes are distinct: `1` the task failed, `2` no such task,
|
|
8
|
+
* `3` an invocation of it is already in flight.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BaseCommand } from '../console/BaseCommand.js'
|
|
12
|
+
import { args } from '../console/decorators.js'
|
|
13
|
+
import type { CommandOptions } from '../console/types.js'
|
|
14
|
+
import { resolveScheduler } from './scheduler_resolver.js'
|
|
15
|
+
|
|
16
|
+
export default class ScheduleRun extends BaseCommand {
|
|
17
|
+
static override commandName = 'schedule:run'
|
|
18
|
+
static override description = 'Run a registered scheduled task once, now (bypasses the lock)'
|
|
19
|
+
static override options: CommandOptions = { startApp: true }
|
|
20
|
+
|
|
21
|
+
@args.string({ description: 'Task name, as `schedule:list` prints it' })
|
|
22
|
+
declare name: string
|
|
23
|
+
|
|
24
|
+
async run(): Promise<void> {
|
|
25
|
+
// Rejected here rather than in the scheduler so the message arrives without
|
|
26
|
+
// the operator first paying for a boot.
|
|
27
|
+
if (this.name.trim() === '') {
|
|
28
|
+
this.logger.error('Task name cannot be empty. Usage: schedule:run <name>')
|
|
29
|
+
this.exitCode = 1
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const scheduler = await resolveScheduler(this)
|
|
34
|
+
if (!scheduler) return
|
|
35
|
+
|
|
36
|
+
const result = await scheduler.runTaskNow(this.name)
|
|
37
|
+
const duration = Math.round(result.durationMs)
|
|
38
|
+
|
|
39
|
+
if (result.outcome === 'unknown') {
|
|
40
|
+
this.logger.error(
|
|
41
|
+
`Unknown task: ${this.name}. Run "schedule:list" to see the registered tasks.`,
|
|
42
|
+
)
|
|
43
|
+
this.exitCode = 2
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (result.outcome === 'already-running') {
|
|
48
|
+
this.logger.error(
|
|
49
|
+
`Task ${this.name} is already running in this process — skipped. Try again once it completes.`,
|
|
50
|
+
)
|
|
51
|
+
this.exitCode = 3
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (result.outcome === 'completed') {
|
|
56
|
+
this.logger.success(`${this.name} completed in ${duration} ms`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.logger.error(
|
|
61
|
+
`${this.name} failed after ${duration} ms: ${result.error?.message || 'unknown error'}`,
|
|
62
|
+
)
|
|
63
|
+
this.exitCode = 1
|
|
64
|
+
}
|
|
65
|
+
}
|