@geekmidas/constructs 10.0.0-alpha.51 → 10.0.0-alpha.53
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/{Endpoint-CwM8Rzfs.d.mts → Endpoint-KyMeR-B-.d.mts} +2 -2
- package/dist/{Endpoint-CwM8Rzfs.d.mts.map → Endpoint-KyMeR-B-.d.mts.map} +1 -1
- package/dist/{EndpointFactory-0plFjvb7.d.mts → EndpointFactory-GYkAuG3u.d.mts} +3 -3
- package/dist/{EndpointFactory-0plFjvb7.d.mts.map → EndpointFactory-GYkAuG3u.d.mts.map} +1 -1
- package/dist/{HonoEndpointAdaptor-BIS0O3M9.mjs → HonoEndpointAdaptor-DW4q9db0.mjs} +2 -2
- package/dist/{HonoEndpointAdaptor-BIS0O3M9.mjs.map → HonoEndpointAdaptor-DW4q9db0.mjs.map} +1 -1
- package/dist/{HonoEndpointAdaptor-CMi0MJsF.cjs → HonoEndpointAdaptor-ZDmoZ5LJ.cjs} +2 -2
- package/dist/{HonoEndpointAdaptor-CMi0MJsF.cjs.map → HonoEndpointAdaptor-ZDmoZ5LJ.cjs.map} +1 -1
- package/dist/adaptors/aws.cjs +13 -196
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts +0 -55
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts +1 -56
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +14 -196
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +1 -1
- package/dist/adaptors/hono.d.mts +1 -1
- package/dist/adaptors/hono.mjs +1 -1
- package/dist/adaptors/testing.cjs +1 -1
- package/dist/adaptors/testing.d.mts +1 -1
- package/dist/adaptors/testing.mjs +1 -1
- package/dist/crons/index.cjs +100 -0
- package/dist/crons/index.cjs.map +1 -1
- package/dist/crons/index.d.cts +36 -1
- package/dist/crons/index.d.cts.map +1 -1
- package/dist/crons/index.d.mts +36 -1
- package/dist/crons/index.d.mts.map +1 -1
- package/dist/crons/index.mjs +99 -1
- package/dist/crons/index.mjs.map +1 -1
- package/dist/endpoints/index.d.mts +2 -2
- package/dist/rest-api.d.mts +2 -2
- package/dist/runSubscriber-BRp9a_qi.cjs +126 -0
- package/dist/runSubscriber-BRp9a_qi.cjs.map +1 -0
- package/dist/runSubscriber-BYEY_bfA.mjs +109 -0
- package/dist/runSubscriber-BYEY_bfA.mjs.map +1 -0
- package/dist/subscribers/index.cjs +107 -0
- package/dist/subscribers/index.cjs.map +1 -0
- package/dist/subscribers/index.d.cts +48 -1
- package/dist/subscribers/index.d.cts.map +1 -0
- package/dist/subscribers/index.d.mts +48 -1
- package/dist/subscribers/index.d.mts.map +1 -0
- package/dist/subscribers/index.mjs +107 -1
- package/dist/subscribers/index.mjs.map +1 -0
- package/package.json +32 -31
package/dist/crons/index.d.cts
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { i as ScheduleExpression, n as CronExpression, r as RateExpression, t as Cron } from "../Cron-DI3nKoic.cjs";
|
|
2
2
|
import { t as CronBuilder } from "../CronBuilder-g1lZLWyG.cjs";
|
|
3
|
+
import { ServiceDiscovery } from "@geekmidas/services";
|
|
4
|
+
//#region src/crons/inProcess.d.ts
|
|
5
|
+
/** A schedule that is running, and the way to stop it. */
|
|
6
|
+
interface InProcessSchedule {
|
|
7
|
+
stop(): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run `fire` on a cron expression, in this process, by timer.
|
|
11
|
+
*
|
|
12
|
+
* For the one process that has no broker to keep a schedule: `gkm dev` on a
|
|
13
|
+
* target whose crons are infrastructure once deployed — an EventBridge rule on
|
|
14
|
+
* AWS. Deployed, a timer is the wrong tool: each replica would fire every job
|
|
15
|
+
* once, and nothing would say so. That is why a server schedules through
|
|
16
|
+
* pg-boss, and why this is never what a deployed process does.
|
|
17
|
+
*
|
|
18
|
+
* In UTC, as the deployed schedule is. The next firing is worked out from the
|
|
19
|
+
* clock after each run rather than read off a sequence, so a run that outlasts
|
|
20
|
+
* its interval skips what it overran instead of firing them back to back.
|
|
21
|
+
*/
|
|
22
|
+
export declare function scheduleInProcess(expression: string, fire: () => Promise<unknown> | unknown): InProcessSchedule;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/crons/runCron.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Run a cron once, in this process: what an EventBridge rule's invocation does
|
|
27
|
+
* on Lambda, for a server that schedules its own.
|
|
28
|
+
*
|
|
29
|
+
* The same steps as the Lambda adaptor — its services, its worker's database
|
|
30
|
+
* as `db`, an auditor when it declared one, its output parsed, its events
|
|
31
|
+
* published — because a cron that ran differently on a server than deployed
|
|
32
|
+
* would pass locally and fail where it matters. Services come through the
|
|
33
|
+
* discovery, which keeps one instance of each, so running every minute does not
|
|
34
|
+
* reconnect a database every minute.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runCron(cron: Cron<any, any, any, any, any, any>, serviceDiscovery: ServiceDiscovery<any>): Promise<unknown>;
|
|
37
|
+
//#endregion
|
|
3
38
|
//#region src/crons/schedule.d.ts
|
|
4
39
|
/**
|
|
5
40
|
* A schedule a cron runner can take, as standard five-field cron.
|
|
@@ -30,5 +65,5 @@ export declare class UnrepresentableSchedule extends Error {
|
|
|
30
65
|
*/
|
|
31
66
|
export declare function toCronExpression(schedule: ScheduleExpression): string;
|
|
32
67
|
//#endregion
|
|
33
|
-
export { Cron, CronBuilder, type CronExpression, type RateExpression, type ScheduleExpression };
|
|
68
|
+
export { Cron, CronBuilder, type CronExpression, type InProcessSchedule, type RateExpression, type ScheduleExpression };
|
|
34
69
|
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/crons/schedule.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"mappings":";;;;;UASiB;EAChB;;;;;;;;;;;;;;;wBAgBe,kBACf,oBACA,YAAY,6BACV;;;;;;;;;;;;;;wBCVmB,QACrB,MAAM,oCACN,kBAAkB,wBAChB;;;;;;;;;;;;;;;;;;;;;qBCFU,gCAAgC;WAElC;EADV,YACU,kBACT;;;;;;;wBAuBc,iBAAiB,UAAU"}
|
package/dist/crons/index.d.mts
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import { i as ScheduleExpression, n as CronExpression, r as RateExpression, t as Cron } from "../Cron-uYAHPnp7.mjs";
|
|
2
2
|
import { t as CronBuilder } from "../CronBuilder-DaSUm2Na.mjs";
|
|
3
|
+
import { ServiceDiscovery } from "@geekmidas/services";
|
|
4
|
+
//#region src/crons/inProcess.d.ts
|
|
5
|
+
/** A schedule that is running, and the way to stop it. */
|
|
6
|
+
interface InProcessSchedule {
|
|
7
|
+
stop(): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run `fire` on a cron expression, in this process, by timer.
|
|
11
|
+
*
|
|
12
|
+
* For the one process that has no broker to keep a schedule: `gkm dev` on a
|
|
13
|
+
* target whose crons are infrastructure once deployed — an EventBridge rule on
|
|
14
|
+
* AWS. Deployed, a timer is the wrong tool: each replica would fire every job
|
|
15
|
+
* once, and nothing would say so. That is why a server schedules through
|
|
16
|
+
* pg-boss, and why this is never what a deployed process does.
|
|
17
|
+
*
|
|
18
|
+
* In UTC, as the deployed schedule is. The next firing is worked out from the
|
|
19
|
+
* clock after each run rather than read off a sequence, so a run that outlasts
|
|
20
|
+
* its interval skips what it overran instead of firing them back to back.
|
|
21
|
+
*/
|
|
22
|
+
export declare function scheduleInProcess(expression: string, fire: () => Promise<unknown> | unknown): InProcessSchedule;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/crons/runCron.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* Run a cron once, in this process: what an EventBridge rule's invocation does
|
|
27
|
+
* on Lambda, for a server that schedules its own.
|
|
28
|
+
*
|
|
29
|
+
* The same steps as the Lambda adaptor — its services, its worker's database
|
|
30
|
+
* as `db`, an auditor when it declared one, its output parsed, its events
|
|
31
|
+
* published — because a cron that ran differently on a server than deployed
|
|
32
|
+
* would pass locally and fail where it matters. Services come through the
|
|
33
|
+
* discovery, which keeps one instance of each, so running every minute does not
|
|
34
|
+
* reconnect a database every minute.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runCron(cron: Cron<any, any, any, any, any, any>, serviceDiscovery: ServiceDiscovery<any>): Promise<unknown>;
|
|
37
|
+
//#endregion
|
|
3
38
|
//#region src/crons/schedule.d.ts
|
|
4
39
|
/**
|
|
5
40
|
* A schedule a cron runner can take, as standard five-field cron.
|
|
@@ -30,5 +65,5 @@ export declare class UnrepresentableSchedule extends Error {
|
|
|
30
65
|
*/
|
|
31
66
|
export declare function toCronExpression(schedule: ScheduleExpression): string;
|
|
32
67
|
//#endregion
|
|
33
|
-
export { Cron, CronBuilder, type CronExpression, type RateExpression, type ScheduleExpression };
|
|
68
|
+
export { Cron, CronBuilder, type CronExpression, type InProcessSchedule, type RateExpression, type ScheduleExpression };
|
|
34
69
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/crons/schedule.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"mappings":";;;;;UASiB;EAChB;;;;;;;;;;;;;;;wBAgBe,kBACf,oBACA,YAAY,6BACV;;;;;;;;;;;;;;wBCVmB,QACrB,MAAM,oCACN,kBAAkB,wBAChB;;;;;;;;;;;;;;;;;;;;;qBCFU,gCAAgC;WAElC;EADV,YACU,kBACT;;;;;;;wBAuBc,iBAAiB,UAAU"}
|
package/dist/crons/index.mjs
CHANGED
|
@@ -1,4 +1,102 @@
|
|
|
1
|
+
import { a as publishEvents } from "../functions-Di6n5ltG.mjs";
|
|
1
2
|
import { n as Cron, t as CronBuilder } from "../CronBuilder-CCgt0wCE.mjs";
|
|
3
|
+
import { CronExpressionParser } from "cron-parser";
|
|
4
|
+
import { runWithRequestContext } from "@geekmidas/services";
|
|
5
|
+
//#region src/crons/inProcess.ts
|
|
6
|
+
/**
|
|
7
|
+
* The longest delay a timer takes. Node clamps anything longer to 1ms, so a
|
|
8
|
+
* monthly cron would fire at once and then every millisecond.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_DELAY = 2 ** 31 - 1;
|
|
11
|
+
/**
|
|
12
|
+
* Run `fire` on a cron expression, in this process, by timer.
|
|
13
|
+
*
|
|
14
|
+
* For the one process that has no broker to keep a schedule: `gkm dev` on a
|
|
15
|
+
* target whose crons are infrastructure once deployed — an EventBridge rule on
|
|
16
|
+
* AWS. Deployed, a timer is the wrong tool: each replica would fire every job
|
|
17
|
+
* once, and nothing would say so. That is why a server schedules through
|
|
18
|
+
* pg-boss, and why this is never what a deployed process does.
|
|
19
|
+
*
|
|
20
|
+
* In UTC, as the deployed schedule is. The next firing is worked out from the
|
|
21
|
+
* clock after each run rather than read off a sequence, so a run that outlasts
|
|
22
|
+
* its interval skips what it overran instead of firing them back to back.
|
|
23
|
+
*/
|
|
24
|
+
function scheduleInProcess(expression, fire) {
|
|
25
|
+
CronExpressionParser.parse(expression, { tz: "UTC" });
|
|
26
|
+
let timer;
|
|
27
|
+
let stopped = false;
|
|
28
|
+
const arm = (at) => {
|
|
29
|
+
if (stopped) return;
|
|
30
|
+
const delay = at - Date.now();
|
|
31
|
+
timer = setTimeout(async () => {
|
|
32
|
+
if (Date.now() < at) return arm(at);
|
|
33
|
+
await fire();
|
|
34
|
+
arm(nextAfter(expression, /* @__PURE__ */ new Date()));
|
|
35
|
+
}, Math.min(Math.max(delay, 0), MAX_DELAY));
|
|
36
|
+
timer.unref?.();
|
|
37
|
+
};
|
|
38
|
+
arm(nextAfter(expression, /* @__PURE__ */ new Date()));
|
|
39
|
+
return { stop() {
|
|
40
|
+
stopped = true;
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
} };
|
|
43
|
+
}
|
|
44
|
+
function nextAfter(expression, from) {
|
|
45
|
+
return CronExpressionParser.parse(expression, {
|
|
46
|
+
tz: "UTC",
|
|
47
|
+
currentDate: from
|
|
48
|
+
}).next().getTime();
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/crons/runCron.ts
|
|
52
|
+
/**
|
|
53
|
+
* Run a cron once, in this process: what an EventBridge rule's invocation does
|
|
54
|
+
* on Lambda, for a server that schedules its own.
|
|
55
|
+
*
|
|
56
|
+
* The same steps as the Lambda adaptor — its services, its worker's database
|
|
57
|
+
* as `db`, an auditor when it declared one, its output parsed, its events
|
|
58
|
+
* published — because a cron that ran differently on a server than deployed
|
|
59
|
+
* would pass locally and fail where it matters. Services come through the
|
|
60
|
+
* discovery, which keeps one instance of each, so running every minute does not
|
|
61
|
+
* reconnect a database every minute.
|
|
62
|
+
*/
|
|
63
|
+
async function runCron(cron, serviceDiscovery) {
|
|
64
|
+
const services = cron.services.length > 0 ? await serviceDiscovery.register(cron.services) : {};
|
|
65
|
+
const db = cron.databaseService ? (await serviceDiscovery.register([cron.databaseService]))[cron.databaseService.serviceName] : void 0;
|
|
66
|
+
let auditor;
|
|
67
|
+
if (cron.auditorStorageService) {
|
|
68
|
+
const { DefaultAuditor } = await import("@geekmidas/audit");
|
|
69
|
+
const storage = (await serviceDiscovery.register([cron.auditorStorageService]))[cron.auditorStorageService.serviceName];
|
|
70
|
+
auditor = new DefaultAuditor({
|
|
71
|
+
actor: {
|
|
72
|
+
id: "system",
|
|
73
|
+
type: "system"
|
|
74
|
+
},
|
|
75
|
+
storage,
|
|
76
|
+
metadata: { function: cron.type }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const logger = cron.logger;
|
|
80
|
+
const requestId = `cron-${Date.now()}`;
|
|
81
|
+
return runWithRequestContext({
|
|
82
|
+
logger,
|
|
83
|
+
requestId,
|
|
84
|
+
startTime: Date.now()
|
|
85
|
+
}, async () => {
|
|
86
|
+
const response = await cron.fn({
|
|
87
|
+
input: void 0,
|
|
88
|
+
services,
|
|
89
|
+
logger,
|
|
90
|
+
db,
|
|
91
|
+
auditor
|
|
92
|
+
});
|
|
93
|
+
const output = await cron.parseOutput(response);
|
|
94
|
+
await auditor?.flush();
|
|
95
|
+
await publishEvents(logger, serviceDiscovery, cron.events, output);
|
|
96
|
+
return output;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
2
100
|
//#region src/crons/schedule.ts
|
|
3
101
|
/**
|
|
4
102
|
* A schedule a cron runner can take, as standard five-field cron.
|
|
@@ -63,6 +161,6 @@ function toCronExpression(schedule) {
|
|
|
63
161
|
}
|
|
64
162
|
}
|
|
65
163
|
//#endregion
|
|
66
|
-
export { Cron, CronBuilder, UnrepresentableSchedule, toCronExpression };
|
|
164
|
+
export { Cron, CronBuilder, UnrepresentableSchedule, runCron, scheduleInProcess, toCronExpression };
|
|
67
165
|
|
|
68
166
|
//# sourceMappingURL=index.mjs.map
|
package/dist/crons/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/crons/schedule.ts"],"sourcesContent":["import type { ScheduleExpression } from './Cron';\n\n/**\n * A schedule a cron runner can take, as standard five-field cron.\n *\n * `ScheduleExpression` has two spellings. `cron(m h dom mon dow)` is already\n * five-field cron and unwraps; `rate(n unit)` is an interval, and an interval\n * is not a schedule — it says how often, where cron says when.\n *\n * Most intervals still have an exact cron equivalent, because they divide their\n * unit evenly: `rate(5 minutes)` is every fifth minute of every hour, and that\n * is `*/5 * * * *` precisely. `rate(7 hours)` has none. There is no cron field\n * that means \"every seven hours\" — `*/7` in the hour field fires at 0, 7, 14\n * and 21 and then starts over at midnight, so the gap between the last firing\n * of one day and the first of the next is three hours rather than seven.\n *\n * Those are refused rather than rounded. A job that runs at the wrong hour is\n * far harder to notice than one that refused to build, and a schedule quietly\n * adjusted by the framework is a bug nobody goes looking for.\n */\nexport class UnrepresentableSchedule extends Error {\n\tconstructor(\n\t\treadonly schedule: string,\n\t\treason: string,\n\t) {\n\t\tsuper(\n\t\t\t`\\`${schedule}\\` has no exact cron equivalent: ${reason}.\\n\\n` +\n\t\t\t\t'Cron says *when*, not *how often*, so an interval is only exact when ' +\n\t\t\t\t'it divides its unit evenly.\\n' +\n\t\t\t\t'Write the schedule you mean instead — `cron(0 */6 * * *)` for every ' +\n\t\t\t\t'sixth hour, say — or pick an interval that divides: minutes into 60, ' +\n\t\t\t\t'hours into 24.',\n\t\t);\n\t\tthis.name = 'UnrepresentableSchedule';\n\t}\n}\n\n/** Units `rate(…)` accepts, singular or plural. */\nconst UNITS = ['minute', 'hour', 'day'] as const;\ntype Unit = (typeof UNITS)[number];\n\n/**\n * Standard five-field cron for a schedule expression.\n *\n * @throws {UnrepresentableSchedule} when a `rate(…)` has no exact equivalent.\n */\nexport function toCronExpression(schedule: ScheduleExpression): string {\n\tconst cron = /^cron\\((.+)\\)$/.exec(schedule);\n\tif (cron?.[1]) return cron[1].trim();\n\n\tconst rate = /^rate\\(\\s*(\\d+)\\s+(\\w+?)s?\\s*\\)$/.exec(schedule);\n\tif (!rate) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'it is neither `cron(…)` nor `rate(n unit)`',\n\t\t);\n\t}\n\n\tconst every = Number(rate[1]);\n\tconst unit = rate[2] as Unit;\n\t/* c8 ignore next */ if (!unit)\n\t\tthrow new UnrepresentableSchedule(schedule, 'no unit');\n\n\tif (!UNITS.includes(unit)) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t`\\`${unit}\\` is not one of ${UNITS.join(', ')}`,\n\t\t);\n\t}\n\n\tif (every < 1) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'an interval must be at least 1',\n\t\t);\n\t}\n\n\tswitch (unit) {\n\t\tcase 'minute':\n\t\t\tif (every === 1) return '* * * * *';\n\t\t\tif (60 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 60, so the interval would reset at the top of each hour`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `*/${every} * * * *`;\n\n\t\tcase 'hour':\n\t\t\tif (every === 1) return '0 * * * *';\n\t\t\tif (24 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 24, so the interval would reset at midnight`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `0 */${every} * * *`;\n\n\t\tcase 'day':\n\t\t\tif (every === 1) return '0 0 * * *';\n\t\t\t// `*/n` in day-of-month restarts on the 1st, so a 2-day interval fires\n\t\t\t// on the 1st and 3rd of a 31-day month and then again on the 1st — one\n\t\t\t// day later, not two. No month length makes that come out even.\n\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\tschedule,\n\t\t\t\t'day-of-month restarts every month, so no multi-day interval is even',\n\t\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,IAAa,0BAAb,cAA6C,MAAM;CAExC;CADV,YACC,UACA,QACC;EACD,MACC,KAAK,SAAS,mCAAmC,OAAO;0JAMzD;EAVS,KAAA,WAAA;EAWT,KAAK,OAAO;CACb;AACD;;AAGA,MAAM,QAAQ;CAAC;CAAU;CAAQ;AAAK;;;;;;AAQtC,SAAgB,iBAAiB,UAAsC;CACtE,MAAM,OAAO,iBAAiB,KAAK,QAAQ;CAC3C,IAAI,OAAO,IAAI,OAAO,KAAK,EAAE,CAAC,KAAK;CAEnC,MAAM,OAAO,mCAAmC,KAAK,QAAQ;CAC7D,IAAI,CAAC,MACJ,MAAM,IAAI,wBACT,UACA,4CACD;CAGD,MAAM,QAAQ,OAAO,KAAK,EAAE;CAC5B,MAAM,OAAO,KAAK;sBACG,IAAI,CAAC,MACzB,MAAM,IAAI,wBAAwB,UAAU,SAAS;CAEtD,IAAI,CAAC,MAAM,SAAS,IAAI,GACvB,MAAM,IAAI,wBACT,UACA,KAAK,KAAK,mBAAmB,MAAM,KAAK,IAAI,GAC7C;CAGD,IAAI,QAAQ,GACX,MAAM,IAAI,wBACT,UACA,gCACD;CAGD,QAAQ,MAAR;EACC,KAAK;GACJ,IAAI,UAAU,GAAG,OAAO;GACxB,IAAI,KAAK,UAAU,GAClB,MAAM,IAAI,wBACT,UACA,GAAG,MAAM,yEACV;GAED,OAAO,KAAK,MAAM;EAEnB,KAAK;GACJ,IAAI,UAAU,GAAG,OAAO;GACxB,IAAI,KAAK,UAAU,GAClB,MAAM,IAAI,wBACT,UACA,GAAG,MAAM,6DACV;GAED,OAAO,OAAO,MAAM;EAErB,KAAK;GACJ,IAAI,UAAU,GAAG,OAAO;GAIxB,MAAM,IAAI,wBACT,UACA,qEACD;CACF;AACD"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/crons/inProcess.ts","../../src/crons/runCron.ts","../../src/crons/schedule.ts"],"sourcesContent":["import { CronExpressionParser } from 'cron-parser';\n\n/**\n * The longest delay a timer takes. Node clamps anything longer to 1ms, so a\n * monthly cron would fire at once and then every millisecond.\n */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/** A schedule that is running, and the way to stop it. */\nexport interface InProcessSchedule {\n\tstop(): void;\n}\n\n/**\n * Run `fire` on a cron expression, in this process, by timer.\n *\n * For the one process that has no broker to keep a schedule: `gkm dev` on a\n * target whose crons are infrastructure once deployed — an EventBridge rule on\n * AWS. Deployed, a timer is the wrong tool: each replica would fire every job\n * once, and nothing would say so. That is why a server schedules through\n * pg-boss, and why this is never what a deployed process does.\n *\n * In UTC, as the deployed schedule is. The next firing is worked out from the\n * clock after each run rather than read off a sequence, so a run that outlasts\n * its interval skips what it overran instead of firing them back to back.\n */\nexport function scheduleInProcess(\n\texpression: string,\n\tfire: () => Promise<unknown> | unknown,\n): InProcessSchedule {\n\t// Parsed once up front, so a bad expression throws here — to the caller\n\t// scheduling it — rather than inside a timer.\n\tCronExpressionParser.parse(expression, { tz: 'UTC' });\n\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tlet stopped = false;\n\n\tconst arm = (at: number) => {\n\t\tif (stopped) return;\n\t\tconst delay = at - Date.now();\n\t\ttimer = setTimeout(\n\t\t\tasync () => {\n\t\t\t\tif (Date.now() < at) return arm(at);\n\t\t\t\tawait fire();\n\t\t\t\tarm(nextAfter(expression, new Date()));\n\t\t\t},\n\t\t\tMath.min(Math.max(delay, 0), MAX_DELAY),\n\t\t);\n\t\t// The server keeps the process alive; a schedule alone should not.\n\t\ttimer.unref?.();\n\t};\n\n\tarm(nextAfter(expression, new Date()));\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tclearTimeout(timer);\n\t\t},\n\t};\n}\n\nfunction nextAfter(expression: string, from: Date): number {\n\treturn CronExpressionParser.parse(expression, {\n\t\ttz: 'UTC',\n\t\tcurrentDate: from,\n\t})\n\t\t.next()\n\t\t.getTime();\n}\n","import type { AuditStorage } from '@geekmidas/audit';\nimport {\n\trunWithRequestContext,\n\ttype ServiceDiscovery,\n} from '@geekmidas/services';\nimport { publishEvents } from '../publisher';\nimport type { Cron } from './Cron';\n\n/**\n * Run a cron once, in this process: what an EventBridge rule's invocation does\n * on Lambda, for a server that schedules its own.\n *\n * The same steps as the Lambda adaptor — its services, its worker's database\n * as `db`, an auditor when it declared one, its output parsed, its events\n * published — because a cron that ran differently on a server than deployed\n * would pass locally and fail where it matters. Services come through the\n * discovery, which keeps one instance of each, so running every minute does not\n * reconnect a database every minute.\n */\nexport async function runCron(\n\tcron: Cron<any, any, any, any, any, any>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n): Promise<unknown> {\n\tconst services =\n\t\tcron.services.length > 0\n\t\t\t? await serviceDiscovery.register(cron.services)\n\t\t\t: {};\n\n\tconst db = cron.databaseService\n\t\t? (await serviceDiscovery.register([cron.databaseService]))[\n\t\t\t\tcron.databaseService.serviceName\n\t\t\t]\n\t\t: undefined;\n\n\t// Only imported for a cron that audits: `@geekmidas/audit` is optional.\n\tlet auditor: { flush(): Promise<void> } | undefined;\n\tif (cron.auditorStorageService) {\n\t\tconst { DefaultAuditor } = await import('@geekmidas/audit');\n\t\tconst registered: Record<string, unknown> = await serviceDiscovery.register(\n\t\t\t[cron.auditorStorageService],\n\t\t);\n\t\tconst storage = registered[\n\t\t\tcron.auditorStorageService.serviceName\n\t\t] as AuditStorage;\n\t\tauditor = new DefaultAuditor({\n\t\t\tactor: { id: 'system', type: 'system' },\n\t\t\tstorage,\n\t\t\tmetadata: { function: cron.type },\n\t\t});\n\t}\n\n\tconst logger = cron.logger;\n\tconst requestId = `cron-${Date.now()}`;\n\n\treturn runWithRequestContext(\n\t\t{ logger, requestId, startTime: Date.now() },\n\t\tasync () => {\n\t\t\tconst response = await cron.fn({\n\t\t\t\tinput: undefined,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t\tauditor,\n\t\t\t} as any);\n\t\t\tconst output = await cron.parseOutput(response);\n\n\t\t\tawait auditor?.flush();\n\t\t\t// The cron's output schema is erased by the `any` it is typed with here.\n\t\t\tawait publishEvents(\n\t\t\t\tlogger,\n\t\t\t\tserviceDiscovery,\n\t\t\t\tcron.events,\n\t\t\t\toutput as never,\n\t\t\t);\n\n\t\t\treturn output;\n\t\t},\n\t);\n}\n","import type { ScheduleExpression } from './Cron';\n\n/**\n * A schedule a cron runner can take, as standard five-field cron.\n *\n * `ScheduleExpression` has two spellings. `cron(m h dom mon dow)` is already\n * five-field cron and unwraps; `rate(n unit)` is an interval, and an interval\n * is not a schedule — it says how often, where cron says when.\n *\n * Most intervals still have an exact cron equivalent, because they divide their\n * unit evenly: `rate(5 minutes)` is every fifth minute of every hour, and that\n * is `*/5 * * * *` precisely. `rate(7 hours)` has none. There is no cron field\n * that means \"every seven hours\" — `*/7` in the hour field fires at 0, 7, 14\n * and 21 and then starts over at midnight, so the gap between the last firing\n * of one day and the first of the next is three hours rather than seven.\n *\n * Those are refused rather than rounded. A job that runs at the wrong hour is\n * far harder to notice than one that refused to build, and a schedule quietly\n * adjusted by the framework is a bug nobody goes looking for.\n */\nexport class UnrepresentableSchedule extends Error {\n\tconstructor(\n\t\treadonly schedule: string,\n\t\treason: string,\n\t) {\n\t\tsuper(\n\t\t\t`\\`${schedule}\\` has no exact cron equivalent: ${reason}.\\n\\n` +\n\t\t\t\t'Cron says *when*, not *how often*, so an interval is only exact when ' +\n\t\t\t\t'it divides its unit evenly.\\n' +\n\t\t\t\t'Write the schedule you mean instead — `cron(0 */6 * * *)` for every ' +\n\t\t\t\t'sixth hour, say — or pick an interval that divides: minutes into 60, ' +\n\t\t\t\t'hours into 24.',\n\t\t);\n\t\tthis.name = 'UnrepresentableSchedule';\n\t}\n}\n\n/** Units `rate(…)` accepts, singular or plural. */\nconst UNITS = ['minute', 'hour', 'day'] as const;\ntype Unit = (typeof UNITS)[number];\n\n/**\n * Standard five-field cron for a schedule expression.\n *\n * @throws {UnrepresentableSchedule} when a `rate(…)` has no exact equivalent.\n */\nexport function toCronExpression(schedule: ScheduleExpression): string {\n\tconst cron = /^cron\\((.+)\\)$/.exec(schedule);\n\tif (cron?.[1]) return cron[1].trim();\n\n\tconst rate = /^rate\\(\\s*(\\d+)\\s+(\\w+?)s?\\s*\\)$/.exec(schedule);\n\tif (!rate) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'it is neither `cron(…)` nor `rate(n unit)`',\n\t\t);\n\t}\n\n\tconst every = Number(rate[1]);\n\tconst unit = rate[2] as Unit;\n\t/* c8 ignore next */ if (!unit)\n\t\tthrow new UnrepresentableSchedule(schedule, 'no unit');\n\n\tif (!UNITS.includes(unit)) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t`\\`${unit}\\` is not one of ${UNITS.join(', ')}`,\n\t\t);\n\t}\n\n\tif (every < 1) {\n\t\tthrow new UnrepresentableSchedule(\n\t\t\tschedule,\n\t\t\t'an interval must be at least 1',\n\t\t);\n\t}\n\n\tswitch (unit) {\n\t\tcase 'minute':\n\t\t\tif (every === 1) return '* * * * *';\n\t\t\tif (60 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 60, so the interval would reset at the top of each hour`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `*/${every} * * * *`;\n\n\t\tcase 'hour':\n\t\t\tif (every === 1) return '0 * * * *';\n\t\t\tif (24 % every !== 0) {\n\t\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\t\tschedule,\n\t\t\t\t\t`${every} does not divide 24, so the interval would reset at midnight`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn `0 */${every} * * *`;\n\n\t\tcase 'day':\n\t\t\tif (every === 1) return '0 0 * * *';\n\t\t\t// `*/n` in day-of-month restarts on the 1st, so a 2-day interval fires\n\t\t\t// on the 1st and 3rd of a 31-day month and then again on the 1st — one\n\t\t\t// day later, not two. No month length makes that come out even.\n\t\t\tthrow new UnrepresentableSchedule(\n\t\t\t\tschedule,\n\t\t\t\t'day-of-month restarts every month, so no multi-day interval is even',\n\t\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;AAMA,MAAM,YAAY,KAAK,KAAK;;;;;;;;;;;;;;AAoB5B,SAAgB,kBACf,YACA,MACoB;CAGpB,qBAAqB,MAAM,YAAY,EAAE,IAAI,MAAM,CAAC;CAEpD,IAAI;CACJ,IAAI,UAAU;CAEd,MAAM,OAAO,OAAe;EAC3B,IAAI,SAAS;EACb,MAAM,QAAQ,KAAK,KAAK,IAAI;EAC5B,QAAQ,WACP,YAAY;GACX,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,IAAI,EAAE;GAClC,MAAM,KAAK;GACX,IAAI,UAAU,4BAAY,IAAI,KAAK,CAAC,CAAC;EACtC,GACA,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,SAAS,CACvC;EAEA,MAAM,QAAQ;CACf;CAEA,IAAI,UAAU,4BAAY,IAAI,KAAK,CAAC,CAAC;CAErC,OAAO,EACN,OAAO;EACN,UAAU;EACV,aAAa,KAAK;CACnB,EACD;AACD;AAEA,SAAS,UAAU,YAAoB,MAAoB;CAC1D,OAAO,qBAAqB,MAAM,YAAY;EAC7C,IAAI;EACJ,aAAa;CACd,CAAC,CAAC,CACA,KAAK,CAAC,CACN,QAAQ;AACX;;;;;;;;;;;;;;AClDA,eAAsB,QACrB,MACA,kBACmB;CACnB,MAAM,WACL,KAAK,SAAS,SAAS,IACpB,MAAM,iBAAiB,SAAS,KAAK,QAAQ,IAC7C,CAAC;CAEL,MAAM,KAAK,KAAK,mBACZ,MAAM,iBAAiB,SAAS,CAAC,KAAK,eAAe,CAAC,EAAA,CACvD,KAAK,gBAAgB,eAErB,KAAA;CAGH,IAAI;CACJ,IAAI,KAAK,uBAAuB;EAC/B,MAAM,EAAE,mBAAmB,MAAM,OAAO;EAIxC,MAAM,WAAU,MAHkC,iBAAiB,SAClE,CAAC,KAAK,qBAAqB,CAC5B,EAAA,CAEC,KAAK,sBAAsB;EAE5B,UAAU,IAAI,eAAe;GAC5B,OAAO;IAAE,IAAI;IAAU,MAAM;GAAS;GACtC;GACA,UAAU,EAAE,UAAU,KAAK,KAAK;EACjC,CAAC;CACF;CAEA,MAAM,SAAS,KAAK;CACpB,MAAM,YAAY,QAAQ,KAAK,IAAI;CAEnC,OAAO,sBACN;EAAE;EAAQ;EAAW,WAAW,KAAK,IAAI;CAAE,GAC3C,YAAY;EACX,MAAM,WAAW,MAAM,KAAK,GAAG;GAC9B,OAAO,KAAA;GACP;GACA;GACA;GACA;EACD,CAAQ;EACR,MAAM,SAAS,MAAM,KAAK,YAAY,QAAQ;EAE9C,MAAM,SAAS,MAAM;EAErB,MAAM,cACL,QACA,kBACA,KAAK,QACL,MACD;EAEA,OAAO;CACR,CACD;AACD;;;;;;;;;;;;;;;;;;;;;AC1DA,IAAa,0BAAb,cAA6C,MAAM;CAExC;CADV,YACC,UACA,QACC;EACD,MACC,KAAK,SAAS,mCAAmC,OAAO;0JAMzD;EAVS,KAAA,WAAA;EAWT,KAAK,OAAO;CACb;AACD;;AAGA,MAAM,QAAQ;CAAC;CAAU;CAAQ;AAAK;;;;;;AAQtC,SAAgB,iBAAiB,UAAsC;CACtE,MAAM,OAAO,iBAAiB,KAAK,QAAQ;CAC3C,IAAI,OAAO,IAAI,OAAO,KAAK,EAAE,CAAC,KAAK;CAEnC,MAAM,OAAO,mCAAmC,KAAK,QAAQ;CAC7D,IAAI,CAAC,MACJ,MAAM,IAAI,wBACT,UACA,4CACD;CAGD,MAAM,QAAQ,OAAO,KAAK,EAAE;CAC5B,MAAM,OAAO,KAAK;sBACG,IAAI,CAAC,MACzB,MAAM,IAAI,wBAAwB,UAAU,SAAS;CAEtD,IAAI,CAAC,MAAM,SAAS,IAAI,GACvB,MAAM,IAAI,wBACT,UACA,KAAK,KAAK,mBAAmB,MAAM,KAAK,IAAI,GAC7C;CAGD,IAAI,QAAQ,GACX,MAAM,IAAI,wBACT,UACA,gCACD;CAGD,QAAQ,MAAR;EACC,KAAK;GACJ,IAAI,UAAU,GAAG,OAAO;GACxB,IAAI,KAAK,UAAU,GAClB,MAAM,IAAI,wBACT,UACA,GAAG,MAAM,yEACV;GAED,OAAO,KAAK,MAAM;EAEnB,KAAK;GACJ,IAAI,UAAU,GAAG,OAAO;GACxB,IAAI,KAAK,UAAU,GAClB,MAAM,IAAI,wBACT,UACA,GAAG,MAAM,6DACV;GAED,OAAO,OAAO,MAAM;EAErB,KAAK;GACJ,IAAI,UAAU,GAAG,OAAO;GAIxB,MAAM,IAAI,wBACT,UACA,qEACD;CACF;AACD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as publishConstructEvents } from "../Construct-QXnl3VqT.mjs";
|
|
2
|
-
import { A as getSecurityScheme, C as Authorizer, D as OAuthFlows, E as OAuthFlow, O as SecurityScheme, S as MappedAudit, T as BuiltInSecuritySchemeId, _ as RlsBypass, a as EndpointContext, b as RlsContextExtractor, c as EndpointSchemas, d as ResponseBuilder, g as RLS_BYPASS, h as SessionAuth, i as Endpoint, j as isBuiltInSecurityScheme, k as createAuthorizer, l as EndpointSurface, m as NoAuthenticator, n as ConvertRouteParams, o as EndpointHandler, r as CookieFn, s as EndpointOutput, u as HeaderFn, v as RlsConfig, w as BUILT_IN_SECURITY_SCHEMES, x as ActorExtractor, y as RlsContext } from "../Endpoint-
|
|
3
|
-
import { n as EndpointBuilder, t as EndpointFactory } from "../EndpointFactory-
|
|
2
|
+
import { A as getSecurityScheme, C as Authorizer, D as OAuthFlows, E as OAuthFlow, O as SecurityScheme, S as MappedAudit, T as BuiltInSecuritySchemeId, _ as RlsBypass, a as EndpointContext, b as RlsContextExtractor, c as EndpointSchemas, d as ResponseBuilder, g as RLS_BYPASS, h as SessionAuth, i as Endpoint, j as isBuiltInSecurityScheme, k as createAuthorizer, l as EndpointSurface, m as NoAuthenticator, n as ConvertRouteParams, o as EndpointHandler, r as CookieFn, s as EndpointOutput, u as HeaderFn, v as RlsConfig, w as BUILT_IN_SECURITY_SCHEMES, x as ActorExtractor, y as RlsContext } from "../Endpoint-KyMeR-B-.mjs";
|
|
3
|
+
import { n as EndpointBuilder, t as EndpointFactory } from "../EndpointFactory-GYkAuG3u.mjs";
|
|
4
4
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
5
5
|
import { Context } from "hono";
|
|
6
6
|
//#region src/endpoints/lazyAccessors.d.ts
|
package/dist/rest-api.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as Declarable, t as Authenticator } from "./construct-interface-DluK3_da.mjs";
|
|
2
|
-
import { T as BuiltInSecuritySchemeId } from "./Endpoint-
|
|
3
|
-
import { t as EndpointFactory } from "./EndpointFactory-
|
|
2
|
+
import { T as BuiltInSecuritySchemeId } from "./Endpoint-KyMeR-B-.mjs";
|
|
3
|
+
import { t as EndpointFactory } from "./EndpointFactory-GYkAuG3u.mjs";
|
|
4
4
|
import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
|
|
5
5
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
6
6
|
import { Logger } from "@geekmidas/logger";
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
let _geekmidas_services = require("@geekmidas/services");
|
|
2
|
+
//#region src/subscribers/runSubscriber.ts
|
|
3
|
+
/** The subscriber's output did not match its `.output()` schema. */
|
|
4
|
+
var SubscriberOutputInvalid = class extends Error {
|
|
5
|
+
issues;
|
|
6
|
+
constructor(issues) {
|
|
7
|
+
super("Subscriber output validation failed: the handler's result does not match its .output() schema.");
|
|
8
|
+
this.issues = issues;
|
|
9
|
+
this.name = "SubscriberOutputInvalid";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */
|
|
13
|
+
function subscribedEvents(subscriber, rawEvent, logger) {
|
|
14
|
+
const events = [];
|
|
15
|
+
const records = rawEvent.Records ?? [];
|
|
16
|
+
const first = records[0];
|
|
17
|
+
const parse = first?.eventSource === "aws:sqs" ? (record) => parseSQSRecord(record) : first?.EventSource === "aws:sns" ? (record) => parseSNSRecord(record) : void 0;
|
|
18
|
+
if (!parse) return events;
|
|
19
|
+
for (const record of records) try {
|
|
20
|
+
const event = parse(record);
|
|
21
|
+
if (isSubscribed(subscriber, event)) events.push(event);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error({
|
|
24
|
+
error,
|
|
25
|
+
record
|
|
26
|
+
}, "Failed to parse record");
|
|
27
|
+
}
|
|
28
|
+
return events;
|
|
29
|
+
}
|
|
30
|
+
/** The subscriber's services and database, registered as functions do. */
|
|
31
|
+
async function subscriberContext(subscriber, envParser) {
|
|
32
|
+
const discovery = _geekmidas_services.ServiceDiscovery.getInstance(envParser);
|
|
33
|
+
const services = subscriber.services.length > 0 ? await discovery.register(subscriber.services) : {};
|
|
34
|
+
const service = subscriber.databaseService;
|
|
35
|
+
return {
|
|
36
|
+
services,
|
|
37
|
+
db: service ? (await discovery.register([service]))[service.serviceName] : void 0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Runs the handler on `events`, and checks its output against its schema. */
|
|
41
|
+
async function runSubscriber(subscriber, context) {
|
|
42
|
+
if (context.events.length === 0) {
|
|
43
|
+
context.logger.info("No subscribed events to process");
|
|
44
|
+
return { batchItemFailures: [] };
|
|
45
|
+
}
|
|
46
|
+
const { events, services, db, logger } = context;
|
|
47
|
+
const result = await subscriber.handler({
|
|
48
|
+
events,
|
|
49
|
+
services,
|
|
50
|
+
logger,
|
|
51
|
+
db
|
|
52
|
+
});
|
|
53
|
+
if (subscriber.outputSchema && result) {
|
|
54
|
+
const validation = await subscriber.outputSchema["~standard"].validate(result);
|
|
55
|
+
if (validation.issues) {
|
|
56
|
+
context.logger.error({ issues: validation.issues }, "Subscriber output validation failed");
|
|
57
|
+
throw new SubscriberOutputInvalid(validation.issues);
|
|
58
|
+
}
|
|
59
|
+
return validation.value;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function isSubscribed(subscriber, event) {
|
|
64
|
+
if (typeof event !== "object" || !event?.type) return true;
|
|
65
|
+
if (!subscriber.subscribedEvents) return true;
|
|
66
|
+
return subscriber.subscribedEvents.includes(event.type);
|
|
67
|
+
}
|
|
68
|
+
function parseSNSRecord(record) {
|
|
69
|
+
const message = safeJsonParse(record.Sns.Message);
|
|
70
|
+
const messageType = record.Sns.MessageAttributes?.type?.Value;
|
|
71
|
+
if (message === null) return messageType ? {
|
|
72
|
+
type: messageType,
|
|
73
|
+
payload: record.Sns.Message
|
|
74
|
+
} : record.Sns.Message;
|
|
75
|
+
if (message.type) return message;
|
|
76
|
+
return messageType ? {
|
|
77
|
+
type: messageType,
|
|
78
|
+
payload: message
|
|
79
|
+
} : message;
|
|
80
|
+
}
|
|
81
|
+
function parseSQSRecord(record) {
|
|
82
|
+
const body = safeJsonParse(record.body);
|
|
83
|
+
if (body === null) return record.body;
|
|
84
|
+
if (body.Type === "Notification" && body.Message) {
|
|
85
|
+
const snsMessage = safeJsonParse(body.Message);
|
|
86
|
+
const messageType = body.MessageAttributes?.type?.Value;
|
|
87
|
+
if (snsMessage === null) return messageType ? {
|
|
88
|
+
type: messageType,
|
|
89
|
+
payload: body.Message
|
|
90
|
+
} : body.Message;
|
|
91
|
+
if (snsMessage.type) return snsMessage;
|
|
92
|
+
return messageType ? {
|
|
93
|
+
type: messageType,
|
|
94
|
+
payload: snsMessage
|
|
95
|
+
} : snsMessage;
|
|
96
|
+
}
|
|
97
|
+
return body;
|
|
98
|
+
}
|
|
99
|
+
function safeJsonParse(value) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(value);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
Object.defineProperty(exports, "runSubscriber", {
|
|
108
|
+
enumerable: true,
|
|
109
|
+
get: function() {
|
|
110
|
+
return runSubscriber;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
Object.defineProperty(exports, "subscribedEvents", {
|
|
114
|
+
enumerable: true,
|
|
115
|
+
get: function() {
|
|
116
|
+
return subscribedEvents;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
Object.defineProperty(exports, "subscriberContext", {
|
|
120
|
+
enumerable: true,
|
|
121
|
+
get: function() {
|
|
122
|
+
return subscriberContext;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=runSubscriber-BRp9a_qi.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runSubscriber-BRp9a_qi.cjs","names":["ServiceDiscovery"],"sources":["../src/subscribers/runSubscriber.ts"],"sourcesContent":["/**\n * What a subscriber does with a batch of records, however they arrived: parse\n * them into events, keep the ones it subscribed to, and run its handler with\n * its services and database.\n *\n * Shared by the Lambda adaptor, which wraps it in middy, and the SNS push\n * adaptor, which a server runs and which must not need middy to load.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport type { SNSEvent, SNSEventRecord, SQSEvent, SQSRecord } from 'aws-lambda';\nimport type { Subscriber } from './Subscriber';\n\ntype AnySubscriber = Subscriber<any, any, any, any, any, any, any>;\n\n/** The subscriber's output did not match its `.output()` schema. */\nexport class SubscriberOutputInvalid extends Error {\n\tconstructor(readonly issues: ReadonlyArray<unknown>) {\n\t\tsuper(\n\t\t\t\"Subscriber output validation failed: the handler's result does not match its .output() schema.\",\n\t\t);\n\t\tthis.name = 'SubscriberOutputInvalid';\n\t}\n}\n\n/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */\nexport function subscribedEvents(\n\tsubscriber: AnySubscriber,\n\trawEvent: { Records?: readonly unknown[] },\n\tlogger: Logger,\n): any[] {\n\tconst events: any[] = [];\n\tconst records = rawEvent.Records ?? [];\n\tconst first = records[0] as any;\n\tconst parse =\n\t\tfirst?.eventSource === 'aws:sqs'\n\t\t\t? (record: any) => parseSQSRecord(record)\n\t\t\t: first?.EventSource === 'aws:sns'\n\t\t\t\t? (record: any) => parseSNSRecord(record)\n\t\t\t\t: undefined;\n\tif (!parse) return events;\n\n\tfor (const record of records) {\n\t\ttry {\n\t\t\tconst event = parse(record);\n\t\t\tif (isSubscribed(subscriber, event)) events.push(event);\n\t\t} catch (error) {\n\t\t\tlogger.error({ error, record }, 'Failed to parse record');\n\t\t}\n\t}\n\treturn events;\n}\n\n/** The subscriber's services and database, registered as functions do. */\nexport async function subscriberContext(\n\tsubscriber: AnySubscriber,\n\tenvParser: EnvironmentParser<{}>,\n): Promise<{ services: Record<string, unknown>; db: unknown }> {\n\tconst discovery = ServiceDiscovery.getInstance(envParser);\n\tconst services =\n\t\tsubscriber.services.length > 0\n\t\t\t? await discovery.register(subscriber.services)\n\t\t\t: {};\n\tconst service = subscriber.databaseService;\n\tconst db = service\n\t\t? (await discovery.register([service]))[service.serviceName]\n\t\t: undefined;\n\treturn { services, db };\n}\n\n/** Runs the handler on `events`, and checks its output against its schema. */\nexport async function runSubscriber(\n\tsubscriber: AnySubscriber,\n\tcontext: {\n\t\tevents: any[];\n\t\tservices: Record<string, unknown>;\n\t\tdb: unknown;\n\t\tlogger: Logger;\n\t},\n): Promise<unknown> {\n\tif (context.events.length === 0) {\n\t\tcontext.logger.info('No subscribed events to process');\n\t\treturn { batchItemFailures: [] };\n\t}\n\n\tconst { events, services, db, logger } = context;\n\tconst result = await subscriber.handler({\n\t\tevents,\n\t\tservices,\n\t\tlogger,\n\t\tdb,\n\t} as any);\n\n\tif (subscriber.outputSchema && result) {\n\t\tconst validation =\n\t\t\tawait subscriber.outputSchema['~standard'].validate(result);\n\t\tif (validation.issues) {\n\t\t\tcontext.logger.error(\n\t\t\t\t{ issues: validation.issues },\n\t\t\t\t'Subscriber output validation failed',\n\t\t\t);\n\t\t\tthrow new SubscriberOutputInvalid(validation.issues);\n\t\t}\n\t\treturn validation.value;\n\t}\n\n\treturn result;\n}\n\nfunction isSubscribed(subscriber: AnySubscriber, event: any): boolean {\n\t// No event type (raw string/non-object) — always include\n\tif (typeof event !== 'object' || !event?.type) return true;\n\t// No filter configured — accept all\n\tif (!subscriber.subscribedEvents) return true;\n\treturn subscriber.subscribedEvents.includes(event.type);\n}\n\nfunction parseSNSRecord(record: SNSEventRecord): any {\n\tconst message = safeJsonParse(record.Sns.Message);\n\tconst messageType = record.Sns.MessageAttributes?.type?.Value;\n\n\t// Not JSON — wrap raw string with type from MessageAttributes if available\n\tif (message === null) {\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: record.Sns.Message }\n\t\t\t: record.Sns.Message;\n\t}\n\n\tif (message.type) {\n\t\treturn message; // Full event format: { type, payload }\n\t}\n\n\t// Payload-only format: type is in MessageAttributes\n\treturn messageType ? { type: messageType, payload: message } : message;\n}\n\nfunction parseSQSRecord(record: SQSRecord): any {\n\tconst body = safeJsonParse(record.body);\n\n\t// Not JSON — return raw body as-is\n\tif (body === null) {\n\t\treturn record.body;\n\t}\n\n\t// Check if this is an SNS message wrapped in SQS\n\tif (body.Type === 'Notification' && body.Message) {\n\t\tconst snsMessage = safeJsonParse(body.Message);\n\t\tconst messageType = body.MessageAttributes?.type?.Value;\n\n\t\t// SNS Message not JSON — wrap with type from MessageAttributes if available\n\t\tif (snsMessage === null) {\n\t\t\treturn messageType\n\t\t\t\t? { type: messageType, payload: body.Message }\n\t\t\t\t: body.Message;\n\t\t}\n\n\t\tif (snsMessage.type) {\n\t\t\treturn snsMessage; // Full event format: { type, payload }\n\t\t}\n\n\t\t// Payload-only format: type is in MessageAttributes\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: snsMessage }\n\t\t\t: snsMessage;\n\t}\n\n\t// Direct SQS message\n\treturn body;\n}\n\nfunction safeJsonParse(value: string): any | null {\n\ttry {\n\t\treturn JSON.parse(value);\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ServiceDiscovery } from "@geekmidas/services";
|
|
2
|
+
//#region src/subscribers/runSubscriber.ts
|
|
3
|
+
/** The subscriber's output did not match its `.output()` schema. */
|
|
4
|
+
var SubscriberOutputInvalid = class extends Error {
|
|
5
|
+
issues;
|
|
6
|
+
constructor(issues) {
|
|
7
|
+
super("Subscriber output validation failed: the handler's result does not match its .output() schema.");
|
|
8
|
+
this.issues = issues;
|
|
9
|
+
this.name = "SubscriberOutputInvalid";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */
|
|
13
|
+
function subscribedEvents(subscriber, rawEvent, logger) {
|
|
14
|
+
const events = [];
|
|
15
|
+
const records = rawEvent.Records ?? [];
|
|
16
|
+
const first = records[0];
|
|
17
|
+
const parse = first?.eventSource === "aws:sqs" ? (record) => parseSQSRecord(record) : first?.EventSource === "aws:sns" ? (record) => parseSNSRecord(record) : void 0;
|
|
18
|
+
if (!parse) return events;
|
|
19
|
+
for (const record of records) try {
|
|
20
|
+
const event = parse(record);
|
|
21
|
+
if (isSubscribed(subscriber, event)) events.push(event);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error({
|
|
24
|
+
error,
|
|
25
|
+
record
|
|
26
|
+
}, "Failed to parse record");
|
|
27
|
+
}
|
|
28
|
+
return events;
|
|
29
|
+
}
|
|
30
|
+
/** The subscriber's services and database, registered as functions do. */
|
|
31
|
+
async function subscriberContext(subscriber, envParser) {
|
|
32
|
+
const discovery = ServiceDiscovery.getInstance(envParser);
|
|
33
|
+
const services = subscriber.services.length > 0 ? await discovery.register(subscriber.services) : {};
|
|
34
|
+
const service = subscriber.databaseService;
|
|
35
|
+
return {
|
|
36
|
+
services,
|
|
37
|
+
db: service ? (await discovery.register([service]))[service.serviceName] : void 0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Runs the handler on `events`, and checks its output against its schema. */
|
|
41
|
+
async function runSubscriber(subscriber, context) {
|
|
42
|
+
if (context.events.length === 0) {
|
|
43
|
+
context.logger.info("No subscribed events to process");
|
|
44
|
+
return { batchItemFailures: [] };
|
|
45
|
+
}
|
|
46
|
+
const { events, services, db, logger } = context;
|
|
47
|
+
const result = await subscriber.handler({
|
|
48
|
+
events,
|
|
49
|
+
services,
|
|
50
|
+
logger,
|
|
51
|
+
db
|
|
52
|
+
});
|
|
53
|
+
if (subscriber.outputSchema && result) {
|
|
54
|
+
const validation = await subscriber.outputSchema["~standard"].validate(result);
|
|
55
|
+
if (validation.issues) {
|
|
56
|
+
context.logger.error({ issues: validation.issues }, "Subscriber output validation failed");
|
|
57
|
+
throw new SubscriberOutputInvalid(validation.issues);
|
|
58
|
+
}
|
|
59
|
+
return validation.value;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function isSubscribed(subscriber, event) {
|
|
64
|
+
if (typeof event !== "object" || !event?.type) return true;
|
|
65
|
+
if (!subscriber.subscribedEvents) return true;
|
|
66
|
+
return subscriber.subscribedEvents.includes(event.type);
|
|
67
|
+
}
|
|
68
|
+
function parseSNSRecord(record) {
|
|
69
|
+
const message = safeJsonParse(record.Sns.Message);
|
|
70
|
+
const messageType = record.Sns.MessageAttributes?.type?.Value;
|
|
71
|
+
if (message === null) return messageType ? {
|
|
72
|
+
type: messageType,
|
|
73
|
+
payload: record.Sns.Message
|
|
74
|
+
} : record.Sns.Message;
|
|
75
|
+
if (message.type) return message;
|
|
76
|
+
return messageType ? {
|
|
77
|
+
type: messageType,
|
|
78
|
+
payload: message
|
|
79
|
+
} : message;
|
|
80
|
+
}
|
|
81
|
+
function parseSQSRecord(record) {
|
|
82
|
+
const body = safeJsonParse(record.body);
|
|
83
|
+
if (body === null) return record.body;
|
|
84
|
+
if (body.Type === "Notification" && body.Message) {
|
|
85
|
+
const snsMessage = safeJsonParse(body.Message);
|
|
86
|
+
const messageType = body.MessageAttributes?.type?.Value;
|
|
87
|
+
if (snsMessage === null) return messageType ? {
|
|
88
|
+
type: messageType,
|
|
89
|
+
payload: body.Message
|
|
90
|
+
} : body.Message;
|
|
91
|
+
if (snsMessage.type) return snsMessage;
|
|
92
|
+
return messageType ? {
|
|
93
|
+
type: messageType,
|
|
94
|
+
payload: snsMessage
|
|
95
|
+
} : snsMessage;
|
|
96
|
+
}
|
|
97
|
+
return body;
|
|
98
|
+
}
|
|
99
|
+
function safeJsonParse(value) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(value);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { subscribedEvents as n, subscriberContext as r, runSubscriber as t };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=runSubscriber-BYEY_bfA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runSubscriber-BYEY_bfA.mjs","names":[],"sources":["../src/subscribers/runSubscriber.ts"],"sourcesContent":["/**\n * What a subscriber does with a batch of records, however they arrived: parse\n * them into events, keep the ones it subscribed to, and run its handler with\n * its services and database.\n *\n * Shared by the Lambda adaptor, which wraps it in middy, and the SNS push\n * adaptor, which a server runs and which must not need middy to load.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport type { SNSEvent, SNSEventRecord, SQSEvent, SQSRecord } from 'aws-lambda';\nimport type { Subscriber } from './Subscriber';\n\ntype AnySubscriber = Subscriber<any, any, any, any, any, any, any>;\n\n/** The subscriber's output did not match its `.output()` schema. */\nexport class SubscriberOutputInvalid extends Error {\n\tconstructor(readonly issues: ReadonlyArray<unknown>) {\n\t\tsuper(\n\t\t\t\"Subscriber output validation failed: the handler's result does not match its .output() schema.\",\n\t\t);\n\t\tthis.name = 'SubscriberOutputInvalid';\n\t}\n}\n\n/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */\nexport function subscribedEvents(\n\tsubscriber: AnySubscriber,\n\trawEvent: { Records?: readonly unknown[] },\n\tlogger: Logger,\n): any[] {\n\tconst events: any[] = [];\n\tconst records = rawEvent.Records ?? [];\n\tconst first = records[0] as any;\n\tconst parse =\n\t\tfirst?.eventSource === 'aws:sqs'\n\t\t\t? (record: any) => parseSQSRecord(record)\n\t\t\t: first?.EventSource === 'aws:sns'\n\t\t\t\t? (record: any) => parseSNSRecord(record)\n\t\t\t\t: undefined;\n\tif (!parse) return events;\n\n\tfor (const record of records) {\n\t\ttry {\n\t\t\tconst event = parse(record);\n\t\t\tif (isSubscribed(subscriber, event)) events.push(event);\n\t\t} catch (error) {\n\t\t\tlogger.error({ error, record }, 'Failed to parse record');\n\t\t}\n\t}\n\treturn events;\n}\n\n/** The subscriber's services and database, registered as functions do. */\nexport async function subscriberContext(\n\tsubscriber: AnySubscriber,\n\tenvParser: EnvironmentParser<{}>,\n): Promise<{ services: Record<string, unknown>; db: unknown }> {\n\tconst discovery = ServiceDiscovery.getInstance(envParser);\n\tconst services =\n\t\tsubscriber.services.length > 0\n\t\t\t? await discovery.register(subscriber.services)\n\t\t\t: {};\n\tconst service = subscriber.databaseService;\n\tconst db = service\n\t\t? (await discovery.register([service]))[service.serviceName]\n\t\t: undefined;\n\treturn { services, db };\n}\n\n/** Runs the handler on `events`, and checks its output against its schema. */\nexport async function runSubscriber(\n\tsubscriber: AnySubscriber,\n\tcontext: {\n\t\tevents: any[];\n\t\tservices: Record<string, unknown>;\n\t\tdb: unknown;\n\t\tlogger: Logger;\n\t},\n): Promise<unknown> {\n\tif (context.events.length === 0) {\n\t\tcontext.logger.info('No subscribed events to process');\n\t\treturn { batchItemFailures: [] };\n\t}\n\n\tconst { events, services, db, logger } = context;\n\tconst result = await subscriber.handler({\n\t\tevents,\n\t\tservices,\n\t\tlogger,\n\t\tdb,\n\t} as any);\n\n\tif (subscriber.outputSchema && result) {\n\t\tconst validation =\n\t\t\tawait subscriber.outputSchema['~standard'].validate(result);\n\t\tif (validation.issues) {\n\t\t\tcontext.logger.error(\n\t\t\t\t{ issues: validation.issues },\n\t\t\t\t'Subscriber output validation failed',\n\t\t\t);\n\t\t\tthrow new SubscriberOutputInvalid(validation.issues);\n\t\t}\n\t\treturn validation.value;\n\t}\n\n\treturn result;\n}\n\nfunction isSubscribed(subscriber: AnySubscriber, event: any): boolean {\n\t// No event type (raw string/non-object) — always include\n\tif (typeof event !== 'object' || !event?.type) return true;\n\t// No filter configured — accept all\n\tif (!subscriber.subscribedEvents) return true;\n\treturn subscriber.subscribedEvents.includes(event.type);\n}\n\nfunction parseSNSRecord(record: SNSEventRecord): any {\n\tconst message = safeJsonParse(record.Sns.Message);\n\tconst messageType = record.Sns.MessageAttributes?.type?.Value;\n\n\t// Not JSON — wrap raw string with type from MessageAttributes if available\n\tif (message === null) {\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: record.Sns.Message }\n\t\t\t: record.Sns.Message;\n\t}\n\n\tif (message.type) {\n\t\treturn message; // Full event format: { type, payload }\n\t}\n\n\t// Payload-only format: type is in MessageAttributes\n\treturn messageType ? { type: messageType, payload: message } : message;\n}\n\nfunction parseSQSRecord(record: SQSRecord): any {\n\tconst body = safeJsonParse(record.body);\n\n\t// Not JSON — return raw body as-is\n\tif (body === null) {\n\t\treturn record.body;\n\t}\n\n\t// Check if this is an SNS message wrapped in SQS\n\tif (body.Type === 'Notification' && body.Message) {\n\t\tconst snsMessage = safeJsonParse(body.Message);\n\t\tconst messageType = body.MessageAttributes?.type?.Value;\n\n\t\t// SNS Message not JSON — wrap with type from MessageAttributes if available\n\t\tif (snsMessage === null) {\n\t\t\treturn messageType\n\t\t\t\t? { type: messageType, payload: body.Message }\n\t\t\t\t: body.Message;\n\t\t}\n\n\t\tif (snsMessage.type) {\n\t\t\treturn snsMessage; // Full event format: { type, payload }\n\t\t}\n\n\t\t// Payload-only format: type is in MessageAttributes\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: snsMessage }\n\t\t\t: snsMessage;\n\t}\n\n\t// Direct SQS message\n\treturn body;\n}\n\nfunction safeJsonParse(value: string): any | null {\n\ttry {\n\t\treturn JSON.parse(value);\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|