@rexezuge/runtime 1.0.8 → 1.1.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/dist/index.d.ts CHANGED
@@ -21,4 +21,6 @@ export { createLogger, isLogLevel, resolveLogLevel } from './logger';
21
21
  export type { LogLevel, Logger } from './logger';
22
22
  export { KvCache, KV_DOMAINS, buildKvKey, clampTtl, digest128 } from './kv';
23
23
  export type { KvDomainDef, KvDomainName, KvNamespaceLike } from './kv';
24
+ export { AbstractScheduledTask, TaskRegistry } from './scheduled';
25
+ export type { ApplicationRunHandle, ScheduledTaskEnv, TaskLogger, TaskRunRecorder, TaskRunSummary } from './scheduled';
24
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AACzG,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAC5E,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AACzG,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAC5E,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -14,4 +14,5 @@ export { AbstractQueueWorker } from './base/AbstractQueueWorker';
14
14
  export { AbstractWorkflowWorker } from './base/AbstractWorkflowWorker';
15
15
  export { createLogger, isLogLevel, resolveLogLevel } from './logger';
16
16
  export { KvCache, KV_DOMAINS, buildKvKey, clampTtl, digest128 } from './kv';
17
+ export { AbstractScheduledTask, TaskRegistry } from './scheduled';
17
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAE3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAErE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC","sourcesContent":["/**\n * `@rexezuge/runtime` — the platform foundation layer: DI, configuration,\n * worker bases, logger, KV cache. Zero dependencies (the one platform import\n * is type-only, resolved through each consumer's generated wrangler types).\n */\n\nexport { Container } from './di/Container';\nexport type { Factory, Token } from './di/Container';\nexport { asScopedContext, getRequestScope, setRequestScope } from './di/RequestScope';\nexport type { ScopedContext } from './di/RequestScope';\nexport { AppConfiguration } from './config/AppConfiguration';\nexport { EnvParser } from './config/EnvParser';\nexport { DEFAULT_SITE_URL } from './config/ConfigurationDefaults';\nexport { AbstractEntrypointWorker } from './base/AbstractEntrypointWorker';\nexport type { WorkerExecutionContext, WorkerScheduledController } from './base/AbstractEntrypointWorker';\nexport { AbstractDurableObjectWorker } from './base/AbstractDurableObjectWorker';\nexport { AbstractQueueWorker } from './base/AbstractQueueWorker';\nexport type { WorkerMessageBatch } from './base/AbstractQueueWorker';\nexport { AbstractWorkflowWorker } from './base/AbstractWorkflowWorker';\nexport type { WorkflowEventLike, WorkflowStepLike } from './base/AbstractWorkflowWorker';\nexport { createLogger, isLogLevel, resolveLogLevel } from './logger';\nexport type { LogLevel, Logger } from './logger';\nexport { KvCache, KV_DOMAINS, buildKvKey, clampTtl, digest128 } from './kv';\nexport type { KvDomainDef, KvDomainName, KvNamespaceLike } from './kv';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAE3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAErE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAE5E,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC","sourcesContent":["/**\n * `@rexezuge/runtime` — the platform foundation layer: DI, configuration,\n * worker bases, logger, KV cache. Zero dependencies (the one platform import\n * is type-only, resolved through each consumer's generated wrangler types).\n */\n\nexport { Container } from './di/Container';\nexport type { Factory, Token } from './di/Container';\nexport { asScopedContext, getRequestScope, setRequestScope } from './di/RequestScope';\nexport type { ScopedContext } from './di/RequestScope';\nexport { AppConfiguration } from './config/AppConfiguration';\nexport { EnvParser } from './config/EnvParser';\nexport { DEFAULT_SITE_URL } from './config/ConfigurationDefaults';\nexport { AbstractEntrypointWorker } from './base/AbstractEntrypointWorker';\nexport type { WorkerExecutionContext, WorkerScheduledController } from './base/AbstractEntrypointWorker';\nexport { AbstractDurableObjectWorker } from './base/AbstractDurableObjectWorker';\nexport { AbstractQueueWorker } from './base/AbstractQueueWorker';\nexport type { WorkerMessageBatch } from './base/AbstractQueueWorker';\nexport { AbstractWorkflowWorker } from './base/AbstractWorkflowWorker';\nexport type { WorkflowEventLike, WorkflowStepLike } from './base/AbstractWorkflowWorker';\nexport { createLogger, isLogLevel, resolveLogLevel } from './logger';\nexport type { LogLevel, Logger } from './logger';\nexport { KvCache, KV_DOMAINS, buildKvKey, clampTtl, digest128 } from './kv';\nexport type { KvDomainDef, KvDomainName, KvNamespaceLike } from './kv';\nexport { AbstractScheduledTask, TaskRegistry } from './scheduled';\nexport type { ApplicationRunHandle, ScheduledTaskEnv, TaskLogger, TaskRunRecorder, TaskRunSummary } from './scheduled';\n"]}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The scheduled-task Template Method, and the run-record port it records through.
3
+ *
4
+ * Converged from `apps/background/src/scheduled/{IScheduledTask,TaskRegistry}.ts`
5
+ * in seven repos, where the copies range from 13 lines (CalDAV-Bridge) to 103
6
+ * (Mail-Otter). Mail-Otter's was the one that had solved the defects the others
7
+ * carried, so this is that one.
8
+ *
9
+ * ## What the smaller copies were missing
10
+ *
11
+ * - **Run bookkeeping.** A task that fails silently leaves an operator with a
12
+ * cron that appears to run and never does. `runId` here is the record that
13
+ * says which.
14
+ * - **A failure that is recorded as a failure.** The template's `catch` writes
15
+ * `failRun`, so the task-runs table shows the reason rather than a row that
16
+ * stopped at `running`.
17
+ * - **A test seam.** `createRunRecorder` is a Factory Method so a suite
18
+ * substitutes a double without a D1 binding.
19
+ *
20
+ * ## Why the recorder is a port and not a DAO
21
+ *
22
+ * `runtime` is the DI and platform layer and has no `d1` dependency — that
23
+ * would invert the package graph. The port is four methods; the consumer binds
24
+ * its `BackgroundTaskRunDAO` at the composition root.
25
+ */
26
+ /** The summary a task reports when it finishes. */
27
+ interface TaskRunSummary {
28
+ itemsProcessed: number;
29
+ itemsFailed: number;
30
+ summary?: string;
31
+ details?: unknown;
32
+ }
33
+ /**
34
+ * Where run records go.
35
+ *
36
+ * Implemented by each repo's `BackgroundTaskRunDAO`; supplied as a factory so a
37
+ * task can be constructed before the binding is known.
38
+ */
39
+ interface TaskRunRecorder {
40
+ startRun(fields: {
41
+ taskType: string;
42
+ applicationId?: string;
43
+ }): Promise<string>;
44
+ succeedRun(runId: string, result: TaskRunSummary): Promise<void>;
45
+ failRun(runId: string, errorMessage: string, partial?: Partial<TaskRunSummary>): Promise<void>;
46
+ skipRun(runId: string, reason?: string): Promise<void>;
47
+ }
48
+ /**
49
+ * A handle to one run's completion.
50
+ *
51
+ * Every method swallows its own recording failure and logs a warning. A task
52
+ * whose work succeeded must not be reported as failed because the bookkeeping
53
+ * write did not land — that is the same class of defect as a status read that
54
+ * writes.
55
+ */
56
+ interface ApplicationRunHandle {
57
+ succeed(result: TaskRunSummary): Promise<void>;
58
+ fail(errorMessage: string, partial?: Partial<TaskRunSummary>): Promise<void>;
59
+ skip(reason?: string): Promise<void>;
60
+ }
61
+ /** The minimum a task needs from `env`: a D1 binding, when the repo has one. */
62
+ interface ScheduledTaskEnv {
63
+ DB?: unknown;
64
+ }
65
+ /**
66
+ * The cron event, structurally.
67
+ *
68
+ * Declared here rather than taken from `@cloudflare/workers-types`: this package
69
+ * stays typecheckable with no platform types installed, the same trade
70
+ * `AbstractEntrypointWorker` makes. A consumer's real `ScheduledController`
71
+ * satisfies it.
72
+ */
73
+ interface ScheduledControllerLike {
74
+ cron: string;
75
+ scheduledTime: number;
76
+ noRetry(): void;
77
+ }
78
+ /** The execution context, structurally. See {@link ScheduledControllerLike}. */
79
+ interface ExecutionContextLike {
80
+ waitUntil(promise: Promise<unknown>): void;
81
+ passThroughOnException(): void;
82
+ }
83
+ type TaskLogger = (level: 'warn' | 'error', message: string) => void;
84
+ /**
85
+ * The base every scheduled task extends.
86
+ *
87
+ * Subclasses implement {@link handleScheduledTask} and, when they want their
88
+ * runs recorded, override {@link getTaskType}.
89
+ */
90
+ declare abstract class AbstractScheduledTask<TEnv extends ScheduledTaskEnv = ScheduledTaskEnv> {
91
+ /**
92
+ * The task type recorded against each run, or `null` to opt out.
93
+ *
94
+ * Opting **in** rather than out is deliberate: a task that never reports is
95
+ * invisible, and invisibility is the failure mode worth avoiding by default.
96
+ * A per-application task should leave this `null` and use
97
+ * {@link createApplicationRun} instead, so it does not produce a redundant
98
+ * global record beside its per-application ones.
99
+ */
100
+ protected getTaskType(): string | null;
101
+ /** Factory Method for the recorder. Override in tests. */
102
+ protected createRunRecorder(): TaskRunRecorder | null;
103
+ /** Override to route this task's failures somewhere other than the console. */
104
+ protected logger(): TaskLogger;
105
+ /**
106
+ * The entry point the platform calls.
107
+ *
108
+ * Owns the try/catch so a task cannot forget it: a scheduled task that throws
109
+ * uncaught is retried by the platform with no record of why.
110
+ */
111
+ handle(event: ScheduledControllerLike, env: TEnv, ctx: ExecutionContextLike): Promise<void>;
112
+ /**
113
+ * Open a per-application run record.
114
+ *
115
+ * Call inside a loop over applications, so the operator sees which one failed
116
+ * rather than a single row for the whole cron.
117
+ */
118
+ protected createApplicationRun(taskType: string, applicationId: string): Promise<ApplicationRunHandle | null>;
119
+ /**
120
+ * The task's work.
121
+ *
122
+ * Returns a summary to be recorded, or nothing for a task whose runs are not
123
+ * tracked.
124
+ */
125
+ protected abstract handleScheduledTask(event: ScheduledControllerLike, env: TEnv, ctx: ExecutionContextLike): Promise<TaskRunSummary | void>;
126
+ }
127
+ /**
128
+ * The phase registry.
129
+ *
130
+ * Two phases, run in order, each internally parallel. The split is what keeps
131
+ * one slow task from delaying the pruning that has to run behind it — and it is
132
+ * why this is a table rather than a list: adding a task at index 0 cannot
133
+ * silently reclassify every task after it.
134
+ */
135
+ declare class TaskRegistry {
136
+ private readonly byPhase;
137
+ register(phase: number, task: AbstractScheduledTask): this;
138
+ /** Every task in a phase, in registration order. */
139
+ tasksForPhase(phase: number): readonly AbstractScheduledTask[];
140
+ /** The phases that have at least one task, ascending. */
141
+ phases(): readonly number[];
142
+ }
143
+ export { AbstractScheduledTask, TaskRegistry };
144
+ export type { ApplicationRunHandle, ScheduledTaskEnv, TaskLogger, TaskRunRecorder, TaskRunSummary };
145
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduled/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The scheduled-task Template Method, and the run-record port it records through.
3
+ *
4
+ * Converged from `apps/background/src/scheduled/{IScheduledTask,TaskRegistry}.ts`
5
+ * in seven repos, where the copies range from 13 lines (CalDAV-Bridge) to 103
6
+ * (Mail-Otter). Mail-Otter's was the one that had solved the defects the others
7
+ * carried, so this is that one.
8
+ *
9
+ * ## What the smaller copies were missing
10
+ *
11
+ * - **Run bookkeeping.** A task that fails silently leaves an operator with a
12
+ * cron that appears to run and never does. `runId` here is the record that
13
+ * says which.
14
+ * - **A failure that is recorded as a failure.** The template's `catch` writes
15
+ * `failRun`, so the task-runs table shows the reason rather than a row that
16
+ * stopped at `running`.
17
+ * - **A test seam.** `createRunRecorder` is a Factory Method so a suite
18
+ * substitutes a double without a D1 binding.
19
+ *
20
+ * ## Why the recorder is a port and not a DAO
21
+ *
22
+ * `runtime` is the DI and platform layer and has no `d1` dependency — that
23
+ * would invert the package graph. The port is four methods; the consumer binds
24
+ * its `BackgroundTaskRunDAO` at the composition root.
25
+ */
26
+ const consoleLogger = (level, message) => {
27
+ if (level === 'warn')
28
+ console.warn(message);
29
+ else
30
+ console.error(message);
31
+ };
32
+ /**
33
+ * The base every scheduled task extends.
34
+ *
35
+ * Subclasses implement {@link handleScheduledTask} and, when they want their
36
+ * runs recorded, override {@link getTaskType}.
37
+ */
38
+ class AbstractScheduledTask {
39
+ /**
40
+ * The task type recorded against each run, or `null` to opt out.
41
+ *
42
+ * Opting **in** rather than out is deliberate: a task that never reports is
43
+ * invisible, and invisibility is the failure mode worth avoiding by default.
44
+ * A per-application task should leave this `null` and use
45
+ * {@link createApplicationRun} instead, so it does not produce a redundant
46
+ * global record beside its per-application ones.
47
+ */
48
+ getTaskType() {
49
+ return null;
50
+ }
51
+ /** Factory Method for the recorder. Override in tests. */
52
+ createRunRecorder() {
53
+ return null;
54
+ }
55
+ /** Override to route this task's failures somewhere other than the console. */
56
+ logger() {
57
+ return consoleLogger;
58
+ }
59
+ /**
60
+ * The entry point the platform calls.
61
+ *
62
+ * Owns the try/catch so a task cannot forget it: a scheduled task that throws
63
+ * uncaught is retried by the platform with no record of why.
64
+ */
65
+ async handle(event, env, ctx) {
66
+ const taskType = this.getTaskType();
67
+ const recorder = taskType === null ? null : this.createRunRecorder();
68
+ let runId;
69
+ if (recorder && taskType !== null) {
70
+ runId = await recorder.startRun({ taskType }).catch((error) => {
71
+ // Losing the record must not lose the run.
72
+ this.logger()('warn', `[${this.constructor.name}] failed to start task run record`);
73
+ void error;
74
+ return undefined;
75
+ });
76
+ }
77
+ try {
78
+ const result = await this.handleScheduledTask(event, env, ctx);
79
+ if (recorder && runId) {
80
+ await recorder.succeedRun(runId, result ?? { itemsProcessed: 0, itemsFailed: 0 }).catch(() => {
81
+ this.logger()('warn', `[${this.constructor.name}] failed to mark task run succeeded`);
82
+ });
83
+ }
84
+ }
85
+ catch (error) {
86
+ this.logger()('error', `[${this.constructor.name}] uncaught error`);
87
+ if (recorder && runId) {
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ await recorder.failRun(runId, message).catch(() => {
90
+ this.logger()('warn', `[${this.constructor.name}] failed to mark task run failed`);
91
+ });
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Open a per-application run record.
97
+ *
98
+ * Call inside a loop over applications, so the operator sees which one failed
99
+ * rather than a single row for the whole cron.
100
+ */
101
+ async createApplicationRun(taskType, applicationId) {
102
+ const recorder = this.createRunRecorder();
103
+ if (!recorder)
104
+ return null;
105
+ const runId = await recorder.startRun({ taskType, applicationId });
106
+ const warn = (op) => () => {
107
+ this.logger()('warn', `[${this.constructor.name}] failed to mark application run ${op}`);
108
+ };
109
+ return {
110
+ succeed: (result) => recorder
111
+ .succeedRun(runId, result)
112
+ .catch(warn('succeeded'))
113
+ .then(() => undefined),
114
+ fail: (errorMessage, partial) => recorder
115
+ .failRun(runId, errorMessage, partial)
116
+ .catch(warn('failed'))
117
+ .then(() => undefined),
118
+ skip: (reason) => recorder
119
+ .skipRun(runId, reason)
120
+ .catch(warn('skipped'))
121
+ .then(() => undefined),
122
+ };
123
+ }
124
+ }
125
+ /**
126
+ * The phase registry.
127
+ *
128
+ * Two phases, run in order, each internally parallel. The split is what keeps
129
+ * one slow task from delaying the pruning that has to run behind it — and it is
130
+ * why this is a table rather than a list: adding a task at index 0 cannot
131
+ * silently reclassify every task after it.
132
+ */
133
+ class TaskRegistry {
134
+ byPhase = new Map();
135
+ register(phase, task) {
136
+ const existing = this.byPhase.get(phase) ?? [];
137
+ existing.push(task);
138
+ this.byPhase.set(phase, existing);
139
+ return this;
140
+ }
141
+ /** Every task in a phase, in registration order. */
142
+ tasksForPhase(phase) {
143
+ return this.byPhase.get(phase) ?? [];
144
+ }
145
+ /** The phases that have at least one task, ascending. */
146
+ phases() {
147
+ return [...this.byPhase.keys()].sort((left, right) => left - right);
148
+ }
149
+ }
150
+ export { AbstractScheduledTask, TaskRegistry };
151
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scheduled/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAgEH,MAAM,aAAa,GAAe,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACnD,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;;QACvC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AAC9B,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAe,qBAAqB;IAClC;;;;;;;;OAQG;IACO,WAAW;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0DAA0D;IAChD,iBAAiB;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,+EAA+E;IACrE,MAAM;QACd,OAAO,aAAa,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,MAAM,CAAC,KAA8B,EAAE,GAAS,EAAE,GAAyB;QACtF,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAErE,IAAI,KAAyB,CAAC;QAC9B,IAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YAClC,KAAK,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBACrE,2CAA2C;gBAC3C,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,mCAAmC,CAAC,CAAC;gBACpF,KAAK,KAAK,CAAC;gBACX,OAAO,SAAS,CAAC;YACnB,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAC/D,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,IAAI,EAAE,cAAc,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;oBAC3F,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,qCAAqC,CAAC,CAAC;gBACxF,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,kBAAkB,CAAC,CAAC;YACpE,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,MAAM,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;oBAChD,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,kCAAkC,CAAC,CAAC;gBACrF,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACO,KAAK,CAAC,oBAAoB,CAAC,QAAgB,EAAE,aAAqB;QAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC1C,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;QAEnE,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,GAAS,EAAE;YACtC,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,oCAAoC,EAAE,EAAE,CAAC,CAAC;QAC3F,CAAC,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,CAAC,MAAsB,EAAiB,EAAE,CACjD,QAAQ;iBACL,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC;iBACzB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;iBACxB,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;YAC1B,IAAI,EAAE,CAAC,YAAoB,EAAE,OAAiC,EAAiB,EAAE,CAC/E,QAAQ;iBACL,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC;iBACrC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;iBACrB,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;YAC1B,IAAI,EAAE,CAAC,MAAe,EAAiB,EAAE,CACvC,QAAQ;iBACL,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;iBACtB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;iBACtB,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;SAC3B,CAAC;IACJ,CAAC;CAaF;AAED;;;;;;;GAOG;AACH,MAAM,YAAY;IACC,OAAO,GAAG,IAAI,GAAG,EAAmC,CAAC;IAE/D,QAAQ,CAAC,KAAa,EAAE,IAA2B;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,oDAAoD;IAC7C,aAAa,CAAC,KAAa;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACvC,CAAC;IAED,yDAAyD;IAClD,MAAM;QACX,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;IACtE,CAAC;CACF;AAED,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,CAAC","sourcesContent":["/**\n * The scheduled-task Template Method, and the run-record port it records through.\n *\n * Converged from `apps/background/src/scheduled/{IScheduledTask,TaskRegistry}.ts`\n * in seven repos, where the copies range from 13 lines (CalDAV-Bridge) to 103\n * (Mail-Otter). Mail-Otter's was the one that had solved the defects the others\n * carried, so this is that one.\n *\n * ## What the smaller copies were missing\n *\n * - **Run bookkeeping.** A task that fails silently leaves an operator with a\n * cron that appears to run and never does. `runId` here is the record that\n * says which.\n * - **A failure that is recorded as a failure.** The template's `catch` writes\n * `failRun`, so the task-runs table shows the reason rather than a row that\n * stopped at `running`.\n * - **A test seam.** `createRunRecorder` is a Factory Method so a suite\n * substitutes a double without a D1 binding.\n *\n * ## Why the recorder is a port and not a DAO\n *\n * `runtime` is the DI and platform layer and has no `d1` dependency — that\n * would invert the package graph. The port is four methods; the consumer binds\n * its `BackgroundTaskRunDAO` at the composition root.\n */\n\n/** The summary a task reports when it finishes. */\ninterface TaskRunSummary {\n itemsProcessed: number;\n itemsFailed: number;\n summary?: string;\n details?: unknown;\n}\n\n/**\n * Where run records go.\n *\n * Implemented by each repo's `BackgroundTaskRunDAO`; supplied as a factory so a\n * task can be constructed before the binding is known.\n */\ninterface TaskRunRecorder {\n startRun(fields: { taskType: string; applicationId?: string }): Promise<string>;\n succeedRun(runId: string, result: TaskRunSummary): Promise<void>;\n failRun(runId: string, errorMessage: string, partial?: Partial<TaskRunSummary>): Promise<void>;\n skipRun(runId: string, reason?: string): Promise<void>;\n}\n\n/**\n * A handle to one run's completion.\n *\n * Every method swallows its own recording failure and logs a warning. A task\n * whose work succeeded must not be reported as failed because the bookkeeping\n * write did not land — that is the same class of defect as a status read that\n * writes.\n */\ninterface ApplicationRunHandle {\n succeed(result: TaskRunSummary): Promise<void>;\n fail(errorMessage: string, partial?: Partial<TaskRunSummary>): Promise<void>;\n skip(reason?: string): Promise<void>;\n}\n\n/** The minimum a task needs from `env`: a D1 binding, when the repo has one. */\ninterface ScheduledTaskEnv {\n DB?: unknown;\n}\n\n/**\n * The cron event, structurally.\n *\n * Declared here rather than taken from `@cloudflare/workers-types`: this package\n * stays typecheckable with no platform types installed, the same trade\n * `AbstractEntrypointWorker` makes. A consumer's real `ScheduledController`\n * satisfies it.\n */\ninterface ScheduledControllerLike {\n cron: string;\n scheduledTime: number;\n noRetry(): void;\n}\n\n/** The execution context, structurally. See {@link ScheduledControllerLike}. */\ninterface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void;\n passThroughOnException(): void;\n}\n\ntype TaskLogger = (level: 'warn' | 'error', message: string) => void;\n\nconst consoleLogger: TaskLogger = (level, message) => {\n if (level === 'warn') console.warn(message);\n else console.error(message);\n};\n\n/**\n * The base every scheduled task extends.\n *\n * Subclasses implement {@link handleScheduledTask} and, when they want their\n * runs recorded, override {@link getTaskType}.\n */\nabstract class AbstractScheduledTask<TEnv extends ScheduledTaskEnv = ScheduledTaskEnv> {\n /**\n * The task type recorded against each run, or `null` to opt out.\n *\n * Opting **in** rather than out is deliberate: a task that never reports is\n * invisible, and invisibility is the failure mode worth avoiding by default.\n * A per-application task should leave this `null` and use\n * {@link createApplicationRun} instead, so it does not produce a redundant\n * global record beside its per-application ones.\n */\n protected getTaskType(): string | null {\n return null;\n }\n\n /** Factory Method for the recorder. Override in tests. */\n protected createRunRecorder(): TaskRunRecorder | null {\n return null;\n }\n\n /** Override to route this task's failures somewhere other than the console. */\n protected logger(): TaskLogger {\n return consoleLogger;\n }\n\n /**\n * The entry point the platform calls.\n *\n * Owns the try/catch so a task cannot forget it: a scheduled task that throws\n * uncaught is retried by the platform with no record of why.\n */\n public async handle(event: ScheduledControllerLike, env: TEnv, ctx: ExecutionContextLike): Promise<void> {\n const taskType = this.getTaskType();\n const recorder = taskType === null ? null : this.createRunRecorder();\n\n let runId: string | undefined;\n if (recorder && taskType !== null) {\n runId = await recorder.startRun({ taskType }).catch((error: unknown) => {\n // Losing the record must not lose the run.\n this.logger()('warn', `[${this.constructor.name}] failed to start task run record`);\n void error;\n return undefined;\n });\n }\n\n try {\n const result = await this.handleScheduledTask(event, env, ctx);\n if (recorder && runId) {\n await recorder.succeedRun(runId, result ?? { itemsProcessed: 0, itemsFailed: 0 }).catch(() => {\n this.logger()('warn', `[${this.constructor.name}] failed to mark task run succeeded`);\n });\n }\n } catch (error: unknown) {\n this.logger()('error', `[${this.constructor.name}] uncaught error`);\n if (recorder && runId) {\n const message = error instanceof Error ? error.message : String(error);\n await recorder.failRun(runId, message).catch(() => {\n this.logger()('warn', `[${this.constructor.name}] failed to mark task run failed`);\n });\n }\n }\n }\n\n /**\n * Open a per-application run record.\n *\n * Call inside a loop over applications, so the operator sees which one failed\n * rather than a single row for the whole cron.\n */\n protected async createApplicationRun(taskType: string, applicationId: string): Promise<ApplicationRunHandle | null> {\n const recorder = this.createRunRecorder();\n if (!recorder) return null;\n const runId = await recorder.startRun({ taskType, applicationId });\n\n const warn = (op: string) => (): void => {\n this.logger()('warn', `[${this.constructor.name}] failed to mark application run ${op}`);\n };\n\n return {\n succeed: (result: TaskRunSummary): Promise<void> =>\n recorder\n .succeedRun(runId, result)\n .catch(warn('succeeded'))\n .then(() => undefined),\n fail: (errorMessage: string, partial?: Partial<TaskRunSummary>): Promise<void> =>\n recorder\n .failRun(runId, errorMessage, partial)\n .catch(warn('failed'))\n .then(() => undefined),\n skip: (reason?: string): Promise<void> =>\n recorder\n .skipRun(runId, reason)\n .catch(warn('skipped'))\n .then(() => undefined),\n };\n }\n\n /**\n * The task's work.\n *\n * Returns a summary to be recorded, or nothing for a task whose runs are not\n * tracked.\n */\n protected abstract handleScheduledTask(\n event: ScheduledControllerLike,\n env: TEnv,\n ctx: ExecutionContextLike,\n ): Promise<TaskRunSummary | void>;\n}\n\n/**\n * The phase registry.\n *\n * Two phases, run in order, each internally parallel. The split is what keeps\n * one slow task from delaying the pruning that has to run behind it — and it is\n * why this is a table rather than a list: adding a task at index 0 cannot\n * silently reclassify every task after it.\n */\nclass TaskRegistry {\n private readonly byPhase = new Map<number, AbstractScheduledTask[]>();\n\n public register(phase: number, task: AbstractScheduledTask): this {\n const existing = this.byPhase.get(phase) ?? [];\n existing.push(task);\n this.byPhase.set(phase, existing);\n return this;\n }\n\n /** Every task in a phase, in registration order. */\n public tasksForPhase(phase: number): readonly AbstractScheduledTask[] {\n return this.byPhase.get(phase) ?? [];\n }\n\n /** The phases that have at least one task, ascending. */\n public phases(): readonly number[] {\n return [...this.byPhase.keys()].sort((left, right) => left - right);\n }\n}\n\nexport { AbstractScheduledTask, TaskRegistry };\nexport type { ApplicationRunHandle, ScheduledTaskEnv, TaskLogger, TaskRunRecorder, TaskRunSummary };\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexezuge/runtime",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {